diff --git a/packages/playwright/src/isomorphic/testServerConnection.ts b/packages/playwright/src/isomorphic/testServerConnection.ts index 00dafdf20b..22bdce30ff 100644 --- a/packages/playwright/src/isomorphic/testServerConnection.ts +++ b/packages/playwright/src/isomorphic/testServerConnection.ts @@ -246,4 +246,4 @@ export class TestServerConnection implements TestServerInterface, TestServerInte } catch { } } -} \ No newline at end of file +} diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 803b9d2291..4b5bea19d3 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -24,9 +24,9 @@ import { stopProfiling, startProfiling, gracefullyProcessExitDoNotHang } from 'p import { serializeError } from './util'; import { showHTMLReport } from './reporters/html'; import { createMergedReport } from './reporters/merge'; -import { loadConfigFromFileRestartIfNeeded, loadEmptyConfigForMergeReports } from './common/configLoader'; +import { loadConfigFromFileRestartIfNeeded, loadEmptyConfigForMergeReports, resolveConfigLocation } from './common/configLoader'; import type { ConfigCLIOverrides } from './common/ipc'; -import type { FullResult, TestError } from '../types/testReporter'; +import type { TestError } from '../types/testReporter'; import type { TraceMode } from '../types/test'; import { builtInReporters, defaultReporter, defaultTimeout } from './common/config'; import { program } from 'playwright-core/lib/cli/program'; @@ -35,6 +35,7 @@ import type { ReporterDescription } from '../types/test'; import { prepareErrorStack } from './reporters/base'; import * as testServer from './runner/testServer'; import { clearCacheAndLogToConsole } from './runner/testServer'; +import { runWatchModeLoop } from './runner/watchMode'; function addTestCommand(program: Command) { const command = program.command('test [test-filter...]'); @@ -183,6 +184,26 @@ async function runTests(args: string[], opts: { [key: string]: any }) { return; } + 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.`); + + const status = await runWatchModeLoop( + resolveConfigLocation(opts.config), + { + projects: opts.project, + files: args, + grep: opts.grep + } + ); + await stopProfiling('runner'); + if (status === 'restarted') + return; + const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1); + gracefullyProcessExitDoNotHang(exitCode); + return; + } + const config = await loadConfigFromFileRestartIfNeeded(opts.config, cliOverrides, opts.deps === false); if (!config) return; @@ -202,11 +223,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { config.cliFailOnFlakyTests = !!opts.failOnFlakyTests; const runner = new Runner(config); - let status: FullResult['status']; - if (process.env.PWTEST_WATCH) - status = await runner.watchAllTests(); - else - status = await runner.runAllTests(); + const status = await runner.runAllTests(); await stopProfiling('runner'); const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1); gracefullyProcessExitDoNotHang(exitCode); diff --git a/packages/playwright/src/runner/DEPS.list b/packages/playwright/src/runner/DEPS.list index cdf6044844..bc1cc6d763 100644 --- a/packages/playwright/src/runner/DEPS.list +++ b/packages/playwright/src/runner/DEPS.list @@ -7,6 +7,5 @@ ../plugins/ ../util.ts ../utilsBundle.ts -../isomorphic/folders.ts -../isomorphic/teleReceiver.ts +../isomorphic/ ../fsWatcher.ts diff --git a/packages/playwright/src/runner/loadUtils.ts b/packages/playwright/src/runner/loadUtils.ts index cd735ceca3..63a2307507 100644 --- a/packages/playwright/src/runner/loadUtils.ts +++ b/packages/playwright/src/runner/loadUtils.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; diff --git a/packages/playwright/src/runner/runner.ts b/packages/playwright/src/runner/runner.ts index a7fd28ec87..05cf8a6aac 100644 --- a/packages/playwright/src/runner/runner.ts +++ b/packages/playwright/src/runner/runner.ts @@ -24,7 +24,6 @@ import { collectFilesForProject, filterProjects } from './projectUtils'; import { createReporters } from './reporters'; import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks'; import type { FullConfigInternal } from '../common/config'; -import { runWatchModeLoop } from './watchMode'; import type { Suite } from '../common/test'; import { wrapReporterAsV2 } from '../reporters/reporterV2'; import { affectedTestFiles } from '../transform/compilationCache'; @@ -131,12 +130,6 @@ export class Runner { return { status, suite: testRun.rootSuite, errors }; } - async watchAllTests(): Promise { - const config = this._config; - webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); - return await runWatchModeLoop(config); - } - async findRelatedTestFiles(mode: 'in-process' | 'out-of-process', files: string[]): Promise { const result = await this.loadAllTests(mode); if (result.status !== 'passed' || !result.suite) diff --git a/packages/playwright/src/runner/tasks.ts b/packages/playwright/src/runner/tasks.ts index 0a1e001a91..09a8a1fdaf 100644 --- a/packages/playwright/src/runner/tasks.ts +++ b/packages/playwright/src/runner/tasks.ts @@ -74,13 +74,6 @@ export function createTaskRunnerForWatchSetup(config: FullConfigInternal, report return taskRunner; } -export function createTaskRunnerForWatch(config: FullConfigInternal, reporters: ReporterV2[], additionalFileMatcher?: Matcher): TaskRunner { - const taskRunner = TaskRunner.create(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 { const taskRunner = TaskRunner.create(reporters); taskRunner.addTask('load tests', createLoadTask('out-of-process', { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true })); @@ -222,10 +215,10 @@ function createListFilesTask(): Task { }; } -function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean, additionalFileMatcher?: Matcher }): Task { +function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filterOnly: boolean, failOnLoadErrors: boolean, doNotRunDepsOutsideProjectFilter?: boolean }): Task { 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; diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 474ed6ae5d..1df2d72e8c 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -62,7 +62,7 @@ class TestServer { } } -class TestServerDispatcher implements TestServerInterface { +export class TestServerDispatcher implements TestServerInterface { private _configLocation: ConfigLocation; private _watcher: Watcher; diff --git a/packages/playwright/src/runner/watchMode.ts b/packages/playwright/src/runner/watchMode.ts index 709e39100b..924449adce 100644 --- a/packages/playwright/src/runner/watchMode.ts +++ b/packages/playwright/src/runner/watchMode.ts @@ -15,130 +15,130 @@ */ 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 TestServerTransport, TestServerConnection } from '../isomorphic/testServerConnection'; +import { TeleSuiteUpdater } from '../isomorphic/teleSuiteUpdater'; +import { restartWithExperimentalTsEsm } from '../common/configLoader'; -class FSWatcher { - private _dirtyTestFiles = new Map>(); - private _notifyDirtyFiles: (() => void) | undefined; - private _watcher: CFSWatcher | undefined; - private _timer: NodeJS.Timeout | undefined; - - 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(); - 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); - }); - } - - 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(); - 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); - }); +class InMemoryTransport extends EventEmitter implements TestServerTransport { + public readonly _send: (data: string) => void; + constructor(send: (data: any) => void) { + super(); + this._send = send; } - async onDirtyTestFiles(): Promise { - if (this._dirtyTestFiles.size) - return; - await new Promise(f => this._notifyDirtyFiles = f); + close() { + this.emit('close'); } - takeDirtyTestFiles(): Map> { - const result = this._dirtyTestFiles; - this._dirtyTestFiles = new Map(); - return result; + onclose(listener: () => void): void { + this.on('close', listener); + } + + onerror(listener: () => void): void { + // no-op to fulfil the interface, the user of InMemoryTransport doesn't emit any errors. + } + + onmessage(listener: (message: string) => void): void { + this.on('message', listener); + } + + onopen(listener: () => void): void { + this.on('open', listener); + } + + send(data: string): void { + this._send(data); } } -export async function runWatchModeLoop(config: FullConfigInternal): Promise { - // 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; +} - // 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 { + if (restartWithExperimentalTsEsm(undefined, true)) + return 'restarted'; - // Prepare projects that will be watched, set up watcher. - const failedTestIdCollector = new Set(); - const originalWorkers = config.config.workers; - const fsWatcher = new FSWatcher(); - await fsWatcher.update(config); + const options: WatchModeOptions = { ...initialOptions }; - let lastRun: { type: 'changed' | 'regular' | 'failed', failedTestIds?: Set, dirtyTestFiles?: Map> } = { type: 'regular' }; + const testServerDispatcher = new TestServerDispatcher(configLocation); + const transport = new InMemoryTransport( + async data => { + const { id, method, params } = JSON.parse(data); + try { + const result = await testServerDispatcher.transport.dispatch(method, params); + transport.emit('message', JSON.stringify({ id, result })); + } catch (e) { + transport.emit('message', JSON.stringify({ id, error: String(e) })); + } + } + ); + testServerDispatcher.transport.sendEvent = (method, params) => { + transport.emit('message', JSON.stringify({ method, params })); + }; + const testServerConnection = new TestServerConnection(transport); + transport.emit('open'); + + const teleSuiteUpdater = new TeleSuiteUpdater({ pathSeparator: path.sep, onUpdate() { } }); + + const dirtyTestIds = new Set(); + let onDirtyTests = new ManualPromise(); + + let queue = Promise.resolve(); + const changedFiles = new Set(); + testServerConnection.onTestFilesChanged(({ testFiles }) => { + testFiles.forEach(file => changedFiles.add(file)); + + queue = queue.then(async () => { + if (changedFiles.size === 0) + return; + + const { report } = await testServerConnection.listTests({ locations: options.files, projects: options.projects, grep: options.grep }); + teleSuiteUpdater.processListReport(report); + + for (const test of teleSuiteUpdater.rootSuite!.allTests()) { + if (changedFiles.has(test.location.file)) + dirtyTestIds.add(test.id); + } + + changedFiles.clear(); + + if (dirtyTestIds.size > 0) + onDirtyTests.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[], dirtyTestIds?: 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(), + onDirtyTests, readCommandPromise, ]); if (!readCommandPromise.isDone()) @@ -147,32 +147,32 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise({ + 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 +186,10 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise 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 runTests(options, testServerConnection, { title: 're-running tests', testIds: lastRun.dirtyTestIds }); } 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 +244,27 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise, filesByProject: Map>, title?: string) { - const testFiles = new Set(); - 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'); - - // 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' }); -} - -async function runTests(config: FullConfigInternal, failedTestIdCollector: Set, options?: { - projectsToIgnore?: Set, - additionalFileMatcher?: Matcher, +async function runTests(watchOptions: WatchModeOptions, testServerConnection: TestServerConnection, options?: { title?: string, + testIds?: 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 { - const result = new Set(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: watchOptions?.files, + projects: watchOptions.projects, + connectWsEndpoint, + reuseContext: connectWsEndpoint ? true : undefined, + workers: connectWsEndpoint ? 1 : undefined, + headed: connectWsEndpoint ? true : undefined, + }); } function readCommand(): ManualPromise { @@ -377,17 +327,19 @@ 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.files) + tokens.push(...options.files.map(a => colors.bold(a))); if (title) tokens.push(colors.dim(`(${title})`)); if (seq) @@ -409,25 +361,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`); } } diff --git a/tests/playwright-test/only-changed.spec.ts b/tests/playwright-test/only-changed.spec.ts index 6fe915f702..674234a240 100644 --- a/tests/playwright-test/only-changed.spec.ts +++ b/tests/playwright-test/only-changed.spec.ts @@ -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, git }) => { const result = await runInlineTest({}, { 'only-changed': `this-commit-does-not-exist` }); expect(result.exitCode).toBe(1); diff --git a/tests/playwright-test/watch.spec.ts b/tests/playwright-test/watch.spec.ts index 946377a357..0dba9dd861 100644 --- a/tests/playwright-test/watch.spec.ts +++ b/tests/playwright-test/watch.spec.ts @@ -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' }); @@ -418,6 +419,17 @@ test('should run on changed files', async ({ runWatchTest, writeFiles }) => { 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(); + await writeFiles({ + 'b.test.ts': ` + import { test, expect } from '@playwright/test'; + test('passes', () => {}); + `, + }); + + await testProcess.waitForOutput('b.test.ts:3:11 › passes'); + expect(testProcess.output).not.toContain('c.test.ts:3:11 › passes'); }); test('should run on changed deps', async ({ runWatchTest, writeFiles }) => { @@ -545,7 +557,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 +615,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.'); });