Skip to content

Commit dd1280c

Browse files
author
maruthan
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. Fixes #32159
1 parent f1ed025 commit dd1280c

File tree

2 files changed

+65
-0
lines changed

2 files changed

+65
-0
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export class VitestExecutor implements TestExecutor {
141141
if (buildResult.kind === ResultKind.Incremental) {
142142
// To rerun tests, Vitest needs the original test file paths, not the output paths.
143143
const modifiedSourceFiles = new Set<string>();
144+
const modifiedNonTestFiles = new Set<string>();
144145
for (const modifiedFile of [...buildResult.modified, ...buildResult.added]) {
145146
// The `modified` files in the build result are the output paths.
146147
// We need to find the original source file path to pass to Vitest.
@@ -156,6 +157,10 @@ export class VitestExecutor implements TestExecutor {
156157
DebugLogLevel.Verbose,
157158
`Could not map output file '${modifiedFile}' to a source file. It may not be a test file.`,
158159
);
160+
// Track non-test output files so we can find dependent test specs later.
161+
modifiedNonTestFiles.add(
162+
this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile)),
163+
);
159164
}
160165
vitest.invalidateFile(
161166
this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile)),
@@ -171,6 +176,19 @@ export class VitestExecutor implements TestExecutor {
171176
}
172177
}
173178

179+
// For non-test files (e.g., services, components), find dependent test specs
180+
// via Vitest's module graph so that changes to these files trigger test re-runs.
181+
for (const file of modifiedNonTestFiles) {
182+
const specs = vitest.getModuleSpecifications(file);
183+
if (specs) {
184+
this.debugLog(
185+
DebugLogLevel.Verbose,
186+
`Found ${specs.length} dependent test specification(s) for non-test file '${file}'.`,
187+
);
188+
specsToRerun.push(...specs);
189+
}
190+
}
191+
174192
if (specsToRerun.length > 0) {
175193
this.debugLog(DebugLogLevel.Info, `Re-running ${specsToRerun.length} test specifications.`);
176194
this.debugLog(DebugLogLevel.Verbose, 'Specs to rerun:', specsToRerun);

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)