diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts index 603f066601..a47ca0fe32 100644 --- a/packages/playwright/src/runner/watchMode.ts +++ b/packages/playwright/src/runner/watchMode.ts @@ -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(); const dirtyTestIds = new Set(); - let onDirtyTests = new ManualPromise(); + let onDirtyTests = new ManualPromise<'changed'>(); let queue = Promise.resolve(); const changedFiles = new Set(); @@ -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) { - printPrompt(); - const readCommandPromise = readCommand(); - await Promise.race([ + if (bufferMode) + printBufferPrompt(dirtyTestFiles, teleSuiteUpdater.config!.rootDir); + else + printPrompt(); + + 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, 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'; diff --git a/tests/playwright-test/watch.spec.ts b/tests/playwright-test/watch.spec.ts index ec05e5bb68..b56fb470ea 100644 --- a/tests/playwright-test/watch.spec.ts +++ b/tests/playwright-test/watch.spec.ts @@ -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'); +}); \ No newline at end of file