diff --git a/packages/playwright-core/src/cli/cli.ts b/packages/playwright-core/src/cli/cli.ts index fb2983d136..fe9bbca945 100755 --- a/packages/playwright-core/src/cli/cli.ts +++ b/packages/playwright-core/src/cli/cli.ts @@ -376,6 +376,7 @@ async function launchContext(options: Options, headless: boolean, executablePath const launchOptions: LaunchOptions = { headless, executablePath }; if (options.channel) launchOptions.channel = options.channel as any; + launchOptions.handleSIGINT = false; const contextOptions: BrowserContextOptions = // Copy the device descriptor since we have to compare and modify the options. @@ -522,6 +523,11 @@ async function launchContext(options: Options, headless: boolean, executablePath closeBrowser().catch(e => null); }); }); + process.on('SIGINT', async () => { + await closeBrowser(); + process.exit(130); + }); + const timeout = options.timeout ? parseInt(options.timeout, 10) : 0; context.setDefaultTimeout(timeout); context.setDefaultNavigationTimeout(timeout); @@ -532,6 +538,7 @@ async function launchContext(options: Options, headless: boolean, executablePath // Omit options that we add automatically for presentation purpose. delete launchOptions.headless; delete launchOptions.executablePath; + delete launchOptions.handleSIGINT; delete contextOptions.deviceScaleFactor; return { browser, browserName: browserType.name(), context, contextOptions, launchOptions }; } @@ -571,7 +578,8 @@ async function codegen(options: Options, url: string | undefined, language: stri device: options.device, saveStorage: options.saveStorage, mode: 'recording', - outputFile: outputFile ? path.resolve(outputFile) : undefined + outputFile: outputFile ? path.resolve(outputFile) : undefined, + handleSIGINT: false, }); await openPage(context, url); if (process.env.PWTEST_CLI_EXIT) diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 0d06fe1f55..33000c5745 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -374,7 +374,8 @@ export class BrowserContext extends ChannelOwner device?: string, saveStorage?: string, mode?: 'recording' | 'inspecting', - outputFile?: string + outputFile?: string, + handleSIGINT?: boolean, }) { await this._channel.recorderSupplementEnable(params); } diff --git a/packages/playwright-core/src/protocol/channels.ts b/packages/playwright-core/src/protocol/channels.ts index a1cc4c4c76..9b22e629b5 100644 --- a/packages/playwright-core/src/protocol/channels.ts +++ b/packages/playwright-core/src/protocol/channels.ts @@ -1423,6 +1423,7 @@ export type BrowserContextRecorderSupplementEnableParams = { device?: string, saveStorage?: string, outputFile?: string, + handleSIGINT?: boolean, }; export type BrowserContextRecorderSupplementEnableOptions = { language?: string, @@ -1433,6 +1434,7 @@ export type BrowserContextRecorderSupplementEnableOptions = { device?: string, saveStorage?: string, outputFile?: string, + handleSIGINT?: boolean, }; export type BrowserContextRecorderSupplementEnableResult = void; export type BrowserContextNewCDPSessionParams = { diff --git a/packages/playwright-core/src/protocol/protocol.yml b/packages/playwright-core/src/protocol/protocol.yml index aea955d24e..162f4d20b8 100644 --- a/packages/playwright-core/src/protocol/protocol.yml +++ b/packages/playwright-core/src/protocol/protocol.yml @@ -948,6 +948,7 @@ BrowserContext: device: string? saveStorage: string? outputFile: string? + handleSIGINT: boolean? newCDPSession: parameters: diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 9771966703..962544d1cf 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -772,6 +772,7 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({ device: tOptional(tString), saveStorage: tOptional(tString), outputFile: tOptional(tString), + handleSIGINT: tOptional(tBoolean), }); scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({})); scheme.BrowserContextNewCDPSessionParams = tObject({ diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 5b9308fd00..2b2bcd0276 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -56,6 +56,7 @@ export class Recorder implements InstrumentationListener { private _allMetadatas = new Map(); private _debugger: Debugger; private _contextRecorder: ContextRecorder; + private _handleSIGINT: boolean | undefined; private _recorderAppFactory: (recorder: Recorder) => Promise; static showInspector(context: BrowserContext) { @@ -78,13 +79,14 @@ export class Recorder implements InstrumentationListener { this._contextRecorder = new ContextRecorder(context, params); this._context = context; this._debugger = Debugger.lookup(context)!; + this._handleSIGINT = params.handleSIGINT; context.instrumentation.addListener(this, context); } private static async defaultRecorderAppFactory(recorder: Recorder) { if (process.env.PW_CODEGEN_NO_INSPECTOR) return new EmptyRecorderApp(); - return await RecorderApp.open(recorder, recorder._context); + return await RecorderApp.open(recorder, recorder._context, recorder._handleSIGINT); } async install() { diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index 2ca29e93ca..045e025776 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -109,7 +109,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { await mainFrame.goto(serverSideCallMetadata(), 'https://playwright/index.html'); } - static async open(recorder: Recorder, inspectedContext: BrowserContext): Promise { + static async open(recorder: Recorder, inspectedContext: BrowserContext, handleSIGINT: boolean | undefined): Promise { const sdkLanguage = inspectedContext._browser.options.sdkLanguage; const headed = !!inspectedContext._browser.options.headful; const recorderPlaywright = (require('../playwright').createPlaywright as typeof import('../playwright').createPlaywright)('javascript', true); @@ -127,7 +127,8 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { noDefaultViewport: true, ignoreDefaultArgs: ['--enable-automation'], headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed), - useWebSocket: !!process.env.PWTEST_RECORDER_PORT + useWebSocket: !!process.env.PWTEST_RECORDER_PORT, + handleSIGINT, }); const controller = new ProgressController(serverSideCallMetadata(), context._browser); await controller.run(async progress => { @@ -165,7 +166,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { // Testing harness for runCLI mode. { - if (process.env.PWTEST_CLI_EXIT && sources.length) { + if ((process.env.PWTEST_CLI_IS_UNDER_TEST || process.env.PWTEST_CLI_EXIT) && sources.length) { process.stdout.write('\n-------------8<-------------\n'); process.stdout.write(sources[0].text); process.stdout.write('\n-------------8<-------------\n'); diff --git a/tests/library/inspector/cli-codegen-2.spec.ts b/tests/library/inspector/cli-codegen-2.spec.ts index f9d99c0aaf..18398dedee 100644 --- a/tests/library/inspector/cli-codegen-2.spec.ts +++ b/tests/library/inspector/cli-codegen-2.spec.ts @@ -544,6 +544,24 @@ test.describe('cli codegen', () => { expect(fs.existsSync(traceFileName)).toBeTruthy(); }); + test('should save assets via SIGINT', async ({ runCLI, platform }, testInfo) => { + test.skip(platform === 'win32', 'SIGINT not supported on Windows'); + + const traceFileName = testInfo.outputPath('trace.zip'); + const storageFileName = testInfo.outputPath('auth.json'); + const harFileName = testInfo.outputPath('har.har'); + const cli = runCLI([`--save-trace=${traceFileName}`, `--save-storage=${storageFileName}`, `--save-har=${harFileName}`], { + noAutoExit: true, + }); + await cli.waitFor(`import { test, expect } from '@playwright/test'`); + cli.exit('SIGINT'); + const { exitCode } = await cli.process.exited; + expect(exitCode).toBe(130); + expect(fs.existsSync(traceFileName)).toBeTruthy(); + expect(fs.existsSync(storageFileName)).toBeTruthy(); + expect(fs.existsSync(harFileName)).toBeTruthy(); + }); + test('should fill tricky characters', async ({ page, openRecorder }) => { const recorder = await openRecorder(); diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index 2552b209ff..fe88e18d6d 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -25,7 +25,7 @@ type CLITestArgs = { recorderPageGetter: () => Promise; closeRecorder: () => Promise; openRecorder: () => Promise; - runCLI: (args: string[]) => CLIMock; + runCLI: (args: string[], options?: { noAutoExit?: boolean }) => CLIMock; }; const playwrightToAutomateInspector = require('../../../packages/playwright-core/lib/inProcessFactory').createInProcessPlaywright(); @@ -55,8 +55,8 @@ export const test = contextTest.extend({ testInfo.skip(mode === 'service'); let cli: CLIMock | undefined; - await run(cliArgs => { - cli = new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath); + await run((cliArgs, { noAutoExit } = {}) => { + cli = new CLIMock(childProcess, browserName, channel, headless, cliArgs, launchOptions.executablePath, noAutoExit); return cli; }); if (cli) @@ -178,12 +178,12 @@ class Recorder { } class CLIMock { - private process: TestChildProcess; + process: TestChildProcess; private waitForText: string; private waitForCallback: () => void; exited: Promise; - constructor(childProcess: CommonFixtures['childProcess'], browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined) { + constructor(childProcess: CommonFixtures['childProcess'], browserName: string, channel: string | undefined, headless: boolean | undefined, args: string[], executablePath: string | undefined, noAutoExit: boolean | undefined) { const nodeArgs = [ 'node', path.join(__dirname, '..', '..', '..', 'packages', 'playwright-core', 'lib', 'cli', 'cli.js'), @@ -196,7 +196,8 @@ class CLIMock { this.process = childProcess({ command: nodeArgs, env: { - PWTEST_CLI_EXIT: '1', + PWTEST_CLI_IS_UNDER_TEST: '1', + PWTEST_CLI_EXIT: !noAutoExit ? '1' : undefined, PWTEST_CLI_HEADLESS: headless ? '1' : undefined, PWTEST_CLI_EXECUTABLE_PATH: executablePath, DEBUG: (process.env.DEBUG ?? '') + ',pw:browser*', @@ -226,6 +227,10 @@ class CLIMock { text() { return removeAnsiColors(this.process.output); } + + exit(signal: NodeJS.Signals | number) { + this.process.process.kill(signal); + } } function removeAnsiColors(input: string): string {