diff --git a/packages/playwright-test/src/cli.ts b/packages/playwright-test/src/cli.ts index b1c8c26eaf..dd4e45fd25 100644 --- a/packages/playwright-test/src/cli.ts +++ b/packages/playwright-test/src/cli.ts @@ -23,7 +23,7 @@ import path from 'path'; import { Runner, builtInReporters, kDefaultConfigFiles } from './runner'; import type { ConfigCLIOverrides } from './runner'; import { stopProfiling, startProfiling } from './profiler'; -import type { FilePatternFilter } from './util'; +import type { TestFileFilter } from './util'; import { showHTMLReport } from './reporters/html'; import { baseFullConfig, defaultTimeout, fileIsModule } from './loader'; @@ -31,7 +31,6 @@ export function addTestCommands(program: Command) { addTestCommand(program); addShowReportCommand(program); addListFilesCommand(program); - addTestServerCommand(program); } function addTestCommand(program: Command) { @@ -93,20 +92,6 @@ function addListFilesCommand(program: Command) { }); } -function addTestServerCommand(program: Command) { - const command = program.command('test-server', { hidden: true }); - command.option('-c, --config ', `Configuration file, or a test directory with optional ${kDefaultConfigFiles.map(file => `"${file}"`).join('/')}`); - command.option('--reporter ', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${baseFullConfig.reporter[0]}")`); - command.action(async opts => { - try { - await runTestServer(opts); - } catch (e) { - console.error(e); - process.exit(1); - } - }); -} - function addShowReportCommand(program: Command) { const command = program.command('show-report [report]'); command.description('show HTML report'); @@ -158,7 +143,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) { else await runner.loadEmptyConfig(configFileOrDirectory); - const filePatternFilter: FilePatternFilter[] = args.map(arg => { + const testFileFilters: TestFileFilter[] = args.map(arg => { const match = /^(.*?):(\d+):?(\d+)?$/.exec(arg); return { re: forceRegExp(match ? match[1] : arg), @@ -169,8 +154,9 @@ async function runTests(args: string[], opts: { [key: string]: any }) { const result = await runner.runAllTests({ listOnly: !!opts.list, - filePatternFilter, + testFileFilters, projectFilter: opts.project || undefined, + watchMode: !!process.env.PW_TEST_WATCH, }); await stopProfiling(undefined); @@ -197,25 +183,6 @@ async function listTestFiles(opts: { [key: string]: any }) { }); } -async function runTestServer(opts: { [key: string]: any }) { - const overrides = overridesFromOptions(opts); - - overrides.use = { headless: false }; - overrides.maxFailures = 1; - overrides.timeout = 0; - overrides.workers = 1; - - // When no --config option is passed, let's look for the config file in the current directory. - const configFileOrDirectory = opts.config ? path.resolve(process.cwd(), opts.config) : process.cwd(); - const resolvedConfigFile = Runner.resolveConfigFile(configFileOrDirectory)!; - if (restartWithExperimentalTsEsm(resolvedConfigFile)) - return; - - const runner = new Runner(overrides); - await runner.loadConfigFromResolvedFile(resolvedConfigFile); - await runner.runTestServer(); -} - function forceRegExp(pattern: string): RegExp { const match = pattern.match(/^\/(.*)\/([gi]*)$/); if (match) diff --git a/packages/playwright-test/src/dispatcher.ts b/packages/playwright-test/src/dispatcher.ts index 3026389ea8..6ecdd37b67 100644 --- a/packages/playwright-test/src/dispatcher.ts +++ b/packages/playwright-test/src/dispatcher.ts @@ -17,7 +17,7 @@ import child_process from 'child_process'; import path from 'path'; import { EventEmitter } from 'events'; -import type { RunPayload, TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, WorkerInitParams, StepBeginPayload, StepEndPayload, SerializedLoaderData, TeardownErrorsPayload, TestServerTestResolvedPayload, WorkerIsolation } from './ipc'; +import type { RunPayload, TestBeginPayload, TestEndPayload, DonePayload, TestOutputPayload, WorkerInitParams, StepBeginPayload, StepEndPayload, SerializedLoaderData, TeardownErrorsPayload, WatchTestResolvedPayload, WorkerIsolation } from './ipc'; import type { TestResult, Reporter, TestStep, TestError } from '../types/testReporter'; import type { Suite } from './test'; import type { Loader } from './loader'; @@ -31,7 +31,7 @@ export type TestGroup = { repeatEachIndex: number; projectId: string; tests: TestCase[]; - testServerTestLine?: number; + watchMode: boolean; }; type TestResultData = { @@ -175,7 +175,7 @@ export class Dispatcher { let doneCallback = () => {}; const result = new Promise(f => doneCallback = f); const doneWithJob = () => { - worker.removeListener('testServer:testResolved', onTestServerTestResolved); + worker.removeListener('watchTestResolved', onWatchTestResolved); worker.removeListener('testBegin', onTestBegin); worker.removeListener('testEnd', onTestEnd); worker.removeListener('stepBegin', onStepBegin); @@ -188,11 +188,11 @@ export class Dispatcher { const remainingByTestId = new Map(testGroup.tests.map(e => [ e.id, e ])); const failedTestIds = new Set(); - const onTestServerTestResolved = (params: TestServerTestResolvedPayload) => { + const onWatchTestResolved = (params: WatchTestResolvedPayload) => { const test = new TestCase(params.title, () => {}, new TestTypeImpl([]), params.location); this._testById.set(params.testId, { test, resultByWorkerIndex: new Map() }); }; - worker.addListener('testServer:testResolved', onTestServerTestResolved); + worker.addListener('watchTestResolved', onWatchTestResolved); const onTestBegin = (params: TestBeginPayload) => { const data = this._testById.get(params.testId)!; @@ -463,6 +463,8 @@ export class Dispatcher { } async stop() { + if (this._isStopped) + return; this._isStopped = true; await Promise.all(this._workerSlots.map(({ worker }) => worker?.stop())); this._checkFinished(); @@ -564,7 +566,7 @@ class Worker extends EventEmitter { entries: testGroup.tests.map(test => { return { testId: test.id, retry: test.results.length }; }), - testServerTestLine: testGroup.testServerTestLine, + watchMode: testGroup.watchMode, }; this.send({ method: 'run', params: runPayload }); } diff --git a/packages/playwright-test/src/ipc.ts b/packages/playwright-test/src/ipc.ts index 65677a2525..18e397525d 100644 --- a/packages/playwright-test/src/ipc.ts +++ b/packages/playwright-test/src/ipc.ts @@ -46,7 +46,7 @@ export type WorkerInitParams = { stderrParams: TtyParams; }; -export type TestServerTestResolvedPayload = { +export type WatchTestResolvedPayload = { testId: string; title: string; location: { file: string, line: number, column: number }; @@ -95,7 +95,7 @@ export type TestEntry = { export type RunPayload = { file: string; entries: TestEntry[]; - testServerTestLine?: number; + watchMode: boolean; }; export type DonePayload = { diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index f0476361ee..dc3f085167 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -646,6 +646,7 @@ export const baseFullConfig: FullConfigInternal = { version: require('../package.json').version, workers, webServer: null, + _watchMode: false, _webServers: [], _globalOutputDir: path.resolve(process.cwd()), _configDir: '', diff --git a/packages/playwright-test/src/reporters/base.ts b/packages/playwright-test/src/reporters/base.ts index 409836a03b..1a1771489a 100644 --- a/packages/playwright-test/src/reporters/base.ts +++ b/packages/playwright-test/src/reporters/base.ts @@ -118,7 +118,10 @@ export class BaseReporter implements Reporter { protected generateStartingMessage() { const jobs = Math.min(this.config.workers, this.config._testGroupsCount); const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : ''; - return `\nRunning ${this.totalTestCount} test${this.totalTestCount !== 1 ? 's' : ''} using ${jobs} worker${jobs !== 1 ? 's' : ''}${shardDetails}`; + if (this.config._watchMode) + return `\nRunning tests in the --watch mode`; + else + return `\nRunning ${this.totalTestCount} test${this.totalTestCount !== 1 ? 's' : ''} using ${jobs} worker${jobs !== 1 ? 's' : ''}${shardDetails}`; } protected getSlowTests(): [string, number][] { diff --git a/packages/playwright-test/src/runner.ts b/packages/playwright-test/src/runner.ts index 6e451226c6..51b7c569e1 100644 --- a/packages/playwright-test/src/runner.ts +++ b/packages/playwright-test/src/runner.ts @@ -21,7 +21,7 @@ import * as path from 'path'; import { promisify } from 'util'; import type { TestGroup } from './dispatcher'; import { Dispatcher } from './dispatcher'; -import type { FilePatternFilter } from './util'; +import type { TestFileFilter } from './util'; import { createFileMatcher, createTitleMatcher, serializeError } from './util'; import type { TestCase } from './test'; import { Suite } from './test'; @@ -45,7 +45,6 @@ import type { TestRunnerPlugin } from './plugins'; import { setRunnerToAddPluginsTo } from './plugins'; import { webServerPluginsForConfig } from './plugins/webServerPlugin'; import { MultiMap } from 'playwright-core/lib/utils/multimap'; -import { createGuid } from 'playwright-core/lib/utils'; const removeFolderAsync = promisify(rimraf); const readDirAsync = promisify(fs.readdir); @@ -54,8 +53,9 @@ export const kDefaultConfigFiles = ['playwright.config.ts', 'playwright.config.j type RunOptions = { listOnly?: boolean; - filePatternFilter?: FilePatternFilter[]; + testFileFilters?: TestFileFilter[]; projectFilter?: string[]; + watchMode?: boolean; }; export type ConfigCLIOverrides = { @@ -78,10 +78,17 @@ export type ConfigCLIOverrides = { use?: any; }; +type WatchProgress = { + canceled: boolean; + dispatcher: Dispatcher | undefined; +}; + export class Runner { private _loader: Loader; private _reporter!: Reporter; private _plugins: TestRunnerPlugin[] = []; + private _watchRepeatEachIndex = 0; + private _watchJobsQueue = Promise.resolve(); constructor(configCLIOverrides?: ConfigCLIOverrides) { this._loader = new Loader(configCLIOverrides); @@ -174,8 +181,13 @@ export class Runner { async runAllTests(options: RunOptions = {}): Promise { this._reporter = await this._createReporter(!!options.listOnly); const config = this._loader.fullConfig(); + if (options.watchMode) { + config._watchMode = true; + config._workerIsolation = 'isolate-projects'; + return await this._watch(options); + } - const result = await raceAgainstTimeout(() => this._run(!!options.listOnly, options.filePatternFilter || [], options.projectFilter), config.globalTimeout); + const result = await raceAgainstTimeout(() => this._run(options), config.globalTimeout); let fullResult: FullResult; if (result.timedOut) { this._reporter.onError?.(createStacklessError(`Timed out waiting ${config.globalTimeout / 1000}s for the entire test run`)); @@ -210,44 +222,8 @@ export class Runner { return report; } - async runTestServer(): Promise { - const config = this._loader.fullConfig(); - this._reporter = await this._createReporter(false); - const rootSuite = new Suite('', 'root'); - this._reporter.onBegin?.(config, rootSuite); - const result: FullResult = { status: 'passed' }; - const globalTearDown = await this._performGlobalAndProjectSetup(config, rootSuite, config.projects, result); - if (result.status !== 'passed') - return; - - while (true) { - const nextTest = await (this._reporter as any)._nextTest!(); - if (!nextTest) - break; - const { projectId, file, line } = nextTest; - const testGroup: TestGroup = { - workerHash: createGuid(), // Create new worker for each test. - requireFile: file, - repeatEachIndex: 0, - projectId, - tests: [], - testServerTestLine: line, - }; - const dispatcher = new Dispatcher(this._loader, [testGroup], this._reporter); - await dispatcher.run(); - } - - await globalTearDown?.(); - await this._reporter.onEnd?.(result); - } - - private async _run(list: boolean, testFileReFilters: FilePatternFilter[], projectNames?: string[]): Promise { - const filesByProject = await this._collectFiles(testFileReFilters, projectNames); - return await this._runFiles(list, filesByProject, testFileReFilters); - } - - private async _collectFiles(testFileReFilters: FilePatternFilter[], projectNames?: string[]): Promise> { - const testFileFilter = testFileReFilters.length ? createFileMatcher(testFileReFilters.map(e => e.re)) : () => true; + private async _collectFiles(testFileFilters: TestFileFilter[], projectNames?: string[]): Promise> { + const testFileFilter = testFileFilters.length ? createFileMatcher(testFileFilters.map(e => e.re || e.exact || '')) : () => true; let projectsToFind: Set | undefined; let unknownProjects: Map | undefined; if (projectNames) { @@ -288,7 +264,10 @@ export class Runner { return files; } - private async _runFiles(list: boolean, filesByProject: Map, testFileReFilters: FilePatternFilter[]): Promise { + private async _run(options: RunOptions): Promise { + const testFileFilters = options.testFileFilters || []; + const filesByProject = await this._collectFiles(testFileFilters, options.projectFilter); + const allTestFiles = new Set(); for (const files of filesByProject.values()) files.forEach(file => allTestFiles.add(file)); @@ -312,7 +291,7 @@ export class Runner { fatalErrors.push(duplicateTitlesError); // 3. Filter tests to respect line/column filter. - filterByFocusedLine(preprocessRoot, testFileReFilters); + filterByFocusedLine(preprocessRoot, testFileFilters); // 4. Complain about only. if (config.forbidOnly) { @@ -322,7 +301,7 @@ export class Runner { } // 5. Filter only. - if (!list) + if (!options.listOnly) filterOnly(preprocessRoot); // 6. Generate projects. @@ -406,27 +385,12 @@ export class Runner { } // 11. Bail out if list mode only, don't do any work. - if (list) + if (options.listOnly) return { status: 'passed' }; // 12. Remove output directores. - try { - const outputDirs = new Set([...filesByProject.keys()].map(project => project.outputDir)); - await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(async error => { - if ((error as any).code === 'EBUSY') { - // We failed to remove folder, might be due to the whole folder being mounted inside a container: - // https://github.com/microsoft/playwright/issues/12106 - // Do a best-effort to remove all files inside of it instead. - const entries = await readDirAsync(outputDir).catch(e => []); - await Promise.all(entries.map(entry => removeFolderAsync(path.join(outputDir, entry)))); - } else { - throw error; - } - }))); - } catch (e) { - this._reporter.onError?.(serializeError(e)); + if (!this._removeOutputDirs(options)) return { status: 'failed' }; - } // 13. Run Global setup. const result: FullResult = { status: 'passed' }; @@ -464,6 +428,154 @@ export class Runner { return result; } + private async _watch(options: RunOptions): Promise { + const config = this._loader.fullConfig(); + + // 1. Create empty suite. + const rootSuite = new Suite('', 'root'); + + // 2. Report begin. + this._reporter.onBegin?.(config, rootSuite); + + // 3. Remove output directores. + if (!this._removeOutputDirs(options)) + return { status: 'failed' }; + + // 4. Run Global setup. + const result: FullResult = { status: 'passed' }; + const globalTearDown = await this._performGlobalAndProjectSetup(config, rootSuite, config.projects.filter(p => !options.projectFilter || options.projectFilter.includes(p.name)), result); + if (result.status !== 'passed') + return result; + + const progress: WatchProgress = { canceled: false, dispatcher: undefined }; + + const runAndWatch = async () => { + // 5. Collect all files. + const testFileFilters = options.testFileFilters || []; + const filesByProject = await this._collectFiles(testFileFilters, options.projectFilter); + + const allTestFiles = new Set(); + for (const files of filesByProject.values()) + files.forEach(file => allTestFiles.add(file)); + + // 6. Trigger 'all files changed'. + await this._runAndReportError(async () => { + await this._runModifiedTestFilesForWatch(progress, options, allTestFiles); + }, result); + + // 7. Start watching the filesystem for modifications. + await this._watchTestFiles(progress, options); + }; + + try { + const sigintWatcher = new SigIntWatcher(); + await Promise.race([runAndWatch(), sigintWatcher.promise()]); + if (!sigintWatcher.hadSignal()) + sigintWatcher.disarm(); + progress.canceled = true; + await progress.dispatcher?.stop(); + } finally { + await globalTearDown?.(); + } + return result; + } + + private async _runModifiedTestFilesForWatch(progress: WatchProgress, options: RunOptions, testFiles: Set): Promise { + if (progress.canceled) + return; + + const testFileFilters: TestFileFilter[] = [...testFiles].map(f => ({ + exact: f, + line: null, + column: null, + })); + const filesByProject = await this._collectFiles(testFileFilters, options.projectFilter); + + if (progress.canceled) + return; + + const testGroups: TestGroup[] = []; + const repeatEachIndex = ++this._watchRepeatEachIndex; + for (const [project, files] of filesByProject) { + for (const file of files) { + const group: TestGroup = { + workerHash: `run${project._id}-repeat${repeatEachIndex}`, + requireFile: file, + repeatEachIndex, + projectId: project._id, + tests: [], + watchMode: true, + }; + testGroups.push(group); + } + } + + const dispatcher = new Dispatcher(this._loader, testGroups, this._reporter); + progress.dispatcher = dispatcher; + await dispatcher.run(); + await dispatcher.stop(); + progress.dispatcher = undefined; + } + + private async _watchTestFiles(progress: WatchProgress, options: RunOptions): Promise { + const folders = new Set(); + const config = this._loader.fullConfig(); + for (const project of config.projects) { + if (options.projectFilter && !options.projectFilter.includes(project.name)) + continue; + folders.add(project.testDir); + } + + for (const folder of folders) { + const changedFiles = new Set(); + let throttleTimer: NodeJS.Timeout | undefined; + fs.watch(folder, (event, filename) => { + if (event !== 'change') + return; + + const fullName = path.join(folder, filename); + changedFiles.add(fullName); + if (throttleTimer) + clearTimeout(throttleTimer); + + throttleTimer = setTimeout(() => { + const copy = new Set(changedFiles); + changedFiles.clear(); + this._watchJobsQueue = this._watchJobsQueue.then(() => this._runModifiedTestFilesForWatch(progress, options, copy)); + }, 250); + }); + } + + await new Promise(() => {}); + } + + private async _removeOutputDirs(options: RunOptions): Promise { + const config = this._loader.fullConfig(); + const outputDirs = new Set(); + for (const p of config.projects) { + if (!options.projectFilter || options.projectFilter.includes(p.name)) + outputDirs.add(p.outputDir); + } + + try { + await Promise.all(Array.from(outputDirs).map(outputDir => removeFolderAsync(outputDir).catch(async (error: any) => { + if ((error as any).code === 'EBUSY') { + // We failed to remove folder, might be due to the whole folder being mounted inside a container: + // https://github.com/microsoft/playwright/issues/12106 + // Do a best-effort to remove all files inside of it instead. + const entries = await readDirAsync(outputDir).catch(e => []); + await Promise.all(entries.map(entry => removeFolderAsync(path.join(outputDir, entry)))); + } else { + throw error; + } + }))); + } catch (e) { + this._reporter.onError?.(serializeError(e)); + return false; + } + return true; + } + private async _performGlobalAndProjectSetup(config: FullConfigInternal, rootSuite: Suite, projects: FullProjectInternal[], result: FullResult): Promise<(() => Promise) | undefined> { type SetupData = { setupFile?: string | null; @@ -569,14 +681,20 @@ function filterOnly(suite: Suite) { return filterSuiteWithOnlySemantics(suite, suiteFilter, testFilter); } -function filterByFocusedLine(suite: Suite, focusedTestFileLines: FilePatternFilter[]) { +function filterByFocusedLine(suite: Suite, focusedTestFileLines: TestFileFilter[]) { const filterWithLine = !!focusedTestFileLines.find(f => f.line !== null); if (!filterWithLine) return; - const testFileLineMatches = (testFileName: string, testLine: number, testColumn: number) => focusedTestFileLines.some(({ re, line, column }) => { - re.lastIndex = 0; - return re.test(testFileName) && (line === testLine || line === null) && (column === testColumn || column === null); + const testFileLineMatches = (testFileName: string, testLine: number, testColumn: number) => focusedTestFileLines.some(({ re, exact, line, column }) => { + const lineColumnOk = (line === testLine || line === null) && (column === testColumn || column === null); + if (!lineColumnOk) + return false; + if (re) { + re.lastIndex = 0; + return re.test(testFileName); + } + return testFileName === exact; }); const suiteFilter = (suite: Suite) => { return !!suite.location && testFileLineMatches(suite.location.file, suite.location.line, suite.location.column); @@ -727,6 +845,7 @@ function createTestGroups(rootSuite: Suite, workers: number): TestGroup[] { repeatEachIndex: test.repeatEachIndex, projectId: test._projectId, tests: [], + watchMode: false, }; }; diff --git a/packages/playwright-test/src/types.ts b/packages/playwright-test/src/types.ts index a9addb2b80..8d9066b65f 100644 --- a/packages/playwright-test/src/types.ts +++ b/packages/playwright-test/src/types.ts @@ -45,6 +45,7 @@ export interface FullConfigInternal extends FullConfigPublic { _globalOutputDir: string; _configDir: string; _testGroupsCount: number; + _watchMode: boolean; _workerIsolation: WorkerIsolation; /** * If populated, this should also be the first/only entry in _webServers. Legacy singleton `webServer` as well as those provided via an array in the user-facing playwright.config.{ts,js} will be in `_webServers`. The legacy field (`webServer`) field additionally stores the backwards-compatible singleton `webServer` since it had been showing up in globalSetup to the user. diff --git a/packages/playwright-test/src/util.ts b/packages/playwright-test/src/util.ts index d43ea19bb7..87b7ba2c7f 100644 --- a/packages/playwright-test/src/util.ts +++ b/packages/playwright-test/src/util.ts @@ -101,8 +101,9 @@ export function serializeError(error: Error | any): TestError { export type Matcher = (value: string) => boolean; -export type FilePatternFilter = { - re: RegExp; +export type TestFileFilter = { + re?: RegExp; + exact?: string; line: number | null; column: number | null; }; diff --git a/packages/playwright-test/src/worker.ts b/packages/playwright-test/src/worker.ts index 8e4be1d1dc..1a8c9ac8b2 100644 --- a/packages/playwright-test/src/worker.ts +++ b/packages/playwright-test/src/worker.ts @@ -69,7 +69,7 @@ process.on('message', async message => { initConsoleParameters(initParams); startProfiling(); workerRunner = new WorkerRunner(initParams); - for (const event of ['testServer:testResolved', 'testBegin', 'testEnd', 'stepBegin', 'stepEnd', 'done', 'teardownErrors']) + for (const event of ['watchTestResolved', 'testBegin', 'testEnd', 'stepBegin', 'stepEnd', 'done', 'teardownErrors']) workerRunner.on(event, sendMessageToParent.bind(null, event)); return; } diff --git a/packages/playwright-test/src/workerRunner.ts b/packages/playwright-test/src/workerRunner.ts index bb6c49ce26..f58ce6ea1c 100644 --- a/packages/playwright-test/src/workerRunner.ts +++ b/packages/playwright-test/src/workerRunner.ts @@ -18,7 +18,7 @@ import { colors, rimraf } from 'playwright-core/lib/utilsBundle'; import util from 'util'; import { EventEmitter } from 'events'; import { relativeFilePath, serializeError } from './util'; -import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerInitParams, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, TestServerTestResolvedPayload } from './ipc'; +import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerInitParams, StepBeginPayload, StepEndPayload, TeardownErrorsPayload, WatchTestResolvedPayload } from './ipc'; import { setCurrentTestInfo } from './globals'; import { Loader } from './loader'; import type { Suite, TestCase } from './test'; @@ -171,13 +171,13 @@ export class WorkerRunner extends EventEmitter { await this._loadIfNeeded(); const fileSuite = await this._loader.loadTestFile(runPayload.file, 'worker'); const suite = this._loader.buildFileSuiteForProject(this._project, fileSuite, this._params.repeatEachIndex, test => { - if (test.location.line === runPayload.testServerTestLine) { - const testResolvedPayload: TestServerTestResolvedPayload = { + if (runPayload.watchMode) { + const testResolvedPayload: WatchTestResolvedPayload = { testId: test.id, title: test.title, location: test.location }; - this.emit('testServer:testResolved', testResolvedPayload); + this.emit('watchTestResolved', testResolvedPayload); entries.set(test.id, { testId: test.id, retry: 0 }); } if (!entries.has(test.id))