diff --git a/packages/playwright-test/src/common/configLoader.ts b/packages/playwright-test/src/common/configLoader.ts index 40ef762241..8f05fa31a5 100644 --- a/packages/playwright-test/src/common/configLoader.ts +++ b/packages/playwright-test/src/common/configLoader.ts @@ -216,7 +216,6 @@ export class ConfigLoader { return { _internal: { id: '', - type: 'top-level', fullConfig: fullConfig, fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined), expect: takeFirst(projectConfig.expect, config.expect, {}), diff --git a/packages/playwright-test/src/common/types.ts b/packages/playwright-test/src/common/types.ts index d61bc75537..e3af8e82ab 100644 --- a/packages/playwright-test/src/common/types.ts +++ b/packages/playwright-test/src/common/types.ts @@ -61,7 +61,6 @@ export interface FullConfigInternal extends FullConfigPublic { type ProjectInternal = { id: string; - type: 'top-level' | 'dependency'; fullConfig: FullConfigInternal; fullyParallel: boolean; expect: Project['expect']; diff --git a/packages/playwright-test/src/runner/loadUtils.ts b/packages/playwright-test/src/runner/loadUtils.ts index 57841299ad..35201e1496 100644 --- a/packages/playwright-test/src/runner/loadUtils.ts +++ b/packages/playwright-test/src/runner/loadUtils.ts @@ -23,6 +23,7 @@ import type { FullConfigInternal, FullProjectInternal } from '../common/types'; import { createFileMatcherFromArguments, createFileFiltersFromArguments, createTitleMatcher, errorWithFile, forceRegExp } from '../util'; import type { Matcher, TestFileFilter } from '../util'; import { buildProjectsClosure, collectFilesForProject, filterProjects } from './projectUtils'; +import type { TestRun } from './tasks'; import { requireOrImport } from '../common/transform'; import { buildFileSuiteForProject, filterByFocusedLine, filterByTestIds, filterOnly, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; import { createTestGroups, filterForShard, type TestGroup } from './testGroups'; @@ -30,7 +31,8 @@ import { dependenciesForTestFile } from '../common/compilationCache'; import { sourceMapSupport } from '../utilsBundle'; import type { RawSourceMap } from 'source-map'; -export async function collectProjectsAndTestFiles(config: FullConfigInternal, projectsToIgnore: Set, additionalFileMatcher: Matcher | undefined) { +export async function collectProjectsAndTestFiles(testRun: TestRun, additionalFileMatcher: Matcher | undefined) { + const config = testRun.config; const fsCache = new Map(); const sourceMapCache = new Map(); const cliFileMatcher = config._internal.cliArgs.length ? createFileMatcherFromArguments(config._internal.cliArgs) : null; @@ -38,8 +40,6 @@ export async function collectProjectsAndTestFiles(config: FullConfigInternal, pr // First collect all files for the projects in the command line, don't apply any file filters. const allFilesForProject = new Map(); for (const project of filterProjects(config.projects, config._internal.cliProjectFilter)) { - if (projectsToIgnore.has(project)) - continue; const files = await collectFilesForProject(project, fsCache); allFilesForProject.set(project, files); } @@ -63,22 +63,26 @@ export async function collectProjectsAndTestFiles(config: FullConfigInternal, pr } // (Re-)add all files for dependent projects, disregard filters. - const projectClosure = buildProjectsClosure([...filesToRunByProject.keys()]).filter(p => !projectsToIgnore.has(p)); - for (const project of projectClosure) { - if (project._internal.type === 'dependency') { + const projectClosure = buildProjectsClosure([...filesToRunByProject.keys()]); + for (const [project, type] of projectClosure) { + if (type === 'dependency') { filesToRunByProject.delete(project); const files = allFilesForProject.get(project) || await collectFilesForProject(project, fsCache); filesToRunByProject.set(project, files); } } - return filesToRunByProject; + testRun.projects = [...filesToRunByProject.keys()]; + testRun.projectFiles = filesToRunByProject; + testRun.projectType = projectClosure; + testRun.projectSuites = new Map(); } -export async function loadFileSuites(mode: 'out-of-process' | 'in-process', config: FullConfigInternal, filesToRunByProject: Map, errors: TestError[]): Promise> { +export async function loadFileSuites(testRun: TestRun, mode: 'out-of-process' | 'in-process', errors: TestError[]) { // Determine all files to load. + const config = testRun.config; const allTestFiles = new Set(); - for (const files of filesToRunByProject.values()) + for (const files of testRun.projectFiles.values()) files.forEach(file => allTestFiles.add(file)); // Load test files. @@ -107,15 +111,14 @@ export async function loadFileSuites(mode: 'out-of-process' | 'in-process', conf } // Collect file suites for each project. - const fileSuitesByProject = new Map(); - for (const [project, files] of filesToRunByProject) { + for (const [project, files] of testRun.projectFiles) { const suites = files.map(file => fileSuiteByFile.get(file)).filter(Boolean) as Suite[]; - fileSuitesByProject.set(project, suites); + testRun.projectSuites.set(project, suites); } - return fileSuitesByProject; } -export async function createRootSuite(config: FullConfigInternal, fileSuitesByProject: Map, errors: TestError[], shouldFilterOnly: boolean): Promise { +export async function createRootSuite(testRun: TestRun, errors: TestError[], shouldFilterOnly: boolean): Promise { + const config = testRun.config; // Create root suite, where each child will be a project suite with cloned file suites inside it. const rootSuite = new Suite('', 'root'); @@ -128,8 +131,8 @@ export async function createRootSuite(config: FullConfigInternal, fileSuitesByPr const cliTitleMatcher = (title: string) => !grepInvertMatcher(title) && grepMatcher(title); // Clone file suites for top-level projects. - for (const [project, fileSuites] of fileSuitesByProject) { - if (project._internal.type === 'top-level') + for (const [project, fileSuites] of testRun.projectSuites) { + if (testRun.projectType.get(project) === 'top-level') rootSuite._addSuite(await createProjectSuite(fileSuites, project, { cliFileFilters, cliTitleMatcher, testIdMatcher: config._internal.testIdMatcher })); } } @@ -168,11 +171,11 @@ export async function createRootSuite(config: FullConfigInternal, fileSuitesByPr { // Filtering only and sharding might have reduced the number of top-level projects. // Build the project closure to only include dependencies that are still needed. - const projectClosure = new Set(buildProjectsClosure(rootSuite.suites.map(suite => suite.project() as FullProjectInternal))); + const projectClosure = new Map(buildProjectsClosure(rootSuite.suites.map(suite => suite.project() as FullProjectInternal))); // Clone file suites for dependency projects. - for (const [project, fileSuites] of fileSuitesByProject) { - if (project._internal.type === 'dependency' && projectClosure.has(project)) + for (const [project, fileSuites] of testRun.projectSuites) { + if (testRun.projectType.get(project) === 'dependency' && projectClosure.has(project)) rootSuite._prependSuite(await createProjectSuite(fileSuites, project, { cliFileFilters: [], cliTitleMatcher: undefined })); } } diff --git a/packages/playwright-test/src/runner/projectUtils.ts b/packages/playwright-test/src/runner/projectUtils.ts index 886bd75f8d..547906065c 100644 --- a/packages/playwright-test/src/runner/projectUtils.ts +++ b/packages/playwright-test/src/runner/projectUtils.ts @@ -49,24 +49,22 @@ export function filterProjects(projects: FullProjectInternal[], projectNames?: s return result; } -export function buildProjectsClosure(projects: FullProjectInternal[]): FullProjectInternal[] { - const result = new Set(); +export function buildProjectsClosure(projects: FullProjectInternal[]): Map { + const result = new Map(); const visit = (depth: number, project: FullProjectInternal) => { if (depth > 100) { const error = new Error('Circular dependency detected between projects.'); error.stack = ''; throw error; } - if (depth) - project._internal.type = 'dependency'; - result.add(project); + result.set(project, depth ? 'dependency' : 'top-level'); project._internal.deps.map(visit.bind(undefined, depth + 1)); }; for (const p of projects) - p._internal.type = 'top-level'; + result.set(p, 'top-level'); for (const p of projects) visit(0, p); - return [...result]; + return result; } export async function collectFilesForProject(project: FullProjectInternal, fsCache = new Map()): Promise { diff --git a/packages/playwright-test/src/runner/runner.ts b/packages/playwright-test/src/runner/runner.ts index 5173428c1f..d5c0d5b5e8 100644 --- a/packages/playwright-test/src/runner/runner.ts +++ b/packages/playwright-test/src/runner/runner.ts @@ -20,8 +20,7 @@ import type { FullResult } from '../../types/testReporter'; import { webServerPluginsForConfig } from '../plugins/webServerPlugin'; import { collectFilesForProject, filterProjects } from './projectUtils'; import { createReporter } from './reporters'; -import { createTaskRunner, createTaskRunnerForList } from './tasks'; -import type { TaskRunnerState } from './tasks'; +import { TestRun, createTaskRunner, createTaskRunnerForList } from './tasks'; import type { FullConfigInternal } from '../common/types'; import { colors } from 'playwright-core/lib/utilsBundle'; import { runWatchModeLoop } from './watchMode'; @@ -60,12 +59,7 @@ export class Runner { const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process') : createTaskRunner(config, reporter); - const context: TaskRunnerState = { - config, - reporter, - phases: [], - }; - + const testRun = new TestRun(config, reporter); reporter.onConfigure(config); if (!listOnly && config._internal.ignoreSnapshots) { @@ -77,9 +71,9 @@ export class Runner { ].join('\n'))); } - const taskStatus = await taskRunner.run(context, deadline); + const taskStatus = await taskRunner.run(testRun, deadline); let status: FullResult['status'] = 'passed'; - if (context.phases.find(p => p.dispatcher.hasWorkerErrors()) || context.rootSuite?.allTests().some(test => !test.ok())) + if (testRun.phases.find(p => p.dispatcher.hasWorkerErrors()) || testRun.rootSuite?.allTests().some(test => !test.ok())) status = 'failed'; if (status === 'passed' && taskStatus !== 'passed') status = taskStatus; diff --git a/packages/playwright-test/src/runner/tasks.ts b/packages/playwright-test/src/runner/tasks.ts index 63396e1347..e9b831b29d 100644 --- a/packages/playwright-test/src/runner/tasks.ts +++ b/packages/playwright-test/src/runner/tasks.ts @@ -24,10 +24,10 @@ import type { Multiplexer } from '../reporters/multiplexer'; import { createTestGroups, type TestGroup } from '../runner/testGroups'; import type { Task } from './taskRunner'; import { TaskRunner } from './taskRunner'; -import type { Suite } from '../common/test'; import type { FullConfigInternal, FullProjectInternal } from '../common/types'; import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils'; import type { Matcher } from '../util'; +import type { Suite } from '../common/test'; const removeFolderAsync = promisify(rimraf); const readDirAsync = promisify(fs.readdir); @@ -38,40 +38,49 @@ type ProjectWithTestGroups = { testGroups: TestGroup[]; }; -type Phase = { +export type Phase = { dispatcher: Dispatcher, projects: ProjectWithTestGroups[] }; -export type TaskRunnerState = { - reporter: Multiplexer; - config: FullConfigInternal; - rootSuite?: Suite; - phases: Phase[]; -}; +export class TestRun { + readonly reporter: Multiplexer; + readonly config: FullConfigInternal; + rootSuite: Suite | undefined = undefined; + readonly phases: Phase[] = []; + projects: FullProjectInternal[] = []; + projectFiles: Map = new Map(); + projectType: Map = new Map(); + projectSuites: Map = new Map(); -export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner { - const taskRunner = new TaskRunner(reporter, config.globalTimeout); + constructor(config: FullConfigInternal, reporter: Multiplexer) { + this.config = config; + this.reporter = reporter; + } +} + +export function createTaskRunner(config: FullConfigInternal, reporter: Multiplexer): TaskRunner { + const taskRunner = new TaskRunner(reporter, config.globalTimeout); addGlobalSetupTasks(taskRunner, config); taskRunner.addTask('load tests', createLoadTask('in-process', true)); addRunTasks(taskRunner, config); return taskRunner; } -export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: Multiplexer): TaskRunner { - const taskRunner = new TaskRunner(reporter, 0); +export function createTaskRunnerForWatchSetup(config: FullConfigInternal, reporter: Multiplexer): TaskRunner { + const taskRunner = new TaskRunner(reporter, 0); addGlobalSetupTasks(taskRunner, config); return taskRunner; } -export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: Multiplexer, projectsToIgnore?: Set, additionalFileMatcher?: Matcher): TaskRunner { - const taskRunner = new TaskRunner(reporter, 0); - taskRunner.addTask('load tests', createLoadTask('out-of-process', true, projectsToIgnore, additionalFileMatcher)); +export function createTaskRunnerForWatch(config: FullConfigInternal, reporter: Multiplexer, additionalFileMatcher?: Matcher): TaskRunner { + const taskRunner = new TaskRunner(reporter, 0); + taskRunner.addTask('load tests', createLoadTask('out-of-process', true, additionalFileMatcher)); addRunTasks(taskRunner, config); return taskRunner; } -function addGlobalSetupTasks(taskRunner: TaskRunner, config: FullConfigInternal) { +function addGlobalSetupTasks(taskRunner: TaskRunner, config: FullConfigInternal) { for (const plugin of config._internal.plugins) taskRunner.addTask('plugin setup', createPluginSetupTask(plugin)); if (config.globalSetup || config.globalTeardown) @@ -79,7 +88,7 @@ function addGlobalSetupTasks(taskRunner: TaskRunner, config: Fu taskRunner.addTask('clear output', createRemoveOutputDirsTask()); } -function addRunTasks(taskRunner: TaskRunner, config: FullConfigInternal) { +function addRunTasks(taskRunner: TaskRunner, config: FullConfigInternal) { taskRunner.addTask('create phases', createPhasesTask()); taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => { reporter.onBegin?.(config, rootSuite!); @@ -92,8 +101,8 @@ function addRunTasks(taskRunner: TaskRunner, config: FullConfig return taskRunner; } -export function createTaskRunnerForList(config: FullConfigInternal, reporter: Multiplexer, mode: 'in-process' | 'out-of-process'): TaskRunner { - const taskRunner = new TaskRunner(reporter, config.globalTimeout); +export function createTaskRunnerForList(config: FullConfigInternal, reporter: Multiplexer, mode: 'in-process' | 'out-of-process'): TaskRunner { + const taskRunner = new TaskRunner(reporter, config.globalTimeout); taskRunner.addTask('load tests', createLoadTask(mode, false)); taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => { reporter.onBegin?.(config, rootSuite!); @@ -102,7 +111,7 @@ export function createTaskRunnerForList(config: FullConfigInternal, reporter: Mu return taskRunner; } -function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task { +function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task { return async ({ config, reporter }) => { if (typeof plugin.factory === 'function') plugin.instance = await plugin.factory(); @@ -113,14 +122,14 @@ function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task { +function createPluginBeginTask(plugin: TestRunnerPluginRegistration): Task { return async ({ rootSuite }) => { await plugin.instance?.begin?.(rootSuite!); return () => plugin.instance?.end?.(); }; } -function createGlobalSetupTask(): Task { +function createGlobalSetupTask(): Task { return async ({ config }) => { const setupHook = config.globalSetup ? await loadGlobalHook(config, config.globalSetup) : undefined; const teardownHook = config.globalTeardown ? await loadGlobalHook(config, config.globalTeardown) : undefined; @@ -133,7 +142,7 @@ function createGlobalSetupTask(): Task { }; } -function createRemoveOutputDirsTask(): Task { +function createRemoveOutputDirsTask(): Task { return async ({ config }) => { const outputDirs = new Set(); for (const p of config.projects) { @@ -155,24 +164,23 @@ function createRemoveOutputDirsTask(): Task { }; } -function createLoadTask(mode: 'out-of-process' | 'in-process', shouldFilterOnly: boolean, projectsToIgnore = new Set(), additionalFileMatcher?: Matcher): Task { - return async (context, errors) => { - const { config } = context; - const filesToRunByProject = await collectProjectsAndTestFiles(config, projectsToIgnore, additionalFileMatcher); - const fileSuitesByProject = await loadFileSuites(mode, config, filesToRunByProject, errors); - context.rootSuite = await createRootSuite(config, fileSuitesByProject, errors, shouldFilterOnly); +function createLoadTask(mode: 'out-of-process' | 'in-process', shouldFilterOnly: boolean, additionalFileMatcher?: Matcher): Task { + return async (testRun, errors) => { + await collectProjectsAndTestFiles(testRun, additionalFileMatcher); + await loadFileSuites(testRun, mode, errors); + testRun.rootSuite = await createRootSuite(testRun, errors, shouldFilterOnly); // Fail when no tests. - if (!context.rootSuite.allTests().length && !config._internal.passWithNoTests && !config.shard) + if (!testRun.rootSuite.allTests().length && !testRun.config._internal.passWithNoTests && !testRun.config.shard) throw new Error(`No tests found`); }; } -function createPhasesTask(): Task { - return async context => { - context.config._internal.maxConcurrentTestGroups = 0; +function createPhasesTask(): Task { + return async testRun => { + testRun.config._internal.maxConcurrentTestGroups = 0; const processed = new Set(); - const projectToSuite = new Map(context.rootSuite!.suites.map(suite => [suite.project() as FullProjectInternal, suite])); + const projectToSuite = new Map(testRun.rootSuite!.suites.map(suite => [suite.project() as FullProjectInternal, suite])); for (let i = 0; i < projectToSuite.size; i++) { // Find all projects that have all their dependencies processed by previous phases. const phaseProjects: FullProjectInternal[] = []; @@ -189,22 +197,22 @@ function createPhasesTask(): Task { processed.add(project); if (phaseProjects.length) { let testGroupsInPhase = 0; - const phase: Phase = { dispatcher: new Dispatcher(context.config, context.reporter), projects: [] }; - context.phases.push(phase); + const phase: Phase = { dispatcher: new Dispatcher(testRun.config, testRun.reporter), projects: [] }; + testRun.phases.push(phase); for (const project of phaseProjects) { const projectSuite = projectToSuite.get(project)!; - const testGroups = createTestGroups(projectSuite, context.config.workers); + const testGroups = createTestGroups(projectSuite, testRun.config.workers); phase.projects.push({ project, projectSuite, testGroups }); testGroupsInPhase += testGroups.length; } - debug('pw:test:task')(`created phase #${context.phases.length} with ${phase.projects.map(p => p.project.name).sort()} projects, ${testGroupsInPhase} testGroups`); - context.config._internal.maxConcurrentTestGroups = Math.max(context.config._internal.maxConcurrentTestGroups, testGroupsInPhase); + debug('pw:test:task')(`created phase #${testRun.phases.length} with ${phase.projects.map(p => p.project.name).sort()} projects, ${testGroupsInPhase} testGroups`); + testRun.config._internal.maxConcurrentTestGroups = Math.max(testRun.config._internal.maxConcurrentTestGroups, testGroupsInPhase); } } }; } -function createWorkersTask(): Task { +function createWorkersTask(): Task { return async ({ phases }) => { return async () => { for (const { dispatcher } of phases.reverse()) @@ -213,9 +221,9 @@ function createWorkersTask(): Task { }; } -function createRunTestsTask(): Task { - return async context => { - const { phases } = context; +function createRunTestsTask(): Task { + return async testRun => { + const { phases } = testRun; const successfulProjects = new Set(); const extraEnvByProjectId: EnvByProjectId = new Map(); diff --git a/packages/playwright-test/src/runner/uiMode.ts b/packages/playwright-test/src/runner/uiMode.ts index 3f6b97f7dc..fd40afeb23 100644 --- a/packages/playwright-test/src/runner/uiMode.ts +++ b/packages/playwright-test/src/runner/uiMode.ts @@ -23,8 +23,7 @@ import type { FullConfigInternal } from '../common/types'; import { Multiplexer } from '../reporters/multiplexer'; import { TeleReporterEmitter } from '../reporters/teleEmitter'; import { createReporter } from './reporters'; -import type { TaskRunnerState } from './tasks'; -import { createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks'; +import { TestRun, createTaskRunnerForList, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks'; import { chokidar } from '../utilsBundle'; import type { FSWatcher } from 'chokidar'; import { open } from '../utilsBundle'; @@ -72,12 +71,8 @@ class UIMode { const reporter = new Multiplexer([new ListReporter()]); const taskRunner = createTaskRunnerForWatchSetup(this._config, reporter); reporter.onConfigure(this._config); - const context: TaskRunnerState = { - config: this._config, - reporter, - phases: [], - }; - const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(context, 0); + const testRun = new TestRun(this._config, reporter); + const { status, cleanup: globalCleanup } = await taskRunner.runDeferCleanup(testRun, 0); await reporter.onExit({ status }); if (status !== 'passed') { await globalCleanup(); @@ -156,10 +151,10 @@ class UIMode { this._config._internal.listOnly = true; this._config._internal.testIdMatcher = undefined; const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process'); - const context: TaskRunnerState = { config: this._config, reporter, phases: [] }; + const testRun = new TestRun(this._config, reporter); clearCompilationCache(); reporter.onConfigure(this._config); - const status = await taskRunner.run(context, 0); + const status = await taskRunner.run(testRun, 0); await reporter.onExit({ status }); const projectDirs = new Set(); @@ -178,11 +173,11 @@ class UIMode { const runReporter = new TeleReporterEmitter(e => this._dispatchEvent(e)); const reporter = await createReporter(this._config, 'ui', [runReporter]); const taskRunner = createTaskRunnerForWatch(this._config, reporter); - const context: TaskRunnerState = { config: this._config, reporter, phases: [] }; + const testRun = new TestRun(this._config, reporter); clearCompilationCache(); reporter.onConfigure(this._config); const stop = new ManualPromise(); - const run = taskRunner.run(context, 0, stop).then(async status => { + const run = taskRunner.run(testRun, 0, stop).then(async status => { await reporter.onExit({ status }); this._testRun = undefined; this._config._internal.testIdMatcher = undefined; diff --git a/packages/playwright-test/src/runner/watchMode.ts b/packages/playwright-test/src/runner/watchMode.ts index df1ca74082..23ceea813a 100644 --- a/packages/playwright-test/src/runner/watchMode.ts +++ b/packages/playwright-test/src/runner/watchMode.ts @@ -20,8 +20,7 @@ import type { FullConfigInternal, FullProjectInternal } from '../common/types'; import { Multiplexer } from '../reporters/multiplexer'; import { createFileMatcher, createFileMatcherFromArguments } from '../util'; import type { Matcher } from '../util'; -import { createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks'; -import type { TaskRunnerState } from './tasks'; +import { TestRun, createTaskRunnerForWatch, createTaskRunnerForWatchSetup } from './tasks'; import { buildProjectsClosure, filterProjects } from './projectUtils'; import { clearCompilationCache, collectAffectedTestFiles } from '../common/compilationCache'; import type { FullResult } from 'packages/playwright-test/reporter'; @@ -45,13 +44,13 @@ class FSWatcher { const projects = filterProjects(config.projects, config._internal.cliProjectFilter); const projectClosure = buildProjectsClosure(projects); const projectFilters = new Map(); - for (const project of projectClosure) { + for (const [project, type] of projectClosure) { const testMatch = createFileMatcher(project.testMatch); const testIgnore = createFileMatcher(project.testIgnore); projectFilters.set(project, file => { if (!file.startsWith(project.testDir) || !testMatch(file) || testIgnore(file)) return false; - return project._internal.type === 'dependency' || commandLineFileMatcher(file); + return type === 'dependency' || commandLineFileMatcher(file); }); } @@ -60,7 +59,7 @@ class FSWatcher { if (this._watcher) await this._watcher.close(); - this._watcher = chokidar.watch(projectClosure.map(p => p.testDir), { ignoreInitial: true }).on('all', async (event, file) => { + this._watcher = chokidar.watch([...projectClosure.keys()].map(p => p.testDir), { ignoreInitial: true }).on('all', async (event, file) => { if (event !== 'add' && event !== 'change') return; @@ -115,14 +114,10 @@ export async function runWatchModeLoop(config: FullConfigInternal): Promise p._internal.type === 'dependency'); - const projectsToIgnore = new Set(projectClosure.filter(p => !affectedProjects.has(p))); + 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, { projectsToIgnore, additionalFileMatcher, title: title || 'files changed' }); + await runTests(config, failedTestIdCollector, { additionalFileMatcher, title: title || 'files changed' }); } async function runTests(config: FullConfigInternal, failedTestIdCollector: Set, options?: { @@ -283,19 +277,15 @@ async function runTests(config: FullConfigInternal, failedTestIdCollector: Set p.dispatcher.hasWorkerErrors()) || hasFailedTests) + if (testRun.phases.find(p => p.dispatcher.hasWorkerErrors()) || hasFailedTests) status = 'failed'; if (status === 'passed' && taskStatus !== 'passed') status = taskStatus;