Skip to content

Commit 165d521

Browse files
committed
fix(@angular/build): trigger test re-run on non-spec file changes in watch mode
When non-test files (services, components, etc.) change during watch mode, use Vitest's module graph to find dependent test specifications and include them in the re-run set. Previously only direct .spec.ts file changes triggered test re-runs. Consolidates the file processing into a single loop for clarity. Fixes #32159
1 parent f1ed025 commit 165d521

File tree

2 files changed

+69
-14
lines changed

2 files changed

+69
-14
lines changed

packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,14 @@ export class VitestExecutor implements TestExecutor {
140140
let testResults;
141141
if (buildResult.kind === ResultKind.Incremental) {
142142
// To rerun tests, Vitest needs the original test file paths, not the output paths.
143-
const modifiedSourceFiles = new Set<string>();
143+
// Process all modified files in a single loop.
144+
const specsToRerun = [];
144145
for (const modifiedFile of [...buildResult.modified, ...buildResult.added]) {
146+
const absoluteOutputFile = this.normalizePath(
147+
path.join(this.options.workspaceRoot, modifiedFile),
148+
);
149+
vitest.invalidateFile(absoluteOutputFile);
150+
145151
// The `modified` files in the build result are the output paths.
146152
// We need to find the original source file path to pass to Vitest.
147153
const source = this.entryPointToTestFile.get(modifiedFile);
@@ -150,24 +156,26 @@ export class VitestExecutor implements TestExecutor {
150156
DebugLogLevel.Verbose,
151157
`Mapped output file '${modifiedFile}' to source file '${source}' for re-run.`,
152158
);
153-
modifiedSourceFiles.add(source);
159+
vitest.invalidateFile(source);
160+
const specs = vitest.getModuleSpecifications(source);
161+
if (specs) {
162+
specsToRerun.push(...specs);
163+
}
154164
} else {
165+
// For non-test files (e.g., services, components), find dependent test specs
166+
// via Vitest's module graph so that changes to these files trigger test re-runs.
155167
this.debugLog(
156168
DebugLogLevel.Verbose,
157169
`Could not map output file '${modifiedFile}' to a source file. It may not be a test file.`,
158170
);
159-
}
160-
vitest.invalidateFile(
161-
this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile)),
162-
);
163-
}
164-
165-
const specsToRerun = [];
166-
for (const file of modifiedSourceFiles) {
167-
vitest.invalidateFile(file);
168-
const specs = vitest.getModuleSpecifications(file);
169-
if (specs) {
170-
specsToRerun.push(...specs);
171+
const specs = vitest.getModuleSpecifications(absoluteOutputFile);
172+
if (specs) {
173+
this.debugLog(
174+
DebugLogLevel.Verbose,
175+
`Found ${specs.length} dependent test specification(s) for non-test file '${absoluteOutputFile}'.`,
176+
);
177+
specsToRerun.push(...specs);
178+
}
171179
}
172180
}
173181

packages/angular/build/src/builders/unit-test/tests/behavior/watch_rebuild_spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,53 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
2020
setupApplicationTarget(harness);
2121
});
2222

23+
it('should re-run tests when a non-spec file changes', async () => {
24+
// Set up a component with a testable value and a spec that checks it
25+
harness.writeFiles({
26+
'src/app/app.component.ts': `
27+
import { Component } from '@angular/core';
28+
@Component({ selector: 'app-root', template: '' })
29+
export class AppComponent {
30+
title = 'hello';
31+
}`,
32+
'src/app/app.component.spec.ts': `
33+
import { describe, expect, test } from 'vitest';
34+
import { AppComponent } from './app.component';
35+
describe('AppComponent', () => {
36+
test('should have correct title', () => {
37+
const app = new AppComponent();
38+
expect(app.title).toBe('hello');
39+
});
40+
});`,
41+
});
42+
43+
harness.useTarget('test', {
44+
...BASE_OPTIONS,
45+
watch: true,
46+
});
47+
48+
await harness.executeWithCases([
49+
// 1. Initial run should succeed
50+
({ result }) => {
51+
expect(result?.success).toBeTrue();
52+
53+
// 2. Modify only the non-spec component file (change the title value)
54+
harness.writeFiles({
55+
'src/app/app.component.ts': `
56+
import { Component } from '@angular/core';
57+
@Component({ selector: 'app-root', template: '' })
58+
export class AppComponent {
59+
title = 'changed';
60+
}`,
61+
});
62+
},
63+
// 3. Test should re-run and fail because the title changed
64+
({ result }) => {
65+
expect(result?.success).toBeFalse();
66+
},
67+
]);
68+
});
69+
2370
it('should run tests when a compilation error is fixed and a test failure is introduced simultaneously', async () => {
2471
harness.useTarget('test', {
2572
...BASE_OPTIONS,

0 commit comments

Comments
 (0)