mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore(watch): print current filters (#20696)
This commit is contained in:
parent
303c5998f8
commit
98e348d16a
@ -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;
|
||||
|
||||
@ -450,8 +450,9 @@ export const baseFullConfig: FullConfigInternal = {
|
||||
maxConcurrentTestGroups: 0,
|
||||
ignoreSnapshots: false,
|
||||
plugins: [],
|
||||
cliTitleMatcher: () => true,
|
||||
cliFileFilters: [],
|
||||
cliArgs: [],
|
||||
cliGrep: undefined,
|
||||
cliGrepInvert: undefined,
|
||||
listOnly: false,
|
||||
}
|
||||
};
|
||||
|
||||
@ -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<FullConfigPublic['webServer'], null>[];
|
||||
plugins: TestRunnerPluginRegistration[];
|
||||
listOnly: boolean;
|
||||
cliFileFilters: TestFileFilter[];
|
||||
cliTitleMatcher: Matcher;
|
||||
cliArgs: string[];
|
||||
cliGrep: string | undefined;
|
||||
cliGrepInvert: string | undefined;
|
||||
cliProjectFilter?: string[];
|
||||
testIdMatcher?: Matcher;
|
||||
passWithNoTests?: boolean;
|
||||
|
||||
@ -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)));
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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<TaskRunnerState> {
|
||||
function createLoadTask(mode: 'out-of-process' | 'in-process', projectsToIgnore = new Set<FullProjectInternal>(), additionalFileMatcher?: Matcher): Task<TaskRunnerState> {
|
||||
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.
|
||||
|
||||
@ -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<string>();
|
||||
@ -61,22 +62,21 @@ class FSWatcher {
|
||||
}
|
||||
}
|
||||
|
||||
export async function runWatchModeLoop(config: FullConfigInternal, failedTests: TestCase[]) {
|
||||
export async function runWatchModeLoop(config: FullConfigInternal, failedTests: TestCase[]): Promise<FullResult['status']> {
|
||||
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<string>, projectClosure: FullProjectInternal[], changedFiles: Set<string>) {
|
||||
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<string>();
|
||||
@ -234,19 +227,24 @@ function readCommand(): ManualPromise<Command> {
|
||||
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'],
|
||||
|
||||
@ -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 || ''));
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user