11import { describe , expect , it , vi , beforeEach } from 'vitest' ;
22import type { Config , Test , TestWatcher } from 'jest-runner' ;
33import type { TestResult as JestTestResult } from '@jest/test-result' ;
4- import type { TestSuiteResult } from '@react-native-harness/bridge' ;
4+ import type {
5+ TestRunnerTestFinishedEvent ,
6+ TestRunnerTestStartedEvent ,
7+ TestSuiteResult ,
8+ } from '@react-native-harness/bridge' ;
59import { NativeCrashError , StartupStallError } from '../errors.js' ;
610import { DeviceNotRespondingError } from '@react-native-harness/bridge/server' ;
711import type { HarnessSession } from '../harness-session.js' ;
812import { executeRun } from '../execute-run.js' ;
913
14+ type EmitEvent = Parameters < typeof executeRun > [ 3 ] ;
15+ type RecordedEmitEvent = {
16+ emitEvent : EmitEvent ;
17+ calls : Array < unknown [ ] > ;
18+ } ;
19+
1020const resolveUndefined = async ( ) => undefined ;
1121
1222// Mock the file-runner so we control what jestResult/harnessResult each test returns
@@ -93,11 +103,21 @@ const makeSession = (overrides: Partial<HarnessSession> = {}): HarnessSession =>
93103 resetCrashState : vi . fn ( ) ,
94104 flushClientLogs : vi . fn ( ( ) => [ ] ) ,
95105 callHook : vi . fn ( resolveUndefined ) ,
106+ onTestRunnerEvent : vi . fn ( ( ) => ( ) => undefined ) ,
96107 setRunState : vi . fn ( ) ,
97108 dispose : vi . fn ( resolveUndefined ) ,
98109 ...overrides ,
99110} ) ;
100111
112+ const makeEmitEvent = ( ) : RecordedEmitEvent => {
113+ const calls : Array < unknown [ ] > = [ ] ;
114+ const emitEvent : EmitEvent = ( async ( ...eventData ) => {
115+ calls . push ( eventData ) ;
116+ } ) as EmitEvent ;
117+
118+ return { emitEvent, calls } ;
119+ } ;
120+
101121// ---------------------------------------------------------------------------
102122// Tests
103123// ---------------------------------------------------------------------------
@@ -115,7 +135,7 @@ describe('executeRun', () => {
115135 callHook : vi . fn ( async ( name ) => { hookNames . push ( name as string ) ; } ) ,
116136 } ) ;
117137
118- await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , vi . fn ( ) , vi . fn ( ) , vi . fn ( ) , makeGlobalConfig ( ) ) ;
138+ await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , makeEmitEvent ( ) . emitEvent , makeGlobalConfig ( ) ) ;
119139
120140 expect ( hookNames [ 0 ] ) . toBe ( 'run:started' ) ;
121141 expect ( hookNames [ hookNames . length - 1 ] ) . toBe ( 'run:finished' ) ;
@@ -127,7 +147,7 @@ describe('executeRun', () => {
127147 callHook : vi . fn ( async ( name ) => { hookNames . push ( name as string ) ; } ) ,
128148 } ) ;
129149
130- await executeRun ( session , [ makeTest ( '/a.ts' ) , makeTest ( '/b.ts' ) ] , makeWatcher ( ) , vi . fn ( ) , vi . fn ( ) , vi . fn ( ) , makeGlobalConfig ( ) ) ;
150+ await executeRun ( session , [ makeTest ( '/a.ts' ) , makeTest ( '/b.ts' ) ] , makeWatcher ( ) , makeEmitEvent ( ) . emitEvent , makeGlobalConfig ( ) ) ;
131151
132152 const fileHooks = hookNames . filter ( ( n ) =>
133153 n === 'test-file:started' || n === 'test-file:finished' ,
@@ -145,29 +165,41 @@ describe('executeRun', () => {
145165 ensureAppReady : vi . fn ( async ( ) => { throw new Error ( 'unexpected' ) ; } ) ,
146166 } ) ;
147167
148- await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , vi . fn ( ) , vi . fn ( ) , vi . fn ( ) , makeGlobalConfig ( ) ) ;
168+ await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , makeEmitEvent ( ) . emitEvent , makeGlobalConfig ( ) ) ;
149169
150170 expect ( hookNames ) . toContain ( 'run:finished' ) ;
151171 } ) ;
152172 } ) ;
153173
154174 describe ( 'happy path' , ( ) => {
155- it ( 'calls onStart , ensureAppReady, runTestFile, onResult in order' , async ( ) => {
175+ it ( 'emits file start , ensureAppReady, runTestFile, file success in order' , async ( ) => {
156176 const order : string [ ] = [ ] ;
157177 const session = makeSession ( {
158178 ensureAppReady : vi . fn ( async ( ) => { order . push ( 'ensureAppReady' ) ; } ) ,
159179 } ) ;
160- const onStart = vi . fn ( async ( ) => { order . push ( 'onStart' ) ; } ) ;
161- const onResult = vi . fn ( async ( ) => { order . push ( 'onResult' ) ; } ) ;
180+ const emitEvent : EmitEvent = ( async ( eventName , ..._eventData ) => {
181+ if ( eventName === 'test-file-start' ) {
182+ order . push ( 'test-file-start' ) ;
183+ }
184+
185+ if ( eventName === 'test-file-success' ) {
186+ order . push ( 'test-file-success' ) ;
187+ }
188+ } ) as EmitEvent ;
162189
163190 mockRunHarnessTestFile . mockImplementation ( async ( ) => {
164191 order . push ( 'runTestFile' ) ;
165192 return makeFileRunResult ( ) ;
166193 } ) ;
167194
168- await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , onStart , onResult , vi . fn ( ) , makeGlobalConfig ( ) ) ;
195+ await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , emitEvent , makeGlobalConfig ( ) ) ;
169196
170- expect ( order ) . toEqual ( [ 'onStart' , 'ensureAppReady' , 'runTestFile' , 'onResult' ] ) ;
197+ expect ( order ) . toEqual ( [
198+ 'test-file-start' ,
199+ 'ensureAppReady' ,
200+ 'runTestFile' ,
201+ 'test-file-success' ,
202+ ] ) ;
171203 } ) ;
172204
173205 it ( 'accumulates passing test counts in run:finished summary' , async ( ) => {
@@ -182,7 +214,7 @@ describe('executeRun', () => {
182214 makeFileRunResult ( { jestResult : makeJestResult ( { numPassingTests : 3 } ) } ) ,
183215 ) ;
184216
185- await executeRun ( session , [ makeTest ( ) , makeTest ( '/b.ts' ) ] , makeWatcher ( ) , vi . fn ( ) , vi . fn ( ) , vi . fn ( ) , makeGlobalConfig ( ) ) ;
217+ await executeRun ( session , [ makeTest ( ) , makeTest ( '/b.ts' ) ] , makeWatcher ( ) , makeEmitEvent ( ) . emitEvent , makeGlobalConfig ( ) ) ;
186218
187219 const payload = finishedPayload as { summary : { passed : number } ; status : string } ;
188220 expect ( payload . summary . passed ) . toBe ( 6 ) ;
@@ -196,55 +228,126 @@ describe('executeRun', () => {
196228 . mockReturnValueOnce ( [ ] )
197229 . mockReturnValueOnce ( clientLogs ) ,
198230 } ) ;
199- const onResult = vi . fn ( ) ;
231+ const { emitEvent , calls } = makeEmitEvent ( ) ;
200232
201233 await executeRun (
202234 session ,
203235 [ makeTest ( ) ] ,
204236 makeWatcher ( ) ,
205- vi . fn ( ) ,
206- onResult ,
207- vi . fn ( ) ,
237+ emitEvent ,
208238 makeGlobalConfig ( ) ,
209239 ) ;
210240
211241 expect ( session . flushClientLogs ) . toHaveBeenCalledTimes ( 2 ) ;
212- expect ( onResult ) . toHaveBeenCalledWith (
242+ expect ( calls ) . toContainEqual ( [
243+ 'test-file-success' ,
213244 expect . anything ( ) ,
214245 expect . objectContaining ( { console : clientLogs } ) ,
215- ) ;
246+ ] ) ;
247+ } ) ;
248+
249+ it ( 'emits test-case events before file success' , async ( ) => {
250+ let testRunnerListener :
251+ | ( ( event : TestRunnerTestStartedEvent | TestRunnerTestFinishedEvent ) => void )
252+ | undefined ;
253+ const { emitEvent, calls : emittedEvents } = makeEmitEvent ( ) ;
254+ const session = makeSession ( {
255+ onTestRunnerEvent : vi . fn ( ( listener ) => {
256+ testRunnerListener = listener as typeof testRunnerListener ;
257+ return ( ) => undefined ;
258+ } ) ,
259+ } ) ;
260+
261+ mockRunHarnessTestFile . mockImplementation ( async ( ) => {
262+ testRunnerListener ?.( {
263+ type : 'test-started' ,
264+ file : 'example.ts' ,
265+ suite : 'suite' ,
266+ name : 'works' ,
267+ ancestorTitles : [ 'suite' ] ,
268+ fullName : 'suite works' ,
269+ startedAt : 10 ,
270+ declarationMode : 'only' ,
271+ } ) ;
272+ testRunnerListener ?.( {
273+ type : 'test-finished' ,
274+ file : 'example.ts' ,
275+ suite : 'suite' ,
276+ name : 'works' ,
277+ ancestorTitles : [ 'suite' ] ,
278+ fullName : 'suite works' ,
279+ startedAt : 10 ,
280+ declarationMode : 'only' ,
281+ duration : 5 ,
282+ status : 'passed' ,
283+ } ) ;
284+
285+ return makeFileRunResult ( ) ;
286+ } ) ;
287+
288+ await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , emitEvent , makeGlobalConfig ( ) ) ;
289+
290+ expect ( emittedEvents ) . toEqual ( [
291+ [ 'test-file-start' , expect . anything ( ) ] ,
292+ [
293+ 'test-case-start' ,
294+ 'example.ts' ,
295+ expect . objectContaining ( {
296+ ancestorTitles : [ 'suite' ] ,
297+ fullName : 'suite works' ,
298+ mode : 'only' ,
299+ title : 'works' ,
300+ startedAt : 10 ,
301+ } ) ,
302+ ] ,
303+ [
304+ 'test-case-result' ,
305+ 'example.ts' ,
306+ expect . objectContaining ( {
307+ ancestorTitles : [ 'suite' ] ,
308+ fullName : 'suite works' ,
309+ numPassingAsserts : 1 ,
310+ startedAt : 10 ,
311+ status : 'passed' ,
312+ title : 'works' ,
313+ } ) ,
314+ ] ,
315+ [ 'test-file-success' , expect . anything ( ) , expect . anything ( ) ] ,
316+ ] ) ;
216317 } ) ;
217318 } ) ;
218319
219320 describe ( 'runtime failures' , ( ) => {
220321 it ( 'passes StartupStallError to onFailure with an empty stack' , async ( ) => {
221- const onFailure = vi . fn ( ) ;
322+ const { emitEvent , calls } = makeEmitEvent ( ) ;
222323 const session = makeSession ( {
223324 ensureAppReady : vi . fn ( ) . mockRejectedValue ( new StartupStallError ( 1500 , 3 ) ) ,
224325 } ) ;
225326
226- await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , vi . fn ( ) , vi . fn ( ) , onFailure , makeGlobalConfig ( ) ) ;
327+ await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , emitEvent , makeGlobalConfig ( ) ) ;
227328
228- expect ( onFailure ) . toHaveBeenCalledWith (
329+ expect ( calls ) . toContainEqual ( [
330+ 'test-file-failure' ,
229331 expect . objectContaining ( { path : '/test/example.ts' } ) ,
230332 expect . objectContaining ( { message : expect . stringContaining ( '1500' ) , stack : '' } ) ,
231- ) ;
333+ ] ) ;
232334 } ) ;
233335
234336 it ( 'passes DeviceNotRespondingError to onFailure with an empty stack' , async ( ) => {
235- const onFailure = vi . fn ( ) ;
337+ const { emitEvent , calls } = makeEmitEvent ( ) ;
236338 const session = makeSession ( {
237339 ensureAppReady : vi . fn ( ) . mockRejectedValue (
238340 new DeviceNotRespondingError ( 'runTests' , [ ] ) ,
239341 ) ,
240342 } ) ;
241343
242- await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , vi . fn ( ) , vi . fn ( ) , onFailure , makeGlobalConfig ( ) ) ;
344+ await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , emitEvent , makeGlobalConfig ( ) ) ;
243345
244- expect ( onFailure ) . toHaveBeenCalledWith (
346+ expect ( calls ) . toContainEqual ( [
347+ 'test-file-failure' ,
245348 expect . anything ( ) ,
246349 expect . objectContaining ( { stack : '' } ) ,
247- ) ;
350+ ] ) ;
248351 } ) ;
249352
250353 it ( 'calls resetCrashState after a NativeCrashError' , async ( ) => {
@@ -254,7 +357,7 @@ describe('executeRun', () => {
254357 ) ,
255358 } ) ;
256359
257- await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , vi . fn ( ) , vi . fn ( ) , vi . fn ( ) , makeGlobalConfig ( ) ) ;
360+ await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , makeEmitEvent ( ) . emitEvent , makeGlobalConfig ( ) ) ;
258361
259362 expect ( session . resetCrashState ) . toHaveBeenCalled ( ) ;
260363 } ) ;
@@ -268,7 +371,7 @@ describe('executeRun', () => {
268371 ensureAppReady : vi . fn ( ) . mockRejectedValue ( new StartupStallError ( 1000 , 2 ) ) ,
269372 } ) ;
270373
271- await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , vi . fn ( ) , vi . fn ( ) , vi . fn ( ) , makeGlobalConfig ( ) ) ;
374+ await executeRun ( session , [ makeTest ( ) ] , makeWatcher ( ) , makeEmitEvent ( ) . emitEvent , makeGlobalConfig ( ) ) ;
272375
273376 const payload = finishedPayload as { summary : { failed : number } ; status : string } ;
274377 expect ( payload . summary . failed ) . toBe ( 1 ) ;
@@ -282,20 +385,18 @@ describe('executeRun', () => {
282385 const session = makeSession ( {
283386 callHook : vi . fn ( async ( name ) => { hookNames . push ( name as string ) ; } ) ,
284387 } ) ;
285- const onStart = vi . fn ( ) ;
388+ const { emitEvent , calls } = makeEmitEvent ( ) ;
286389
287390 await executeRun (
288391 session ,
289392 [ makeTest ( '/a.ts' ) , makeTest ( '/b.ts' ) ] ,
290393 makeWatcher ( true /* interrupted */ ) ,
291- onStart ,
292- vi . fn ( ) ,
293- vi . fn ( ) ,
394+ emitEvent ,
294395 makeGlobalConfig ( ) ,
295396 ) ;
296397
297398 // No test was started; watcher was already interrupted.
298- expect ( onStart ) . not . toHaveBeenCalled ( ) ;
399+ expect ( calls ) . not . toContainEqual ( [ 'test-file-start' , expect . anything ( ) ] ) ;
299400 expect ( hookNames ) . toContain ( 'run:finished' ) ;
300401 } ) ;
301402 } ) ;
@@ -314,7 +415,7 @@ describe('executeRun', () => {
314415 session ,
315416 [ makeTest ( '/a.ts' ) , makeTest ( '/b.ts' ) , makeTest ( '/c.ts' ) ] ,
316417 makeWatcher ( ) ,
317- vi . fn ( ) , vi . fn ( ) , vi . fn ( ) ,
418+ makeEmitEvent ( ) . emitEvent ,
318419 makeGlobalConfig ( ) ,
319420 ) ;
320421
0 commit comments