feat(test runner): allow multiple global setups (#32955)
Signed-off-by: Simon Knott <info@simonknott.de> Co-authored-by: Dmitry Gozman <dgozman@gmail.com>
This commit is contained in:
parent
29c84a33c3
commit
0d63df4875
|
@ -110,9 +110,9 @@ export default defineConfig({
|
||||||
|
|
||||||
## property: TestConfig.globalSetup
|
## property: TestConfig.globalSetup
|
||||||
* since: v1.10
|
* 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).
|
Learn more about [global setup and teardown](../test-global-setup-teardown.md).
|
||||||
|
|
||||||
|
@ -128,9 +128,9 @@ export default defineConfig({
|
||||||
|
|
||||||
## property: TestConfig.globalTeardown
|
## property: TestConfig.globalTeardown
|
||||||
* since: v1.10
|
* 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).
|
Learn more about [global setup and teardown](../test-global-setup-teardown.md).
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,9 @@ export class FullConfigInternal {
|
||||||
testIdMatcher?: Matcher;
|
testIdMatcher?: Matcher;
|
||||||
defineConfigWasUsed = false;
|
defineConfigWasUsed = false;
|
||||||
|
|
||||||
|
globalSetups: string[] = [];
|
||||||
|
globalTeardowns: string[] = [];
|
||||||
|
|
||||||
constructor(location: ConfigLocation, userConfig: Config, configCLIOverrides: ConfigCLIOverrides) {
|
constructor(location: ConfigLocation, userConfig: Config, configCLIOverrides: ConfigCLIOverrides) {
|
||||||
if (configCLIOverrides.projects && userConfig.projects)
|
if (configCLIOverrides.projects && userConfig.projects)
|
||||||
throw new Error(`Cannot use --browser option when configuration file defines projects. Specify browserName in the projects instead.`);
|
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.plugins = (privateConfiguration?.plugins || []).map((p: any) => ({ factory: p }));
|
||||||
this.singleTSConfigPath = pathResolve(configDir, userConfig.tsconfig);
|
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 = {
|
this.config = {
|
||||||
configFile: resolvedConfigFile,
|
configFile: resolvedConfigFile,
|
||||||
rootDir: pathResolve(configDir, userConfig.testDir) || configDir,
|
rootDir: pathResolve(configDir, userConfig.testDir) || configDir,
|
||||||
forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false),
|
forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false),
|
||||||
fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false),
|
fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false),
|
||||||
globalSetup: takeFirst(resolveScript(userConfig.globalSetup, configDir), null),
|
globalSetup: this.globalSetups[0] ?? null,
|
||||||
globalTeardown: takeFirst(resolveScript(userConfig.globalTeardown, configDir), null),
|
globalTeardown: this.globalTeardowns[0] ?? null,
|
||||||
globalTimeout: takeFirst(configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0),
|
globalTimeout: takeFirst(configCLIOverrides.globalTimeout, userConfig.globalTimeout, 0),
|
||||||
grep: takeFirst(userConfig.grep, defaultGrep),
|
grep: takeFirst(userConfig.grep, defaultGrep),
|
||||||
grepInvert: takeFirst(userConfig.grepInvert, null),
|
grepInvert: takeFirst(userConfig.grepInvert, null),
|
||||||
|
|
|
@ -139,13 +139,25 @@ function validateConfig(file: string, config: Config) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('globalSetup' in config && config.globalSetup !== undefined) {
|
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`);
|
throw errorWithFile(file, `config.globalSetup must be a string`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('globalTeardown' in config && config.globalTeardown !== undefined) {
|
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`);
|
throw errorWithFile(file, `config.globalTeardown must be a string`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('globalTimeout' in config && config.globalTimeout !== undefined) {
|
if ('globalTimeout' in config && config.globalTimeout !== undefined) {
|
||||||
|
|
|
@ -98,8 +98,11 @@ export function createGlobalSetupTasks(config: FullConfigInternal) {
|
||||||
if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS)
|
if (!config.configCLIOverrides.preserveOutputDir && !process.env.PW_TEST_NO_REMOVE_OUTPUT_DIRS)
|
||||||
tasks.push(createRemoveOutputDirsTask());
|
tasks.push(createRemoveOutputDirsTask());
|
||||||
tasks.push(...createPluginSetupTasks(config));
|
tasks.push(...createPluginSetupTasks(config));
|
||||||
if (config.config.globalSetup || config.config.globalTeardown)
|
if (config.globalSetups.length || config.globalTeardowns.length) {
|
||||||
tasks.push(createGlobalSetupTask());
|
const length = Math.max(config.globalSetups.length, config.globalTeardowns.length);
|
||||||
|
for (let i = 0; i < length; i++)
|
||||||
|
tasks.push(createGlobalSetupTask(i, length));
|
||||||
|
}
|
||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,15 +164,20 @@ function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task<TestR
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createGlobalSetupTask(): Task<TestRun> {
|
function createGlobalSetupTask(index: number, length: number): Task<TestRun> {
|
||||||
let globalSetupResult: any;
|
let globalSetupResult: any;
|
||||||
let globalSetupFinished = false;
|
let globalSetupFinished = false;
|
||||||
let teardownHook: any;
|
let teardownHook: any;
|
||||||
|
|
||||||
|
let title = 'global setup';
|
||||||
|
if (length > 1)
|
||||||
|
title += ` #${index}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: 'global setup',
|
title,
|
||||||
setup: async ({ config }) => {
|
setup: async ({ config }) => {
|
||||||
const setupHook = config.config.globalSetup ? await loadGlobalHook(config, config.config.globalSetup) : undefined;
|
const setupHook = config.globalSetups[index] ? await loadGlobalHook(config, config.globalSetups[index]) : undefined;
|
||||||
teardownHook = config.config.globalTeardown ? await loadGlobalHook(config, config.config.globalTeardown) : undefined;
|
teardownHook = config.globalTeardowns[index] ? await loadGlobalHook(config, config.globalTeardowns[index]) : undefined;
|
||||||
globalSetupResult = setupHook ? await setupHook(config.config) : undefined;
|
globalSetupResult = setupHook ? await setupHook(config.config) : undefined;
|
||||||
globalSetupFinished = true;
|
globalSetupFinished = true;
|
||||||
},
|
},
|
||||||
|
|
|
@ -1077,7 +1077,8 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Path to the global setup file. This file will be required and run before all the tests. It must export a single
|
* 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).
|
* Learn more about [global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown).
|
||||||
*
|
*
|
||||||
|
@ -1093,12 +1094,13 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
globalSetup?: string;
|
globalSetup?: 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
|
* 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
|
* 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).
|
* Learn more about [global setup and teardown](https://playwright.dev/docs/test-global-setup-teardown).
|
||||||
*
|
*
|
||||||
|
@ -1114,7 +1116,7 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
globalTeardown?: string;
|
globalTeardown?: string|Array<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum time in milliseconds the whole test suite can run. Zero timeout (default) disables this behavior. Useful on
|
* Maximum time in milliseconds the whole test suite can run. Zero timeout (default) disables this behavior. Useful on
|
||||||
|
|
|
@ -386,3 +386,43 @@ test('teardown after error', async ({ runInlineTest }) => {
|
||||||
'teardown 1',
|
'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');
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in New Issue