chore(test runner): refactor watch mode onto testserver
This commit is contained in:
parent
1b220c5289
commit
068379aaa2
|
|
@ -203,10 +203,13 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
|||
|
||||
const runner = new Runner(config);
|
||||
let status: FullResult['status'];
|
||||
if (process.env.PWTEST_WATCH)
|
||||
if (process.env.PWTEST_WATCH) {
|
||||
if (opts.onlyChanged)
|
||||
throw new Error(`--only-changed is not supported in watch mode. If you'd like that to change, file an issue and let us know about your usecase for it.`);
|
||||
status = await runner.watchAllTests();
|
||||
else
|
||||
} else {
|
||||
status = await runner.runAllTests();
|
||||
}
|
||||
await stopProfiling('runner');
|
||||
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
|
||||
gracefullyProcessExitDoNotHang(exitCode);
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@
|
|||
../utilsBundle.ts
|
||||
../isomorphic/folders.ts
|
||||
../isomorphic/teleReceiver.ts
|
||||
../isomorphic/testServerConnection.ts
|
||||
../fsWatcher.ts
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import { sourceMapSupport } from '../utilsBundle';
|
|||
import type { RawSourceMap } from 'source-map';
|
||||
|
||||
|
||||
export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTestsOutsideProjectFilter: boolean, additionalFileMatcher?: Matcher) {
|
||||
export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTestsOutsideProjectFilter: boolean) {
|
||||
const config = testRun.config;
|
||||
const fsCache = new Map();
|
||||
const sourceMapCache = new Map();
|
||||
|
|
@ -52,8 +52,6 @@ export async function collectProjectsAndTestFiles(testRun: TestRun, doNotRunTest
|
|||
for (const [project, files] of allFilesForProject) {
|
||||
const matchedFiles = files.filter(file => {
|
||||
const hasMatchingSources = sourceMapSources(file, sourceMapCache).some(source => {
|
||||
if (additionalFileMatcher && !additionalFileMatcher(source))
|
||||
return false;
|
||||
if (cliFileMatcher && !cliFileMatcher(source))
|
||||
return false;
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -134,7 +134,10 @@ export class Runner {
|
|||
async watchAllTests(): Promise<FullResult['status']> {
|
||||
const config = this._config;
|
||||
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
|
||||
return await runWatchModeLoop(config);
|
||||
return await runWatchModeLoop(
|
||||
{ configDir: config.configDir, resolvedConfigFile: config.config.configFile },
|
||||
{ projects: config.cliProjectFilter, files: config.cliArgs, grep: config.cliGrep, onlyChanged: config.cliOnlyChanged }
|
||||
);
|
||||
}
|
||||
|
||||
async findRelatedTestFiles(mode: 'in-process' | 'out-of-process', files: string[]): Promise<FindRelatedTestFilesReport> {
|
||||
|
|
|
|||
|
|
@ -74,13 +74,6 @@ export function createTaskRunnerForWatchSetup(config: FullConfigInternal, report
|
|||
return taskRunner;
|
||||
}
|
||||
|
||||
export function createTaskRunnerForWatch(config: FullConfigInternal, reporters: ReporterV2[], additionalFileMatcher?: Matcher): TaskRunner<TestRun> {
|
||||
const taskRunner = TaskRunner.create<TestRun>(reporters);
|
||||
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true, additionalFileMatcher }));
|
||||
addRunTasks(taskRunner, config);
|
||||
return taskRunner;
|
||||
}
|
||||
|
||||
export function createTaskRunnerForTestServer(config: FullConfigInternal, reporters: ReporterV2[]): TaskRunner<TestRun> {
|
||||
const taskRunner = TaskRunner.create<TestRun>(reporters);
|
||||
taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }));
|
||||
|
|
@ -222,10 +215,10 @@ function createListFilesTask(): Task<TestRun> {
|
|||
};
|
||||
}
|
||||
|
||||
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task<TestRun> {
|
||||
function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean }): Task<TestRun> {
|
||||
return {
|
||||
setup: async (reporter, testRun, errors, softErrors) => {
|
||||
await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter, options.additionalFileMatcher);
|
||||
await collectProjectsAndTestFiles(testRun, !!options.doNotRunDepsOutsideProjectFilter);
|
||||
await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
|
||||
|
||||
let cliOnlyChangedMatcher: Matcher | undefined = undefined;
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class TestServer {
|
|||
}
|
||||
}
|
||||
|
||||
class TestServerDispatcher implements TestServerInterface {
|
||||
export class TestServerDispatcher implements TestServerInterface {
|
||||
private _configLocation: ConfigLocation;
|
||||
|
||||
private _watcher: Watcher;
|
||||
|
|
|
|||
|
|
@ -15,130 +15,106 @@
|
|||
*/
|
||||
|
||||
import readline from 'readline';
|
||||
import path from 'path';
|
||||
import { createGuid, getPackageManagerExecCommand, ManualPromise } from 'playwright-core/lib/utils';
|
||||
import type { FullConfigInternal, FullProjectInternal } from '../common/config';
|
||||
import { createFileMatcher, createFileMatcherFromArguments } from '../util';
|
||||
import type { Matcher } from '../util';
|
||||
import { TestRun, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks';
|
||||
import { buildProjectsClosure, filterProjects } from './projectUtils';
|
||||
import { collectAffectedTestFiles } from '../transform/compilationCache';
|
||||
import type { ConfigLocation } from '../common/config';
|
||||
import type { FullResult } from '../../types/testReporter';
|
||||
import { chokidar } from '../utilsBundle';
|
||||
import type { FSWatcher as CFSWatcher } from 'chokidar';
|
||||
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';
|
||||
import { TestServerDispatcher } from './testServer';
|
||||
import { EventEmitter } from 'stream';
|
||||
import { type TestServerSocket, TestServerConnection } from '../isomorphic/testServerConnection';
|
||||
import { createFileMatcherFromArguments } from '../util';
|
||||
import { TeleSuiteUpdater } from '../isomorphic/teleSuiteUpdater';
|
||||
|
||||
class FSWatcher {
|
||||
private _dirtyTestFiles = new Map<FullProjectInternal, Set<string>>();
|
||||
private _notifyDirtyFiles: (() => void) | undefined;
|
||||
private _watcher: CFSWatcher | undefined;
|
||||
private _timer: NodeJS.Timeout | undefined;
|
||||
class InMemoryServerSocket extends EventEmitter implements TestServerSocket {
|
||||
public readonly send: (data: string) => void;
|
||||
public readonly close: () => void;
|
||||
|
||||
async update(config: FullConfigInternal) {
|
||||
const commandLineFileMatcher = config.cliArgs.length ? createFileMatcherFromArguments(config.cliArgs) : () => true;
|
||||
const projects = filterProjects(config.projects, config.cliProjectFilter);
|
||||
const projectClosure = buildProjectsClosure(projects);
|
||||
const projectFilters = new Map<FullProjectInternal, Matcher>();
|
||||
for (const [project, type] of projectClosure) {
|
||||
const testMatch = createFileMatcher(project.project.testMatch);
|
||||
const testIgnore = createFileMatcher(project.project.testIgnore);
|
||||
projectFilters.set(project, file => {
|
||||
if (!file.startsWith(project.project.testDir) || !testMatch(file) || testIgnore(file))
|
||||
return false;
|
||||
return type === 'dependency' || commandLineFileMatcher(file);
|
||||
constructor(send: (data: any) => void, close: () => void = () => {}) {
|
||||
super();
|
||||
this.send = send;
|
||||
this.close = close;
|
||||
}
|
||||
|
||||
addEventListener(event: string, listener: (e: any) => void) {
|
||||
this.addListener(event, listener);
|
||||
}
|
||||
}
|
||||
|
||||
interface WatchModeOptions {
|
||||
files?: string[];
|
||||
projects?: string[];
|
||||
grep?: string;
|
||||
onlyChanged?: string;
|
||||
}
|
||||
|
||||
export async function runWatchModeLoop(configLocation: ConfigLocation, initialOptions: WatchModeOptions): Promise<FullResult['status']> {
|
||||
const options: WatchModeOptions = { ...initialOptions };
|
||||
|
||||
const testServerDispatcher = new TestServerDispatcher(configLocation);
|
||||
const inMemorySocket = new InMemoryServerSocket(
|
||||
async data => {
|
||||
const { id, method, params } = JSON.parse(data);
|
||||
try {
|
||||
const result = await testServerDispatcher.transport.dispatch(method, params);
|
||||
inMemorySocket.emit('message', { data: JSON.stringify({ id, result }) });
|
||||
} catch (e) {
|
||||
inMemorySocket.emit('message', { data: JSON.stringify({ id, error: String(e) }) });
|
||||
}
|
||||
}
|
||||
);
|
||||
testServerDispatcher.transport.sendEvent = (method, params) => {
|
||||
inMemorySocket.emit('message', { data: JSON.stringify({ method, params }) });
|
||||
};
|
||||
const testServerConnection = new TestServerConnection(inMemorySocket);
|
||||
inMemorySocket.emit('open');
|
||||
|
||||
const telesuiteUpdater = new TeleSuiteUpdater({ pathSeparator: path.sep, onUpdate() { } });
|
||||
|
||||
const dirtyTestFiles: string[] = []; // we're never clearing this! seems wrong.
|
||||
const onDirtyTestFiles: { resolve?(): void } = {};
|
||||
|
||||
testServerConnection.onTestFilesChanged(async ({ testFiles: changedFiles }) => {
|
||||
if (changedFiles.length === 0)
|
||||
return;
|
||||
|
||||
const { report } = await testServerConnection.listTests({ locations: options.files, projects: options.projects, grep: options.grep });
|
||||
telesuiteUpdater.processListReport(report);
|
||||
|
||||
for (const project of telesuiteUpdater.rootSuite!.suites) {
|
||||
for (const suite of project.suites) {
|
||||
if (suite.location?.file && changedFiles.includes(suite.location.file))
|
||||
dirtyTestFiles.push(suite.location.file);
|
||||
}
|
||||
}
|
||||
|
||||
if (dirtyTestFiles.length === 0)
|
||||
return;
|
||||
|
||||
onDirtyTestFiles.resolve?.();
|
||||
});
|
||||
}
|
||||
testServerConnection.onReport(report => telesuiteUpdater.processTestReportEvent(report));
|
||||
|
||||
if (this._timer)
|
||||
clearTimeout(this._timer);
|
||||
if (this._watcher)
|
||||
await this._watcher.close();
|
||||
await testServerConnection.initialize({ interceptStdio: false, watchTestDirs: true });
|
||||
await testServerConnection.runGlobalSetup({});
|
||||
|
||||
this._watcher = chokidar.watch([...projectClosure.keys()].map(p => p.project.testDir), { ignoreInitial: true }).on('all', async (event, file) => {
|
||||
if (event !== 'add' && event !== 'change')
|
||||
return;
|
||||
const { report } = await testServerConnection.listTests({ locations: options.files, projects: options.projects, grep: options.grep });
|
||||
telesuiteUpdater.processListReport(report);
|
||||
|
||||
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.cliPassWithNoTests = true;
|
||||
for (const p of config.projects)
|
||||
p.project.retries = 0;
|
||||
|
||||
// Perform global setup.
|
||||
const testRun = new TestRun(config);
|
||||
const taskRunner = createTaskRunnerForWatchSetup(config, [new ListReporter()]);
|
||||
taskRunner.reporter.onConfigure(config.config);
|
||||
const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0);
|
||||
if (status !== 'passed')
|
||||
await globalCleanup();
|
||||
await taskRunner.reporter.onEnd({ status });
|
||||
await taskRunner.reporter.onExit();
|
||||
if (status !== 'passed')
|
||||
return status;
|
||||
|
||||
// Prepare projects that will be watched, set up watcher.
|
||||
const failedTestIdCollector = new Set<string>();
|
||||
const originalWorkers = config.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 lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: string[], dirtyTestFiles?: string[] } = { type: 'regular' };
|
||||
let result: FullResult['status'] = 'passed';
|
||||
|
||||
// Enter the watch loop.
|
||||
await runTests(config, failedTestIdCollector);
|
||||
await runTests(options, testServerConnection);
|
||||
|
||||
while (true) {
|
||||
printPrompt();
|
||||
const readCommandPromise = readCommand();
|
||||
await Promise.race([
|
||||
fsWatcher.onDirtyTestFiles(),
|
||||
new Promise<void>(resolve => { onDirtyTestFiles.resolve = resolve; }),
|
||||
readCommandPromise,
|
||||
]);
|
||||
if (!readCommandPromise.isDone())
|
||||
|
|
@ -147,32 +123,29 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
|
|||
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 };
|
||||
await runChangedTests(options, testServerConnection, dirtyTestFiles);
|
||||
lastRun = { type: 'changed', dirtyTestFiles: [...dirtyTestFiles] };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'run') {
|
||||
// All means reset filters.
|
||||
await runTests(config, failedTestIdCollector);
|
||||
await runTests(options, testServerConnection);
|
||||
lastRun = { type: 'regular' };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'project') {
|
||||
const { projectNames } = await enquirer.prompt<{ projectNames: string[] }>({
|
||||
const { selectedProjects } = await enquirer.prompt<{ selectedProjects: string[] }>({
|
||||
type: 'multiselect',
|
||||
name: 'projectNames',
|
||||
name: 'selectedProjects',
|
||||
message: 'Select projects',
|
||||
choices: config.projects.map(p => ({ name: p.project.name })),
|
||||
}).catch(() => ({ projectNames: null }));
|
||||
if (!projectNames)
|
||||
choices: telesuiteUpdater.rootSuite!.suites.map(s => s.title),
|
||||
}).catch(() => ({ selectedProjects: null }));
|
||||
if (!selectedProjects)
|
||||
continue;
|
||||
config.cliProjectFilter = projectNames.length ? projectNames : undefined;
|
||||
await fsWatcher.update(config);
|
||||
await runTests(config, failedTestIdCollector);
|
||||
options.projects = selectedProjects.length ? selectedProjects : undefined;
|
||||
await runTests(options, testServerConnection);
|
||||
lastRun = { type: 'regular' };
|
||||
continue;
|
||||
}
|
||||
|
|
@ -186,11 +159,10 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
|
|||
if (filePattern === null)
|
||||
continue;
|
||||
if (filePattern.trim())
|
||||
config.cliArgs = filePattern.split(' ');
|
||||
options.files = filePattern.split(' ');
|
||||
else
|
||||
config.cliArgs = [];
|
||||
await fsWatcher.update(config);
|
||||
await runTests(config, failedTestIdCollector);
|
||||
options.files = undefined;
|
||||
await runTests(options, testServerConnection);
|
||||
lastRun = { type: 'regular' };
|
||||
continue;
|
||||
}
|
||||
|
|
@ -204,40 +176,35 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
|
|||
if (testPattern === null)
|
||||
continue;
|
||||
if (testPattern.trim())
|
||||
config.cliGrep = testPattern;
|
||||
options.grep = testPattern;
|
||||
else
|
||||
config.cliGrep = undefined;
|
||||
await fsWatcher.update(config);
|
||||
await runTests(config, failedTestIdCollector);
|
||||
options.grep = undefined;
|
||||
await runTests(options, testServerConnection);
|
||||
lastRun = { type: 'regular' };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'failed') {
|
||||
config.testIdMatcher = id => failedTestIdCollector.has(id);
|
||||
const failedTestIds = new Set(failedTestIdCollector);
|
||||
await runTests(config, failedTestIdCollector, { title: 'running failed tests' });
|
||||
config.testIdMatcher = undefined;
|
||||
const failedTestIds = telesuiteUpdater.rootSuite!.allTests().filter(t => !t.ok()).map(t => t.id);
|
||||
await runTests({}, testServerConnection, { title: 'running failed tests', testIds: failedTestIds });
|
||||
lastRun = { type: 'failed', failedTestIds };
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'repeat') {
|
||||
if (lastRun.type === 'regular') {
|
||||
await runTests(config, failedTestIdCollector, { title: 're-running tests' });
|
||||
await runTests(options, testServerConnection, { title: 're-running tests' });
|
||||
continue;
|
||||
} else if (lastRun.type === 'changed') {
|
||||
await runChangedTests(config, failedTestIdCollector, lastRun.dirtyTestFiles!, 're-running tests');
|
||||
await runChangedTests(options, testServerConnection, lastRun.dirtyTestFiles!, 're-running tests');
|
||||
} else if (lastRun.type === 'failed') {
|
||||
config.testIdMatcher = id => lastRun.failedTestIds!.has(id);
|
||||
await runTests(config, failedTestIdCollector, { title: 're-running tests' });
|
||||
config.testIdMatcher = undefined;
|
||||
await runTests({}, testServerConnection, { title: 're-running tests', testIds: [...lastRun.failedTestIds!] });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command === 'toggle-show-browser') {
|
||||
await toggleShowBrowser(config, originalWorkers);
|
||||
await toggleShowBrowser();
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -250,71 +217,36 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise<Full
|
|||
}
|
||||
}
|
||||
|
||||
const cleanupStatus = await globalCleanup();
|
||||
return result === 'passed' ? cleanupStatus : result;
|
||||
const teardown = await testServerConnection.runGlobalTeardown({});
|
||||
|
||||
return result === 'passed' ? teardown.status : 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.cliProjectFilter);
|
||||
const projectClosure = buildProjectsClosure(projects);
|
||||
const affectedProjects = affectedProjectsClosure([...projectClosure.keys()], [...filesByProject.keys()]);
|
||||
const affectsAnyDependency = [...affectedProjects].some(p => projectClosure.get(p) === 'dependency');
|
||||
async function runChangedTests(watchOptions: WatchModeOptions, testServerConnection: TestServerConnection, changedFiles: string[], title?: string) {
|
||||
if (watchOptions.files?.length)
|
||||
changedFiles = changedFiles.filter(createFileMatcherFromArguments(watchOptions.files));
|
||||
|
||||
// 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, { additionalFileMatcher, title: title || 'files changed' });
|
||||
await runTests(watchOptions, testServerConnection, { title: title || 'files changed', locations: changedFiles });
|
||||
}
|
||||
|
||||
async function runTests(config: FullConfigInternal, failedTestIdCollector: Set<string>, options?: {
|
||||
projectsToIgnore?: Set<FullProjectInternal>,
|
||||
additionalFileMatcher?: Matcher,
|
||||
async function runTests(watchOptions: WatchModeOptions, testServerConnection: TestServerConnection, options?: {
|
||||
title?: string,
|
||||
testIds?: string[],
|
||||
locations?: string[],
|
||||
}) {
|
||||
printConfiguration(config, options?.title);
|
||||
const taskRunner = createTaskRunnerForWatch(config, [new ListReporter()], options?.additionalFileMatcher);
|
||||
const testRun = new TestRun(config);
|
||||
taskRunner.reporter.onConfigure(config.config);
|
||||
const taskStatus = await taskRunner.run(testRun, 0);
|
||||
let status: FullResult['status'] = 'passed';
|
||||
printConfiguration(watchOptions, options?.title);
|
||||
|
||||
let hasFailedTests = false;
|
||||
for (const test of testRun.rootSuite?.allTests() || []) {
|
||||
if (test.outcome() === 'unexpected') {
|
||||
failedTestIdCollector.add(test.id);
|
||||
hasFailedTests = true;
|
||||
} else {
|
||||
failedTestIdCollector.delete(test.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (testRun.failureTracker.hasWorkerErrors() || hasFailedTests)
|
||||
status = 'failed';
|
||||
if (status === 'passed' && taskStatus !== 'passed')
|
||||
status = taskStatus;
|
||||
await taskRunner.reporter.onEnd({ status });
|
||||
await taskRunner.reporter.onExit();
|
||||
}
|
||||
|
||||
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.deps) {
|
||||
if (result.has(dep))
|
||||
result.add(p);
|
||||
}
|
||||
if (p.teardown && result.has(p.teardown))
|
||||
result.add(p);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
await testServerConnection.runTests({
|
||||
grep: watchOptions.grep,
|
||||
testIds: options?.testIds,
|
||||
locations: options?.locations ?? watchOptions?.files,
|
||||
projects: watchOptions.projects,
|
||||
connectWsEndpoint,
|
||||
reuseContext: connectWsEndpoint ? true : undefined,
|
||||
workers: connectWsEndpoint ? 1 : undefined,
|
||||
headed: connectWsEndpoint ? true : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function readCommand(): ManualPromise<Command> {
|
||||
|
|
@ -377,17 +309,21 @@ Change settings
|
|||
}
|
||||
|
||||
let showBrowserServer: PlaywrightServer | undefined;
|
||||
let connectWsEndpoint: string | undefined = undefined;
|
||||
let seq = 0;
|
||||
|
||||
function printConfiguration(config: FullConfigInternal, title?: string) {
|
||||
function printConfiguration(options: WatchModeOptions, title?: string) {
|
||||
const packageManagerCommand = getPackageManagerExecCommand();
|
||||
const tokens: string[] = [];
|
||||
tokens.push(`${packageManagerCommand} playwright test`);
|
||||
tokens.push(...(config.cliProjectFilter || [])?.map(p => colors.blue(`--project ${p}`)));
|
||||
if (config.cliGrep)
|
||||
tokens.push(colors.red(`--grep ${config.cliGrep}`));
|
||||
if (config.cliArgs)
|
||||
tokens.push(...config.cliArgs.map(a => colors.bold(a)));
|
||||
if (options.projects)
|
||||
tokens.push(...options.projects.map(p => colors.blue(`--project ${p}`)));
|
||||
if (options.grep)
|
||||
tokens.push(colors.red(`--grep ${options.grep}`));
|
||||
if (options.onlyChanged)
|
||||
tokens.push(colors.yellow(`--only-changed ${options.onlyChanged}`));
|
||||
if (options.files)
|
||||
tokens.push(...options.files.map(a => colors.bold(a)));
|
||||
if (title)
|
||||
tokens.push(colors.dim(`(${title})`));
|
||||
if (seq)
|
||||
|
|
@ -409,25 +345,15 @@ ${colors.dim('Waiting for file changes. Press')} ${colors.bold('enter')} ${color
|
|||
`);
|
||||
}
|
||||
|
||||
async function toggleShowBrowser(config: FullConfigInternal, originalWorkers: number) {
|
||||
async function toggleShowBrowser() {
|
||||
if (!showBrowserServer) {
|
||||
config.config.workers = 1;
|
||||
showBrowserServer = new PlaywrightServer({ mode: 'extension', path: '/' + createGuid(), maxConnections: 1 });
|
||||
const wsEndpoint = await showBrowserServer.listen();
|
||||
config.configCLIOverrides.use = {
|
||||
...config.configCLIOverrides.use,
|
||||
_optionContextReuseMode: 'when-possible',
|
||||
_optionConnectOptions: { wsEndpoint },
|
||||
};
|
||||
connectWsEndpoint = await showBrowserServer.listen();
|
||||
process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('on')}\n`);
|
||||
} else {
|
||||
config.config.workers = originalWorkers;
|
||||
if (config.configCLIOverrides.use) {
|
||||
delete config.configCLIOverrides.use._optionContextReuseMode;
|
||||
delete config.configCLIOverrides.use._optionConnectOptions;
|
||||
}
|
||||
await showBrowserServer?.close();
|
||||
showBrowserServer = undefined;
|
||||
connectWsEndpoint = undefined;
|
||||
process.stdout.write(`${colors.dim('Show & reuse browser:')} ${colors.bold('off')}\n`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,39 +166,13 @@ test('should understand dependency structure', async ({ runInlineTest, git, writ
|
|||
expect(result.output).not.toContain('c.spec.ts');
|
||||
});
|
||||
|
||||
test('should support watch mode', async ({ git, writeFiles, runWatchTest }) => {
|
||||
await writeFiles({
|
||||
'a.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(2); });
|
||||
`,
|
||||
test('watch mode is not supported', async ({ runWatchTest }) => {
|
||||
const testProcess = await runWatchTest({}, { 'only-changed': true });
|
||||
await testProcess.exited;
|
||||
expect(testProcess.output).toContain('--only-changed is not supported in watch mode');
|
||||
});
|
||||
|
||||
git(`add .`);
|
||||
git(`commit -m init`);
|
||||
|
||||
await writeFiles({
|
||||
'b.spec.ts': `
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('fails', () => { expect(1).toBe(3); });
|
||||
`,
|
||||
});
|
||||
git(`commit -a -m update`);
|
||||
|
||||
const testProcess = await runWatchTest({}, { 'only-changed': `HEAD~1` });
|
||||
await testProcess.waitForOutput('Waiting for file changes.');
|
||||
testProcess.clearOutput();
|
||||
testProcess.write('r');
|
||||
|
||||
await testProcess.waitForOutput('b.spec.ts:3:13 › fails');
|
||||
expect(testProcess.output).not.toContain('a.spec');
|
||||
});
|
||||
|
||||
test('should throw nice error message if git doesnt work', async ({ git, runInlineTest }) => {
|
||||
test('should throw nice error message if git doesnt work', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({}, { 'only-changed': `this-commit-does-not-exist` });
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
|
|
|
|||
|
|
@ -251,6 +251,60 @@ test('should run added test in watched file', async ({ runUITest, writeFiles })
|
|||
`);
|
||||
});
|
||||
|
||||
test('should run dependency of watched test', async ({ runUITest, writeFiles }) => {
|
||||
const { page } = await runUITest({
|
||||
'playwright.config.ts': `
|
||||
module.exports = {
|
||||
projects: [
|
||||
{ name: 'setup', testMatch: 'global.setup.ts', },
|
||||
{ name: 'main', dependencies: ['setup'] },
|
||||
],
|
||||
};
|
||||
`,
|
||||
'global.setup.ts': `
|
||||
import { test as setup } from '@playwright/test';
|
||||
|
||||
setup('setup test', async ({ page }) => {
|
||||
console.log('setup test is executed')
|
||||
});
|
||||
`,
|
||||
'a.test.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
test('foo', () => {});
|
||||
`,
|
||||
});
|
||||
|
||||
await page.getByText('Status:').click();
|
||||
await page.getByLabel('setup').setChecked(true);
|
||||
await page.getByLabel('main').setChecked(true);
|
||||
|
||||
await page.getByText('a.test.ts').click();
|
||||
await page.getByRole('listitem').filter({ hasText: 'a.test.ts' }).getByTitle('Watch').click();
|
||||
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ◯ a.test.ts 👁 <=
|
||||
◯ foo
|
||||
▼ ◯ global.setup.ts
|
||||
◯ setup test
|
||||
`);
|
||||
|
||||
await writeFiles({
|
||||
'a.test.ts': `
|
||||
import { test } from '@playwright/test';
|
||||
test('foo', () => {});
|
||||
test('bar', () => {});
|
||||
`,
|
||||
});
|
||||
|
||||
await expect.poll(dumpTestTree(page)).toBe(`
|
||||
▼ ✅ a.test.ts 👁 <=
|
||||
✅ foo
|
||||
✅ bar
|
||||
▼ ✅ global.setup.ts
|
||||
✅ setup test
|
||||
`);
|
||||
});
|
||||
|
||||
test('should queue watches', async ({ runUITest, writeFiles, createLatch }) => {
|
||||
const latch = createLatch();
|
||||
const { page } = await runUITest({
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import timers from 'timers/promises';
|
||||
import { test, expect, playwrightCtConfigText } from './playwright-test-fixtures';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
|
@ -545,7 +546,7 @@ test('should not trigger on changes to non-tests', async ({ runWatchTest, writeF
|
|||
`,
|
||||
});
|
||||
|
||||
await new Promise(f => setTimeout(f, 1000));
|
||||
await timers.setTimeout(1000);
|
||||
expect(testProcess.output).not.toContain('Waiting for file changes.');
|
||||
});
|
||||
|
||||
|
|
@ -603,7 +604,7 @@ test('should watch filtered files', async ({ runWatchTest, writeFiles }) => {
|
|||
`,
|
||||
});
|
||||
|
||||
await new Promise(f => setTimeout(f, 1000));
|
||||
await timers.setTimeout(1000);
|
||||
expect(testProcess.output).not.toContain('Waiting for file changes.');
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue