From 98e348d16a0f79688b8ba55148fbb4eb780f6295 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 7 Feb 2023 09:48:46 -0800 Subject: [PATCH] chore(watch): print current filters (#20696) --- packages/playwright-test/src/cli.ts | 10 +-- .../src/common/configLoader.ts | 5 +- packages/playwright-test/src/common/types.ts | 7 +- .../playwright-test/src/reporters/base.ts | 9 ++- .../playwright-test/src/runner/loadUtils.ts | 10 ++- .../playwright-test/src/runner/reporters.ts | 23 +++++- packages/playwright-test/src/runner/runner.ts | 2 +- packages/playwright-test/src/runner/tasks.ts | 4 +- .../playwright-test/src/runner/watchMode.ts | 76 +++++++++---------- packages/playwright-test/src/util.ts | 19 +++-- 10 files changed, 97 insertions(+), 68 deletions(-) diff --git a/packages/playwright-test/src/cli.ts b/packages/playwright-test/src/cli.ts index 556e6d78c6..f880bfe1a0 100644 --- a/packages/playwright-test/src/cli.ts +++ b/packages/playwright-test/src/cli.ts @@ -21,8 +21,7 @@ import fs from 'fs'; import path from 'path'; import { Runner } from './runner/runner'; import { stopProfiling, startProfiling } from './common/profiler'; -import { createFileFilterForArg, experimentalLoaderOption, fileIsModule, forceRegExp } from './util'; -import { createTitleMatcher } from './util'; +import { experimentalLoaderOption, fileIsModule } from './util'; import { showHTMLReport } from './reporters/html'; import { baseFullConfig, builtInReporters, ConfigLoader, defaultTimeout, kDefaultConfigFiles, resolveConfigFile } from './common/configLoader'; import type { TraceMode } from './common/types'; @@ -160,10 +159,9 @@ async function runTests(args: string[], opts: { [key: string]: any }) { configLoader.ignoreProjectDependencies(); const config = configLoader.fullConfig(); - config._internal.cliFileFilters = args.map(arg => createFileFilterForArg(arg)); - const grepMatcher = opts.grep ? createTitleMatcher(forceRegExp(opts.grep)) : () => true; - const grepInvertMatcher = opts.grepInvert ? createTitleMatcher(forceRegExp(opts.grepInvert)) : () => false; - config._internal.cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title); + config._internal.cliArgs = args; + config._internal.cliGrep = opts.grep as string | undefined; + config._internal.cliGrepInvert = opts.grepInvert as string | undefined; config._internal.listOnly = !!opts.list; config._internal.cliProjectFilter = opts.project || undefined; config._internal.passWithNoTests = !!opts.passWithNoTests; diff --git a/packages/playwright-test/src/common/configLoader.ts b/packages/playwright-test/src/common/configLoader.ts index 87ffb6473c..797f70e480 100644 --- a/packages/playwright-test/src/common/configLoader.ts +++ b/packages/playwright-test/src/common/configLoader.ts @@ -450,8 +450,9 @@ export const baseFullConfig: FullConfigInternal = { maxConcurrentTestGroups: 0, ignoreSnapshots: false, plugins: [], - cliTitleMatcher: () => true, - cliFileFilters: [], + cliArgs: [], + cliGrep: undefined, + cliGrepInvert: undefined, listOnly: false, } }; diff --git a/packages/playwright-test/src/common/types.ts b/packages/playwright-test/src/common/types.ts index 087a3e0559..ce9ecbd0bb 100644 --- a/packages/playwright-test/src/common/types.ts +++ b/packages/playwright-test/src/common/types.ts @@ -17,7 +17,7 @@ import type { Fixtures, TestInfoError, Project } from '../../types/test'; import type { Location } from '../../types/testReporter'; import type { TestRunnerPluginRegistration } from '../plugins'; -import type { Matcher, TestFileFilter } from '../util'; +import type { Matcher } from '../util'; import type { ConfigCLIOverrides } from './ipc'; import type { FullConfig as FullConfigPublic, FullProject as FullProjectPublic } from './types'; export * from '../../types/test'; @@ -50,8 +50,9 @@ type ConfigInternal = { webServers: Exclude[]; plugins: TestRunnerPluginRegistration[]; listOnly: boolean; - cliFileFilters: TestFileFilter[]; - cliTitleMatcher: Matcher; + cliArgs: string[]; + cliGrep: string | undefined; + cliGrepInvert: string | undefined; cliProjectFilter?: string[]; testIdMatcher?: Matcher; passWithNoTests?: boolean; diff --git a/packages/playwright-test/src/reporters/base.ts b/packages/playwright-test/src/reporters/base.ts index 93e209f5c1..0e778d5fd1 100644 --- a/packages/playwright-test/src/reporters/base.ts +++ b/packages/playwright-test/src/reporters/base.ts @@ -231,10 +231,8 @@ export class BaseReporter implements Reporter { } private _printSummary(summary: string) { - if (summary.trim()) { - console.log(''); + if (summary.trim()) console.log(summary); - } } willRetry(test: TestCase): boolean { @@ -487,3 +485,8 @@ function fitToWidth(line: string, width: number, prefix?: string): string { function belongsToNodeModules(file: string) { return file.includes(`${path.sep}node_modules${path.sep}`); } + +export function separator(): string { + const columns = process.stdout?.columns || 30; + return colors.dim('⎯'.repeat(Math.min(100, columns))); +} diff --git a/packages/playwright-test/src/runner/loadUtils.ts b/packages/playwright-test/src/runner/loadUtils.ts index 990e29c520..2fe2002418 100644 --- a/packages/playwright-test/src/runner/loadUtils.ts +++ b/packages/playwright-test/src/runner/loadUtils.ts @@ -21,7 +21,7 @@ import type { LoaderHost } from './loaderHost'; import { Suite } from '../common/test'; import type { TestCase } from '../common/test'; import type { FullConfigInternal, FullProjectInternal } from '../common/types'; -import { createTitleMatcher, errorWithFile } from '../util'; +import { createFileFiltersFromArguments, createTitleMatcher, errorWithFile, forceRegExp } from '../util'; import type { Matcher, TestFileFilter } from '../util'; import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils'; import { requireOrImport } from '../common/transform'; @@ -98,9 +98,15 @@ export async function loadAllTests(mode: 'out-of-process' | 'in-process', config // Create root suites with clones for the projects. const rootSuite = new Suite('', 'root'); + // Interpret cli parameters. + const cliFileFilters = createFileFiltersFromArguments(config._internal.cliArgs); + const grepMatcher = config._internal.cliGrep ? createTitleMatcher(forceRegExp(config._internal.cliGrep)) : () => true; + const grepInvertMatcher = config._internal.cliGrepInvert ? createTitleMatcher(forceRegExp(config._internal.cliGrepInvert)) : () => false; + const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title); + // First iterate leaf projects to focus only, then add all other projects. for (const project of topLevelProjects) { - const projectSuite = await createProjectSuite(fileSuits, project, config._internal, filesToRunByProject.get(project)!); + const projectSuite = await createProjectSuite(fileSuits, project, { cliFileFilters, cliTitleMatcher, testIdMatcher: config._internal.testIdMatcher }, filesToRunByProject.get(project)!); if (projectSuite) rootSuite._addSuite(projectSuite); } diff --git a/packages/playwright-test/src/runner/reporters.ts b/packages/playwright-test/src/runner/reporters.ts index d164ba61ed..1de20189c8 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 { formatError } from '../reporters/base'; +import { separator, formatError } from '../reporters/base'; import DotReporter from '../reporters/dot'; import EmptyReporter from '../reporters/empty'; import GitHubReporter from '../reporters/github'; @@ -30,6 +30,7 @@ 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} = { @@ -104,5 +105,23 @@ export class ListModeReporter implements Reporter { } } -export class WatchModeReporter extends LineReporter { +let seq = 0; + +export class WatchModeReporter extends ListReporter { + 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(' ')}`); + lines.push(sep + super.generateStartingMessage()); + return lines.join('\n'); + } } diff --git a/packages/playwright-test/src/runner/runner.ts b/packages/playwright-test/src/runner/runner.ts index 9b5e25e806..90ccfa461d 100644 --- a/packages/playwright-test/src/runner/runner.ts +++ b/packages/playwright-test/src/runner/runner.ts @@ -86,7 +86,7 @@ export class Runner { await reporter.onExit({ status }); if (watchMode) - await runWatchModeLoop(config, failedTests); + status = await runWatchModeLoop(config, failedTests); // Calling process.exit() might truncate large stdout/stderr output. // See https://github.com/nodejs/node/issues/6456. diff --git a/packages/playwright-test/src/runner/tasks.ts b/packages/playwright-test/src/runner/tasks.ts index fc1ea223f9..3cf6149d1e 100644 --- a/packages/playwright-test/src/runner/tasks.ts +++ b/packages/playwright-test/src/runner/tasks.ts @@ -28,7 +28,7 @@ import { TaskRunner } from './taskRunner'; import type { Suite } from '../common/test'; import type { FullConfigInternal, FullProjectInternal } from '../common/types'; import { loadAllTests, loadGlobalHook } from './loadUtils'; -import { createFileMatcherFromFilters } from '../util'; +import { createFileMatcherFromArguments } from '../util'; import type { Matcher } from '../util'; const removeFolderAsync = promisify(rimraf); @@ -149,7 +149,7 @@ function createRemoveOutputDirsTask(): Task { function createLoadTask(mode: 'out-of-process' | 'in-process', projectsToIgnore = new Set(), additionalFileMatcher?: Matcher): Task { return async (context, errors) => { const { config } = context; - const cliMatcher = config._internal.cliFileFilters.length ? createFileMatcherFromFilters(config._internal.cliFileFilters) : () => true; + const cliMatcher = config._internal.cliArgs.length ? createFileMatcherFromArguments(config._internal.cliArgs) : () => true; const fileMatcher = (value: string) => cliMatcher(value) && (additionalFileMatcher ? additionalFileMatcher(value) : true); context.rootSuite = await loadAllTests(mode, config, projectsToIgnore, fileMatcher, errors); // Fail when no tests. diff --git a/packages/playwright-test/src/runner/watchMode.ts b/packages/playwright-test/src/runner/watchMode.ts index 130c561a48..e87b022b42 100644 --- a/packages/playwright-test/src/runner/watchMode.ts +++ b/packages/playwright-test/src/runner/watchMode.ts @@ -18,7 +18,7 @@ import readline from 'readline'; import { ManualPromise } from 'playwright-core/lib/utils'; import type { FullConfigInternal, FullProjectInternal } from '../common/types'; import { Multiplexer } from '../reporters/multiplexer'; -import { createFileFilterForArg, createFileMatcherFromFilters, createTitleMatcher, forceRegExp } from '../util'; +import { createFileMatcherFromArguments } from '../util'; import type { Matcher } from '../util'; import { createTaskRunnerForWatch } from './tasks'; import type { TaskRunnerState } from './tasks'; @@ -29,6 +29,7 @@ import chokidar from 'chokidar'; import { WatchModeReporter } from './reporters'; import { colors } from 'playwright-core/lib/utilsBundle'; import { enquirer } from '../utilsBundle'; +import { separator } from '../reporters/base'; class FSWatcher { private _dirtyFiles = new Set(); @@ -61,22 +62,21 @@ class FSWatcher { } } -export async function runWatchModeLoop(config: FullConfigInternal, failedTests: TestCase[]) { +export async function runWatchModeLoop(config: FullConfigInternal, failedTests: TestCase[]): Promise { const projects = filterProjects(config.projects, config._internal.cliProjectFilter); const projectClosure = buildProjectsClosure(projects); config._internal.passWithNoTests = true; const failedTestIdCollector = new Set(failedTests.map(t => t.id)); - const originalTitleMatcher = config._internal.cliTitleMatcher; - const originalFileFilters = config._internal.cliFileFilters; + const originalCliArgs = config._internal.cliArgs; + const originalCliGrep = config._internal.cliGrep; const fsWatcher = new FSWatcher(projectClosure.map(p => p.testDir)); - let lastFilePattern: string | undefined; - let lastTestPattern: string | undefined; while (true) { + const sep = separator(); process.stdout.write(` -Waiting for file changes... -${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')} ${colors.bold('q')} ${colors.dim('to quit')} +${sep} +Waiting for file changes. Press ${colors.bold('h')} for help or ${colors.bold('q')} to quit. `); const readCommandPromise = readCommand(); await Promise.race([ @@ -88,17 +88,13 @@ ${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')} const command = await readCommandPromise; if (command === 'changed') { - process.stdout.write('\x1Bc'); await runChangedTests(config, failedTestIdCollector, projectClosure, fsWatcher.takeDirtyFiles()); continue; } if (command === 'all') { - process.stdout.write('\x1Bc'); // All means reset filters. - config._internal.cliTitleMatcher = originalTitleMatcher; - config._internal.cliFileFilters = originalFileFilters; - lastFilePattern = undefined; - lastTestPattern = undefined; + config._internal.cliArgs = originalCliArgs; + config._internal.cliGrep = originalCliGrep; await runTests(config, failedTestIdCollector); continue; } @@ -107,15 +103,12 @@ ${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')} type: 'text', name: 'filePattern', message: 'Input filename pattern (regex)', - initial: lastFilePattern, + initial: config._internal.cliArgs.join(' '), }); - if (filePattern.trim()) { - lastFilePattern = filePattern; - config._internal.cliFileFilters = [createFileFilterForArg(filePattern)]; - } else { - lastFilePattern = undefined; - config._internal.cliFileFilters = originalFileFilters; - } + if (filePattern.trim()) + config._internal.cliArgs = [filePattern]; + else + config._internal.cliArgs = []; await runTests(config, failedTestIdCollector); continue; } @@ -124,20 +117,16 @@ ${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')} type: 'text', name: 'testPattern', message: 'Input test name pattern (regex)', - initial: lastTestPattern, + initial: config._internal.cliGrep, }); - if (testPattern.trim()) { - lastTestPattern = testPattern; - config._internal.cliTitleMatcher = createTitleMatcher(forceRegExp(testPattern)); - } else { - lastTestPattern = undefined; - config._internal.cliTitleMatcher = originalTitleMatcher; - } + if (testPattern.trim()) + config._internal.cliGrep = testPattern; + else + config._internal.cliGrep = undefined; await runTests(config, failedTestIdCollector); continue; } if (command === 'failed') { - process.stdout.write('\x1Bc'); config._internal.testIdMatcher = id => failedTestIdCollector.has(id); try { await runTests(config, failedTestIdCollector); @@ -146,11 +135,15 @@ ${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')} } continue; } + if (command === 'exit') + return 'passed'; + if (command === 'interrupted') + return 'interrupted'; } } async function runChangedTests(config: FullConfigInternal, failedTestIdCollector: Set, projectClosure: FullProjectInternal[], changedFiles: Set) { - const commandLineFileMatcher = config._internal.cliFileFilters.length ? createFileMatcherFromFilters(config._internal.cliFileFilters) : () => true; + const commandLineFileMatcher = config._internal.cliArgs.length ? createFileMatcherFromArguments(config._internal.cliArgs) : () => true; // Resolve files that depend on the changed files. const testFiles = new Set(); @@ -234,19 +227,24 @@ function readCommand(): ManualPromise { 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')) - return process.exit(130); + 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') { process.kill(process.ppid, 'SIGTSTP'); process.kill(process.pid, 'SIGTSTP'); } const name = key?.name; - if (name === 'q') - process.exit(0); + if (name === 'q') { + result.resolve('exit'); + return; + } if (name === 'h') { - process.stdout.write(` + process.stdout.write(`${separator()} Watch Usage -${commands.map(i => colors.dim(' press ') + colors.reset(colors.bold(i[0])) + colors.dim(` to ${i[1]}`)).join('\n')} +${commands.map(i => ' ' + colors.bold(i[0]) + `: ${i[1]}`).join('\n')} + `); return; } @@ -269,7 +267,7 @@ ${commands.map(i => colors.dim(' press ') + colors.reset(colors.bold(i[0])) + c return result; } -type Command = 'all' | 'failed' | 'changed' | 'file' | 'grep'; +type Command = 'all' | 'failed' | 'changed' | 'file' | 'grep' | 'exit' | 'interrupted'; const commands = [ ['a', 'rerun all tests'], diff --git a/packages/playwright-test/src/util.ts b/packages/playwright-test/src/util.ts index fae89f2ae7..6ec4d14c74 100644 --- a/packages/playwright-test/src/util.ts +++ b/packages/playwright-test/src/util.ts @@ -106,16 +106,19 @@ export type TestFileFilter = { column: number | null; }; -export function createFileFilterForArg(arg: string): TestFileFilter { - const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg); - return { - re: forceRegExp(match ? match[1] : arg), - line: match ? parseInt(match[2], 10) : null, - column: match?.[3] ? parseInt(match[3], 10) : null, - }; +export function createFileFiltersFromArguments(args: string[]): TestFileFilter[] { + return args.map(arg => { + const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg); + return { + re: forceRegExp(match ? match[1] : arg), + line: match ? parseInt(match[2], 10) : null, + column: match?.[3] ? parseInt(match[3], 10) : null, + }; + }); } -export function createFileMatcherFromFilters(filters: TestFileFilter[]): Matcher { +export function createFileMatcherFromArguments(args: string[]): Matcher { + const filters = createFileFiltersFromArguments(args); return createFileMatcher(filters.map(filter => filter.re || filter.exact || '')); }