chore(test runner): refactor watch mode onto testserver

This commit is contained in:
Simon Knott 2024-08-13 12:13:13 +02:00
parent 1b220c5289
commit 068379aaa2
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
11 changed files with 209 additions and 256 deletions

View file

@ -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);

View file

@ -9,4 +9,5 @@
../utilsBundle.ts
../isomorphic/folders.ts
../isomorphic/teleReceiver.ts
../isomorphic/testServerConnection.ts
../fsWatcher.ts

View file

@ -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;

View file

@ -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> {

View file

@ -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;

View file

@ -62,7 +62,7 @@ class TestServer {
}
}
class TestServerDispatcher implements TestServerInterface {
export class TestServerDispatcher implements TestServerInterface {
private _configLocation: ConfigLocation;
private _watcher: Watcher;

View file

@ -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;
}
if (this._timer)
clearTimeout(this._timer);
if (this._watcher)
await this._watcher.close();
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 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;
addEventListener(event: string, listener: (e: any) => void) {
this.addListener(event, listener);
}
}
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;
interface WatchModeOptions {
files?: string[];
projects?: string[];
grep?: string;
onlyChanged?: string;
}
// 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;
export async function runWatchModeLoop(configLocation: ConfigLocation, initialOptions: WatchModeOptions): Promise<FullResult['status']> {
const options: WatchModeOptions = { ...initialOptions };
// 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);
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');
let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: Set<string>, dirtyTestFiles?: Map<FullProjectInternal, Set<string>> } = { type: 'regular' };
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));
await testServerConnection.initialize({ interceptStdio: false, watchTestDirs: true });
await testServerConnection.runGlobalSetup({});
const { report } = await testServerConnection.listTests({ locations: options.files, projects: options.projects, grep: options.grep });
telesuiteUpdater.processListReport(report);
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`);
}
}

View file

@ -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); });
`,
});
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('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');
});
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);

View file

@ -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({

View file

@ -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.');
});