diff --git a/packages/playwright-test/src/runner.ts b/packages/playwright-test/src/runner.ts index 9c86d72abe..3ea16950c9 100644 --- a/packages/playwright-test/src/runner.ts +++ b/packages/playwright-test/src/runner.ts @@ -397,11 +397,10 @@ export class Runner { } // 13. Run Global setup. - const globalTearDown = await this._performGlobalSetup(config, rootSuite); - if (!globalTearDown) - return { status: 'failed' }; - const result: FullResult = { status: 'passed' }; + const globalTearDown = await this._performGlobalSetup(config, rootSuite, result); + if (result.status !== 'passed') + return result; // 14. Run tests. try { @@ -433,9 +432,10 @@ export class Runner { return result; } - private async _performGlobalSetup(config: FullConfigInternal, rootSuite: Suite): Promise<(() => Promise) | undefined> { - const result: FullResult = { status: 'passed' }; + private async _performGlobalSetup(config: FullConfigInternal, rootSuite: Suite, result: FullResult): Promise<(() => Promise) | undefined> { let globalSetupResult: any; + const pluginsThatWereSetUp: TestRunnerPlugin[] = []; + const sigintWatcher = new SigIntWatcher(); const tearDown = async () => { // Reverse to setup. @@ -445,34 +445,49 @@ export class Runner { }, result); await this._runAndReportError(async () => { - if (config.globalTeardown) + if (globalSetupResult && config.globalTeardown) await (await this._loader.loadGlobalHook(config.globalTeardown, 'globalTeardown'))(this._loader.fullConfig()); }, result); - for (const plugin of [...this._plugins].reverse()) { + for (const plugin of pluginsThatWereSetUp.reverse()) { await this._runAndReportError(async () => { await plugin.teardown?.(); }, result); } }; - await this._runAndReportError(async () => { - // Legacy webServer support. - if (config.webServer) - this._plugins.push(webServerPluginForConfig(config, this._reporter)); + // Legacy webServer support. + if (config.webServer) + this._plugins.push(webServerPluginForConfig(config, this._reporter)); + await this._runAndReportError(async () => { // First run the plugins, if plugin is a web server we want it to run before the // config's global setup. - for (const plugin of this._plugins) - await plugin.setup?.(config, config._configDir, rootSuite); + for (const plugin of this._plugins) { + await Promise.race([ + plugin.setup?.(config, config._configDir, rootSuite), + sigintWatcher.promise(), + ]); + if (sigintWatcher.hadSignal()) + break; + pluginsThatWereSetUp.push(plugin); + } // The do global setup. - if (config.globalSetup) - globalSetupResult = await (await this._loader.loadGlobalHook(config.globalSetup, 'globalSetup'))(this._loader.fullConfig()); + if (!sigintWatcher.hadSignal() && config.globalSetup) { + const hook = await this._loader.loadGlobalHook(config.globalSetup, 'globalSetup'); + await Promise.race([ + Promise.resolve().then(() => hook(this._loader.fullConfig())).then((r: any) => globalSetupResult = r || ''), + sigintWatcher.promise(), + ]); + } }, result); - if (result.status !== 'passed') { + sigintWatcher.disarm(); + + if (result.status !== 'passed' || sigintWatcher.hadSignal()) { await tearDown(); + result.status = sigintWatcher.hadSignal() ? 'interrupted' : 'failed'; return; } diff --git a/tests/playwright-test/runner.spec.ts b/tests/playwright-test/runner.spec.ts index 5f2515db58..6503aa5d5e 100644 --- a/tests/playwright-test/runner.spec.ts +++ b/tests/playwright-test/runner.spec.ts @@ -306,3 +306,120 @@ test('should not hang if test suites in worker are inconsistent with runner', as expect(result.report.suites[0].specs[1].tests[0].results[0].error.message).toBe('Unknown test(s) in worker:\nproject-name > a.spec.js > Test 1 - bar\nproject-name > a.spec.js > Test 2 - baz'); }); +test('sigint should stop global setup', async ({ runInlineTest }) => { + test.skip(process.platform === 'win32', 'No sending SIGINT on Windows'); + + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + globalSetup: './globalSetup', + globalTeardown: './globalTeardown.ts', + }; + `, + 'globalSetup.ts': ` + module.exports = () => { + console.log('Global setup'); + console.log('%%SEND-SIGINT%%'); + return new Promise(f => setTimeout(f, 30000)); + }; + `, + 'globalTeardown.ts': ` + module.exports = () => { + console.log('Global teardown'); + }; + `, + 'a.spec.js': ` + const { test } = pwt; + test('test', async () => { }); + `, + }, { 'workers': 1 }, {}, { sendSIGINTAfter: 1 }); + expect(result.exitCode).toBe(130); + expect(result.passed).toBe(0); + const output = stripAnsi(result.output); + expect(output).toContain('Global setup'); + expect(output).not.toContain('Global teardown'); +}); + +test('sigint should stop plugins', async ({ runInlineTest }) => { + test.skip(process.platform === 'win32', 'No sending SIGINT on Windows'); + + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + }; + + require('@playwright/test')._addRunnerPlugin(() => ({ + setup: async () => { + console.log('Plugin1 setup'); + console.log('%%SEND-SIGINT%%'); + return new Promise(f => setTimeout(f, 30000)); + }, + teardown: async () => { + console.log('Plugin1 teardown'); + } + })); + + require('@playwright/test')._addRunnerPlugin(() => ({ + setup: async () => { + console.log('Plugin2 setup'); + }, + teardown: async () => { + console.log('Plugin2 teardown'); + } + })); + `, + 'a.spec.js': ` + const { test } = pwt; + test('test', async () => { }); + `, + }, { 'workers': 1 }, {}, { sendSIGINTAfter: 1 }); + expect(result.exitCode).toBe(130); + expect(result.passed).toBe(0); + const output = stripAnsi(result.output); + expect(output).toContain('Plugin1 setup'); + expect(output).not.toContain('Plugin1 teardown'); + expect(output).not.toContain('Plugin2 setup'); + expect(output).not.toContain('Plugin2 teardown'); +}); + +test('sigint should stop plugins 2', async ({ runInlineTest }) => { + test.skip(process.platform === 'win32', 'No sending SIGINT on Windows'); + + const result = await runInlineTest({ + 'playwright.config.ts': ` + module.exports = { + }; + + require('@playwright/test')._addRunnerPlugin(() => ({ + setup: async () => { + console.log('Plugin1 setup'); + }, + teardown: async () => { + console.log('Plugin1 teardown'); + } + })); + + require('@playwright/test')._addRunnerPlugin(() => ({ + setup: async () => { + console.log('Plugin2 setup'); + console.log('%%SEND-SIGINT%%'); + return new Promise(f => setTimeout(f, 30000)); + }, + teardown: async () => { + console.log('Plugin2 teardown'); + } + })); + `, + 'a.spec.js': ` + const { test } = pwt; + test('test', async () => { }); + `, + }, { 'workers': 1 }, {}, { sendSIGINTAfter: 1 }); + expect(result.exitCode).toBe(130); + expect(result.passed).toBe(0); + const output = stripAnsi(result.output); + expect(output).toContain('Plugin1 setup'); + expect(output).toContain('Plugin2 setup'); + expect(output).toContain('Plugin1 teardown'); + expect(output).not.toContain('Plugin2 teardown'); +});