diff --git a/docs/src/test-api/class-testconfig.md b/docs/src/test-api/class-testconfig.md index 0d1f4c1538..cd70b21b70 100644 --- a/docs/src/test-api/class-testconfig.md +++ b/docs/src/test-api/class-testconfig.md @@ -110,9 +110,9 @@ export default defineConfig({ ## property: TestConfig.globalSetup * since: v1.10 -- type: ?<[string]> +- type: ?<[string]|[Array]<[string]>> -Path to the global setup file. This file will be required and run before all the tests. It must export a single function that takes a [FullConfig] argument. +Path to the global setup file. This file will be required and run before all the tests. It must export a single function that takes a [FullConfig] argument. Pass an array of paths to specify multiple global setup files. Learn more about [global setup and teardown](../test-global-setup-teardown.md). @@ -128,9 +128,9 @@ export default defineConfig({ ## property: TestConfig.globalTeardown * since: v1.10 -- type: ?<[string]> +- type: ?<[string]|[Array]<[string]>> -Path to the global teardown file. This file will be required and run after all the tests. It must export a single function. See also [`property: TestConfig.globalSetup`]. +Path to the global teardown file. This file will be required and run after all the tests. It must export a single function. See also [`property: TestConfig.globalSetup`]. Pass an array of paths to specify multiple global teardown files. Learn more about [global setup and teardown](../test-global-setup-teardown.md). diff --git a/packages/playwright/src/common/config.ts b/packages/playwright/src/common/config.ts index a694839f81..fd78f0c8d9 100644 --- a/packages/playwright/src/common/config.ts +++ b/packages/playwright/src/common/config.ts @@ -58,6 +58,9 @@ export class FullConfigInternal { testIdMatcher?: Matcher; defineConfigWasUsed = false; + globalSetups: string[] = []; + globalTeardowns: string[] = []; + constructor(location: ConfigLocation, userConfig: Config, configCLIOverrides: ConfigCLIOverrides) { if (configCLIOverrides.projects && userConfig.projects) throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`); @@ -72,13 +75,16 @@ export class FullConfigInternal { this.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p })); this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig); + this.globalSetups = (Array.isArray(userConfig.globalSetup) ? userConfig.globalSetup : [userConfig.globalSetup]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined); + this.globalTeardowns = (Array.isArray(userConfig.globalTeardown) ? userConfig.globalTeardown : [userConfig.globalTeardown]).map(s => resolveScript(s, configDir)).filter(script => script !== undefined); + this.config = { configFile: resolvedConfigFile, rootDir: pathResolve(configDir, userConfig.testDir) || configDir, forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false), fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false), - globalSetup: takeFirst(resolveScript(userConfig.globalSetup, configDir), null), - globalTeardown: takeFirst(resolveScript(userConfig.globalTeardown, configDir), null), + globalSetup: this.globalSetups[0] ?? null, + globalTeardown: this.globalTeardowns[0] ?? null, globalTimeout: takeFirst(configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0), grep: takeFirst(userConfig.grep, defaultGrep), grepInvert: takeFirst(userConfig.grepInvert, null), diff --git a/packages/playwright/src/common/configLoader.ts b/packages/playwright/src/common/configLoader.ts index eef56c4458..37a886d3e8 100644 --- a/packages/playwright/src/common/configLoader.ts +++ b/packages/playwright/src/common/configLoader.ts @@ -139,13 +139,25 @@ function validateConfig(file: string, config: Config) { } if ('globalSetup' in config && config.globalSetup !== undefined) { - if (typeof config.globalSetup !== 'string') + if (Array.isArray(config.globalSetup)) { + config.globalSetup.forEach((item, index) => { + if (typeof item !== 'string') + throw errorWithFile(file, `config.globalSetup[${index}] must be a string`); + }); + } else if (typeof config.globalSetup !== 'string') { throw errorWithFile(file, `config.globalSetup must be a string`); + } } if ('globalTeardown' in config && config.globalTeardown !== undefined) { - if (typeof config.globalTeardown !== 'string') + if (Array.isArray(config.globalTeardown)) { + config.globalTeardown.forEach((item, index) => { + if (typeof item !== 'string') + throw errorWithFile(file, `config.globalTeardown[${index}] must be a string`); + }); + } else if (typeof config.globalTeardown !== 'string') { throw errorWithFile(file, `config.globalTeardown must be a string`); + } } if ('globalTimeout' in config && config.globalTimeout !== undefined) { diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 77d84419f4..528cac47cd 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -98,8 +98,11 @@ export function createGlobalSetupTasks(config: FullConfigInternal) { if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS) tasks.push(createRemoveOutputDirsTask()); tasks.push(...createPluginSetupTasks(config)); - if (config.config.globalSetup || config.config.globalTeardown) - tasks.push(createGlobalSetupTask()); + if (config.globalSetups.length || config.globalTeardowns.length) { + const length = Math.max(config.globalSetups.length, config.globalTeardowns.length); + for (let i = 0; i < length; i++) + tasks.push(createGlobalSetupTask(i, length)); + } return tasks; } @@ -161,15 +164,20 @@ function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task { +function createGlobalSetupTask(index: number, length: number): Task { let globalSetupResult: any; let globalSetupFinished = false; let teardownHook: any; + + let title = 'global setup'; + if (length > 1) + title += ` #${index}`; + return { - title: 'global setup', + title, setup: async ({ config }) => { - const setupHook = config.config.globalSetup ? await loadGlobalHook(config, config.config.globalSetup) : undefined; - teardownHook = config.config.globalTeardown ? await loadGlobalHook(config, config.config.globalTeardown) : undefined; + const setupHook = config.globalSetups[index] ? await loadGlobalHook(config, config.globalSetups[index]) : undefined; + teardownHook = config.globalTeardowns[index] ? await loadGlobalHook(config, config.globalTeardowns[index]) : undefined; globalSetupResult = setupHook ? await setupHook(config.config) : undefined; globalSetupFinished = true; }, diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index a989fe69b2..ae02d1506e 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -1077,7 +1077,8 @@ interface TestConfig { /** * Path to the global setup file. This file will be required and run before all the tests. It must export a single - * function that takes a [FullConfig](https://playwright.dev/docs/api/class-fullconfig) argument. + * function that takes a [FullConfig](https://playwright.dev/docs/api/class-fullconfig) argument. Pass an array of + * paths to specify multiple global setup files. * * Learn more about [global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown). * @@ -1093,12 +1094,13 @@ interface TestConfig { * ``` * */ - globalSetup?: string; + globalSetup?: string|Array; /** * Path to the global teardown file. This file will be required and run after all the tests. It must export a single * function. See also - * [testConfig.globalSetup](https://playwright.dev/docs/api/class-testconfig#test-config-global-setup). + * [testConfig.globalSetup](https://playwright.dev/docs/api/class-testconfig#test-config-global-setup). Pass an array + * of paths to specify multiple global teardown files. * * Learn more about [global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown). * @@ -1114,7 +1116,7 @@ interface TestConfig { * ``` * */ - globalTeardown?: string; + globalTeardown?: string|Array; /** * Maximum time in milliseconds the whole test suite can run. Zero timeout (default) disables this behavior. Useful on diff --git a/tests/playwright-test/global-setup.spec.ts b/tests/playwright-test/global-setup.spec.ts index 3d28be82cd..f1bd7b7458 100644 --- a/tests/playwright-test/global-setup.spec.ts +++ b/tests/playwright-test/global-setup.spec.ts @@ -386,3 +386,43 @@ test('teardown after error', async ({ runInlineTest }) => { 'teardown 1', ]); }); + +test('globalSetup should support multiple', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + globalSetup: ['./globalSetup1.ts','./globalSetup2.ts','./globalSetup3.ts','./globalSetup4.ts'], + globalTeardown: ['./globalTeardown1.ts', './globalTeardown2.ts'], + }; + `, + 'globalSetup1.ts': `module.exports = () => { console.log('%%globalSetup1'); return () => { console.log('%%globalSetup1Function'); throw new Error('kaboom'); } };`, + 'globalSetup2.ts': `module.exports = () => console.log('%%globalSetup2');`, + 'globalSetup3.ts': `module.exports = () => { console.log('%%globalSetup3'); return () => console.log('%%globalSetup3Function'); }`, + 'globalSetup4.ts': `module.exports = () => console.log('%%globalSetup4');`, + 'globalTeardown1.ts': `module.exports = () => console.log('%%globalTeardown1')`, + 'globalTeardown2.ts': `module.exports = () => { console.log('%%globalTeardown2'); throw new Error('kaboom'); }`, + + 'a.test.js': ` + import { test } from '@playwright/test'; + test('a', () => console.log('%%test a')); + test('b', () => console.log('%%test b')); + `, + }, { reporter: 'line' }); + expect(result.passed).toBe(2); + + // behaviour: setups in order, teardowns in reverse order. + // setup-returned functions inherit their position, and take precedence over `globalTeardown` scripts. + expect(result.outputLines).toEqual([ + 'globalSetup1', + 'globalSetup2', + 'globalSetup3', + 'globalSetup4', + 'test a', + 'test b', + 'globalSetup3Function', + 'globalTeardown2', + 'globalSetup1Function', + // 'globalTeardown1' is missing, because globalSetup1Function errored out. + ]); + expect(result.output).toContain('Error: kaboom'); +});