Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,14 @@ export class VitestExecutor implements TestExecutor {
let testResults;
if (buildResult.kind === ResultKind.Incremental) {
// To rerun tests, Vitest needs the original test file paths, not the output paths.
const modifiedSourceFiles = new Set<string>();
// Process all modified files in a single loop.
const specsToRerun = [];
for (const modifiedFile of [...buildResult.modified, ...buildResult.added]) {
const absoluteOutputFile = this.normalizePath(
path.join(this.options.workspaceRoot, modifiedFile),
);
vitest.invalidateFile(absoluteOutputFile);

// The `modified` files in the build result are the output paths.
// We need to find the original source file path to pass to Vitest.
const source = this.entryPointToTestFile.get(modifiedFile);
Expand All @@ -150,24 +156,26 @@ export class VitestExecutor implements TestExecutor {
DebugLogLevel.Verbose,
`Mapped output file '${modifiedFile}' to source file '${source}' for re-run.`,
);
modifiedSourceFiles.add(source);
vitest.invalidateFile(source);
const specs = vitest.getModuleSpecifications(source);
if (specs) {
specsToRerun.push(...specs);
}
} else {
// For non-test files (e.g., services, components), find dependent test specs
// via Vitest's module graph so that changes to these files trigger test re-runs.
this.debugLog(
DebugLogLevel.Verbose,
`Could not map output file '${modifiedFile}' to a source file. It may not be a test file.`,
);
}
vitest.invalidateFile(
this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile)),
);
}

const specsToRerun = [];
for (const file of modifiedSourceFiles) {
vitest.invalidateFile(file);
const specs = vitest.getModuleSpecifications(file);
if (specs) {
specsToRerun.push(...specs);
const specs = vitest.getModuleSpecifications(absoluteOutputFile);
if (specs) {
this.debugLog(
DebugLogLevel.Verbose,
`Found ${specs.length} dependent test specification(s) for non-test file '${absoluteOutputFile}'.`,
);
specsToRerun.push(...specs);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,53 @@ describeBuilder(execute, UNIT_TEST_BUILDER_INFO, (harness) => {
setupApplicationTarget(harness);
});

it('should re-run tests when a non-spec file changes', async () => {
// Set up a component with a testable value and a spec that checks it
harness.writeFiles({
'src/app/app.component.ts': `
import { Component } from '@angular/core';
@Component({ selector: 'app-root', template: '' })
export class AppComponent {
title = 'hello';
}`,
'src/app/app.component.spec.ts': `
import { describe, expect, test } from 'vitest';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
test('should have correct title', () => {
const app = new AppComponent();
expect(app.title).toBe('hello');
});
});`,
});

harness.useTarget('test', {
...BASE_OPTIONS,
watch: true,
});

await harness.executeWithCases([
// 1. Initial run should succeed
({ result }) => {
expect(result?.success).toBeTrue();

// 2. Modify only the non-spec component file (change the title value)
harness.writeFiles({
'src/app/app.component.ts': `
import { Component } from '@angular/core';
@Component({ selector: 'app-root', template: '' })
export class AppComponent {
title = 'changed';
}`,
});
},
// 3. Test should re-run and fail because the title changed
({ result }) => {
expect(result?.success).toBeFalse();
},
]);
});

it('should run tests when a compilation error is fixed and a test failure is introduced simultaneously', async () => {
harness.useTarget('test', {
...BASE_OPTIONS,
Expand Down
Loading