mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-11 12:33:45 +03:00
This reverts commit 86af908fa7
.
This commit is contained in:
parent
5d3128a595
commit
79d55b959b
@ -146,6 +146,8 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||
let status: FullResult['status'];
|
||||
if (opts.ui)
|
||||
status = await runner.uiAllTests();
|
||||
else if (process.env.PWTEST_WATCH)
|
||||
status = await runner.watchAllTests();
|
||||
else
|
||||
status = await runner.runAllTests();
|
||||
await stopProfiling('runner');
|
||||
|
@ -31,7 +31,7 @@ import type { FullConfigInternal } from '../common/types';
|
||||
import { loadReporter } from './loadUtils';
|
||||
import type { BuiltInReporter } from '../common/configLoader';
|
||||
|
||||
export async function createReporter(config: FullConfigInternal, mode: 'list' | 'run' | 'ui', additionalReporters: Reporter[] = []): Promise<Multiplexer> {
|
||||
export async function createReporter(config: FullConfigInternal, mode: 'list' | 'watch' | 'run' | 'ui', additionalReporters: Reporter[] = []): Promise<Multiplexer> {
|
||||
const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = {
|
||||
dot: mode === 'list' ? ListModeReporter : DotReporter,
|
||||
line: mode === 'list' ? ListModeReporter : LineReporter,
|
||||
@ -43,19 +43,23 @@ export async function createReporter(config: FullConfigInternal, mode: 'list' |
|
||||
html: mode === 'ui' ? LineReporter : HtmlReporter,
|
||||
};
|
||||
const reporters: Reporter[] = [];
|
||||
for (const r of config.reporter) {
|
||||
const [name, arg] = r;
|
||||
if (name in defaultReporters) {
|
||||
reporters.push(new defaultReporters[name as keyof typeof defaultReporters](arg));
|
||||
} else {
|
||||
const reporterConstructor = await loadReporter(config, name);
|
||||
reporters.push(new reporterConstructor(arg));
|
||||
if (mode === 'watch') {
|
||||
reporters.push(new ListReporter());
|
||||
} else {
|
||||
for (const r of config.reporter) {
|
||||
const [name, arg] = r;
|
||||
if (name in defaultReporters) {
|
||||
reporters.push(new defaultReporters[name as keyof typeof defaultReporters](arg));
|
||||
} else {
|
||||
const reporterConstructor = await loadReporter(config, name);
|
||||
reporters.push(new reporterConstructor(arg));
|
||||
}
|
||||
}
|
||||
reporters.push(...additionalReporters);
|
||||
if (process.env.PW_TEST_REPORTER) {
|
||||
const reporterConstructor = await loadReporter(config, process.env.PW_TEST_REPORTER);
|
||||
reporters.push(new reporterConstructor());
|
||||
}
|
||||
}
|
||||
reporters.push(...additionalReporters);
|
||||
if (process.env.PW_TEST_REPORTER) {
|
||||
const reporterConstructor = await loadReporter(config, process.env.PW_TEST_REPORTER);
|
||||
reporters.push(new reporterConstructor());
|
||||
}
|
||||
|
||||
const someReporterPrintsToStdio = reporters.some(r => {
|
||||
|
@ -24,6 +24,7 @@ import { createTaskRunner, createTaskRunnerForList } from './tasks';
|
||||
import type { TaskRunnerState } from './tasks';
|
||||
import type { FullConfigInternal } from '../common/types';
|
||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||
import { runWatchModeLoop } from './watchMode';
|
||||
import { runUIMode } from './uiMode';
|
||||
|
||||
export class Runner {
|
||||
@ -92,6 +93,12 @@ export class Runner {
|
||||
return status;
|
||||
}
|
||||
|
||||
async watchAllTests(): Promise<FullResult['status']> {
|
||||
const config = this._config;
|
||||
webServerPluginsForConfig(config).forEach(p => config._internal.plugins.push({ factory: p }));
|
||||
return await runWatchModeLoop(config);
|
||||
}
|
||||
|
||||
async uiAllTests(): Promise<FullResult['status']> {
|
||||
const config = this._config;
|
||||
webServerPluginsForConfig(config).forEach(p => config._internal.plugins.push({ factory: p }));
|
||||
|
@ -58,13 +58,13 @@ export function createTaskRunner(config: FullConfigInternal, reporter: Multiplex
|
||||
return taskRunner;
|
||||
}
|
||||
|
||||
export function createTaskRunnerForGlobalSetup(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TaskRunnerState> {
|
||||
export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: Multiplexer): TaskRunner<TaskRunnerState> {
|
||||
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, 0);
|
||||
addGlobalSetupTasks(taskRunner, config);
|
||||
return taskRunner;
|
||||
}
|
||||
|
||||
export function createTaskRunnerForUIMode(config: FullConfigInternal, reporter: Multiplexer, projectsToIgnore?: Set<FullProjectInternal>, additionalFileMatcher?: Matcher): TaskRunner<TaskRunnerState> {
|
||||
export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: Multiplexer, projectsToIgnore?: Set<FullProjectInternal>, additionalFileMatcher?: Matcher): TaskRunner<TaskRunnerState> {
|
||||
const taskRunner = new TaskRunner<TaskRunnerState>(reporter, 0);
|
||||
taskRunner.addTask('load tests', createLoadTask('out-of-process', true, projectsToIgnore, additionalFileMatcher));
|
||||
addRunTasks(taskRunner, config);
|
||||
|
@ -24,7 +24,7 @@ import { Multiplexer } from '../reporters/multiplexer';
|
||||
import { TeleReporterEmitter } from '../reporters/teleEmitter';
|
||||
import { createReporter } from './reporters';
|
||||
import type { TaskRunnerState } from './tasks';
|
||||
import { createTaskRunnerForList, createTaskRunnerForUIMode, createTaskRunnerForGlobalSetup } from './tasks';
|
||||
import { createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||
import { chokidar } from '../utilsBundle';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
import { open } from '../utilsBundle';
|
||||
@ -67,7 +67,7 @@ class UIMode {
|
||||
|
||||
async runGlobalSetup(): Promise<FullResult['status']> {
|
||||
const reporter = await createReporter(this._config, 'run');
|
||||
const taskRunner = createTaskRunnerForGlobalSetup(this._config, reporter);
|
||||
const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter);
|
||||
reporter.onConfigure(this._config);
|
||||
const context: TaskRunnerState = {
|
||||
config: this._config,
|
||||
@ -169,7 +169,7 @@ class UIMode {
|
||||
|
||||
const runReporter = new TeleReporterEmitter(e => this._dispatchEvent(e));
|
||||
const reporter = await createReporter(this._config, 'ui', [runReporter]);
|
||||
const taskRunner = createTaskRunnerForUIMode(this._config, reporter);
|
||||
const taskRunner = createTaskRunnerForWatch(this._config, reporter);
|
||||
const context: TaskRunnerState = { config: this._config, reporter, phases: [] };
|
||||
clearCompilationCache();
|
||||
reporter.onConfigure(this._config);
|
||||
|
436
packages/playwright-test/src/runner/watchMode.ts
Normal file
436
packages/playwright-test/src/runner/watchMode.ts
Normal file
@ -0,0 +1,436 @@
|
||||
/**
|
||||
* Copyright Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import readline from 'readline';
|
||||
import { createGuid, ManualPromise } from 'playwright-core/lib/utils';
|
||||
import type { FullConfigInternal, FullProjectInternal } from '../common/types';
|
||||
import { Multiplexer } from '../reporters/multiplexer';
|
||||
import { createFileMatcher, createFileMatcherFromArguments } from '../util';
|
||||
import type { Matcher } from '../util';
|
||||
import { createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||
import type { TaskRunnerState } from './tasks';
|
||||
import { buildProjectsClosure, filterProjects } from './projectUtils';
|
||||
import { clearCompilationCache, collectAffectedTestFiles } from '../common/compilationCache';
|
||||
import type { FullResult } from 'packages/playwright-test/reporter';
|
||||
import { chokidar } from '../utilsBundle';
|
||||
import type { FSWatcher as CFSWatcher } from 'chokidar';
|
||||
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 _dirtyTestFiles = new Map<FullProjectInternal, Set<string>>();
|
||||
private _notifyDirtyFiles: (() => void) | undefined;
|
||||
private _watcher: CFSWatcher | undefined;
|
||||
private _timer: NodeJS.Timeout | undefined;
|
||||
|
||||
async update(config: FullConfigInternal) {
|
||||
const commandLineFileMatcher = config._internal.cliArgs.length ? createFileMatcherFromArguments(config._internal.cliArgs) : () => true;
|
||||
const projects = filterProjects(config.projects, config._internal.cliProjectFilter);
|
||||
const projectClosure = buildProjectsClosure(projects);
|
||||
const projectFilters = new Map<FullProjectInternal, Matcher>();
|
||||
for (const project of projectClosure) {
|
||||
const testMatch = createFileMatcher(project.testMatch);
|
||||
const testIgnore = createFileMatcher(project.testIgnore);
|
||||
projectFilters.set(project, file => {
|
||||
if (!file.startsWith(project.testDir) || !testMatch(file) || testIgnore(file))
|
||||
return false;
|
||||
return project._internal.type === 'dependency' || commandLineFileMatcher(file);
|
||||
});
|
||||
}
|
||||
|
||||
if (this._timer)
|
||||
clearTimeout(this._timer);
|
||||
if (this._watcher)
|
||||
await this._watcher.close();
|
||||
|
||||
this._watcher = chokidar.watch(projectClosure.map(p => p.testDir), { ignoreInitial: true }).on('all', async (event, file) => {
|
||||
if (event !== 'add' && event !== 'change')
|
||||
return;
|
||||
|
||||
const testFiles = new Set<string>();
|
||||
collectAffectedTestFiles(file, testFiles);
|
||||
const testFileArray = [...testFiles];
|
||||
|
||||
let hasMatches = false;
|
||||
for (const [project, filter] of projectFilters) {
|
||||
const filteredFiles = testFileArray.filter(filter);
|
||||
if (!filteredFiles.length)
|
||||
continue;
|
||||
let set = this._dirtyTestFiles.get(project);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
this._dirtyTestFiles.set(project, set);
|
||||
}
|
||||
filteredFiles.map(f => set!.add(f));
|
||||
hasMatches = true;
|
||||
}
|
||||
|
||||
if (!hasMatches)
|
||||
return;
|
||||
|
||||
if (this._timer)
|
||||
clearTimeout(this._timer);
|
||||
this._timer = setTimeout(() => {
|
||||
this._notifyDirtyFiles?.();
|
||||
}, 250);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async onDirtyTestFiles(): Promise<void> {
|
||||
if (this._dirtyTestFiles.size)
|
||||
return;
|
||||
await new Promise<void>(f => this._notifyDirtyFiles = f);
|
||||
}
|
||||
|
||||
takeDirtyTestFiles(): Map<FullProjectInternal, Set<string>> {
|
||||
const result = this._dirtyTestFiles;
|
||||
this._dirtyTestFiles = new Map();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
const reporter = await createReporter(config, 'watch');
|
||||
const context: TaskRunnerState = {
|
||||
config,
|
||||
reporter,
|
||||
phases: [],
|
||||
};
|
||||
const taskRunner = createTaskRunnerForWatchSetup(config, reporter);
|
||||
reporter.onConfigure(config);
|
||||
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(context, 0);
|
||||
if (status !== 'passed')
|
||||
return await globalCleanup();
|
||||
|
||||
// Prepare projects that will be watched, set up watcher.
|
||||
const failedTestIdCollector = new Set<string>();
|
||||
const originalWorkers = config.workers;
|
||||
const fsWatcher = new FSWatcher();
|
||||
await fsWatcher.update(config);
|
||||
|
||||
let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: Set<string>, dirtyTestFiles?: Map<FullProjectInternal, Set<string>> } = { type: 'regular' };
|
||||
let result: FullResult['status'] = 'passed';
|
||||
|
||||
// Enter the watch loop.
|
||||
await runTests(config, failedTestIdCollector);
|
||||
|
||||
while (true) {
|
||||
printPrompt();
|
||||
const readCommandPromise = readCommand();
|
||||
await Promise.race([
|
||||
fsWatcher.onDirtyTestFiles(),
|
||||
readCommandPromise,
|
||||
]);
|
||||
if (!readCommandPromise.isDone())
|
||||
readCommandPromise.resolve('changed');
|
||||
|
||||
const command = await readCommandPromise;
|
||||
|
||||
if (command === 'changed') {
|
||||
const dirtyTestFiles = fsWatcher.takeDirtyTestFiles();
|
||||
// Resolve files that depend on the changed files.
|
||||
await runChangedTests(config, failedTestIdCollector, dirtyTestFiles);
|
||||
lastRun = { type: 'changed', dirtyTestFiles };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'run') {
|
||||
// All means reset filters.
|
||||
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.length ? projectNames : undefined;
|
||||
await fsWatcher.update(config);
|
||||
await runTests(config, failedTestIdCollector);
|
||||
lastRun = { type: 'regular' };
|
||||
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.split(' ');
|
||||
else
|
||||
config._internal.cliArgs = [];
|
||||
await fsWatcher.update(config);
|
||||
await runTests(config, failedTestIdCollector);
|
||||
lastRun = { type: 'regular' };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'grep') {
|
||||
const { testPattern } = await enquirer.prompt<{ testPattern: string }>({
|
||||
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 fsWatcher.update(config);
|
||||
await runTests(config, failedTestIdCollector);
|
||||
lastRun = { type: 'regular' };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'failed') {
|
||||
config._internal.testIdMatcher = id => failedTestIdCollector.has(id);
|
||||
const failedTestIds = new Set(failedTestIdCollector);
|
||||
await runTests(config, failedTestIdCollector, { title: 'running failed tests' });
|
||||
config._internal.testIdMatcher = undefined;
|
||||
lastRun = { type: 'failed', failedTestIds };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'repeat') {
|
||||
if (lastRun.type === 'regular') {
|
||||
await runTests(config, failedTestIdCollector, { title: 're-running tests' });
|
||||
continue;
|
||||
} else if (lastRun.type === 'changed') {
|
||||
await runChangedTests(config, failedTestIdCollector, lastRun.dirtyTestFiles!, 're-running tests');
|
||||
} else if (lastRun.type === 'failed') {
|
||||
config._internal.testIdMatcher = id => lastRun.failedTestIds!.has(id);
|
||||
await runTests(config, failedTestIdCollector, { title: 're-running tests' });
|
||||
config._internal.testIdMatcher = undefined;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'toggle-show-browser') {
|
||||
await toggleShowBrowser(config, originalWorkers);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'exit')
|
||||
break;
|
||||
|
||||
if (command === 'interrupted') {
|
||||
result = 'interrupted';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result === 'passed' ? await globalCleanup() : result;
|
||||
}
|
||||
|
||||
async function runChangedTests(config: FullConfigInternal, failedTestIdCollector: Set<string>, filesByProject: Map<FullProjectInternal, Set<string>>, title?: string) {
|
||||
const testFiles = new Set<string>();
|
||||
for (const files of filesByProject.values())
|
||||
files.forEach(f => testFiles.add(f));
|
||||
|
||||
// Collect all the affected projects, follow project dependencies.
|
||||
// Prepare to exclude all the projects that do not depend on this file, as if they did not exist.
|
||||
const projects = filterProjects(config.projects, config._internal.cliProjectFilter);
|
||||
const projectClosure = buildProjectsClosure(projects);
|
||||
const affectedProjects = affectedProjectsClosure(projectClosure, [...filesByProject.keys()]);
|
||||
const affectsAnyDependency = [...affectedProjects].some(p => p._internal.type === 'dependency');
|
||||
const projectsToIgnore = new Set(projectClosure.filter(p => !affectedProjects.has(p)));
|
||||
|
||||
// If there are affected dependency projects, do the full run, respect the original CLI.
|
||||
// if there are no affected dependency projects, intersect CLI with dirty files
|
||||
const additionalFileMatcher = affectsAnyDependency ? () => true : (file: string) => testFiles.has(file);
|
||||
await runTests(config, failedTestIdCollector, { projectsToIgnore, additionalFileMatcher, title: title || 'files changed' });
|
||||
}
|
||||
|
||||
async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<string>, options?: {
|
||||
projectsToIgnore?: Set<FullProjectInternal>,
|
||||
additionalFileMatcher?: Matcher,
|
||||
title?: string,
|
||||
}) {
|
||||
printConfiguration(config, options?.title);
|
||||
const reporter = new Multiplexer([new ListReporter()]);
|
||||
const taskRunner = createTaskRunnerForWatch(config, reporter, options?.projectsToIgnore, options?.additionalFileMatcher);
|
||||
const context: TaskRunnerState = {
|
||||
config,
|
||||
reporter,
|
||||
phases: [],
|
||||
};
|
||||
clearCompilationCache();
|
||||
reporter.onConfigure(config);
|
||||
const taskStatus = await taskRunner.run(context, 0);
|
||||
let status: FullResult['status'] = 'passed';
|
||||
|
||||
let hasFailedTests = false;
|
||||
for (const test of context.rootSuite?.allTests() || []) {
|
||||
if (test.outcome() === 'unexpected') {
|
||||
failedTestIdCollector.add(test.id);
|
||||
hasFailedTests = true;
|
||||
} else {
|
||||
failedTestIdCollector.delete(test.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.phases.find(p => p.dispatcher.hasWorkerErrors()) || hasFailedTests)
|
||||
status = 'failed';
|
||||
if (status === 'passed' && taskStatus !== 'passed')
|
||||
status = taskStatus;
|
||||
await reporter.onExit({ status });
|
||||
}
|
||||
|
||||
function affectedProjectsClosure(projectClosure: FullProjectInternal[], affected: FullProjectInternal[]): Set<FullProjectInternal> {
|
||||
const result = new Set<FullProjectInternal>(affected);
|
||||
for (let i = 0; i < projectClosure.length; ++i) {
|
||||
for (const p of projectClosure) {
|
||||
for (const dep of p._internal.deps) {
|
||||
if (result.has(dep))
|
||||
result.add(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function readCommand(): ManualPromise<Command> {
|
||||
const result = new ManualPromise<Command>();
|
||||
const rl = readline.createInterface({ input: process.stdin, escapeCodeTimeout: 50 });
|
||||
readline.emitKeypressEvents(process.stdin, rl);
|
||||
if (process.stdin.isTTY)
|
||||
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')) {
|
||||
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') {
|
||||
result.resolve('exit');
|
||||
return;
|
||||
}
|
||||
if (name === 'h') {
|
||||
process.stdout.write(`${separator()}
|
||||
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 '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 's': result.resolve('toggle-show-browser'); break;
|
||||
}
|
||||
};
|
||||
|
||||
process.stdin.on('keypress', handler);
|
||||
result.finally(() => {
|
||||
process.stdin.off('keypress', handler);
|
||||
rl.close();
|
||||
if (process.stdin.isTTY)
|
||||
process.stdin.setRawMode(false);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
let showBrowserServer: PlaywrightServer | undefined;
|
||||
let seq = 0;
|
||||
|
||||
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}`));
|
||||
++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;
|
||||
showBrowserServer = new PlaywrightServer({ path: '/' + createGuid(), maxConnections: 1 });
|
||||
const wsEndpoint = await showBrowserServer.listen();
|
||||
process.env.PW_TEST_REUSE_CONTEXT = '1';
|
||||
process.env.PW_TEST_CONNECT_WS_ENDPOINT = wsEndpoint;
|
||||
process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('on')}\n`);
|
||||
} else {
|
||||
config.workers = originalWorkers;
|
||||
await showBrowserServer?.close();
|
||||
showBrowserServer = undefined;
|
||||
delete process.env.PW_TEST_REUSE_CONTEXT;
|
||||
delete process.env.PW_TEST_CONNECT_WS_ENDPOINT;
|
||||
process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('off')}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
type Command = 'run' | 'failed' | 'repeat' | 'changed' | 'project' | 'file' | 'grep' | 'exit' | 'interrupted' | 'toggle-show-browser';
|
@ -20,7 +20,7 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { rimraf, PNG } from 'playwright-core/lib/utilsBundle';
|
||||
import { promisify } from 'util';
|
||||
import type { CommonFixtures, CommonWorkerFixtures } from '../config/commonFixtures';
|
||||
import type { CommonFixtures, CommonWorkerFixtures, TestChildProcess } from '../config/commonFixtures';
|
||||
import { commonFixtures } from '../config/commonFixtures';
|
||||
import type { ServerFixtures, ServerWorkerOptions } from '../config/serverFixtures';
|
||||
import { serverFixtures } from '../config/serverFixtures';
|
||||
@ -155,6 +155,22 @@ async function runPlaywrightTest(childProcess: CommonFixtures['childProcess'], b
|
||||
};
|
||||
}
|
||||
|
||||
function watchPlaywrightTest(childProcess: CommonFixtures['childProcess'], baseDir: string, env: NodeJS.ProcessEnv, options: RunOptions): TestChildProcess {
|
||||
const args = ['test', '--workers=2'];
|
||||
if (options.additionalArgs)
|
||||
args.push(...options.additionalArgs);
|
||||
const cwd = options.cwd ? path.resolve(baseDir, options.cwd) : baseDir;
|
||||
|
||||
const command = ['node', cliEntrypoint];
|
||||
command.push(...args);
|
||||
const testProcess = childProcess({
|
||||
command,
|
||||
env: cleanEnv({ PWTEST_WATCH: '1', ...env }),
|
||||
cwd,
|
||||
});
|
||||
return testProcess;
|
||||
}
|
||||
|
||||
async function runPlaywrightCommand(childProcess: CommonFixtures['childProcess'], cwd: string, commandWithArguments: string[], env: NodeJS.ProcessEnv, sendSIGINTAfter?: number): Promise<CliRunResult> {
|
||||
const command = ['node', cliEntrypoint];
|
||||
command.push(...commandWithArguments);
|
||||
@ -209,6 +225,7 @@ type Fixtures = {
|
||||
writeFiles: (files: Files) => Promise<string>;
|
||||
deleteFile: (file: string) => Promise<void>;
|
||||
runInlineTest: (files: Files, params?: Params, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<RunResult>;
|
||||
runWatchTest: (files: Files, env?: NodeJS.ProcessEnv, options?: RunOptions) => Promise<TestChildProcess>;
|
||||
runTSC: (files: Files) => Promise<TSCResult>;
|
||||
nodeVersion: { major: number, minor: number, patch: number };
|
||||
};
|
||||
@ -237,6 +254,18 @@ export const test = base
|
||||
await removeFolderAsync(cacheDir);
|
||||
},
|
||||
|
||||
runWatchTest: async ({ childProcess }, use, testInfo: TestInfo) => {
|
||||
const cacheDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'playwright-test-cache-'));
|
||||
let testProcess: TestChildProcess | undefined;
|
||||
await use(async (files: Files, env: NodeJS.ProcessEnv = {}, options: RunOptions = {}) => {
|
||||
const baseDir = await writeFiles(testInfo, files, true);
|
||||
testProcess = watchPlaywrightTest(childProcess, baseDir, { ...env, PWTEST_CACHE_DIR: cacheDir }, options);
|
||||
return testProcess;
|
||||
});
|
||||
await testProcess?.kill();
|
||||
await removeFolderAsync(cacheDir);
|
||||
},
|
||||
|
||||
runTSC: async ({ childProcess }, use, testInfo) => {
|
||||
await use(async files => {
|
||||
const baseDir = await writeFiles(testInfo, { 'tsconfig.json': JSON.stringify(TSCONFIG), ...files }, true);
|
||||
|
@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import { test, expect } from './playwright-test-fixtures';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
@ -95,3 +96,606 @@ test('should print dependencies in ESM mode', async ({ runInlineTest, nodeVersio
|
||||
'b.test.ts': ['helperA.ts', 'helperB.ts'],
|
||||
});
|
||||
});
|
||||
|
||||
test('should perform initial run', async ({ runWatchTest }) => {
|
||||
const testProcess = await runWatchTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should quit on Q', async ({ runWatchTest }) => {
|
||||
const testProcess = await runWatchTest({}, {});
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.write('q');
|
||||
await testProcess!.exited;
|
||||
});
|
||||
|
||||
test('should print help on H', async ({ runWatchTest }) => {
|
||||
const testProcess = await runWatchTest({}, {});
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.write('h');
|
||||
await testProcess.waitForOutput('to quit');
|
||||
});
|
||||
|
||||
test('should run tests on Enter', async ({ runWatchTest }) => {
|
||||
const testProcess = await runWatchTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('\r\n');
|
||||
await testProcess.waitForOutput('npx playwright test #1');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should run tests on R', async ({ runWatchTest }) => {
|
||||
const testProcess = await runWatchTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('r');
|
||||
await testProcess.waitForOutput('npx playwright test (re-running tests) #1');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should run failed tests on F', async ({ runWatchTest }) => {
|
||||
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', () => {});
|
||||
`,
|
||||
'c.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
await testProcess.waitForOutput('Error: expect(received).toBe(expected)');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('f');
|
||||
await testProcess.waitForOutput('npx playwright test (running failed tests) #1');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
||||
});
|
||||
|
||||
test('should respect file filter P', async ({ runWatchTest }) => {
|
||||
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', () => {});
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('p');
|
||||
await testProcess.waitForOutput('Input filename pattern (regex)');
|
||||
testProcess.write('b.test\r\n');
|
||||
await testProcess.waitForOutput('npx playwright test b.test #1');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should respect project filter C', async ({ runWatchTest }) => {
|
||||
const testProcess = await runWatchTest({
|
||||
'playwright.config.ts': `
|
||||
import { defineConfig } from '@playwright/test';
|
||||
export default defineConfig({ projects: [{name: 'foo'}, {name: 'bar'}] });
|
||||
`,
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('[bar] › a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('c');
|
||||
await testProcess.waitForOutput('Select projects');
|
||||
await testProcess.waitForOutput('foo');
|
||||
await testProcess.waitForOutput('bar');
|
||||
testProcess.write(' ');
|
||||
testProcess.write('\r\n');
|
||||
await testProcess.waitForOutput('npx playwright test --project foo #1');
|
||||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('[bar] › a.test.ts:3:11 › passes');
|
||||
});
|
||||
|
||||
test('should respect file filter P and split files', async ({ runWatchTest }) => {
|
||||
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', () => {});
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('p');
|
||||
await testProcess.waitForOutput('Input filename pattern (regex)');
|
||||
testProcess.write('a.test b.test\r\n');
|
||||
await testProcess.waitForOutput('npx playwright test a.test b.test #1');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should respect title filter T', async ({ runWatchTest }) => {
|
||||
const testProcess = await runWatchTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('title 1', () => {});
|
||||
`,
|
||||
'b.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('title 2', () => {});
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › title 1');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › title 2');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('t');
|
||||
await testProcess.waitForOutput('Input test name pattern (regex)');
|
||||
testProcess.write('title 2\r\n');
|
||||
await testProcess.waitForOutput('npx playwright test --grep title 2 #1');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › title 2');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should re-run failed tests on F > R', async ({ runWatchTest }) => {
|
||||
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', () => {});
|
||||
`,
|
||||
'c.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
await testProcess.waitForOutput('Error: expect(received).toBe(expected)');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('f');
|
||||
await testProcess.waitForOutput('npx playwright test (running failed tests) #1');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('r');
|
||||
await testProcess.waitForOutput('npx playwright test (re-running tests) #2');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11');
|
||||
});
|
||||
|
||||
test('should run on changed files', 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', () => {});
|
||||
`,
|
||||
'c.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
await testProcess.waitForOutput('Error: expect(received).toBe(expected)');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'c.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should run on changed deps', async ({ runWatchTest, writeFiles }) => {
|
||||
const testProcess = await runWatchTest({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
'b.test.ts': `
|
||||
import './helper';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
'helper.ts': `
|
||||
console.log('old helper');
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:4:11 › passes');
|
||||
await testProcess.waitForOutput('old helper');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'helper.ts': `
|
||||
console.log('new helper');
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('b.test.ts:4:11 › passes');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('new helper');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should run on changed deps in ESM', async ({ runWatchTest, writeFiles, nodeVersion }) => {
|
||||
test.skip(nodeVersion.major < 16);
|
||||
const testProcess = await runWatchTest({
|
||||
'playwright.config.ts': `export default {};`,
|
||||
'package.json': `{ "type": "module" }`,
|
||||
'a.test.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
'b.test.ts': `
|
||||
import './helper.js';
|
||||
import { test } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
'helper.ts': `
|
||||
console.log('old helper');
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:7 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:4:7 › passes');
|
||||
await testProcess.waitForOutput('old helper');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'helper.ts': `
|
||||
console.log('new helper');
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('b.test.ts:4:7 › passes');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:7 › passes');
|
||||
await testProcess.waitForOutput('new helper');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should re-run changed files on R', 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', () => {});
|
||||
`,
|
||||
'c.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › fails');
|
||||
await testProcess.waitForOutput('Error: expect(received).toBe(expected)');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'c.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
});
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('r');
|
||||
await testProcess.waitForOutput('c.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should not trigger on changes to non-tests', 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', () => {});
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('b.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'helper.ts': `
|
||||
console.log('helper');
|
||||
`,
|
||||
});
|
||||
|
||||
await new Promise(f => setTimeout(f, 1000));
|
||||
expect(testProcess.output).not.toContain('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should only watch selected projects', async ({ runWatchTest, writeFiles }) => {
|
||||
const testProcess = await runWatchTest({
|
||||
'playwright.config.ts': `
|
||||
import { defineConfig } from '@playwright/test';
|
||||
export default defineConfig({ projects: [{name: 'foo'}, {name: 'bar'}] });
|
||||
`,
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
}, {}, { additionalArgs: ['--project=foo'] });
|
||||
await testProcess.waitForOutput('npx playwright test --project foo');
|
||||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('[bar]');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
});
|
||||
|
||||
await testProcess.waitForOutput('npx playwright test --project foo');
|
||||
await testProcess.waitForOutput('[foo] › a.test.ts:3:11 › passes');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
expect(testProcess.output).not.toContain('[bar]');
|
||||
});
|
||||
|
||||
test('should watch filtered files', 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', () => {});
|
||||
`,
|
||||
}, {}, { additionalArgs: ['a.test.ts'] });
|
||||
await testProcess.waitForOutput('npx playwright test a.test.ts');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('b.test');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'b.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
});
|
||||
|
||||
await new Promise(f => setTimeout(f, 1000));
|
||||
expect(testProcess.output).not.toContain('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should not watch unfiltered files', 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', () => {});
|
||||
`,
|
||||
}, {}, { additionalArgs: ['a.test.ts'] });
|
||||
await testProcess.waitForOutput('npx playwright test a.test.ts');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('b.test');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'a.test.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('passes', () => {});
|
||||
`,
|
||||
});
|
||||
|
||||
testProcess.clearOutput();
|
||||
await testProcess.waitForOutput('npx playwright test a.test.ts (files changed)');
|
||||
await testProcess.waitForOutput('a.test.ts:3:11 › passes');
|
||||
expect(testProcess.output).not.toContain('b.test');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should run CT on changed deps', async ({ runWatchTest, writeFiles }) => {
|
||||
const testProcess = await runWatchTest({
|
||||
'playwright.config.ts': `
|
||||
import { defineConfig } from '@playwright/experimental-ct-react';
|
||||
export default defineConfig({ projects: [{name: 'default'}] });
|
||||
`,
|
||||
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
|
||||
'playwright/index.ts': ``,
|
||||
'src/button.tsx': `
|
||||
export const Button = () => <button>Button</button>;
|
||||
`,
|
||||
'src/button.spec.tsx': `
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import { Button } from './button';
|
||||
test('pass', async ({ mount }) => {
|
||||
const component = await mount(<Button></Button>);
|
||||
await expect(component).toHaveText('Button', { timeout: 1000 });
|
||||
});
|
||||
`,
|
||||
'src/link.spec.tsx': `
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
test('pass', async ({ mount }) => {
|
||||
const component = await mount(<a>hello</a>);
|
||||
await expect(component).toHaveText('hello');
|
||||
});
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('button.spec.tsx:4:11 › pass');
|
||||
await testProcess.waitForOutput('link.spec.tsx:3:11 › pass');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'src/button.tsx': `
|
||||
export const Button = () => <button>Button 2</button>;
|
||||
`,
|
||||
});
|
||||
|
||||
await testProcess.waitForOutput(`src${path.sep}button.spec.tsx:4:11 › pass`);
|
||||
expect(testProcess.output).not.toContain(`src${path.sep}link.spec.tsx`);
|
||||
await testProcess.waitForOutput('Error: expect(received).toHaveText(expected)');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should run CT on indirect deps change', async ({ runWatchTest, writeFiles }) => {
|
||||
const testProcess = await runWatchTest({
|
||||
'playwright.config.ts': `
|
||||
import { defineConfig } from '@playwright/experimental-ct-react';
|
||||
export default defineConfig({ projects: [{name: 'default'}] });
|
||||
`,
|
||||
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
|
||||
'playwright/index.ts': ``,
|
||||
'src/button.css': `
|
||||
button { color: red; }
|
||||
`,
|
||||
'src/button.tsx': `
|
||||
import './button.css';
|
||||
export const Button = () => <button>Button</button>;
|
||||
`,
|
||||
'src/button.spec.tsx': `
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import { Button } from './button';
|
||||
test('pass', async ({ mount }) => {
|
||||
const component = await mount(<Button></Button>);
|
||||
await expect(component).toHaveText('Button', { timeout: 1000 });
|
||||
});
|
||||
`,
|
||||
'src/link.spec.tsx': `
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
test('pass', async ({ mount }) => {
|
||||
const component = await mount(<a>hello</a>);
|
||||
await expect(component).toHaveText('hello');
|
||||
});
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('button.spec.tsx:4:11 › pass');
|
||||
await testProcess.waitForOutput('link.spec.tsx:3:11 › pass');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'src/button.css': `
|
||||
button { color: blue; }
|
||||
`,
|
||||
});
|
||||
|
||||
await testProcess.waitForOutput(`src${path.sep}button.spec.tsx:4:11 › pass`);
|
||||
expect(testProcess.output).not.toContain(`src${path.sep}link.spec.tsx`);
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
||||
test('should run CT on indirect deps change ESM mode', async ({ runWatchTest, writeFiles, nodeVersion }) => {
|
||||
test.skip(nodeVersion.major < 16);
|
||||
const testProcess = await runWatchTest({
|
||||
'playwright.config.ts': `
|
||||
import { defineConfig } from '@playwright/experimental-ct-react';
|
||||
export default defineConfig({ projects: [{name: 'default'}] });
|
||||
`,
|
||||
'package.json': `{ "type": "module" }`,
|
||||
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
|
||||
'playwright/index.ts': ``,
|
||||
'src/button.css': `
|
||||
button { color: red; }
|
||||
`,
|
||||
'src/button.tsx': `
|
||||
import './button.css';
|
||||
export const Button = () => <button>Button</button>;
|
||||
`,
|
||||
'src/button.spec.tsx': `
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import { Button } from './button.jsx';
|
||||
test('pass', async ({ mount }) => {
|
||||
const component = await mount(<Button></Button>);
|
||||
await expect(component).toHaveText('Button', { timeout: 1000 });
|
||||
});
|
||||
`,
|
||||
'src/link.spec.tsx': `
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
test('pass', async ({ mount }) => {
|
||||
const component = await mount(<a>hello</a>);
|
||||
await expect(component).toHaveText('hello');
|
||||
});
|
||||
`,
|
||||
}, {});
|
||||
await testProcess.waitForOutput('button.spec.tsx:4:7 › pass');
|
||||
await testProcess.waitForOutput('link.spec.tsx:3:7 › pass');
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
await writeFiles({
|
||||
'src/button.css': `
|
||||
button { color: blue; }
|
||||
`,
|
||||
});
|
||||
|
||||
await testProcess.waitForOutput(`src${path.sep}button.spec.tsx:4:7 › pass`);
|
||||
expect(testProcess.output).not.toContain(`src${path.sep}link.spec.tsx`);
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user