From 201bad75d3c44334059da387a947bae68fa500e9 Mon Sep 17 00:00:00 2001 From: Simon Knott Date: Tue, 3 Sep 2024 15:15:44 +0200 Subject: [PATCH] chore(test runner): rebase watch mode onto TestServerConnection (#32156) Closes https://github.com/microsoft/playwright/issues/32076. This PR rewrites `watchMode.ts` to use `TestServer` under the hood. It's essentially a complete rewrite, so don't pay too much attention on the old implementation. Note that there's no changes to tests, so all behaviour we have specced out there still works. To make this work without a superfluous WebSocket connection, I had to refactor `TestServerConnection` a little. Originally, I pulled this into a [separate PR](https://github.com/microsoft/playwright/pull/32132), but then realised how small the refactoring is. So it's in this PR now. Let me know if you'd like to land it separately. --- .../src/isomorphic/testServerConnection.ts | 2 +- packages/playwright/src/program.ts | 31 +- packages/playwright/src/runner/DEPS.list | 3 +- packages/playwright/src/runner/loadUtils.ts | 4 +- packages/playwright/src/runner/runner.ts | 7 - packages/playwright/src/runner/tasks.ts | 11 +- packages/playwright/src/runner/testServer.ts | 2 +- packages/playwright/src/runner/watchMode.ts | 358 ++++++++---------- tests/playwright-test/only-changed.spec.ts | 36 +- tests/playwright-test/watch.spec.ts | 16 +- 10 files changed, 199 insertions(+), 271 deletions(-) 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.'); });