@@ -15,7 +15,7 @@ import type { InlineConfig, Vitest } from 'vitest/node';
1515import { assertIsError } from '../../../../utils/error' ;
1616import { loadEsmModule } from '../../../../utils/load-esm' ;
1717import { toPosixPath } from '../../../../utils/path' ;
18- import type { FullResult , IncrementalResult } from '../../../application/results' ;
18+ import { type FullResult , type IncrementalResult , ResultKind } from '../../../application/results' ;
1919import { writeTestFiles } from '../../../karma/application_builder' ;
2020import { NormalizedUnitTestBuilderOptions } from '../../options' ;
2121import type { TestExecutor } from '../api' ;
@@ -40,20 +40,49 @@ export class VitestExecutor implements TestExecutor {
4040 await writeTestFiles ( buildResult . files , this . outputPath ) ;
4141
4242 this . latestBuildResult = buildResult ;
43+
44+ // Initialize Vitest if not already present.
4345 this . vitest ??= await this . initializeVitest ( ) ;
46+ const vitest = this . vitest ;
47+
48+ let testResults ;
49+ if ( buildResult . kind === ResultKind . Incremental ) {
50+ const addedFiles = buildResult . added . map ( ( file ) => path . join ( this . outputPath , file ) ) ;
51+ const modifiedFiles = buildResult . modified . map ( ( file ) => path . join ( this . outputPath , file ) ) ;
52+
53+ if ( addedFiles . length === 0 && modifiedFiles . length === 0 ) {
54+ yield { success : true } ;
55+
56+ return ;
57+ }
58+
59+ // If new files are added, use `start` to trigger test discovery.
60+ // Also pass modified files to `start` to ensure they are re-run.
61+ if ( addedFiles . length > 0 ) {
62+ await vitest . start ( [ ...addedFiles , ...modifiedFiles ] ) ;
63+ } else {
64+ // For modified files only, use the more efficient `rerunTestSpecifications`
65+ const specsToRerun = modifiedFiles . flatMap ( ( file ) => vitest . getModuleSpecifications ( file ) ) ;
66+
67+ if ( specsToRerun . length > 0 ) {
68+ modifiedFiles . forEach ( ( file ) => vitest . invalidateFile ( file ) ) ;
69+ testResults = await vitest . rerunTestSpecifications ( specsToRerun ) ;
70+ }
71+ }
72+ }
4473
4574 // Check if all the tests pass to calculate the result
46- const testModules = this . vitest . state . getTestModules ( ) ;
75+ const testModules = testResults ?. testModules ;
4776
48- yield { success : testModules . every ( ( testModule ) => testModule . ok ( ) ) } ;
77+ yield { success : testModules ? .every ( ( testModule ) => testModule . ok ( ) ) ?? true } ;
4978 }
5079
5180 async [ Symbol . asyncDispose ] ( ) : Promise < void > {
5281 await this . vitest ?. close ( ) ;
5382 }
5483
5584 private async initializeVitest ( ) : Promise < Vitest > {
56- const { codeCoverage, reporters, watch , workspaceRoot, setupFiles, browsers, debug } =
85+ const { codeCoverage, reporters, workspaceRoot, setupFiles, browsers, debug, watch } =
5786 this . options ;
5887 const { outputPath, projectName, latestBuildResult } = this ;
5988
@@ -101,7 +130,7 @@ export class VitestExecutor implements TestExecutor {
101130
102131 return startVitest (
103132 'test' ,
104- undefined /* cliFilters */ ,
133+ undefined ,
105134 {
106135 // Disable configuration file resolution/loading
107136 config : false ,
@@ -115,6 +144,11 @@ export class VitestExecutor implements TestExecutor {
115144 ...debugOptions ,
116145 } ,
117146 {
147+ server : {
148+ // Disable the actual file watcher. The boolean watch option above should still
149+ // be enabled as it controls other internal behavior related to rerunning tests.
150+ watch : null ,
151+ } ,
118152 plugins : [
119153 {
120154 name : 'angular:project-init' ,
0 commit comments