Skip to content
Closed
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 @@ -141,6 +141,7 @@ export class VitestExecutor implements TestExecutor {
if (buildResult.kind === ResultKind.Incremental) {
// To rerun tests, Vitest needs the original test file paths, not the output paths.
const modifiedSourceFiles = new Set<string>();
const modifiedNonTestFiles = new Set<string>();
for (const modifiedFile of [...buildResult.modified, ...buildResult.added]) {
// The `modified` files in the build result are the output paths.
// We need to find the original source file path to pass to Vitest.
Expand All @@ -156,6 +157,10 @@ export class VitestExecutor implements TestExecutor {
DebugLogLevel.Verbose,
`Could not map output file '${modifiedFile}' to a source file. It may not be a test file.`,
);
// Track non-test output files so we can find dependent test specs later.
modifiedNonTestFiles.add(
this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile)),
);
}
vitest.invalidateFile(
this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile)),
Expand All @@ -171,6 +176,19 @@ export class VitestExecutor implements TestExecutor {
}
}

// 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.
for (const file of modifiedNonTestFiles) {
const specs = vitest.getModuleSpecifications(file);
if (specs) {
this.debugLog(
DebugLogLevel.Verbose,
`Found ${specs.length} dependent test specification(s) for non-test file '${file}'.`,
);
specsToRerun.push(...specs);
}
}
Comment on lines +179 to +190
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for collecting specs to re-run is spread across multiple loops and intermediate collections (modifiedSourceFiles, modifiedNonTestFiles). This could be simplified and made more efficient by processing each modified file in a single loop. This would also naturally handle deduplication of specs if a Set is used.

Consider refactoring the logic from lines 145-190 to something like this:

const specsToRerun = new Set<string>();
for (const modifiedFile of [...buildResult.modified, ...buildResult.added]) {
  const absoluteOutputFile = this.normalizePath(path.join(this.options.workspaceRoot, modifiedFile));
  vitest.invalidateFile(absoluteOutputFile);

  const source = this.entryPointToTestFile.get(modifiedFile);
  if (source) {
    // This is a test file
    vitest.invalidateFile(source);
    const specs = vitest.getModuleSpecifications(source);
    if (specs) {
      specs.forEach(spec => specsToRerun.add(spec));
    }
  } else {
    // This is a non-test file
    const specs = vitest.getModuleSpecifications(absoluteOutputFile);
    if (specs) {
      this.debugLog(
        DebugLogLevel.Verbose,
        `Found ${specs.length} dependent test specification(s) for non-test file '${absoluteOutputFile}'.`,
      );
      specs.forEach(spec => specsToRerun.add(spec));
    }
  }
}

if (specsToRerun.size > 0) {
  const specsToRerunArr = [...specsToRerun];
  this.debugLog(DebugLogLevel.Info, `Re-running ${specsToRerunArr.length} test specifications.`);
  this.debugLog(DebugLogLevel.Verbose, 'Specs to rerun:', specsToRerunArr);
  testResults = await vitest.rerunTestSpecifications(specsToRerunArr);
}


if (specsToRerun.length > 0) {
this.debugLog(DebugLogLevel.Info, `Re-running ${specsToRerun.length} test specifications.`);
this.debugLog(DebugLogLevel.Verbose, 'Specs to rerun:', specsToRerun);
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