diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts index ba2c5a34e7..92df6a0bb3 100644 --- a/packages/playwright/src/runner/watchMode.ts +++ b/packages/playwright/src/runner/watchMode.ts @@ -28,6 +28,7 @@ import { EventEmitter } from 'stream'; import { type TestServerTransport, TestServerConnection } from '../isomorphic/testServerConnection'; import { TeleSuiteUpdater } from '../isomorphic/teleSuiteUpdater'; import { restartWithExperimentalTsEsm } from '../common/configLoader'; +import type { Disposable } from '../isomorphic/events'; class InMemoryTransport extends EventEmitter implements TestServerTransport { public readonly _send: (data: string) => void; @@ -251,12 +252,37 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp return result === 'passed' ? teardown.status : result; } +function readKeyPress(handler: (text: string, key: any) => void): Disposable { + const rl = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 }); + readline.emitKeypressEvents(process.stdin, rl); + if (process.stdin.isTTY) + process.stdin.setRawMode(true); + + process.stdin.on('keypress', handler); + + return { + dispose: () => { + process.stdin.off('keypress', handler); + rl.close(); + if (process.stdin.isTTY) + process.stdin.setRawMode(false); + } + }; +} + +const isInterrupt = (text: string, key: any) => text === '\x03' || text === '\x1B' || (key && key.name === 'escape') || (key && key.ctrl && key.name === 'c'); + async function runTests(watchOptions: WatchModeOptions, testServerConnection: TestServerConnection, options?: { title?: string, testIds?: string[], }) { printConfiguration(watchOptions, options?.title); + const reader = readKeyPress((text: string, key: any) => { + if (isInterrupt(text, key)) + testServerConnection.stopTestsNoReply({}); + }); + await testServerConnection.runTests({ grep: watchOptions.grep, testIds: options?.testIds, @@ -266,18 +292,14 @@ async function runTests(watchOptions: WatchModeOptions, testServerConnection: Te reuseContext: connectWsEndpoint ? true : undefined, workers: connectWsEndpoint ? 1 : undefined, headed: connectWsEndpoint ? true : undefined, - }); + }).finally(reader.dispose); } function readCommand(): ManualPromise { const result = new ManualPromise(); - const rl = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 }); - readline.emitKeypressEvents(process.stdin, rl); - if (process.stdin.isTTY) - process.stdin.setRawMode(true); - const handler = (text: string, key: any) => { - if (text === '\x03' || text === '\x1B' || (key && key.name === 'escape') || (key && key.ctrl && key.name === 'c')) { + const reader = readKeyPress((text: string, key: any) => { + if (isInterrupt(text, key)) { result.resolve('interrupted'); return; } @@ -316,15 +338,9 @@ Change settings case 'f': result.resolve('failed'); break; case 's': result.resolve('toggle-show-browser'); break; } - }; - - process.stdin.on('keypress', handler); - void result.finally(() => { - process.stdin.off('keypress', handler); - rl.close(); - if (process.stdin.isTTY) - process.stdin.setRawMode(false); }); + + void result.finally(reader.dispose); return result; } diff --git a/tests/playwright-test/watch.spec.ts b/tests/playwright-test/watch.spec.ts index f4d1fd72ce..b27ff42137 100644 --- a/tests/playwright-test/watch.spec.ts +++ b/tests/playwright-test/watch.spec.ts @@ -814,3 +814,23 @@ test('should run global teardown before exiting', async ({ runWatchTest }) => { testProcess.write('\x1B'); await testProcess.waitForOutput('running teardown'); }); + +test('should stop testrun on pressing escape', async ({ runWatchTest }) => { + const testProcess = await runWatchTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('stalls', async () => { + await new Promise(() => {}); + }); + `, + }); + // add once https://github.com/microsoft/playwright/pull/32583 lands + // await testProcess.waitForOutput('Waiting for file changes.'); + // testProcess.clearOutput(); + // testProcess.write('\r\n'); + + await testProcess.waitForOutput('Running 1 test'); + await timers.setTimeout(500); + testProcess.write('\x1B'); + await testProcess.waitForOutput('1 interrupted'); +});