chore: set filters and run tests separately (#20759)

This commit is contained in:
Pavel Feldman 2023-02-08 14:30:53 -08:00 committed by GitHub
parent 6e5964cccd
commit 027d6b5239
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 89 additions and 71 deletions

View file

@ -30,4 +30,4 @@ import stoppableLibrary from 'stoppable';
export const stoppable = stoppableLibrary; export const stoppable = stoppableLibrary;
import enquirerLibrary from 'enquirer'; import enquirerLibrary from 'enquirer';
export const enquirer = enquirerLibrary; export const enquirer = enquirerLibrary;

View file

@ -16,7 +16,7 @@
import path from 'path'; import path from 'path';
import type { Reporter, TestError } from '../../types/testReporter'; import type { Reporter, TestError } from '../../types/testReporter';
import { separator, formatError } from '../reporters/base'; import { formatError } from '../reporters/base';
import DotReporter from '../reporters/dot'; import DotReporter from '../reporters/dot';
import EmptyReporter from '../reporters/empty'; import EmptyReporter from '../reporters/empty';
import GitHubReporter from '../reporters/github'; import GitHubReporter from '../reporters/github';
@ -30,7 +30,6 @@ import type { Suite } from '../common/test';
import type { FullConfigInternal } from '../common/types'; import type { FullConfigInternal } from '../common/types';
import { loadReporter } from './loadUtils'; import { loadReporter } from './loadUtils';
import type { BuiltInReporter } from '../common/configLoader'; import type { BuiltInReporter } from '../common/configLoader';
import { colors } from 'playwright-core/lib/utilsBundle';
export async function createReporter(config: FullConfigInternal, mode: 'list' | 'watch' | 'run') { export async function createReporter(config: FullConfigInternal, mode: 'list' | 'watch' | 'run') {
const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = { const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = {
@ -45,7 +44,7 @@ export async function createReporter(config: FullConfigInternal, mode: 'list' |
}; };
const reporters: Reporter[] = []; const reporters: Reporter[] = [];
if (mode === 'watch') { if (mode === 'watch') {
reporters.push(new WatchModeReporter()); reporters.push(new ListReporter());
} else { } else {
for (const r of config.reporter) { for (const r of config.reporter) {
const [name, arg] = r; const [name, arg] = r;
@ -104,32 +103,3 @@ export class ListModeReporter implements Reporter {
console.error('\n' + formatError(this.config, error, false).message); 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');
}
}

View file

@ -26,11 +26,12 @@ import { buildProjectsClosure, filterProjects } from './projectUtils';
import { clearCompilationCache, collectAffectedTestFiles } from '../common/compilationCache'; import { clearCompilationCache, collectAffectedTestFiles } from '../common/compilationCache';
import type { FullResult } from 'packages/playwright-test/reporter'; import type { FullResult } from 'packages/playwright-test/reporter';
import chokidar from 'chokidar'; import chokidar from 'chokidar';
import { createReporter, WatchModeReporter } from './reporters'; import { createReporter } from './reporters';
import { colors } from 'playwright-core/lib/utilsBundle'; import { colors } from 'playwright-core/lib/utilsBundle';
import { enquirer } from '../utilsBundle'; import { enquirer } from '../utilsBundle';
import { separator } from '../reporters/base'; import { separator } from '../reporters/base';
import { PlaywrightServer } from 'playwright-core/lib/remote/playwrightServer'; import { PlaywrightServer } from 'playwright-core/lib/remote/playwrightServer';
import ListReporter from '../reporters/list';
class FSWatcher { class FSWatcher {
private _dirtyFiles = new Set<string>(); private _dirtyFiles = new Set<string>();
@ -64,6 +65,11 @@ class FSWatcher {
} }
export async function runWatchModeLoop(config: FullConfigInternal): Promise<FullResult['status']> { export async function runWatchModeLoop(config: FullConfigInternal): Promise<FullResult['status']> {
// 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. // Perform global setup.
const reporter = await createReporter(config, 'watch'); const reporter = await createReporter(config, 'watch');
const context: TaskRunnerState = { const context: TaskRunnerState = {
@ -77,26 +83,20 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
if (status !== 'passed') if (status !== 'passed')
return await globalCleanup(); return await globalCleanup();
// Prepare projects that will be watched, set up watcher.
const projects = filterProjects(config.projects, config._internal.cliProjectFilter); const projects = filterProjects(config.projects, config._internal.cliProjectFilter);
const projectClosure = buildProjectsClosure(projects); const projectClosure = buildProjectsClosure(projects);
config._internal.passWithNoTests = true;
const failedTestIdCollector = new Set<string>(); const failedTestIdCollector = new Set<string>();
const originalCliArgs = config._internal.cliArgs;
const originalCliGrep = config._internal.cliGrep;
const originalWorkers = config.workers; const originalWorkers = config.workers;
const fsWatcher = new FSWatcher(projectClosure.map(p => p.testDir));
let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: Set<string>, dirtyFiles?: Set<string> } = { type: 'regular' }; let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: Set<string>, dirtyFiles?: Set<string> } = { type: 'regular' };
let result: FullResult['status'] = 'passed'; let result: FullResult['status'] = 'passed';
const fsWatcher = new FSWatcher(projectClosure.map(p => p.testDir)); // Enter the watch loop.
printConfiguration(config);
while (true) { while (true) {
const sep = separator(); printPrompt();
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.
`);
const readCommandPromise = readCommand(); const readCommandPromise = readCommand();
await Promise.race([ await Promise.race([
fsWatcher.onDirtyFiles(), fsWatcher.onDirtyFiles(),
@ -109,32 +109,47 @@ Waiting for file changes. Press ${colors.bold('a')} to run all, ${colors.bold('q
if (command === 'changed') { if (command === 'changed') {
const dirtyFiles = fsWatcher.takeDirtyFiles(); const dirtyFiles = fsWatcher.takeDirtyFiles();
printConfiguration(config, 'files changed');
await runChangedTests(config, failedTestIdCollector, projectClosure, dirtyFiles); await runChangedTests(config, failedTestIdCollector, projectClosure, dirtyFiles);
lastRun = { type: 'changed', dirtyFiles }; lastRun = { type: 'changed', dirtyFiles };
continue; continue;
} }
if (command === 'all') { if (command === 'run') {
// All means reset filters. // All means reset filters.
config._internal.cliArgs = originalCliArgs; printConfiguration(config);
config._internal.cliGrep = originalCliGrep;
await runTests(config, failedTestIdCollector); await runTests(config, failedTestIdCollector);
lastRun = { type: 'regular' }; lastRun = { type: 'regular' };
continue; 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') { if (command === 'file') {
const { filePattern } = await enquirer.prompt<{ filePattern: string }>({ const { filePattern } = await enquirer.prompt<{ filePattern: string }>({
type: 'text', type: 'text',
name: 'filePattern', name: 'filePattern',
message: 'Input filename pattern (regex)', message: 'Input filename pattern (regex)',
}); }).catch(() => ({ filePattern: null }));
if (filePattern === null)
continue;
if (filePattern.trim()) if (filePattern.trim())
config._internal.cliArgs = [filePattern]; config._internal.cliArgs = filePattern.split(' ');
else else
config._internal.cliArgs = []; config._internal.cliArgs = [];
await runTests(config, failedTestIdCollector); printConfiguration(config);
lastRun = { type: 'regular' };
continue; continue;
} }
@ -143,19 +158,21 @@ Waiting for file changes. Press ${colors.bold('a')} to run all, ${colors.bold('q
type: 'text', type: 'text',
name: 'testPattern', name: 'testPattern',
message: 'Input test name pattern (regex)', message: 'Input test name pattern (regex)',
}); }).catch(() => ({ testPattern: null }));
if (testPattern === null)
continue;
if (testPattern.trim()) if (testPattern.trim())
config._internal.cliGrep = testPattern; config._internal.cliGrep = testPattern;
else else
config._internal.cliGrep = undefined; config._internal.cliGrep = undefined;
await runTests(config, failedTestIdCollector); printConfiguration(config);
lastRun = { type: 'regular' };
continue; continue;
} }
if (command === 'failed') { if (command === 'failed') {
config._internal.testIdMatcher = id => failedTestIdCollector.has(id); config._internal.testIdMatcher = id => failedTestIdCollector.has(id);
const failedTestIds = new Set(failedTestIdCollector); const failedTestIds = new Set(failedTestIdCollector);
printConfiguration(config, 'running failed tests');
await runTests(config, failedTestIdCollector); await runTests(config, failedTestIdCollector);
config._internal.testIdMatcher = undefined; config._internal.testIdMatcher = undefined;
lastRun = { type: 'failed', failedTestIds }; 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') { if (command === 'repeat') {
printConfiguration(config, 're-running tests');
if (lastRun.type === 'regular') { if (lastRun.type === 'regular') {
await runTests(config, failedTestIdCollector); await runTests(config, failedTestIdCollector);
continue; continue;
@ -227,8 +245,11 @@ async function runChangedTests(config: FullConfigInternal, failedTestIdCollector
return await runTests(config, failedTestIdCollector, projectsToIgnore, additionalFileMatcher); return await runTests(config, failedTestIdCollector, projectsToIgnore, additionalFileMatcher);
} }
let seq = 0;
async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<string>, projectsToIgnore?: Set<FullProjectInternal>, additionalFileMatcher?: Matcher) { async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<string>, projectsToIgnore?: Set<FullProjectInternal>, 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 taskRunner = createTaskRunnerForWatch(config, reporter, projectsToIgnore, additionalFileMatcher);
const context: TaskRunnerState = { const context: TaskRunnerState = {
config, config,
@ -293,19 +314,28 @@ function readCommand(): ManualPromise<Command> {
} }
if (name === 'h') { if (name === 'h') {
process.stdout.write(`${separator()} process.stdout.write(`${separator()}
Watch Usage Run tests
${commands.map(i => ' ' + colors.bold(i[0]) + `: ${i[1]}`).join('\n')} ${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; return;
} }
switch (name) { 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 'p': result.resolve('file'); break;
case 't': result.resolve('grep'); break; case 't': result.resolve('grep'); break;
case 'f': result.resolve('failed'); break; case 'f': result.resolve('failed'); break;
case 'r': result.resolve('repeat'); break;
case 's': result.resolve('toggle-show-browser'); 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; 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) { async function toggleShowBrowser(config: FullConfigInternal, originalWorkers: number) {
if (!showBrowserServer) { if (!showBrowserServer) {
config.workers = 1; 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'; type Command = 'run' | 'failed' | 'repeat' | 'changed' | 'project' | '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'],
];