feat: config.botName for describing environment in the reports (#28507)

Reference https://github.com/microsoft/playwright/issues/27284
This commit is contained in:
Yury Semikhatsky 2023-12-06 13:34:16 -08:00 committed by GitHub
parent 29a0ea35d0
commit f88288d71d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 111 additions and 13 deletions

View file

@ -17,6 +17,23 @@ export default defineConfig({
}); });
``` ```
## property: TestConfig.botName
* since: v1.41
- type: ?<[string]>
Unique name of the environment where the tests run. It may be composed of, e.g., operating system name and
test run parameters. Test reporters can access the name via `TestProject.botName` property.
**Usage**
```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';
export default defineConfig({
botName: process.env.BOT_NAME,
});
```
## property: TestConfig.build ## property: TestConfig.build
* since: v1.35 * since: v1.35
- type: ?<[Object]> - type: ?<[Object]>

View file

@ -108,7 +108,7 @@ export class Filter {
if (test.outcome === 'skipped') if (test.outcome === 'skipped')
status = 'skipped'; status = 'skipped';
const searchValues: SearchValues = { const searchValues: SearchValues = {
text: (status + ' ' + test.projectName + ' ' + (test.reportName || '') + ' ' + test.location.file + ' ' + test.path.join(' ') + ' ' + test.title).toLowerCase(), text: (status + ' ' + test.projectName + ' ' + (test.botName || '') + ' ' + test.location.file + ' ' + test.path.join(' ') + ' ' + test.title).toLowerCase(),
project: test.projectName.toLowerCase(), project: test.projectName.toLowerCase(),
status: status as any, status: status as any,
file: test.location.file, file: test.location.file,

View file

@ -27,8 +27,8 @@ export function escapeRegExp(string: string) {
export function testCaseLabels(test: TestCaseSummary): string[] { export function testCaseLabels(test: TestCaseSummary): string[] {
const tags = matchTags(test.path.join(' ') + ' ' + test.title).sort((a, b) => a.localeCompare(b)); const tags = matchTags(test.path.join(' ') + ' ' + test.title).sort((a, b) => a.localeCompare(b));
if (test.reportName) if (test.botName)
tags.unshift(test.reportName); tags.unshift(test.botName);
return tags; return tags;
} }

View file

@ -66,7 +66,7 @@ export type TestCaseSummary = {
title: string; title: string;
path: string[]; path: string[];
projectName: string; projectName: string;
reportName?: string; botName?: string;
location: Location; location: Location;
annotations: TestCaseAnnotation[]; annotations: TestCaseAnnotation[];
outcome: 'skipped' | 'expected' | 'unexpected' | 'flaky'; outcome: 'skipped' | 'expected' | 'unexpected' | 'flaky';

View file

@ -168,6 +168,7 @@ export class FullProjectInternal {
this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate); this.snapshotPathTemplate = takeFirst(projectConfig.snapshotPathTemplate, config.snapshotPathTemplate, defaultSnapshotPathTemplate);
this.project = { this.project = {
botName: config.botName,
grep: takeFirst(projectConfig.grep, config.grep, defaultGrep), grep: takeFirst(projectConfig.grep, config.grep, defaultGrep),
grepInvert: takeFirst(projectConfig.grepInvert, config.grepInvert, null), grepInvert: takeFirst(projectConfig.grepInvert, config.grepInvert, null),
outputDir: takeFirst(configCLIOverrides.outputDir, pathResolve(configDir, projectConfig.outputDir), pathResolve(configDir, config.outputDir), path.join(throwawayArtifactsPath, 'test-results')), outputDir: takeFirst(configCLIOverrides.outputDir, pathResolve(configDir, projectConfig.outputDir), pathResolve(configDir, config.outputDir), path.join(throwawayArtifactsPath, 'test-results')),

View file

@ -41,6 +41,7 @@ export type JsonPattern = {
export type JsonProject = { export type JsonProject = {
id: string; id: string;
botName?: string;
grep: JsonPattern[]; grep: JsonPattern[];
grepInvert: JsonPattern[]; grepInvert: JsonPattern[];
metadata: Metadata; metadata: Metadata;
@ -334,6 +335,7 @@ export class TeleReporterReceiver {
private _parseProject(project: JsonProject): TeleFullProject { private _parseProject(project: JsonProject): TeleFullProject {
return { return {
__projectId: project.id, __projectId: project.id,
botName: project.botName,
metadata: project.metadata, metadata: project.metadata,
name: project.name, name: project.name,
outputDir: this._absolutePath(project.outputDir), outputDir: this._absolutePath(project.outputDir),

View file

@ -58,7 +58,7 @@ export class BlobReporter extends TeleReporterEmitter {
const metadata: BlobReportMetadata = { const metadata: BlobReportMetadata = {
version: currentBlobReportVersion, version: currentBlobReportVersion,
userAgent: getUserAgent(), userAgent: getUserAgent(),
name: process.env.PWTEST_BLOB_REPORT_NAME, name: config.botName || process.env.PWTEST_BLOB_REPORT_NAME,
shard: config.shard ?? undefined, shard: config.shard ?? undefined,
pathSeparator: path.sep, pathSeparator: path.sep,
}; };

View file

@ -240,7 +240,7 @@ class HtmlBuilder {
} }
const { testFile, testFileSummary } = fileEntry; const { testFile, testFileSummary } = fileEntry;
const testEntries: TestEntry[] = []; const testEntries: TestEntry[] = [];
this._processJsonSuite(fileSuite, fileId, projectSuite.project()!.name, projectSuite.project()!.metadata?.reportName, [], testEntries); this._processJsonSuite(fileSuite, fileId, projectSuite.project()!.name, projectSuite.project()!.botName, [], testEntries);
for (const test of testEntries) { for (const test of testEntries) {
testFile.tests.push(test.testCase); testFile.tests.push(test.testCase);
testFileSummary.tests.push(test.testCaseSummary); testFileSummary.tests.push(test.testCaseSummary);
@ -340,13 +340,13 @@ class HtmlBuilder {
this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName); this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
} }
private _processJsonSuite(suite: Suite, fileId: string, projectName: string, reportName: string | undefined, path: string[], outTests: TestEntry[]) { private _processJsonSuite(suite: Suite, fileId: string, projectName: string, botName: string | undefined, path: string[], outTests: TestEntry[]) {
const newPath = [...path, suite.title]; const newPath = [...path, suite.title];
suite.suites.forEach(s => this._processJsonSuite(s, fileId, projectName, reportName, newPath, outTests)); suite.suites.forEach(s => this._processJsonSuite(s, fileId, projectName, botName, newPath, outTests));
suite.tests.forEach(t => outTests.push(this._createTestEntry(t, projectName, reportName, newPath))); suite.tests.forEach(t => outTests.push(this._createTestEntry(t, projectName, botName, newPath)));
} }
private _createTestEntry(test: TestCasePublic, projectName: string, reportName: string | undefined, path: string[]): TestEntry { private _createTestEntry(test: TestCasePublic, projectName: string, botName: string | undefined, path: string[]): TestEntry {
const duration = test.results.reduce((a, r) => a + r.duration, 0); const duration = test.results.reduce((a, r) => a + r.duration, 0);
const location = this._relativeLocation(test.location)!; const location = this._relativeLocation(test.location)!;
path = path.slice(1); path = path.slice(1);
@ -358,7 +358,7 @@ class HtmlBuilder {
testId: test.id, testId: test.id,
title: test.title, title: test.title,
projectName, projectName,
reportName, botName,
location, location,
duration, duration,
annotations: test.annotations, annotations: test.annotations,
@ -371,7 +371,7 @@ class HtmlBuilder {
testId: test.id, testId: test.id,
title: test.title, title: test.title,
projectName, projectName,
reportName, botName,
location, location,
duration, duration,
annotations: test.annotations, annotations: test.annotations,

View file

@ -162,6 +162,7 @@ export class TeleReporterEmitter implements ReporterV2 {
const project = suite.project()!; const project = suite.project()!;
const report: JsonProject = { const report: JsonProject = {
id: getProjectId(project), id: getProjectId(project),
botName: project.botName,
metadata: project.metadata, metadata: project.metadata,
name: project.name, name: project.name,
outputDir: this._relativePath(project.outputDir), outputDir: this._relativePath(project.outputDir),

View file

@ -161,6 +161,7 @@ export interface Project<TestArgs = {}, WorkerArgs = {}> extends TestProject {
* *
*/ */
export interface FullProject<TestArgs = {}, WorkerArgs = {}> { export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
botName?: string;
/** /**
* Filter to only run tests with a title matching one of the patterns. For example, passing `grep: /cart/` should only * Filter to only run tests with a title matching one of the patterns. For example, passing `grep: /cart/` should only
* run tests with "cart" in the title. Also available globally and in the [command line](https://playwright.dev/docs/test-cli) with the `-g` * run tests with "cart" in the title. Also available globally and in the [command line](https://playwright.dev/docs/test-cli) with the `-g`
@ -570,6 +571,24 @@ interface TestConfig {
* *
*/ */
webServer?: TestConfigWebServer | TestConfigWebServer[]; webServer?: TestConfigWebServer | TestConfigWebServer[];
/**
* Unique name of the environment where the tests run. It may be composed of, e.g., operating system name and test run
* parameters. Test reporters can access the name via `TestProject.botName` property.
*
* **Usage**
*
* ```js
* // playwright.config.ts
* import { defineConfig } from '@playwright/test';
*
* export default defineConfig({
* botName: process.env.BOT_NAME,
* });
* ```
*
*/
botName?: string;
/** /**
* Playwright transpiler configuration. * Playwright transpiler configuration.
* *
@ -1451,6 +1470,23 @@ export type Metadata = { [key: string]: any };
* *
*/ */
export interface FullConfig<TestArgs = {}, WorkerArgs = {}> { export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
/**
* Unique name of the environment where the tests run. It may be composed of, e.g., operating system name and test run
* parameters. Test reporters can access the name via `TestProject.botName` property.
*
* **Usage**
*
* ```js
* // playwright.config.ts
* import { defineConfig } from '@playwright/test';
*
* export default defineConfig({
* botName: process.env.BOT_NAME,
* });
* ```
*
*/
botName?: string;
/** /**
* Whether to exit with an error if any tests or groups are marked as * Whether to exit with an error if any tests or groups are marked as
* [test.only(title, testFunction)](https://playwright.dev/docs/api/class-test#test-only) or * [test.only(title, testFunction)](https://playwright.dev/docs/api/class-test#test-only) or

View file

@ -1211,6 +1211,45 @@ test('same project different suffixes', async ({ runInlineTest, mergeReports })
expect(output).toContain(`reportNames: first,second`); expect(output).toContain(`reportNames: first,second`);
}); });
test('preserve botName on projects', async ({ runInlineTest, mergeReports }) => {
const files = (botName: string) => ({
'echo-reporter.js': `
import fs from 'fs';
class EchoReporter {
onBegin(config, suite) {
const projects = suite.suites.map(s => s.project()).sort((a, b) => a.metadata.reportName.localeCompare(b.metadata.reportName));
console.log('projectNames: ' + projects.map(p => p.name));
console.log('botNames: ' + projects.map(p => p.botName));
}
}
module.exports = EchoReporter;
`,
'playwright.config.ts': `
module.exports = {
reporter: 'blob',
botName: '${botName}',
projects: [
{ name: 'foo' },
]
};
`,
'a.test.js': `
import { test, expect } from '@playwright/test';
test('math 1 @smoke', async ({}) => {});
`,
});
await runInlineTest(files('first'), undefined, { PWTEST_BLOB_REPORT_NAME: 'first' });
await runInlineTest(files('second'), undefined, { PWTEST_BLOB_REPORT_NAME: 'second', PWTEST_BLOB_DO_NOT_REMOVE: '1' });
const reportDir = test.info().outputPath('blob-report');
const { exitCode, output } = await mergeReports(reportDir, {}, { additionalArgs: ['--reporter', test.info().outputPath('echo-reporter.js')] });
expect(exitCode).toBe(0);
expect(output).toContain(`projectNames: foo,foo`);
expect(output).toContain(`botNames: first,second`);
});
test('no reports error', async ({ runInlineTest, mergeReports }) => { test('no reports error', async ({ runInlineTest, mergeReports }) => {
const reportDir = test.info().outputPath('blob-report'); const reportDir = test.info().outputPath('blob-report');
fs.mkdirSync(reportDir, { recursive: true }); fs.mkdirSync(reportDir, { recursive: true });

View file

@ -113,7 +113,7 @@ class TypesGenerator {
return ''; return '';
this.handledMethods.add(`${className}.${methodName}#${overloadIndex}`); this.handledMethods.add(`${className}.${methodName}#${overloadIndex}`);
if (!method) { if (!method) {
if (new Set(['on', 'addListener', 'off', 'removeListener', 'once', 'prependListener']).has(methodName)) if (new Set(['on', 'addListener', 'off', 'removeListener', 'once', 'prependListener', 'botName']).has(methodName))
return ''; return '';
throw new Error(`Unknown override method "${className}.${methodName}"`); throw new Error(`Unknown override method "${className}.${methodName}"`);
} }

View file

@ -39,6 +39,7 @@ export interface Project<TestArgs = {}, WorkerArgs = {}> extends TestProject {
// [internal] It is part of the public API and is computed from the user's config. // [internal] It is part of the public API and is computed from the user's config.
// [internal] If you need new fields internally, add them to FullProjectInternal instead. // [internal] If you need new fields internally, add them to FullProjectInternal instead.
export interface FullProject<TestArgs = {}, WorkerArgs = {}> { export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
botName?: string;
grep: RegExp | RegExp[]; grep: RegExp | RegExp[];
grepInvert: RegExp | RegExp[] | null; grepInvert: RegExp | RegExp[] | null;
metadata: Metadata; metadata: Metadata;
@ -75,6 +76,7 @@ export type Metadata = { [key: string]: any };
// [internal] It is part of the public API and is computed from the user's config. // [internal] It is part of the public API and is computed from the user's config.
// [internal] If you need new fields internally, add them to FullConfigInternal instead. // [internal] If you need new fields internally, add them to FullConfigInternal instead.
export interface FullConfig<TestArgs = {}, WorkerArgs = {}> { export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
botName?: string;
forbidOnly: boolean; forbidOnly: boolean;
fullyParallel: boolean; fullyParallel: boolean;
globalSetup: string | null; globalSetup: string | null;