feat(watch mode): buffer mode (#32631)
Closes https://github.com/microsoft/playwright/issues/32578. Adds a buffer mode that can be toggled by pressing <kbd>b</kbd>. When engaged, changed test files are collected and shown on screen. The test run is then kicked off by pressing <kbd>Enter</kbd>. This changes the signal to start a test run from <kbd>Cmd+s</kbd> to a <kbd>Enter</kbd> press in the test terminal. It should help users with auto-save and make it easier to run on long-running tests. It feels very similar to running `npx playwright test`, but without having to write a filter. https://github.com/user-attachments/assets/71e16139-9427-4e90-b523-8d218f09ed9d
This commit is contained in:
parent
b0f15b320f
commit
ec2ae1ed2d
|
|
@ -73,6 +73,7 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp
|
|||
return 'restarted';
|
||||
|
||||
const options: WatchModeOptions = { ...initialOptions };
|
||||
let bufferMode = false;
|
||||
|
||||
const testServerDispatcher = new TestServerDispatcher(configLocation);
|
||||
const transport = new InMemoryTransport(
|
||||
|
|
@ -94,8 +95,9 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp
|
|||
|
||||
const teleSuiteUpdater = new TeleSuiteUpdater({ pathSeparator: path.sep, onUpdate() { } });
|
||||
|
||||
const dirtyTestFiles = new Set<string>();
|
||||
const dirtyTestIds = new Set<string>();
|
||||
let onDirtyTests = new ManualPromise();
|
||||
let onDirtyTests = new ManualPromise<'changed'>();
|
||||
|
||||
let queue = Promise.resolve();
|
||||
const changedFiles = new Set<string>();
|
||||
|
|
@ -110,14 +112,17 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp
|
|||
teleSuiteUpdater.processListReport(report);
|
||||
|
||||
for (const test of teleSuiteUpdater.rootSuite!.allTests()) {
|
||||
if (changedFiles.has(test.location.file))
|
||||
if (changedFiles.has(test.location.file)) {
|
||||
dirtyTestFiles.add(test.location.file);
|
||||
dirtyTestIds.add(test.id);
|
||||
}
|
||||
|
||||
}
|
||||
changedFiles.clear();
|
||||
|
||||
if (dirtyTestIds.size > 0)
|
||||
onDirtyTests.resolve?.();
|
||||
if (dirtyTestIds.size > 0) {
|
||||
onDirtyTests.resolve('changed');
|
||||
onDirtyTests = new ManualPromise();
|
||||
}
|
||||
});
|
||||
});
|
||||
testServerConnection.onReport(report => teleSuiteUpdater.processTestReportEvent(report));
|
||||
|
|
@ -134,21 +139,27 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp
|
|||
let result: FullResult['status'] = 'passed';
|
||||
|
||||
while (true) {
|
||||
if (bufferMode)
|
||||
printBufferPrompt(dirtyTestFiles, teleSuiteUpdater.config!.rootDir);
|
||||
else
|
||||
printPrompt();
|
||||
const readCommandPromise = readCommand();
|
||||
await Promise.race([
|
||||
|
||||
const command = await Promise.race([
|
||||
onDirtyTests,
|
||||
readCommandPromise,
|
||||
readCommand(),
|
||||
]);
|
||||
if (!readCommandPromise.isDone())
|
||||
readCommandPromise.resolve('changed');
|
||||
|
||||
const command = await readCommandPromise;
|
||||
if (bufferMode && command === 'changed')
|
||||
continue;
|
||||
|
||||
const shouldRunChangedFiles = bufferMode ? command === 'run' : command === 'changed';
|
||||
if (shouldRunChangedFiles) {
|
||||
if (dirtyTestIds.size === 0)
|
||||
continue;
|
||||
|
||||
if (command === 'changed') {
|
||||
onDirtyTests = new ManualPromise();
|
||||
const testIds = [...dirtyTestIds];
|
||||
dirtyTestIds.clear();
|
||||
dirtyTestFiles.clear();
|
||||
await runTests(options, testServerConnection, { testIds, title: 'files changed' });
|
||||
lastRun = { type: 'changed', dirtyTestIds: testIds };
|
||||
continue;
|
||||
|
|
@ -234,6 +245,11 @@ export async function runWatchModeLoop(configLocation: ConfigLocation, initialOp
|
|||
continue;
|
||||
}
|
||||
|
||||
if (command === 'toggle-buffer-mode') {
|
||||
bufferMode = !bufferMode;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'exit')
|
||||
break;
|
||||
|
||||
|
|
@ -300,6 +316,7 @@ Change settings
|
|||
${colors.bold('p')} ${colors.dim('set file filter')}
|
||||
${colors.bold('t')} ${colors.dim('set title filter')}
|
||||
${colors.bold('s')} ${colors.dim('toggle show & reuse the browser')}
|
||||
${colors.bold('b')} ${colors.dim('toggle buffer mode')}
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
|
@ -312,6 +329,7 @@ Change settings
|
|||
case 't': result.resolve('grep'); break;
|
||||
case 'f': result.resolve('failed'); break;
|
||||
case 's': result.resolve('toggle-show-browser'); break;
|
||||
case 'b': result.resolve('toggle-buffer-mode'); break;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -350,6 +368,22 @@ function printConfiguration(options: WatchModeOptions, title?: string) {
|
|||
process.stdout.write(lines.join('\n'));
|
||||
}
|
||||
|
||||
function printBufferPrompt(dirtyTestFiles: Set<string>, rootDir: string) {
|
||||
const sep = separator();
|
||||
process.stdout.write('\x1Bc');
|
||||
process.stdout.write(`${sep}\n`);
|
||||
|
||||
if (dirtyTestFiles.size === 0) {
|
||||
process.stdout.write(`${colors.dim('Waiting for file changes. Press')} ${colors.bold('q')} ${colors.dim('to quit or')} ${colors.bold('h')} ${colors.dim('for more options.')}\n\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(`${colors.dim(`${dirtyTestFiles.size} test ${dirtyTestFiles.size === 1 ? 'file' : 'files'} changed:`)}\n\n`);
|
||||
for (const file of dirtyTestFiles)
|
||||
process.stdout.write(` · ${path.relative(rootDir, file)}\n`);
|
||||
process.stdout.write(`\n${colors.dim(`Press`)} ${colors.bold('enter')} ${colors.dim('to run')}, ${colors.bold('q')} ${colors.dim('to quit or')} ${colors.bold('h')} ${colors.dim('for more options.')}\n\n`);
|
||||
}
|
||||
|
||||
function printPrompt() {
|
||||
const sep = separator();
|
||||
process.stdout.write(`
|
||||
|
|
@ -371,4 +405,4 @@ async function toggleShowBrowser() {
|
|||
}
|
||||
}
|
||||
|
||||
type Command = 'run' | 'failed' | 'repeat' | 'changed' | 'project' | 'file' | 'grep' | 'exit' | 'interrupted' | 'toggle-show-browser';
|
||||
type Command = 'run' | 'failed' | 'repeat' | 'changed' | 'project' | 'file' | 'grep' | 'exit' | 'interrupted' | 'toggle-show-browser' | 'toggle-buffer-mode';
|
||||
|
|
|
|||
|
|
@ -313,12 +313,17 @@ test('should respect project filter C', async ({ runWatchTest, writeFiles }) =>
|
|||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('[bar] › a.test.ts:3:11 › passes');
|
||||
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
|
||||
await writeFiles(files); // file change triggers listTests with project filter
|
||||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||
|
||||
testProcess.clearOutput();
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
||||
testProcess.write('c');
|
||||
testProcess.clearOutput();
|
||||
await testProcess.waitForOutput('Select projects');
|
||||
await testProcess.waitForOutput('foo');
|
||||
await testProcess.waitForOutput('bar'); // second selection should still show all
|
||||
|
|
@ -812,3 +817,49 @@ test('should run global teardown before exiting', async ({ runWatchTest }) => {
|
|||
testProcess.write('\x1B');
|
||||
await testProcess.waitForOutput('running teardown');
|
||||
});
|
||||
|
||||
test('buffer mode', async ({ runWatchTest, writeFiles }) => {
|
||||
const testProcess = await runWatchTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
'b.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes in b', () => {});
|
||||
`,
|
||||
});
|
||||
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('b');
|
||||
await testProcess.waitForOutput('Waiting for file changes. Press q to quit');
|
||||
|
||||
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes again', () => {});
|
||||
`,
|
||||
});
|
||||
|
||||
await testProcess.waitForOutput('1 test file changed:');
|
||||
await testProcess.waitForOutput('a.test.ts');
|
||||
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'b.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes in b again', () => {});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('2 test files changed:');
|
||||
await testProcess.waitForOutput('a.test.ts');
|
||||
await testProcess.waitForOutput('b.test.ts');
|
||||
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('\r\n');
|
||||
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
});
|
||||
Loading…
Reference in a new issue