Skip to content

Commit 9e091e7

Browse files
authored
cherry-pick(#30611): chore: add common env vars for junit and json re… (#30624)
…porters
1 parent 154694b commit 9e091e7

File tree

8 files changed

+167
-59
lines changed

8 files changed

+167
-59
lines changed

docs/src/test-reporters-js.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ Blob report supports following configuration options and environment variables:
231231
|---|---|---|---|
232232
| `PLAYWRIGHT_BLOB_OUTPUT_DIR` | `outputDir` | Directory to save the output. Existing content is deleted before writing the new report. | `blob-report`
233233
| `PLAYWRIGHT_BLOB_OUTPUT_NAME` | `fileName` | Report file name. | `report-<project>-<hash>-<shard_number>.zip`
234-
| `PLAYWRIGHT_BLOB_OUTPUT_FILE` | `outputFile` | Full path for the output. If defined, `outputDir` and `fileName` will be ignored. | `undefined`
234+
| `PLAYWRIGHT_BLOB_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `outputDir` and `fileName` will be ignored. | `undefined`
235235

236236
### JSON reporter
237237

@@ -267,7 +267,9 @@ JSON report supports following configuration options and environment variables:
267267

268268
| Environment Variable Name | Reporter Config Option| Description | Default
269269
|---|---|---|---|
270-
| `PLAYWRIGHT_JUNIT_OUTPUT_NAME` | `outputFile` | Report file path. | JSON report is printed to stdout.
270+
| `PLAYWRIGHT_JSON_OUTPUT_DIR` | | Directory to save the output file. Ignored if output file is specified. | `cwd` or config directory.
271+
| `PLAYWRIGHT_JSON_OUTPUT_NAME` | `outputFile` | Base file name for the output, relative to the output dir. | JSON report is printed to the stdout.
272+
| `PLAYWRIGHT_JSON_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `PLAYWRIGHT_JSON_OUTPUT_DIR` and `PLAYWRIGHT_JSON_OUTPUT_NAME` will be ignored. | JSON report is printed to the stdout.
271273

272274
### JUnit reporter
273275

@@ -303,7 +305,9 @@ JUnit report supports following configuration options and environment variables:
303305

304306
| Environment Variable Name | Reporter Config Option| Description | Default
305307
|---|---|---|---|
306-
| `PLAYWRIGHT_JUNIT_OUTPUT_NAME` | `outputFile` | Report file path. | JUnit report is printed to stdout.
308+
| `PLAYWRIGHT_JUNIT_OUTPUT_DIR` | | Directory to save the output file. Ignored if output file is not specified. | `cwd` or config directory.
309+
| `PLAYWRIGHT_JUNIT_OUTPUT_NAME` | `outputFile` | Base file name for the output, relative to the output dir. | JUnit report is printed to the stdout.
310+
| `PLAYWRIGHT_JUNIT_OUTPUT_FILE` | `outputFile` | Full path to the output file. If defined, `PLAYWRIGHT_JUNIT_OUTPUT_DIR` and `PLAYWRIGHT_JUNIT_OUTPUT_NAME` will be ignored. | JUnit report is printed to the stdout.
307311
| | `stripANSIControlSequences` | Whether to remove ANSI control sequences from the text before writing it in the report. | By default output text is added as is.
308312
| | `includeProjectInTestName` | Whether to include Playwright project name in every test case as a name prefix. | By default not included.
309313
| `PLAYWRIGHT_JUNIT_SUITE_ID` | | Value of the `id` attribute on the root `<testsuites/>` report entry. | Empty string.

packages/playwright/src/reporters/base.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import path from 'path';
1919
import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, TestStep, Location } from '../../types/testReporter';
2020
import { getPackageManagerExecCommand } from 'playwright-core/lib/utils';
2121
import type { ReporterV2 } from './reporterV2';
22+
import { resolveReporterOutputPath } from '../util';
2223
export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' };
2324
export const kOutputSymbol = Symbol('output');
2425

