diff --git a/packages/playwright-test/bundles/utils/src/utilsBundleImpl.ts b/packages/playwright-test/bundles/utils/src/utilsBundleImpl.ts index 8601827939..f40095b16c 100644 --- a/packages/playwright-test/bundles/utils/src/utilsBundleImpl.ts +++ b/packages/playwright-test/bundles/utils/src/utilsBundleImpl.ts @@ -30,4 +30,4 @@ import stoppableLibrary from 'stoppable'; export const stoppable = stoppableLibrary; import enquirerLibrary from 'enquirer'; -export const enquirer = enquirerLibrary; \ No newline at end of file +export const enquirer = enquirerLibrary; diff --git a/packages/playwright-test/src/runner/reporters.ts b/packages/playwright-test/src/runner/reporters.ts index 8d9020a22c..24d80fda12 100644 --- a/packages/playwright-test/src/runner/reporters.ts +++ b/packages/playwright-test/src/runner/reporters.ts @@ -16,7 +16,7 @@ import path from 'path'; import type { Reporter, TestError } from '../../types/testReporter'; -import { separator, formatError } from '../reporters/base'; +import { formatError } from '../reporters/base'; import DotReporter from '../reporters/dot'; import EmptyReporter from '../reporters/empty'; import GitHubReporter from '../reporters/github'; @@ -30,7 +30,6 @@ import type { Suite } from '../common/test'; import type { FullConfigInternal } from '../common/types'; import { loadReporter } from './loadUtils'; import type { BuiltInReporter } from '../common/configLoader'; -import { colors } from 'playwright-core/lib/utilsBundle'; export async function createReporter(config: FullConfigInternal, mode: 'list' | 'watch' | 'run') { const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = { @@ -45,7 +44,7 @@ export async function createReporter(config: FullConfigInternal, mode: 'list' | }; const reporters: Reporter[] = []; if (mode === 'watch') { - reporters.push(new WatchModeReporter()); + reporters.push(new ListReporter()); } else { for (const r of config.reporter) { const [name, arg] = r; @@ -104,32 +103,3 @@ export class ListModeReporter implements Reporter { console.error('\n' + formatError(this.config, error, false).message); } } - -let seq = 0; - -export class WatchModeReporter extends ListReporter { - private _options: { isShowBrowser?: () => boolean; } | undefined; - constructor(options?: { - isShowBrowser?: () => boolean, - }) { - super(); - this._options = options; - } - - override generateStartingMessage(): string { - const tokens: string[] = []; - tokens.push('npx playwright test'); - tokens.push(...(this.config._internal.cliProjectFilter || [])?.map(p => colors.blue(`--project ${p}`))); - if (this.config._internal.cliGrep) - tokens.push(colors.red(`--grep ${this.config._internal.cliGrep}`)); - if (this.config._internal.cliArgs) - tokens.push(...this.config._internal.cliArgs.map(a => colors.bold(a))); - tokens.push(colors.dim(`#${++seq}`)); - const lines: string[] = []; - const sep = separator(); - lines.push('\x1Bc' + sep); - lines.push(`${tokens.join(' ')}` + super.generateStartingMessage()); - lines.push(`${colors.dim('Show & reuse browser:')} ${colors.bold(this._options?.isShowBrowser?.() ? 'on' : 'off')}${colors.dim(', press')} ${colors.bold('s')} ${colors.dim('to toggle.')}`); - return lines.join('\n'); - } -} diff --git a/packages/playwright-test/src/runner/watchMode.ts b/packages/playwright-test/src/runner/watchMode.ts index 56cad03533..825161f0fc 100644 --- a/packages/playwright-test/src/runner/watchMode.ts +++ b/packages/playwright-test/src/runner/watchMode.ts @@ -26,11 +26,12 @@ import { buildProjectsClosure, filterProjects } from './projectUtils'; import { clearCompilationCache, collectAffectedTestFiles } from '../common/compilationCache'; import type { FullResult } from 'packages/playwright-test/reporter'; import chokidar from 'chokidar'; -import { createReporter, WatchModeReporter } from './reporters'; +import { createReporter } from './reporters'; import { colors } from 'playwright-core/lib/utilsBundle'; import { enquirer } from '../utilsBundle'; import { separator } from '../reporters/base'; import { PlaywrightServer } from 'playwright-core/lib/remote/playwrightServer'; +import ListReporter from '../reporters/list'; class FSWatcher { private _dirtyFiles = new Set(); @@ -64,6 +65,11 @@ class FSWatcher { } export async function runWatchModeLoop(config: FullConfigInternal): Promise { + // Reset the settings that don't apply to watch. + config._internal.passWithNoTests = true; + for (const p of config.projects) + p.retries = 0; + // Perform global setup. const reporter = await createReporter(config, 'watch'); const context: TaskRunnerState = { @@ -77,26 +83,20 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise(); - - const originalCliArgs = config._internal.cliArgs; - const originalCliGrep = config._internal.cliGrep; const originalWorkers = config.workers; + const fsWatcher = new FSWatcher(projectClosure.map(p => p.testDir)); let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: Set, dirtyFiles?: Set } = { type: 'regular' }; - let result: FullResult['status'] = 'passed'; - const fsWatcher = new FSWatcher(projectClosure.map(p => p.testDir)); + // Enter the watch loop. + printConfiguration(config); while (true) { - const sep = separator(); - process.stdout.write(` -${sep} -Waiting for file changes. Press ${colors.bold('a')} to run all, ${colors.bold('q')} to quit or ${colors.bold('h')} for more options. -`); + printPrompt(); const readCommandPromise = readCommand(); await Promise.race([ fsWatcher.onDirtyFiles(), @@ -109,32 +109,47 @@ Waiting for file changes. Press ${colors.bold('a')} to run all, ${colors.bold('q if (command === 'changed') { const dirtyFiles = fsWatcher.takeDirtyFiles(); + printConfiguration(config, 'files changed'); await runChangedTests(config, failedTestIdCollector, projectClosure, dirtyFiles); lastRun = { type: 'changed', dirtyFiles }; continue; } - if (command === 'all') { + if (command === 'run') { // All means reset filters. - config._internal.cliArgs = originalCliArgs; - config._internal.cliGrep = originalCliGrep; + printConfiguration(config); await runTests(config, failedTestIdCollector); lastRun = { type: 'regular' }; continue; } + if (command === 'project') { + const { projectNames } = await enquirer.prompt<{ projectNames: string[] }>({ + type: 'multiselect', + name: 'projectNames', + message: 'Select projects', + choices: config.projects.map(p => ({ name: p.name })), + }).catch(() => ({ projectNames: null })); + if (!projectNames) + continue; + config._internal.cliProjectFilter = projectNames; + printConfiguration(config); + continue; + } + if (command === 'file') { const { filePattern } = await enquirer.prompt<{ filePattern: string }>({ type: 'text', name: 'filePattern', message: 'Input filename pattern (regex)', - }); + }).catch(() => ({ filePattern: null })); + if (filePattern === null) + continue; if (filePattern.trim()) - config._internal.cliArgs = [filePattern]; + config._internal.cliArgs = filePattern.split(' '); else config._internal.cliArgs = []; - await runTests(config, failedTestIdCollector); - lastRun = { type: 'regular' }; + printConfiguration(config); continue; } @@ -143,19 +158,21 @@ Waiting for file changes. Press ${colors.bold('a')} to run all, ${colors.bold('q type: 'text', name: 'testPattern', message: 'Input test name pattern (regex)', - }); + }).catch(() => ({ testPattern: null })); + if (testPattern === null) + continue; if (testPattern.trim()) config._internal.cliGrep = testPattern; else config._internal.cliGrep = undefined; - await runTests(config, failedTestIdCollector); - lastRun = { type: 'regular' }; + printConfiguration(config); continue; } if (command === 'failed') { config._internal.testIdMatcher = id => failedTestIdCollector.has(id); const failedTestIds = new Set(failedTestIdCollector); + printConfiguration(config, 'running failed tests'); await runTests(config, failedTestIdCollector); config._internal.testIdMatcher = undefined; lastRun = { type: 'failed', failedTestIds }; @@ -163,6 +180,7 @@ Waiting for file changes. Press ${colors.bold('a')} to run all, ${colors.bold('q } if (command === 'repeat') { + printConfiguration(config, 're-running tests'); if (lastRun.type === 'regular') { await runTests(config, failedTestIdCollector); continue; @@ -227,8 +245,11 @@ async function runChangedTests(config: FullConfigInternal, failedTestIdCollector return await runTests(config, failedTestIdCollector, projectsToIgnore, additionalFileMatcher); } +let seq = 0; + async function runTests(config: FullConfigInternal, failedTestIdCollector: Set, projectsToIgnore?: Set, additionalFileMatcher?: Matcher) { - const reporter = new Multiplexer([new WatchModeReporter({ isShowBrowser: () => !!showBrowserServer })]); + ++seq; + const reporter = new Multiplexer([new ListReporter()]); const taskRunner = createTaskRunnerForWatch(config, reporter, projectsToIgnore, additionalFileMatcher); const context: TaskRunnerState = { config, @@ -293,19 +314,28 @@ function readCommand(): ManualPromise { } if (name === 'h') { process.stdout.write(`${separator()} -Watch Usage -${commands.map(i => ' ' + colors.bold(i[0]) + `: ${i[1]}`).join('\n')} +Run tests + ${colors.bold('enter')} ${colors.dim('run tests')} + ${colors.bold('f')} ${colors.dim('run failed tests')} + ${colors.bold('r')} ${colors.dim('repeat last run')} + ${colors.bold('q')} ${colors.dim('quit')} +Change settings + ${colors.bold('c')} ${colors.dim('set project')} + ${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')} `); return; } switch (name) { - case 'a': result.resolve('all'); break; + case 'return': result.resolve('run'); break; + case 'r': result.resolve('repeat'); break; + case 'c': result.resolve('project'); break; case 'p': result.resolve('file'); break; case 't': result.resolve('grep'); break; case 'f': result.resolve('failed'); break; - case 'r': result.resolve('repeat'); break; case 's': result.resolve('toggle-show-browser'); break; } }; @@ -322,6 +352,34 @@ ${commands.map(i => ' ' + colors.bold(i[0]) + `: ${i[1]}`).join('\n')} let showBrowserServer: PlaywrightServer | undefined; +function printConfiguration(config: FullConfigInternal, title?: string) { + const tokens: string[] = []; + tokens.push('npx playwright test'); + tokens.push(...(config._internal.cliProjectFilter || [])?.map(p => colors.blue(`--project ${p}`))); + if (config._internal.cliGrep) + tokens.push(colors.red(`--grep ${config._internal.cliGrep}`)); + if (config._internal.cliArgs) + tokens.push(...config._internal.cliArgs.map(a => colors.bold(a))); + if (title) + tokens.push(colors.dim(`(${title})`)); + if (seq) + tokens.push(colors.dim(`#${seq}`)); + const lines: string[] = []; + const sep = separator(); + lines.push('\x1Bc' + sep); + lines.push(`${tokens.join(' ')}`); + lines.push(`${colors.dim('Show & reuse browser:')} ${colors.bold(showBrowserServer ? 'on' : 'off')}`); + process.stdout.write(lines.join('\n')); +} + +function printPrompt() { + const sep = separator(); + process.stdout.write(` +${sep} +${colors.dim('Waiting for file changes. Press')} ${colors.bold('enter')} ${colors.dim('to run tests')}, ${colors.bold('q')} ${colors.dim('to quit or')} ${colors.bold('h')} ${colors.dim('for more options.')} +`); +} + async function toggleShowBrowser(config: FullConfigInternal, originalWorkers: number) { if (!showBrowserServer) { config.workers = 1; @@ -340,14 +398,4 @@ async function toggleShowBrowser(config: FullConfigInternal, originalWorkers: nu } } -type Command = 'all' | 'failed' | 'repeat' | 'changed' | 'file' | 'grep' | 'exit' | 'interrupted' | 'toggle-show-browser'; - -const commands = [ - ['a', 'rerun all tests'], - ['f', 'rerun only failed tests'], - ['r', 'repeat last run'], - ['p', 'filter by a filename'], - ['t', 'filter by a test name regex pattern'], - ['s', 'toggle show & reuse the browser'], - ['q', 'quit'], -]; +type Command = 'run' | 'failed' | 'repeat' | 'changed' | 'project' | 'file' | 'grep' | 'exit' | 'interrupted' | 'toggle-show-browser';