feat(test runner): allow stopping testrun with escape (#32584)

Closes https://github.com/microsoft/playwright/issues/32579
This commit is contained in:
Simon Knott 2024-10-08 20:39:54 +02:00 committed by GitHub
parent 5121b19ac6
commit 892d67ffef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 74 additions and 38 deletions

View File

@ -16,7 +16,7 @@
import readline from 'readline'; import readline from 'readline';
import path from 'path'; import path from 'path';
import { createGuid, getPackageManagerExecCommand, ManualPromise } from 'playwright-core/lib/utils'; import { createGuid, eventsHelper, getPackageManagerExecCommand, ManualPromise } from 'playwright-core/lib/utils';
import type { ConfigLocation } from '../common/config'; import type { ConfigLocation } from '../common/config';
import type { FullResult } from '../../types/testReporter'; import type { FullResult } from '../../types/testReporter';
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
@ -266,12 +266,47 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp
return result === 'passed' ? teardown.status : result; return result === 'passed' ? teardown.status : result;
} }
function readKeyPress<T extends string>(handler: (text: string, key: any) => T | undefined): { cancel(): void; result: Promise<T> } {
const promise = new ManualPromise<T>();
const rl = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 });
readline.emitKeypressEvents(process.stdin, rl);
if (process.stdin.isTTY)
process.stdin.setRawMode(true);
const listener = eventsHelper.addEventListener(process.stdin, 'keypress', (text: string, key: any) => {
const result = handler(text, key);
if (result)
promise.resolve(result);
});
const cancel = () => {
eventsHelper.removeEventListeners([listener]);
rl.close();
if (process.stdin.isTTY)
process.stdin.setRawMode(false);
};
void promise.finally(cancel);
return { result: promise, cancel };
}
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?: { async function runTests(watchOptions: WatchModeOptions, testServerConnection: TestServerConnection, options?: {
title?: string, title?: string,
testIds?: string[], testIds?: string[],
}) { }) {
printConfiguration(watchOptions, options?.title); printConfiguration(watchOptions, options?.title);
const waitForDone = readKeyPress((text: string, key: any) => {
if (isInterrupt(text, key)) {
testServerConnection.stopTestsNoReply({});
return 'done';
}
});
await testServerConnection.runTests({ await testServerConnection.runTests({
grep: watchOptions.grep, grep: watchOptions.grep,
testIds: options?.testIds, testIds: options?.testIds,
@ -281,30 +316,21 @@ async function runTests(watchOptions: WatchModeOptions, testServerConnection: Te
reuseContext: connectWsEndpoint ? true : undefined, reuseContext: connectWsEndpoint ? true : undefined,
workers: connectWsEndpoint ? 1 : undefined, workers: connectWsEndpoint ? 1 : undefined,
headed: connectWsEndpoint ? true : undefined, headed: connectWsEndpoint ? true : undefined,
}); }).finally(() => waitForDone.cancel());
} }
function readCommand(): { result: Promise<Command>, cancel: () => void } { function readCommand() {
const result = new ManualPromise<Command>(); return readKeyPress<Command>((text: string, key: any) => {
const rl = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 }); if (isInterrupt(text, key))
readline.emitKeypressEvents(process.stdin, rl); return 'interrupted';
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')) {
result.resolve('interrupted');
return;
}
if (process.platform !== 'win32' && key && key.ctrl && key.name === 'z') { if (process.platform !== 'win32' && key && key.ctrl && key.name === 'z') {
process.kill(process.ppid, 'SIGTSTP'); process.kill(process.ppid, 'SIGTSTP');
process.kill(process.pid, 'SIGTSTP'); process.kill(process.pid, 'SIGTSTP');
} }
const name = key?.name; const name = key?.name;
if (name === 'q') { if (name === 'q')
result.resolve('exit'); return 'exit';
return;
}
if (name === 'h') { if (name === 'h') {
process.stdout.write(`${separator()} process.stdout.write(`${separator()}
Run tests Run tests
@ -324,26 +350,16 @@ Change settings
} }
switch (name) { switch (name) {
case 'return': result.resolve('run'); break; case 'return': return 'run';
case 'r': result.resolve('repeat'); break; case 'r': return 'repeat';
case 'c': result.resolve('project'); break; case 'c': return 'project';
case 'p': result.resolve('file'); break; case 'p': return 'file';
case 't': result.resolve('grep'); break; case 't': return 'grep';
case 'f': result.resolve('failed'); break; case 'f': return 'failed';
case 's': result.resolve('toggle-show-browser'); break; case 's': return 'toggle-show-browser';
case 'b': result.resolve('toggle-buffer-mode'); break; case 'b': return 'toggle-buffer-mode';
} }
}; });
process.stdin.on('keypress', handler);
const cancel = () => {
process.stdin.off('keypress', handler);
rl.close();
if (process.stdin.isTTY)
process.stdin.setRawMode(false);
};
void result.finally(cancel);
return { result, cancel };
} }
let showBrowserServer: PlaywrightServer | undefined; let showBrowserServer: PlaywrightServer | undefined;

View File

@ -421,6 +421,7 @@ test('should re-run failed tests on F > R', async ({ runWatchTest }) => {
await testProcess.waitForOutput('npx playwright test (running failed tests) #2'); await testProcess.waitForOutput('npx playwright test (running failed tests) #2');
await testProcess.waitForOutput('c.test.ts:3:11 fails'); await testProcess.waitForOutput('c.test.ts:3:11 fails');
expect(testProcess.output).not.toContain('a.test.ts:3:11'); expect(testProcess.output).not.toContain('a.test.ts:3:11');
await testProcess.waitForOutput('Waiting for file changes.');
testProcess.clearOutput(); testProcess.clearOutput();
testProcess.write('r'); testProcess.write('r');
await testProcess.waitForOutput('npx playwright test (re-running tests) #3'); await testProcess.waitForOutput('npx playwright test (re-running tests) #3');
@ -836,6 +837,25 @@ test('should run global teardown before exiting', async ({ runWatchTest }) => {
await testProcess.waitForOutput('running teardown'); 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 () => {
console.log('test started')
await new Promise(() => {});
});
`,
});
await testProcess.waitForOutput('Waiting for file changes.');
testProcess.clearOutput();
testProcess.write('\r\n');
await testProcess.waitForOutput('test started');
testProcess.write('\x1B');
await testProcess.waitForOutput('1 interrupted');
});
test('buffer mode', async ({ runWatchTest, writeFiles }) => { test('buffer mode', async ({ runWatchTest, writeFiles }) => {
const testProcess = await runWatchTest({ const testProcess = await runWatchTest({
'a.test.ts': ` 'a.test.ts': `