@@ -547,3 +548,49 @@ function fitToWidth(line: string, width: number, prefix?: string): string {
547548
function belongsToNodeModules(file: string) {
548549
return file.includes(`${path.sep}node_modules${path.sep}`);
549550
}
551+
552+
function resolveFromEnv(name: string): string | undefined {
553+
const value = process.env[name];
554+
if (value)
555+
return path.resolve(process.cwd(), value);
556+
return undefined;
557+
}
558+
559+
// In addition to `outputFile` the function returns `outputDir` which should
560+
// be cleaned up if present by some reporters contract.
561+
export function resolveOutputFile(reporterName: string, options: {
562+
configDir: string,
563+
outputDir?: string,
564+
fileName?: string,
565+
outputFile?: string,
566+
default?: {
567+
fileName: string,
568+
outputDir: string,
569+
}
570+
}): { outputFile: string, outputDir?: string } |undefined {
571+
const name = reporterName.toUpperCase();
572+
let outputFile;
573+
if (options.outputFile)
574+
outputFile = path.resolve(options.configDir, options.outputFile);
575+
if (!outputFile)
576+
outputFile = resolveFromEnv(`PLAYWRIGHT_${name}_OUTPUT_FILE`);
577+
// Return early to avoid deleting outputDir.
578+
if (outputFile)
579+
return { outputFile };
580+
581+
let outputDir;
582+
if (options.outputDir)
583+
outputDir = path.resolve(options.configDir, options.outputDir);
584+
if (!outputDir)
585+
outputDir = resolveFromEnv(`PLAYWRIGHT_${name}_OUTPUT_DIR`);
586+
if (!outputDir && options.default)
587+
outputDir = resolveReporterOutputPath(options.default.outputDir, options.configDir, undefined);
588+
589+
if (!outputFile) {
590+
const reportName = options.fileName ?? process.env[`PLAYWRIGHT_${name}_OUTPUT_NAME`] ?? options.default?.fileName;
591+
if (!reportName)
592+
return undefined;
593+
outputFile = path.resolve(outputDir ?? process.cwd(), reportName);
594+
}
595+
return { outputFile, outputDir };
596+
}

packages/playwright/src/reporters/blob.ts

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type { FullConfig, FullResult, TestResult } from '../../types/testReporte
2424
import type { JsonAttachment, JsonEvent } from '../isomorphic/teleReceiver';
2525
import { TeleReporterEmitter } from './teleEmitter';
2626
import { yazl } from 'playwright-core/lib/zipBundle';
27-
import { resolveReporterOutputPath } from '../util';
27+
import { resolveOutputFile } from './base';
2828

2929
type BlobReporterOptions = {
3030
configDir: string;
@@ -107,17 +107,15 @@ export class BlobReporter extends TeleReporterEmitter {
107107
}
108108

109109
private async _prepareOutputFile() {
110-
let outputFile = reportOutputFileFromEnv();
111-
if (!outputFile && this._options.outputFile)
112-
outputFile = path.resolve(this._options.configDir, this._options.outputFile);
113-
// Explicit `outputFile` overrides `outputDir` and `fileName` options.
114-
if (!outputFile) {
115-
const reportName = this._options.fileName || process.env[`PLAYWRIGHT_BLOB_OUTPUT_NAME`] || this._defaultReportName(this._config);
116-
const outputDir = resolveReporterOutputPath('blob-report', this._options.configDir, this._options.outputDir ?? reportOutputDirFromEnv());
117-
if (!process.env.PWTEST_BLOB_DO_NOT_REMOVE)
118-
await removeFolders([outputDir]);
119-
outputFile = path.resolve(outputDir, reportName);
120-
}
110+
const { outputFile, outputDir } = resolveOutputFile('BLOB', {
111+
...this._options,
112+
default: {
113+
fileName: this._defaultReportName(this._config),
114+
outputDir: 'blob-report',
115+
}
116+
})!;
117+
if (!process.env.PWTEST_BLOB_DO_NOT_REMOVE)
118+
await removeFolders([outputDir!]);
121119
await fs.promises.mkdir(path.dirname(outputFile), { recursive: true });
122120
return outputFile;
123121
}
@@ -149,15 +147,3 @@ export class BlobReporter extends TeleReporterEmitter {
149147
});
150148
}
151149
}
152-
153-
function reportOutputDirFromEnv(): string | undefined {
154-
if (process.env[`PLAYWRIGHT_BLOB_OUTPUT_DIR`])
155-
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_BLOB_OUTPUT_DIR`]);
156-
return undefined;
157-
}
158-
159-
function reportOutputFileFromEnv(): string | undefined {
160-
if (process.env[`PLAYWRIGHT_BLOB_OUTPUT_FILE`])
161-
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_BLOB_OUTPUT_FILE`]);
162-
return undefined;
163-
}

