diff --git a/packages/playwright-test/src/ipc.ts b/packages/playwright-test/src/ipc.ts index 8b4cf9cab8..0012af44e1 100644 --- a/packages/playwright-test/src/ipc.ts +++ b/packages/playwright-test/src/ipc.ts @@ -16,12 +16,15 @@ import type { TestError } from '../types/testReporter'; import type { ConfigCLIOverrides } from './runner'; -import type { TestStatus } from './types'; +import type { FullConfigInternal, TestStatus } from './types'; export type SerializedLoaderData = { - overrides: ConfigCLIOverrides; - configFile: { file: string } | { configDir: string }; + config: FullConfigInternal; + configFile: string | undefined; + configDir: string; + overridesForLegacyConfigMode?: ConfigCLIOverrides; }; + export type WorkerInitParams = { workerIndex: number; parallelIndex: number; diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index e9ac1d9085..1323ef8c15 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -25,7 +25,6 @@ import * as path from 'path'; import * as url from 'url'; import * as fs from 'fs'; import * as os from 'os'; -import { ProjectImpl } from './project'; import type { BuiltInReporter, ConfigCLIOverrides } from './runner'; import type { Reporter } from '../types/testReporter'; import { builtInReporters } from './runner'; @@ -45,7 +44,6 @@ export class Loader { private _fullConfig: FullConfigInternal; private _configDir: string = ''; private _configFile: string | undefined; - private _projects: ProjectImpl[] = []; constructor(configCLIOverrides?: ConfigCLIOverrides) { this._configCLIOverrides = configCLIOverrides || {}; @@ -53,12 +51,20 @@ export class Loader { } static async deserialize(data: SerializedLoaderData): Promise { - const loader = new Loader(data.overrides); - if ('file' in data.configFile) - await loader.loadConfigFile(data.configFile.file); - else - await loader.loadEmptyConfig(data.configFile.configDir); - return loader; + if (process.env.PLAYWRIGHT_LEGACY_CONFIG_MODE) { + const loader = new Loader(data.overridesForLegacyConfigMode); + if (data.configFile) + await loader.loadConfigFile(data.configFile); + else + await loader.loadEmptyConfig(data.configDir); + return loader; + } else { + const loader = new Loader(); + loader._configFile = data.configFile; + loader._configDir = data.configDir; + loader._fullConfig = data.config; + return loader; + } } async loadConfigFile(file: string): Promise { @@ -107,6 +113,8 @@ export class Loader { config.projects = takeFirst(this._configCLIOverrides.projects, config.projects as any); config.workers = takeFirst(this._configCLIOverrides.workers, config.workers); config.use = mergeObjects(config.use, this._configCLIOverrides.use); + for (const project of config.projects || []) + this._applyCLIOverridesToProject(project); // 3. Run configure plugins phase. for (const plugin of config.plugins || []) @@ -155,11 +163,7 @@ export class Loader { this._fullConfig.workers = takeFirst(config.workers, baseFullConfig.workers); this._fullConfig.webServer = takeFirst(config.webServer, baseFullConfig.webServer); this._fullConfig._plugins = takeFirst(config.plugins, baseFullConfig._plugins); - - const projects: Project[] = this._configCLIOverrides.projects || config.projects || [config]; - for (const project of projects) - this._addProject(config, project, throwawayArtifactsPath); - this._fullConfig.projects = this._projects.map(p => p.config); + this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, p, throwawayArtifactsPath)); } async loadTestFile(file: string, environment: 'runner' | 'worker') { @@ -228,18 +232,28 @@ export class Loader { return this._fullConfig; } - projects() { - return this._projects; - } - serialize(): SerializedLoaderData { - return { - configFile: this._configFile ? { file: this._configFile } : { configDir: this._configDir }, - overrides: this._configCLIOverrides, + const result: SerializedLoaderData = { + configFile: this._configFile, + configDir: this._configDir, + config: this._fullConfig, }; + if (process.env.PLAYWRIGHT_LEGACY_CONFIG_MODE) + result.overridesForLegacyConfigMode = this._configCLIOverrides; + return result; } - private _addProject(config: Config, projectConfig: Project, throwawayArtifactsPath: string) { + private _applyCLIOverridesToProject(projectConfig: Project) { + projectConfig.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, projectConfig.fullyParallel); + projectConfig.grep = takeFirst(this._configCLIOverrides.grep, projectConfig.grep); + projectConfig.grepInvert = takeFirst(this._configCLIOverrides.grepInvert, projectConfig.grepInvert); + projectConfig.outputDir = takeFirst(this._configCLIOverrides.outputDir, projectConfig.outputDir); + projectConfig.repeatEach = takeFirst(this._configCLIOverrides.repeatEach, projectConfig.repeatEach); + projectConfig.retries = takeFirst(this._configCLIOverrides.retries, projectConfig.retries); + projectConfig.timeout = takeFirst(this._configCLIOverrides.timeout, projectConfig.timeout); + } + + private _resolveProject(config: Config, projectConfig: Project, throwawayArtifactsPath: string): FullProjectInternal { // Resolve all config dirs relative to configDir. if (projectConfig.testDir !== undefined) projectConfig.testDir = path.resolve(this._configDir, projectConfig.testDir); @@ -250,21 +264,13 @@ export class Loader { if (projectConfig.snapshotDir !== undefined) projectConfig.snapshotDir = path.resolve(this._configDir, projectConfig.snapshotDir); - projectConfig.fullyParallel = takeFirst(this._configCLIOverrides.fullyParallel, projectConfig.fullyParallel); - projectConfig.grep = takeFirst(this._configCLIOverrides.grep, projectConfig.grep); - projectConfig.grepInvert = takeFirst(this._configCLIOverrides.grepInvert, projectConfig.grepInvert); - projectConfig.outputDir = takeFirst(this._configCLIOverrides.outputDir, projectConfig.outputDir); - projectConfig.repeatEach = takeFirst(this._configCLIOverrides.repeatEach, projectConfig.repeatEach); - projectConfig.retries = takeFirst(this._configCLIOverrides.retries, projectConfig.retries); - projectConfig.timeout = takeFirst(this._configCLIOverrides.timeout, projectConfig.timeout); - const testDir = takeFirst(projectConfig.testDir, config.testDir, this._configDir); const outputDir = takeFirst(projectConfig.outputDir, config.outputDir, path.join(throwawayArtifactsPath, 'test-results')); const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir); const name = takeFirst(projectConfig.name, config.name, ''); const screenshotsDir = takeFirst((projectConfig as any).screenshotsDir, (config as any).screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name)); - const fullProject: FullProjectInternal = { + return { _fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined), _expect: takeFirst(projectConfig.expect, config.expect, undefined), grep: takeFirst(projectConfig.grep, config.grep, baseFullConfig.grep), @@ -282,7 +288,6 @@ export class Loader { timeout: takeFirst(projectConfig.timeout, config.timeout, defaultTimeout), use: mergeObjects(config.use, projectConfig.use), }; - this._projects.push(new ProjectImpl(fullProject, this._projects.length)); } private async _requireOrImport(file: string) { diff --git a/packages/playwright-test/src/project.ts b/packages/playwright-test/src/project.ts index e7454d6e13..90b43816cb 100644 --- a/packages/playwright-test/src/project.ts +++ b/packages/playwright-test/src/project.ts @@ -32,9 +32,9 @@ export class ProjectImpl { this.index = index; } - private buildTestTypePool(testType: TestTypeImpl): FixturePool { + private _buildTestTypePool(testType: TestTypeImpl): FixturePool { if (!this.testTypePools.has(testType)) { - const fixtures = this.resolveFixtures(testType, this.config.use); + const fixtures = this._resolveFixtures(testType, this.config.use); const pool = new FixturePool(fixtures); this.testTypePools.set(testType, pool); } @@ -42,9 +42,9 @@ export class ProjectImpl { } // TODO: we can optimize this function by building the pool inline in cloneSuite - private buildPool(test: TestCase): FixturePool { + private _buildPool(test: TestCase): FixturePool { if (!this.testPools.has(test)) { - let pool = this.buildTestTypePool(test._testType); + let pool = this._buildTestTypePool(test._testType); const parents: Suite[] = []; for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent) @@ -88,7 +88,7 @@ export class ProjectImpl { to._entries.pop(); to.tests.pop(); } else { - const pool = this.buildPool(entry); + const pool = this._buildPool(entry); test._workerHash = `run${this.index}-${pool.digest}-repeat${repeatEachIndex}`; test._pool = pool; } @@ -104,7 +104,7 @@ export class ProjectImpl { return this._cloneEntries(suite, result, repeatEachIndex, filter, '') ? result : undefined; } - private resolveFixtures(testType: TestTypeImpl, configUse: Fixtures): FixturesWithLocation[] { + private _resolveFixtures(testType: TestTypeImpl, configUse: Fixtures): FixturesWithLocation[] { return testType.fixtures.map(f => { const configKeys = new Set(Object.keys(configUse || {})); const resolved = { ...f.fixtures }; diff --git a/packages/playwright-test/src/runner.ts b/packages/playwright-test/src/runner.ts index 57e59a96d1..4ab1738912 100644 --- a/packages/playwright-test/src/runner.ts +++ b/packages/playwright-test/src/runner.ts @@ -37,8 +37,8 @@ import JSONReporter from './reporters/json'; import JUnitReporter from './reporters/junit'; import EmptyReporter from './reporters/empty'; import HtmlReporter from './reporters/html'; -import type { ProjectImpl } from './project'; -import type { Config } from './types'; +import { ProjectImpl } from './project'; +import type { Config, FullProjectInternal } from './types'; import type { FullConfigInternal } from './types'; import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner'; import { SigIntWatcher } from './sigIntWatcher'; @@ -194,8 +194,8 @@ export class Runner { }; for (const [project, files] of filesByProject) { report.projects.push({ - name: project.config.name, - testDir: path.resolve(configFile, project.config.testDir), + name: project.name, + testDir: path.resolve(configFile, project.testDir), files: files }); } @@ -207,7 +207,7 @@ export class Runner { return await this._runFiles(list, filesByProject, testFileReFilters); } - private async _collectFiles(testFileReFilters: FilePatternFilter[], projectNames?: string[]): Promise> { + private async _collectFiles(testFileReFilters: FilePatternFilter[], projectNames?: string[]): Promise> { const testFileFilter = testFileReFilters.length ? createFileMatcher(testFileReFilters.map(e => e.re)) : () => true; let projectsToFind: Set | undefined; let unknownProjects: Map | undefined; @@ -220,26 +220,27 @@ export class Runner { unknownProjects!.set(name, n); }); } - const projects = this._loader.projects().filter(project => { + const fullConfig = this._loader.fullConfig(); + const projects = fullConfig.projects.filter(project => { if (!projectsToFind) return true; - const name = project.config.name.toLocaleLowerCase(); + const name = project.name.toLocaleLowerCase(); unknownProjects!.delete(name); return projectsToFind.has(name); }); if (unknownProjects && unknownProjects.size) { - const names = this._loader.projects().map(p => p.config.name).filter(name => !!name); + const names = fullConfig.projects.map(p => p.name).filter(name => !!name); if (!names.length) throw new Error(`No named projects are specified in the configuration file`); const unknownProjectNames = Array.from(unknownProjects.values()).map(n => `"${n}"`).join(', '); throw new Error(`Project(s) ${unknownProjectNames} not found. Available named projects: ${names.map(name => `"${name}"`).join(', ')}`); } - const files = new Map(); + const files = new Map(); for (const project of projects) { - const allFiles = await collectFiles(project.config.testDir); - const testMatch = createFileMatcher(project.config.testMatch); - const testIgnore = createFileMatcher(project.config.testIgnore); + const allFiles = await collectFiles(project.testDir); + const testMatch = createFileMatcher(project.testMatch); + const testIgnore = createFileMatcher(project.testIgnore); const extensions = ['.js', '.ts', '.mjs', '.tsx', '.jsx']; const testFileExtension = (file: string) => extensions.includes(path.extname(file)); const testFiles = allFiles.filter(file => !testIgnore(file) && testMatch(file) && testFileFilter(file) && testFileExtension(file)); @@ -248,7 +249,7 @@ export class Runner { return files; } - private async _runFiles(list: boolean, filesByProject: Map, testFileReFilters: FilePatternFilter[]): Promise { + private async _runFiles(list: boolean, filesByProject: Map, testFileReFilters: FilePatternFilter[]): Promise { const allTestFiles = new Set(); for (const files of filesByProject.values()) files.forEach(file => allTestFiles.add(file)); @@ -293,19 +294,20 @@ export class Runner { const outputDirs = new Set(); const rootSuite = new Suite(''); for (const [project, files] of filesByProject) { - const grepMatcher = createTitleMatcher(project.config.grep); - const grepInvertMatcher = project.config.grepInvert ? createTitleMatcher(project.config.grepInvert) : null; - const projectSuite = new Suite(project.config.name); - projectSuite._projectConfig = project.config; - if (project.config._fullyParallel) + const projectImpl = new ProjectImpl(project, config.projects.indexOf(project)); + const grepMatcher = createTitleMatcher(project.grep); + const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null; + const projectSuite = new Suite(project.name); + projectSuite._projectConfig = project; + if (project._fullyParallel) projectSuite._parallelMode = 'parallel'; rootSuite._addSuite(projectSuite); for (const file of files) { const fileSuite = fileSuites.get(file); if (!fileSuite) continue; - for (let repeatEachIndex = 0; repeatEachIndex < project.config.repeatEach; repeatEachIndex++) { - const cloned = project.cloneFileSuite(fileSuite, repeatEachIndex, test => { + for (let repeatEachIndex = 0; repeatEachIndex < project.repeatEach; repeatEachIndex++) { + const cloned = projectImpl.cloneFileSuite(fileSuite, repeatEachIndex, test => { const grepTitle = test.titlePath().join(' '); if (grepInvertMatcher?.(grepTitle)) return false; @@ -315,7 +317,7 @@ export class Runner { projectSuite._addSuite(cloned); } } - outputDirs.add(project.config.outputDir); + outputDirs.add(project.outputDir); } // 7. Fail when no tests. diff --git a/packages/playwright-test/src/testInfo.ts b/packages/playwright-test/src/testInfo.ts index fdbdc48852..7a06c06920 100644 --- a/packages/playwright-test/src/testInfo.ts +++ b/packages/playwright-test/src/testInfo.ts @@ -85,12 +85,13 @@ export class TestInfoImpl implements TestInfo { constructor( loader: Loader, + projectImpl: ProjectImpl, workerParams: WorkerInitParams, test: TestCase, retry: number, addStepImpl: (data: Omit) => TestStepInternal, ) { - this._projectImpl = loader.projects()[workerParams.projectIndex]; + this._projectImpl = projectImpl; this._test = test; this._addStepImpl = addStepImpl; this._startTime = monotonicTime(); @@ -113,10 +114,10 @@ export class TestInfoImpl implements TestInfo { this._timeoutManager = new TimeoutManager(this.project.timeout); this.outputDir = (() => { - const sameName = loader.projects().filter(project => project.config.name === this.project.name); + const sameName = loader.fullConfig().projects.filter(project => project.name === this.project.name); let uniqueProjectNamePathSegment: string; if (sameName.length > 1) - uniqueProjectNamePathSegment = this.project.name + (sameName.indexOf(this._projectImpl) + 1); + uniqueProjectNamePathSegment = this.project.name + (sameName.indexOf(this._projectImpl.config) + 1); else uniqueProjectNamePathSegment = this.project.name; diff --git a/packages/playwright-test/src/workerRunner.ts b/packages/playwright-test/src/workerRunner.ts index c03c24b828..89cf9375e9 100644 --- a/packages/playwright-test/src/workerRunner.ts +++ b/packages/playwright-test/src/workerRunner.ts @@ -23,7 +23,7 @@ import { setCurrentTestInfo } from './globals'; import { Loader } from './loader'; import type { Suite, TestCase } from './test'; import type { Annotation, TestError, TestStepInternal } from './types'; -import type { ProjectImpl } from './project'; +import { ProjectImpl } from './project'; import { FixtureRunner } from './fixtures'; import { ManualPromise } from 'playwright-core/lib/utils/manualPromise'; import { TestInfoImpl } from './testInfo'; @@ -151,7 +151,7 @@ export class WorkerRunner extends EventEmitter { return; this._loader = await Loader.deserialize(this._params.loader); - this._project = this._loader.projects()[this._params.projectIndex]; + this._project = new ProjectImpl(this._loader.fullConfig().projects[this._params.projectIndex], this._params.projectIndex); } async runTestGroup(runPayload: RunPayload) { @@ -207,7 +207,7 @@ export class WorkerRunner extends EventEmitter { private async _runTest(test: TestCase, retry: number, nextTest: TestCase | undefined) { let lastStepId = 0; - const testInfo = new TestInfoImpl(this._loader, this._params, test, retry, data => { + const testInfo = new TestInfoImpl(this._loader, this._project, this._params, test, retry, data => { const stepId = `${data.category}@${data.title}@${++lastStepId}`; let callbackHandled = false; const step: TestStepInternal = { diff --git a/tests/playwright-test/playwright-test-fixtures.ts b/tests/playwright-test/playwright-test-fixtures.ts index 0277e9845e..a3be3540cf 100644 --- a/tests/playwright-test/playwright-test-fixtures.ts +++ b/tests/playwright-test/playwright-test-fixtures.ts @@ -215,6 +215,7 @@ type RunOptions = { cwd?: string, }; type Fixtures = { + legacyConfigLoader: boolean; writeFiles: (files: Files) => Promise; runInlineTest: (files: Files, params?: Params, env?: Env, options?: RunOptions, beforeRunPlaywrightTest?: ({ baseDir }: { baseDir: string }) => Promise) => Promise; runTSC: (files: Files) => Promise; @@ -224,15 +225,19 @@ export const test = base .extend(commonFixtures) .extend(serverFixtures) .extend({ + legacyConfigLoader: [false, { option: true }], + writeFiles: async ({}, use, testInfo) => { await use(files => writeFiles(testInfo, files)); }, - runInlineTest: async ({ childProcess }, use, testInfo: TestInfo) => { + runInlineTest: async ({ childProcess, legacyConfigLoader }, use, testInfo: TestInfo) => { await use(async (files: Files, params: Params = {}, env: Env = {}, options: RunOptions = {}, beforeRunPlaywrightTest?: ({ baseDir: string }) => Promise) => { const baseDir = await writeFiles(testInfo, files); if (beforeRunPlaywrightTest) await beforeRunPlaywrightTest({ baseDir }); + if (legacyConfigLoader) + env = { ...env, PLAYWRIGHT_LEGACY_CONFIG_MODE: '1' }; return await runPlaywrightTest(childProcess, baseDir, params, env, options); }); }, diff --git a/tests/playwright-test/playwright.config.ts b/tests/playwright-test/playwright.config.ts index 396bcc5ffc..8ef397e238 100644 --- a/tests/playwright-test/playwright.config.ts +++ b/tests/playwright-test/playwright.config.ts @@ -29,7 +29,13 @@ const config: Config = { workers: process.env.CI ? 1 : undefined, preserveOutput: process.env.CI ? 'failures-only' : 'always', projects: [ - { name: 'playwright-test' }, + { + name: 'playwright-test' + }, + { + name: 'playwright-test-legacy-config', + use: { legacyConfigLoader: true }, + } as any, ], reporter: process.env.CI ? [ ['dot'], diff --git a/tests/playwright-test/playwright.connect.spec.ts b/tests/playwright-test/playwright.connect.spec.ts index 143a2db790..cdf9481c50 100644 --- a/tests/playwright-test/playwright.connect.spec.ts +++ b/tests/playwright-test/playwright.connect.spec.ts @@ -16,7 +16,8 @@ import { test, expect } from './playwright-test-fixtures'; -test('should work with connectOptions', async ({ runInlineTest }) => { +test('should work with connectOptions (legacy)', async ({ runInlineTest, legacyConfigLoader }) => { + test.skip(!legacyConfigLoader, 'Not supported in the new mode'); const result = await runInlineTest({ 'playwright.config.js': ` module.exports = { @@ -49,6 +50,42 @@ test('should work with connectOptions', async ({ runInlineTest }) => { expect(result.passed).toBe(1); }); +test('should work with connectOptions', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.js': ` + module.exports = { plugins: [require('./plugin')] }; + `, + 'plugin.js': ` + let server; + module.exports = { + configure: async (config) => { + server = await pwt.chromium.launchServer(); + config.use = { + connectOptions: { + wsEndpoint: server.wsEndpoint() + } + }; + }, + + teardown: async () => { + await server.close(); + } + }; + `, + 'a.test.ts': ` + const { test } = pwt; + test.use({ locale: 'fr-CH' }); + test('pass', async ({ page }) => { + await page.setContent('
PASS
'); + await expect(page.locator('div')).toHaveText('PASS'); + expect(await page.evaluate(() => navigator.language)).toBe('fr-CH'); + }); + `, + }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); +}); + test('should throw with bad connectOptions', async ({ runInlineTest }) => { const result = await runInlineTest({ 'playwright.config.js': ` diff --git a/tests/playwright-test/plugins.spec.ts b/tests/playwright-test/plugins.spec.ts index 01b47d24b5..2df73c104e 100644 --- a/tests/playwright-test/plugins.spec.ts +++ b/tests/playwright-test/plugins.spec.ts @@ -16,7 +16,8 @@ import fs from 'fs'; import { test, expect } from './playwright-test-fixtures'; -test('event order', async ({ runInlineTest }, testInfo) => { +test('event order', async ({ runInlineTest, legacyConfigLoader }, testInfo) => { + test.skip(legacyConfigLoader); const log = testInfo.outputPath('logs.txt'); const result = await runInlineTest({ 'log.ts': ` @@ -87,8 +88,6 @@ test('event order', async ({ runInlineTest }, testInfo) => { 'a setup', 'b setup', 'globalSetup', - 'a configure', - 'b configure', 'baseURL a | b | ', 'globalTeardown', 'b teardown',