diff --git a/packages/playwright-test/src/isomorphic/teleReceiver.ts b/packages/playwright-test/src/isomorphic/teleReceiver.ts index 3382adfa16..e7b77a7d31 100644 --- a/packages/playwright-test/src/isomorphic/teleReceiver.ts +++ b/packages/playwright-test/src/isomorphic/teleReceiver.ts @@ -24,13 +24,12 @@ export type JsonLocation = Location; export type JsonError = string; export type JsonStackFrame = { file: string, line: number, column: number }; -export type JsonConfig = { - rootDir: string; - configFile: string | undefined; +export type JsonConfig = Pick & { listOnly: boolean; - workers: number; }; +export type MergeReporterConfig = Pick; + export type JsonPattern = { s?: string; r?: { source: string, flags: string }; @@ -122,11 +121,13 @@ export class TeleReporterReceiver { private _tests = new Map(); private _rootDir!: string; private _clearPreviousResultsWhenTestBegins: boolean = false; + private _reportConfig: MergeReporterConfig | undefined; - constructor(pathSeparator: string, reporter: Reporter) { + constructor(pathSeparator: string, reporter: Reporter, reportConfig?: MergeReporterConfig) { this._rootSuite = new TeleSuite('', 'root'); this._pathSeparator = pathSeparator; this._reporter = reporter; + this._reportConfig = reportConfig; } dispatch(message: JsonEvent): Promise | undefined { @@ -282,11 +283,13 @@ export class TeleReporterReceiver { } private _parseConfig(config: JsonConfig): FullConfig { - const fullConfig = baseFullConfig; - fullConfig.rootDir = config.rootDir; - fullConfig.configFile = config.configFile; - fullConfig.workers = config.workers; - return fullConfig; + const result = { ...baseFullConfig, ...config }; + if (this._reportConfig) { + result.configFile = this._reportConfig.configFile; + result.reportSlowTests = this._reportConfig.reportSlowTests; + result.quiet = this._reportConfig.quiet; + } + return result; } private _parseProject(project: JsonProject): TeleFullProject { diff --git a/packages/playwright-test/src/reporters/merge.ts b/packages/playwright-test/src/reporters/merge.ts index f6072ff36f..61534e4b25 100644 --- a/packages/playwright-test/src/reporters/merge.ts +++ b/packages/playwright-test/src/reporters/merge.ts @@ -16,21 +16,22 @@ import fs from 'fs'; import path from 'path'; -import type { ReporterDescription } from '../../types/test'; +import type { FullConfig, ReporterDescription } from '../../types/test'; import type { FullResult } from '../../types/testReporter'; import type { FullConfigInternal } from '../common/config'; -import { TeleReporterReceiver, type JsonEvent, type JsonProject, type JsonSuite, type JsonTestResultEnd } from '../isomorphic/teleReceiver'; +import { TeleReporterReceiver, type JsonEvent, type JsonProject, type JsonSuite, type JsonTestResultEnd, type JsonConfig } from '../isomorphic/teleReceiver'; import { createReporters } from '../runner/reporters'; import { Multiplexer } from './multiplexer'; export async function createMergedReport(config: FullConfigInternal, dir: string, reporterDescriptions: ReporterDescription[], resolvePaths: boolean) { const shardFiles = await sortedShardFiles(dir); - const events = await mergeEvents(dir, shardFiles); + const events = await mergeEvents(dir, shardFiles, config.config); if (resolvePaths) patchAttachmentPaths(events, dir); const reporters = await createReporters(config, 'merge', reporterDescriptions); - const receiver = new TeleReporterReceiver(path.sep, new Multiplexer(reporters)); + const receiver = new TeleReporterReceiver(path.sep, new Multiplexer(reporters), config.config); + for (const event of events) await receiver.dispatch(event); } @@ -52,7 +53,7 @@ function parseEvents(reportJsonl: string): JsonEvent[] { return reportJsonl.toString().split('\n').filter(line => line.length).map(line => JSON.parse(line)) as JsonEvent[]; } -async function mergeEvents(dir: string, shardReportFiles: string[]) { +async function mergeEvents(dir: string, shardReportFiles: string[], reportConfig: FullConfig) { const events: JsonEvent[] = []; const beginEvents: JsonEvent[] = []; const endEvents: JsonEvent[] = []; @@ -68,16 +69,25 @@ async function mergeEvents(dir: string, shardReportFiles: string[]) { events.push(event); } } - return [mergeBeginEvents(beginEvents), ...events, mergeEndEvents(endEvents), { method: 'onExit', params: undefined }]; + return [mergeBeginEvents(beginEvents, reportConfig), ...events, mergeEndEvents(endEvents), { method: 'onExit', params: undefined }]; } -function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent { +function mergeBeginEvents(beginEvents: JsonEvent[], reportConfig: FullConfig): JsonEvent { if (!beginEvents.length) throw new Error('No begin events found'); const projects: JsonProject[] = []; - let totalWorkers = 0; + let config: JsonConfig = { + configFile: undefined, + globalTimeout: 0, + maxFailures: 0, + metadata: {}, + rootDir: '', + version: '', + workers: 0, + listOnly: false + }; for (const event of beginEvents) { - totalWorkers += event.params.config.workers; + config = mergeConfigs(config, event.params.config); const shardProjects: JsonProject[] = event.params.projects; for (const shardProject of shardProjects) { const mergedProject = projects.find(p => p.id === shardProject.id); @@ -87,11 +97,6 @@ function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent { mergeJsonSuites(shardProject.suites, mergedProject); } } - const config = { - ...beginEvents[0].params.config, - workers: totalWorkers, - shard: undefined - }; return { method: 'onBegin', params: { @@ -101,6 +106,18 @@ function mergeBeginEvents(beginEvents: JsonEvent[]): JsonEvent { }; } +function mergeConfigs(to: JsonConfig, from: JsonConfig): JsonConfig { + return { + ...to, + ...from, + metadata: { + ...to.metadata, + ...from.metadata, + }, + workers: to.workers + from.workers, + }; +} + function mergeJsonSuites(jsonSuites: JsonSuite[], parent: JsonSuite | JsonProject) { for (const jsonSuite of jsonSuites) { const existingSuite = parent.suites.find(s => s.title === jsonSuite.title); diff --git a/packages/playwright-test/src/reporters/teleEmitter.ts b/packages/playwright-test/src/reporters/teleEmitter.ts index 79b0c3e16a..b65c3e57d1 100644 --- a/packages/playwright-test/src/reporters/teleEmitter.ts +++ b/packages/playwright-test/src/reporters/teleEmitter.ts @@ -127,10 +127,15 @@ export class TeleReporterEmitter implements Reporter { private _serializeConfig(config: FullConfig): JsonConfig { return { - rootDir: config.rootDir, configFile: this._relativePath(config.configFile), - listOnly: FullConfigInternal.from(config)?.cliListOnly, + globalTimeout: config.globalTimeout, + maxFailures: config.maxFailures, + metadata: config.metadata, + rootDir: config.rootDir, + version: config.version, workers: config.workers, + + listOnly: FullConfigInternal.from(config)?.cliListOnly, }; } diff --git a/tests/playwright-test/reporter-blob.spec.ts b/tests/playwright-test/reporter-blob.spec.ts index 7359f733e2..f6d46fbc79 100644 --- a/tests/playwright-test/reporter-blob.spec.ts +++ b/tests/playwright-test/reporter-blob.spec.ts @@ -21,6 +21,7 @@ import type { HttpServer } from '../../packages/playwright-core/src/utils'; import { startHtmlReportServer } from '../../packages/playwright-test/lib/reporters/html'; import { type CliRunResult, type RunOptions, stripAnsi } from './playwright-test-fixtures'; import { cleanEnv, cliEntrypoint, expect, test as baseTest } from './playwright-test-fixtures'; +import type { PlaywrightTestConfig } from 'packages/playwright-test'; const DOES_NOT_SUPPORT_UTF8_IN_TERMINAL = process.platform === 'win32' && process.env.TERM_PROGRAM !== 'vscode' && !process.env.WT_SESSION; const POSITIVE_STATUS_MARK = DOES_NOT_SUPPORT_UTF8_IN_TERMINAL ? 'ok' : '✓ '; @@ -759,3 +760,90 @@ test('onError in the report', async ({ runInlineTest, mergeReports, showReport, await expect(page.locator('.subnav-item:has-text("Flaky") .counter')).toHaveText('0'); await expect(page.locator('.subnav-item:has-text("Skipped") .counter')).toHaveText('1'); }); + +test('preserve config fields', async ({ runInlineTest, mergeReports }) => { + test.slow(); + const reportDir = test.info().outputPath('blob-report'); + const config: PlaywrightTestConfig = { + // Runner options: + globalTimeout: 202300, + maxFailures: 3, + metadata: { + 'a': 'b', + 'b': 100, + }, + workers: 1, + retries: 1, + reporter: [['blob', { outputDir: `${reportDir.replace(/\\/g, '/')}` }]], + // Reporter options: + reportSlowTests: { + max: 7, + threshold: 15_000, + }, + quiet: true + }; + const files = { + 'echo-reporter.js': ` + import fs from 'fs'; + + class EchoReporter { + onBegin(config, suite) { + fs.writeFileSync('config.json', JSON.stringify(config)); + } + } + module.exports = EchoReporter; + `, + 'playwright.config.ts': ` + module.exports = ${JSON.stringify(config, null, 2)}; + `, + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('math 1', async ({}) => { + expect(1 + 1).toBe(2); + }); + `, + 'b.test.js': ` + import { test, expect } from '@playwright/test'; + test('math 2', async ({}) => { + expect(1 + 1).toBe(2); + }); + `, + 'c.test.js': ` + import { test, expect } from '@playwright/test'; + test('math 3', async ({}) => { + expect(1 + 1).toBe(2); + }); + ` + }; + + await runInlineTest(files, { shard: `1/3` }); + await runInlineTest(files, { shard: `3/3` }); + + const mergeConfig = { + reportSlowTests: { + max: 2, + threshold: 1_000, + }, + quiet: false + }; + await fs.promises.writeFile(test.info().outputPath('merge.config.ts'), `module.exports = ${JSON.stringify(mergeConfig, null, 2)};`); + + const reportFiles = await fs.promises.readdir(reportDir); + reportFiles.sort(); + expect(reportFiles).toEqual([expect.stringMatching(/report-1-of-3.*.jsonl/), expect.stringMatching(/report-3-of-3.*.jsonl/), 'resources']); + const { exitCode } = await mergeReports(reportDir, {}, { additionalArgs: ['--reporter', test.info().outputPath('echo-reporter.js'), '-c', test.info().outputPath('merge.config.ts')] }); + expect(exitCode).toBe(0); + const json = JSON.parse(fs.readFileSync(test.info().outputPath('config.json')).toString()); + // Test shard parameters. + expect(json.rootDir).toBe(test.info().outputDir); + expect(json.globalTimeout).toBe(config.globalTimeout); + expect(json.maxFailures).toBe(config.maxFailures); + expect(json.metadata).toEqual(config.metadata); + expect(json.workers).toBe(2); + expect(json.version).toBeTruthy(); + expect(json.version).not.toEqual(test.info().config.version); + // Reporter config parameters. + expect(json.reportSlowTests).toEqual(mergeConfig.reportSlowTests); + expect(json.configFile).toEqual(test.info().outputPath('merge.config.ts')); + expect(json.quiet).toEqual(mergeConfig.quiet); +}); \ No newline at end of file