packages/playwright/src/reporters/json.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,29 @@
1717
import fs from 'fs';
1818
import path from 'path';
1919
import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, FullResult, Location, JSONReport, JSONReportSuite, JSONReportSpec, JSONReportTest, JSONReportTestResult, JSONReportTestStep, JSONReportError } from '../../types/testReporter';
20-
import { formatError, prepareErrorStack } from './base';
21-
import { MultiMap, assert, toPosixPath } from 'playwright-core/lib/utils';
20+
import { formatError, prepareErrorStack, resolveOutputFile } from './base';
21+
import { MultiMap, toPosixPath } from 'playwright-core/lib/utils';
2222
import { getProjectId } from '../common/config';
2323
import EmptyReporter from './empty';
2424

25+
type JSONOptions = {
26+
outputFile?: string,
27+
configDir: string,
28+
};
29+
2530
class JSONReporter extends EmptyReporter {
2631
config!: FullConfig;
2732
suite!: Suite;
2833
private _errors: TestError[] = [];
29-
private _outputFile: string | undefined;
34+
private _resolvedOutputFile: string | undefined;
3035

31-
constructor(options: { outputFile?: string } = {}) {
36+
constructor(options: JSONOptions) {
3237
super();
33-
this._outputFile = options.outputFile || reportOutputNameFromEnv();
38+
this._resolvedOutputFile = resolveOutputFile('JSON', options)?.outputFile;
3439
}
3540

3641
override printsToStdio() {
37-
return !this._outputFile;
42+
return !this._resolvedOutputFile;
3843
}
3944

4045
override onConfigure(config: FullConfig) {
@@ -50,7 +55,7 @@ class JSONReporter extends EmptyReporter {
5055
}
5156

5257
override async onEnd(result: FullResult) {
53-
await outputReport(this._serializeReport(result), this.config, this._outputFile);
58+
await outputReport(this._serializeReport(result), this._resolvedOutputFile);
5459
}
5560

5661
private _serializeReport(result: FullResult): JSONReport {
@@ -228,13 +233,11 @@ class JSONReporter extends EmptyReporter {
228233
}
229234
}
230235

231-
async function outputReport(report: JSONReport, config: FullConfig, outputFile: string | undefined) {
236+
async function outputReport(report: JSONReport, resolvedOutputFile: string | undefined) {
232237
const reportString = JSON.stringify(report, undefined, 2);
233-
if (outputFile) {
234-
assert(config.configFile || path.isAbsolute(outputFile), 'Expected fully resolved path if not using config file.');
235-
outputFile = config.configFile ? path.resolve(path.dirname(config.configFile), outputFile) : outputFile;
236-
await fs.promises.mkdir(path.dirname(outputFile), { recursive: true });
237-
await fs.promises.writeFile(outputFile, reportString);
238+
if (resolvedOutputFile) {
239+
await fs.promises.mkdir(path.dirname(resolvedOutputFile), { recursive: true });
240+
await fs.promises.writeFile(resolvedOutputFile, reportString);
238241
} else {
239242
console.log(reportString);
240243
}
@@ -250,12 +253,6 @@ function removePrivateFields(config: FullConfig): FullConfig {
250253
return Object.fromEntries(Object.entries(config).filter(([name, value]) => !name.startsWith('_'))) as FullConfig;
251254
}
252255

253-
function reportOutputNameFromEnv(): string | undefined {
254-
if (process.env[`PLAYWRIGHT_JSON_OUTPUT_NAME`])
255-
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_JSON_OUTPUT_NAME`]);
256-
return undefined;
257-
}
258-
259256
export function serializePatterns(patterns: string | RegExp | (string | RegExp)[]): string[] {
260257
if (!Array.isArray(patterns))
261258
patterns = [patterns];

packages/playwright/src/reporters/junit.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,15 @@
1717
import fs from 'fs';
1818
import path from 'path';
1919
import type { FullConfig, FullResult, Suite, TestCase } from '../../types/testReporter';
20-
import { formatFailure, stripAnsiEscapes } from './base';
20+
import { formatFailure, resolveOutputFile, stripAnsiEscapes } from './base';
2121
import EmptyReporter from './empty';
2222

2323
type JUnitOptions = {
2424
outputFile?: string,
2525
stripANSIControlSequences?: boolean,
2626
includeProjectInTestName?: boolean,
2727

28-
configDir?: string,
28+
configDir: string,
2929
};
3030

3131
class JUnitReporter extends EmptyReporter {
@@ -40,14 +40,12 @@ class JUnitReporter extends EmptyReporter {
4040
private stripANSIControlSequences = false;
4141
private includeProjectInTestName = false;
4242

43-
constructor(options: JUnitOptions = {}) {
43+
constructor(options: JUnitOptions) {
4444
super();
4545
this.stripANSIControlSequences = options.stripANSIControlSequences || false;
4646
this.includeProjectInTestName = options.includeProjectInTestName || false;
47-
this.configDir = options.configDir || '';
48-
const outputFile = options.outputFile || reportOutputNameFromEnv();
49-
if (outputFile)
50-
this.resolvedOutputFile = path.resolve(this.configDir, outputFile);
47+
this.configDir = options.configDir;
48+
this.resolvedOutputFile = resolveOutputFile('JUNIT', options)?.outputFile;
5149
}
5250

5351
override printsToStdio() {
@@ -261,10 +259,4 @@ function escape(text: string, stripANSIControlSequences: boolean, isCharacterDat
261259
return text;
262260
}
263261

264-
function reportOutputNameFromEnv(): string | undefined {
265-
if (process.env[`PLAYWRIGHT_JUNIT_OUTPUT_NAME`])
266-
return path.resolve(process.cwd(), process.env[`PLAYWRIGHT_JUNIT_OUTPUT_NAME`]);
267-
return undefined;
268-
}
269-
270262
export default JUnitReporter;

tests/playwright-test/reporter-blob.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,12 +1292,18 @@ test('support PLAYWRIGHT_BLOB_OUTPUT_FILE environment variable', async ({ runInl
12921292
test('math 1 @smoke', async ({}) => {});
12931293
`,
12941294
};
1295+
const defaultDir = test.info().outputPath('blob-report');
1296+
fs.mkdirSync(defaultDir, { recursive: true });
1297+
const file = path.join(defaultDir, 'some.file');
1298+
fs.writeFileSync(file, 'content');
12951299

12961300
await runInlineTest(files, { shard: `1/2` }, { PLAYWRIGHT_BLOB_OUTPUT_FILE: 'subdir/report-one.zip' });
12971301
await runInlineTest(files, { shard: `2/2` }, { PLAYWRIGHT_BLOB_OUTPUT_FILE: test.info().outputPath('subdir/report-two.zip') });
12981302
const reportDir = test.info().outputPath('subdir');
12991303
const reportFiles = await fs.promises.readdir(reportDir);
13001304
expect(reportFiles.sort()).toEqual(['report-one.zip', 'report-two.zip']);
1305+
1306+
expect(fs.existsSync(file), 'Default directory should not be cleaned up if output file is specified.').toBe(true);
13011307
});
13021308

13031309
test('keep projects with same name different bot name separate', async ({ runInlineTest, mergeReports, showReport, page }) => {

tests/playwright-test/reporter-json.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,4 +288,42 @@ test.describe('report location', () => {
288288
expect(result.passed).toBe(1);
289289
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.json'))).toBe(true);
290290
});
291+
292+
test('support PLAYWRIGHT_JSON_OUTPUT_FILE', async ({ runInlineTest }, testInfo) => {
293+
const result = await runInlineTest({
294+
'foo/package.json': `{ "name": "foo" }`,
295+
// unused config along "search path"
296+
'foo/bar/playwright.config.js': `
297+
module.exports = { projects: [ {} ] };
298+
`,
299+
'foo/bar/baz/tests/a.spec.js': `
300+
import { test, expect } from '@playwright/test';
301+
const fs = require('fs');
302+
test('pass', ({}, testInfo) => {
303+
});
304+
`
305+
}, { 'reporter': 'json' }, { 'PW_TEST_HTML_REPORT_OPEN': 'never', 'PLAYWRIGHT_JSON_OUTPUT_FILE': '../my-report.json' }, {
306+
cwd: 'foo/bar/baz/tests',
307+
});
308+
expect(result.exitCode).toBe(0);
309+
expect(result.passed).toBe(1);
310+
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.json'))).toBe(true);
311+
});
312+
313+
test('support PLAYWRIGHT_JSON_OUTPUT_DIR and PLAYWRIGHT_JSON_OUTPUT_NAME', async ({ runInlineTest }, testInfo) => {
314+
const result = await runInlineTest({
315+
'playwright.config.js': `
316+
module.exports = { projects: [ {} ] };
317+
`,
318+
'tests/a.spec.js': `
319+
import { test, expect } from '@playwright/test';
320+
const fs = require('fs');
321+
test('pass', ({}, testInfo) => {
322+
});
323+
`
324+
}, { 'reporter': 'json' }, { 'PLAYWRIGHT_JSON_OUTPUT_DIR': 'foo/bar', 'PLAYWRIGHT_JSON_OUTPUT_NAME': 'baz/my-report.json' });
325+
expect(result.exitCode).toBe(0);
326+
expect(result.passed).toBe(1);
327+
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.json'))).toBe(true);
328+
});
291329
});

tests/playwright-test/reporter-junit.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,44 @@ for (const useIntermediateMergeReport of [false, true] as const) {
504504
expect(result.passed).toBe(1);
505505
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.xml'))).toBe(true);
506506
});
507+
508+
test('support PLAYWRIGHT_JUNIT_OUTPUT_FILE', async ({ runInlineTest }, testInfo) => {
509+
const result = await runInlineTest({
510+
'foo/package.json': `{ "name": "foo" }`,
511+
// unused config along "search path"
512+
'foo/bar/playwright.config.js': `
513+
module.exports = { projects: [ {} ] };
514+
`,
515+
'foo/bar/baz/tests/a.spec.js': `
516+
import { test, expect } from '@playwright/test';
517+
const fs = require('fs');
518+
test('pass', ({}, testInfo) => {
519+
});
520+
`
521+
}, { 'reporter': 'junit,line' }, { 'PLAYWRIGHT_JUNIT_OUTPUT_FILE': '../my-report.xml' }, {
522+
cwd: 'foo/bar/baz/tests',
523+
});
524+
expect(result.exitCode).toBe(0);
525+
expect(result.passed).toBe(1);
526+
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.xml'))).toBe(true);
527+
});
528+
529+
test('support PLAYWRIGHT_JUNIT_OUTPUT_DIR and PLAYWRIGHT_JUNIT_OUTPUT_NAME', async ({ runInlineTest }, testInfo) => {
530+
const result = await runInlineTest({
531+
'playwright.config.js': `
532+
module.exports = { projects: [ {} ] };
533+
`,
534+
'tests/a.spec.js': `
535+
import { test, expect } from '@playwright/test';
536+
const fs = require('fs');
537+
test('pass', ({}, testInfo) => {
538+
});
539+
`
540+
}, { 'reporter': 'junit,line' }, { 'PLAYWRIGHT_JUNIT_OUTPUT_DIR': 'foo/bar', 'PLAYWRIGHT_JUNIT_OUTPUT_NAME': 'baz/my-report.xml' });
541+
expect(result.exitCode).toBe(0);
542+
expect(result.passed).toBe(1);
543+
expect(fs.existsSync(testInfo.outputPath('foo', 'bar', 'baz', 'my-report.xml'))).toBe(true);
544+
});
507545
});
508546

509547
test('testsuites time is test run wall time', async ({ runInlineTest }) => {

0 commit comments

Comments
 (0)