diff --git a/packages/playwright-test/src/common/compilationCache.ts b/packages/playwright-test/src/common/compilationCache.ts index 3c4879c433..215bca061b 100644 --- a/packages/playwright-test/src/common/compilationCache.ts +++ b/packages/playwright-test/src/common/compilationCache.ts @@ -32,7 +32,7 @@ const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwri const sourceMaps: Map = new Map(); const memoryCache = new Map(); -const fileDependencies = new Map(); +const fileDependencies = new Map>(); Error.stackTraceLimit = 200; @@ -92,7 +92,7 @@ export function serializeCompilationCache(): any { return { sourceMaps: [...sourceMaps.entries()], memoryCache: [...memoryCache.entries()], - fileDependencies: [...fileDependencies.entries()], + fileDependencies: [...fileDependencies.entries()].map(([filename, deps]) => ([filename, [...deps]])), }; } @@ -107,7 +107,7 @@ export function addToCompilationCache(payload: any) { for (const entry of payload.memoryCache) memoryCache.set(entry[0], entry[1]); for (const entry of payload.fileDependencies) - fileDependencies.set(entry[0], entry[1]); + fileDependencies.set(entry[0], new Set(entry[1])); } function calculateCachePath(content: string, filePath: string, isModule: boolean): string { @@ -134,8 +134,11 @@ export function stopCollectingFileDeps(filename: string) { if (!depsCollector) return; depsCollector.delete(filename); - const deps = [...depsCollector!].filter(f => !belongsToNodeModules(f)); - fileDependencies.set(filename, deps); + for (const dep of depsCollector) { + if (belongsToNodeModules(dep)) + depsCollector.delete(dep); + } + fileDependencies.set(filename, depsCollector); depsCollector = undefined; } @@ -147,6 +150,14 @@ export function fileDependenciesForTest() { return fileDependencies; } +export function collectAffectedTestFiles(dependency: string, testFileCollector: Set) { + testFileCollector.add(dependency); + for (const [testFile, deps] of fileDependencies) { + if (deps.has(dependency)) + testFileCollector.add(testFile); + } +} + // These two are only used in the dev mode, they are specifically excluding // files from packages/playwright*. In production mode, node_modules covers // that. diff --git a/packages/playwright-test/src/common/transform.ts b/packages/playwright-test/src/common/transform.ts index 1a63e777c9..24ad15ae42 100644 --- a/packages/playwright-test/src/common/transform.ts +++ b/packages/playwright-test/src/common/transform.ts @@ -221,10 +221,8 @@ function installTransform(): () => void { } const collectCJSDependencies = (module: Module, dependencies: Set) => { - if (dependencies.has(module.filename)) - return; module.children.forEach(child => { - if (!belongsToNodeModules(child.filename)) { + if (!belongsToNodeModules(child.filename) && !dependencies.has(child.filename)) { dependencies.add(child.filename); collectCJSDependencies(child, dependencies); } diff --git a/packages/playwright-test/src/internalsForTest.ts b/packages/playwright-test/src/internalsForTest.ts index 84b9c0b352..0b45178627 100644 --- a/packages/playwright-test/src/internalsForTest.ts +++ b/packages/playwright-test/src/internalsForTest.ts @@ -19,6 +19,6 @@ import { fileDependenciesForTest } from './common/compilationCache'; export function fileDependencies() { return Object.fromEntries([...fileDependenciesForTest().entries()].map(entry => ( - [path.basename(entry[0]), entry[1].map(f => path.basename(f))] + [path.basename(entry[0]), [...entry[1]].map(f => path.basename(f)).sort()] ))); } diff --git a/packages/playwright-test/src/runner/watchMode.ts b/packages/playwright-test/src/runner/watchMode.ts index 8d5b81bfbb..130c561a48 100644 --- a/packages/playwright-test/src/runner/watchMode.ts +++ b/packages/playwright-test/src/runner/watchMode.ts @@ -23,7 +23,7 @@ import type { Matcher } from '../util'; import { createTaskRunnerForWatch } from './tasks'; import type { TaskRunnerState } from './tasks'; import { buildProjectsClosure, filterProjects } from './projectUtils'; -import { clearCompilationCache } from '../common/compilationCache'; +import { clearCompilationCache, collectAffectedTestFiles } from '../common/compilationCache'; import type { FullResult, TestCase } from 'packages/playwright-test/reporter'; import chokidar from 'chokidar'; import { WatchModeReporter } from './reporters'; @@ -149,14 +149,19 @@ ${colors.dim('press')} ${colors.bold('h')} ${colors.dim('to show help, press')} } } -async function runChangedTests(config: FullConfigInternal, failedTestIds: Set, projectClosure: FullProjectInternal[], files: Set) { +async function runChangedTests(config: FullConfigInternal, failedTestIdCollector: Set, projectClosure: FullProjectInternal[], changedFiles: Set) { const commandLineFileMatcher = config._internal.cliFileFilters.length ? createFileMatcherFromFilters(config._internal.cliFileFilters) : () => true; + // Resolve files that depend on the changed files. + const testFiles = new Set(); + for (const file of changedFiles) + collectAffectedTestFiles(file, testFiles); + // Collect projects with changes. const filesByProject = new Map(); for (const project of projectClosure) { const projectFiles: string[] = []; - for (const file of files) { + for (const file of testFiles) { if (!file.startsWith(project.testDir)) continue; if (project._internal.type === 'dependency' || commandLineFileMatcher(file)) @@ -174,11 +179,11 @@ async function runChangedTests(config: FullConfigInternal, failedTestIds: Set true : (file: string) => files.has(file); - return await runTests(config, failedTestIds, projectsToIgnore, additionalFileMatcher); + const additionalFileMatcher = affectsAnyDependency ? () => true : (file: string) => testFiles.has(file); + return await runTests(config, failedTestIdCollector, projectsToIgnore, additionalFileMatcher); } -async function runTests(config: FullConfigInternal, failedTestIds: Set, projectsToIgnore?: Set, additionalFileMatcher?: Matcher) { +async function runTests(config: FullConfigInternal, failedTestIdCollector: Set, projectsToIgnore?: Set, additionalFileMatcher?: Matcher) { const reporter = new Multiplexer([new WatchModeReporter()]); const taskRunner = createTaskRunnerForWatch(config, reporter, projectsToIgnore, additionalFileMatcher); const context: TaskRunnerState = { @@ -194,9 +199,9 @@ async function runTests(config: FullConfigInternal, failedTestIds: Set, let hasFailedTests = false; for (const test of context.rootSuite?.allTests() || []) { if (test.outcome() === 'expected') { - failedTestIds.delete(test.id); + failedTestIdCollector.delete(test.id); } else { - failedTestIds.add(test.id); + failedTestIdCollector.add(test.id); hasFailedTests = true; } } diff --git a/tests/playwright-test/watch.spec.ts b/tests/playwright-test/watch.spec.ts index f0184d503c..d0ddf2fc61 100644 --- a/tests/playwright-test/watch.spec.ts +++ b/tests/playwright-test/watch.spec.ts @@ -24,13 +24,14 @@ test('should print dependencies in CJS mode', async ({ runInlineTest }) => { globalTeardown: './globalTeardown.ts', }); `, - 'helper.ts': `export function foo() {}`, + 'helperA.ts': `export function foo() {}`, + 'helperB.ts': `import './helperA';`, 'a.test.ts': ` - import './helper'; + import './helperA'; pwt.test('passes', () => {}); `, 'b.test.ts': ` - import './helper'; + import './helperB'; pwt.test('passes', () => {}); `, 'globalTeardown.ts': ` @@ -46,8 +47,8 @@ test('should print dependencies in CJS mode', async ({ runInlineTest }) => { const output = stripAnsi(result.output); const deps = JSON.parse(output.match(/###(.*)###/)![1]); expect(deps).toEqual({ - 'a.test.ts': ['helper.ts'], - 'b.test.ts': ['helper.ts'], + 'a.test.ts': ['helperA.ts'], + 'b.test.ts': ['helperA.ts', 'helperB.ts'], }); }); @@ -61,13 +62,14 @@ test('should print dependencies in ESM mode', async ({ runInlineTest, nodeVersio globalTeardown: './globalTeardown.ts', }); `, - 'helper.ts': `export function foo() {}`, + 'helperA.ts': `export function foo() {}`, + 'helperB.ts': `import './helperA.js';`, 'a.test.ts': ` - import './helper.js'; + import './helperA.js'; pwt.test('passes', () => {}); `, 'b.test.ts': ` - import './helper.js'; + import './helperB.js'; pwt.test('passes', () => {}); `, 'globalTeardown.ts': ` @@ -83,7 +85,7 @@ test('should print dependencies in ESM mode', async ({ runInlineTest, nodeVersio const output = stripAnsi(result.output); const deps = JSON.parse(output.match(/###(.*)###/)![1]); expect(deps).toEqual({ - 'a.test.ts': ['helper.ts'], - 'b.test.ts': ['helper.ts'], + 'a.test.ts': ['helperA.ts'], + 'b.test.ts': ['helperA.ts', 'helperB.ts'], }); });