@@ -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,10 +40,41 @@ 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+ if ( buildResult . kind === ResultKind . Incremental ) {
49+ const addedFiles = buildResult . added . map ( ( file ) => path . join ( this . outputPath , file ) ) ;
50+ const modifiedFiles = buildResult . modified . map ( ( file ) => path . join ( this . outputPath , file ) ) ;
51+
52+ if ( addedFiles . length === 0 && modifiedFiles . length === 0 ) {
53+ yield { success : true } ;
54+
55+ return ;
56+ }
57+
58+ // If new files are added, use `start` to trigger test discovery.
59+ // Also pass modified files to `start` to ensure they are re-run.
60+ if ( addedFiles . length > 0 ) {
61+ await vitest . start ( [ ...addedFiles , ...modifiedFiles ] ) ;
62+ } else {
63+ // For modified files only, use the more efficient `rerunTestSpecifications`
64+ const specsToRerun = modifiedFiles . flatMap ( ( file ) => vitest . getModuleSpecifications ( file ) ) ;
65+
66+ if ( specsToRerun . length > 0 ) {
67+ modifiedFiles . forEach ( ( file ) => vitest . invalidateFile ( file ) ) ;
68+ await vitest . rerunTestSpecifications ( specsToRerun ) ;
69+ }
70+ }
71+ } else {
72+ // For the first build, perform a full run to discover and cache all specs
73+ await vitest . start ( ) ;
74+ }
4475
4576 // Check if all the tests pass to calculate the result
46- const testModules = this . vitest . state . getTestModules ( ) ;
77+ const testModules = vitest . state . getTestModules ( ) ;
4778
4879 yield { success : testModules . every ( ( testModule ) => testModule . ok ( ) ) } ;
4980 }
@@ -53,7 +84,7 @@ export class VitestExecutor implements TestExecutor {
5384 }
5485
5586 private async initializeVitest ( ) : Promise < Vitest > {
56- const { codeCoverage, reporters, watch , workspaceRoot, setupFiles, browsers, debug } =
87+ const { codeCoverage, reporters, workspaceRoot, setupFiles, browsers, debug, watch } =
5788 this . options ;
5889 const { outputPath, projectName, latestBuildResult } = this ;
5990
@@ -69,7 +100,7 @@ export class VitestExecutor implements TestExecutor {
69100 'The `vitest` package was not found. Please install the package and rerun the test command.' ,
70101 ) ;
71102 }
72- const { startVitest } = vitestNodeModule ;
103+ const { createVitest } = vitestNodeModule ;
73104
74105 // Setup vitest browser options if configured
75106 const browserOptions = setupBrowserConfiguration (
@@ -99,9 +130,8 @@ export class VitestExecutor implements TestExecutor {
99130 }
100131 : { } ;
101132
102- return startVitest (
133+ return createVitest (
103134 'test' ,
104- undefined /* cliFilters */ ,
105135 {
106136 // Disable configuration file resolution/loading
107137 config : false ,
@@ -115,6 +145,11 @@ export class VitestExecutor implements TestExecutor {
115145 ...debugOptions ,
116146 } ,
117147 {
148+ server : {
149+ // Disable the actual file watcher. The boolean watch option above should still
150+ // be enabled as it controls other internal behavior related to rerunning tests.
151+ watch : null ,
152+ } ,
118153 plugins : [
119154 {
120155 name : 'angular:project-init' ,
0 commit comments