@react-native-harness/runtime 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/collector/functions.d.ts +3 -3
  2. package/dist/collector/functions.d.ts.map +1 -1
  3. package/dist/collector/types.d.ts +3 -2
  4. package/dist/collector/types.d.ts.map +1 -1
  5. package/dist/collector/validation.d.ts +2 -2
  6. package/dist/collector/validation.d.ts.map +1 -1
  7. package/dist/device/index.d.ts +12 -0
  8. package/dist/device/index.d.ts.map +1 -0
  9. package/dist/device/index.js +62 -0
  10. package/dist/hmr.d.ts +2 -0
  11. package/dist/hmr.d.ts.map +1 -0
  12. package/dist/hmr.js +5 -0
  13. package/dist/logbox.d.ts +4 -0
  14. package/dist/logbox.d.ts.map +1 -0
  15. package/dist/logbox.js +18 -0
  16. package/dist/runner/hooks.d.ts +2 -1
  17. package/dist/runner/hooks.d.ts.map +1 -1
  18. package/dist/runner/hooks.js +27 -17
  19. package/dist/runner/runSuite.d.ts.map +1 -1
  20. package/dist/runner/runSuite.js +56 -6
  21. package/dist/runner/test-context.d.ts +16 -0
  22. package/dist/runner/test-context.d.ts.map +1 -0
  23. package/dist/runner/test-context.js +57 -0
  24. package/dist/runner/types.d.ts +2 -1
  25. package/dist/runner/types.d.ts.map +1 -1
  26. package/dist/test-utils/react-native-url-polyfill.d.ts +9 -0
  27. package/dist/test-utils/react-native-url-polyfill.d.ts.map +1 -0
  28. package/dist/test-utils/react-native-url-polyfill.js +1 -0
  29. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  30. package/out-tsc/vitest/src/__tests__/device.test.d.ts +2 -0
  31. package/out-tsc/vitest/src/__tests__/device.test.d.ts.map +1 -0
  32. package/out-tsc/vitest/src/__tests__/logbox.test.d.ts +2 -0
  33. package/out-tsc/vitest/src/__tests__/logbox.test.d.ts.map +1 -0
  34. package/out-tsc/vitest/src/__tests__/runner-context.test.d.ts +2 -0
  35. package/out-tsc/vitest/src/__tests__/runner-context.test.d.ts.map +1 -0
  36. package/out-tsc/vitest/src/collector/functions.d.ts +3 -3
  37. package/out-tsc/vitest/src/collector/functions.d.ts.map +1 -1
  38. package/out-tsc/vitest/src/collector/types.d.ts +3 -2
  39. package/out-tsc/vitest/src/collector/types.d.ts.map +1 -1
  40. package/out-tsc/vitest/src/collector/validation.d.ts +2 -2
  41. package/out-tsc/vitest/src/collector/validation.d.ts.map +1 -1
  42. package/out-tsc/vitest/src/device/index.d.ts +12 -0
  43. package/out-tsc/vitest/src/device/index.d.ts.map +1 -0
  44. package/out-tsc/vitest/src/hmr.d.ts +2 -0
  45. package/out-tsc/vitest/src/hmr.d.ts.map +1 -0
  46. package/out-tsc/vitest/src/logbox.d.ts +4 -0
  47. package/out-tsc/vitest/src/logbox.d.ts.map +1 -0
  48. package/out-tsc/vitest/src/runner/hooks.d.ts +2 -1
  49. package/out-tsc/vitest/src/runner/hooks.d.ts.map +1 -1
  50. package/out-tsc/vitest/src/runner/runSuite.d.ts.map +1 -1
  51. package/out-tsc/vitest/src/runner/test-context.d.ts +16 -0
  52. package/out-tsc/vitest/src/runner/test-context.d.ts.map +1 -0
  53. package/out-tsc/vitest/src/runner/types.d.ts +2 -1
  54. package/out-tsc/vitest/src/runner/types.d.ts.map +1 -1
  55. package/out-tsc/vitest/src/test-utils/react-native-url-polyfill.d.ts +9 -0
  56. package/out-tsc/vitest/src/test-utils/react-native-url-polyfill.d.ts.map +1 -0
  57. package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +1 -1
  58. package/out-tsc/vitest/vite.config.d.ts.map +1 -1
  59. package/package.json +2 -2
  60. package/src/__tests__/runner-context.test.ts +483 -0
  61. package/src/collector/functions.ts +5 -4
  62. package/src/collector/types.ts +4 -1
  63. package/src/collector/validation.ts +2 -2
  64. package/src/runner/hooks.ts +43 -19
  65. package/src/runner/runSuite.ts +75 -9
  66. package/src/runner/test-context.ts +84 -0
  67. package/src/runner/types.ts +3 -0
  68. package/src/test-utils/react-native-url-polyfill.ts +1 -0
  69. package/vite.config.ts +4 -0
@@ -1 +1 @@
1
- {"version":3,"file":"vite.config.d.ts","sourceRoot":"","sources":["../../vite.config.ts"],"names":[],"mappings":";AAIA,wBAsBI"}
1
+ {"version":3,"file":"vite.config.d.ts","sourceRoot":"","sources":["../../vite.config.ts"],"names":[],"mappings":";AAIA,wBA0BI"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@react-native-harness/runtime",
3
3
  "description": "The core test runtime that executes on React Native devices, providing Jest-compatible APIs (describe, it, expect) and managing test collection, execution, and result reporting in native environments.",
4
- "version": "1.2.0",
4
+ "version": "1.3.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -47,7 +47,7 @@
47
47
  "react-native-url-polyfill": "^3.0.0",
48
48
  "use-sync-external-store": "^1.6.0",
49
49
  "zustand": "^5.0.5",
50
- "@react-native-harness/bridge": "1.2.0"
50
+ "@react-native-harness/bridge": "1.3.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/chai": "^5.2.2"
@@ -0,0 +1,483 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe as harnessDescribe,
5
+ getTestCollector,
6
+ it as harnessIt,
7
+ } from '../collector/index.js';
8
+ import type { HarnessTestContext } from '@react-native-harness/bridge';
9
+ import { getTestRunner } from '../runner/index.js';
10
+ import { describe, expect, it, vi } from 'vitest';
11
+
12
+ vi.mock('../symbolicate.js', async () => {
13
+ return {
14
+ getCodeFrame: vi.fn(async () => null),
15
+ };
16
+ });
17
+
18
+ const getTask = (context: HarnessTestContext) => {
19
+ return context.task;
20
+ };
21
+
22
+ const getTaskContext = (context: HarnessTestContext) => {
23
+ return context;
24
+ };
25
+
26
+ describe('runner task context', () => {
27
+ it('passes minimal task metadata to tests and per-test hooks', async () => {
28
+ const observedTasks: Array<{
29
+ source: 'beforeEach' | 'test' | 'afterEach';
30
+ task: {
31
+ name: string;
32
+ type: 'test';
33
+ mode: 'run' | 'skip' | 'todo';
34
+ file: { name: string };
35
+ suite: { name: string };
36
+ };
37
+ }> = [];
38
+ const collector = getTestCollector();
39
+ const runner = getTestRunner();
40
+
41
+ try {
42
+ const collection = await collector.collect(() => {
43
+ harnessDescribe('Task Context Suite', () => {
44
+ beforeEach((context: HarnessTestContext) => {
45
+ observedTasks.push({ source: 'beforeEach', task: getTask(context) });
46
+ });
47
+
48
+ afterEach((context: HarnessTestContext) => {
49
+ observedTasks.push({ source: 'afterEach', task: getTask(context) });
50
+ });
51
+
52
+ harnessIt('exposes task metadata', (context: HarnessTestContext) => {
53
+ observedTasks.push({ source: 'test', task: getTask(context) });
54
+ });
55
+ });
56
+ }, 'runtime/context.test.ts');
57
+
58
+ const result = await runner.run({
59
+ testSuite: collection.testSuite,
60
+ testFilePath: 'runtime/context.test.ts',
61
+ runner: 'ios',
62
+ });
63
+
64
+ expect(result.status).toBe('passed');
65
+ expect(result.suites[0].tests[0]).toMatchObject({
66
+ name: 'exposes task metadata',
67
+ status: 'passed',
68
+ });
69
+ expect(observedTasks).toEqual([
70
+ {
71
+ source: 'beforeEach',
72
+ task: {
73
+ name: 'exposes task metadata',
74
+ type: 'test',
75
+ mode: 'run',
76
+ file: { name: 'runtime/context.test.ts' },
77
+ suite: { name: 'Task Context Suite' },
78
+ },
79
+ },
80
+ {
81
+ source: 'test',
82
+ task: {
83
+ name: 'exposes task metadata',
84
+ type: 'test',
85
+ mode: 'run',
86
+ file: { name: 'runtime/context.test.ts' },
87
+ suite: { name: 'Task Context Suite' },
88
+ },
89
+ },
90
+ {
91
+ source: 'afterEach',
92
+ task: {
93
+ name: 'exposes task metadata',
94
+ type: 'test',
95
+ mode: 'run',
96
+ file: { name: 'runtime/context.test.ts' },
97
+ suite: { name: 'Task Context Suite' },
98
+ },
99
+ },
100
+ ]);
101
+ } finally {
102
+ collector.dispose();
103
+ runner.dispose();
104
+ }
105
+ });
106
+
107
+ it('keeps zero-argument tests and hooks working', async () => {
108
+ const calls: string[] = [];
109
+ const collector = getTestCollector();
110
+ const runner = getTestRunner();
111
+
112
+ try {
113
+ const collection = await collector.collect(() => {
114
+ harnessDescribe('Compatibility Suite', () => {
115
+ beforeEach(() => {
116
+ calls.push('beforeEach');
117
+ });
118
+
119
+ afterEach(() => {
120
+ calls.push('afterEach');
121
+ });
122
+
123
+ harnessIt('still runs', () => {
124
+ calls.push('test');
125
+ });
126
+ });
127
+ }, 'runtime/compatibility.test.ts');
128
+
129
+ const result = await runner.run({
130
+ testSuite: collection.testSuite,
131
+ testFilePath: 'runtime/compatibility.test.ts',
132
+ runner: 'android',
133
+ });
134
+
135
+ expect(result.suites[0].tests[0]).toMatchObject({ status: 'passed' });
136
+ expect(calls).toEqual(['beforeEach', 'test', 'afterEach']);
137
+ } finally {
138
+ collector.dispose();
139
+ runner.dispose();
140
+ }
141
+ });
142
+
143
+ it('marks dynamically skipped tests as skipped and still runs afterEach', async () => {
144
+ const calls: string[] = [];
145
+ const collector = getTestCollector();
146
+ const runner = getTestRunner();
147
+
148
+ try {
149
+ const collection = await collector.collect(() => {
150
+ harnessDescribe('Skip Suite', () => {
151
+ afterEach(() => {
152
+ calls.push('afterEach');
153
+ });
154
+
155
+ harnessIt('skips from context', (context: HarnessTestContext) => {
156
+ const { skip } = getTaskContext(context);
157
+
158
+ calls.push('before-skip');
159
+ skip('skip this test');
160
+ calls.push('after-skip');
161
+ });
162
+
163
+ harnessIt('still runs sibling test', () => {
164
+ calls.push('sibling');
165
+ });
166
+ });
167
+ }, 'runtime/skip.test.ts');
168
+
169
+ const result = await runner.run({
170
+ testSuite: collection.testSuite,
171
+ testFilePath: 'runtime/skip.test.ts',
172
+ runner: 'ios',
173
+ });
174
+
175
+ expect(result.suites[0].tests).toMatchObject([
176
+ { name: 'skips from context', status: 'skipped' },
177
+ { name: 'still runs sibling test', status: 'passed' },
178
+ ]);
179
+ expect(calls).toEqual([
180
+ 'before-skip',
181
+ 'afterEach',
182
+ 'sibling',
183
+ 'afterEach',
184
+ ]);
185
+ } finally {
186
+ collector.dispose();
187
+ runner.dispose();
188
+ }
189
+ });
190
+
191
+ it('supports conditional skipping without changing false conditions', async () => {
192
+ const calls: string[] = [];
193
+ const collector = getTestCollector();
194
+ const runner = getTestRunner();
195
+
196
+ try {
197
+ const collection = await collector.collect(() => {
198
+ harnessDescribe('Conditional Skip Suite', () => {
199
+ harnessIt('continues when condition is false', (context: HarnessTestContext) => {
200
+ const { skip } = getTaskContext(context);
201
+
202
+ calls.push('before');
203
+ skip(false, 'do not skip');
204
+ calls.push('after');
205
+ });
206
+ });
207
+ }, 'runtime/conditional-skip.test.ts');
208
+
209
+ const result = await runner.run({
210
+ testSuite: collection.testSuite,
211
+ testFilePath: 'runtime/conditional-skip.test.ts',
212
+ runner: 'android',
213
+ });
214
+
215
+ expect(result.suites[0].tests[0]).toMatchObject({ status: 'passed' });
216
+ expect(calls).toEqual(['before', 'after']);
217
+ } finally {
218
+ collector.dispose();
219
+ runner.dispose();
220
+ }
221
+ });
222
+
223
+ it('runs onTestFinished after afterEach for passing tests', async () => {
224
+ const calls: string[] = [];
225
+ const collector = getTestCollector();
226
+ const runner = getTestRunner();
227
+
228
+ try {
229
+ const collection = await collector.collect(() => {
230
+ harnessDescribe('Finished Suite', () => {
231
+ afterEach(() => {
232
+ calls.push('afterEach');
233
+ });
234
+
235
+ harnessIt('runs finished callbacks', (context: HarnessTestContext) => {
236
+ const { onTestFinished } = getTaskContext(context);
237
+
238
+ onTestFinished(() => {
239
+ calls.push('onTestFinished:first');
240
+ });
241
+ onTestFinished(() => {
242
+ calls.push('onTestFinished:second');
243
+ });
244
+
245
+ calls.push('test');
246
+ });
247
+ });
248
+ }, 'runtime/on-test-finished-pass.test.ts');
249
+
250
+ const result = await runner.run({
251
+ testSuite: collection.testSuite,
252
+ testFilePath: 'runtime/on-test-finished-pass.test.ts',
253
+ runner: 'ios',
254
+ });
255
+
256
+ expect(result.suites[0].tests[0]).toMatchObject({ status: 'passed' });
257
+ expect(calls).toEqual([
258
+ 'test',
259
+ 'afterEach',
260
+ 'onTestFinished:second',
261
+ 'onTestFinished:first',
262
+ ]);
263
+ } finally {
264
+ collector.dispose();
265
+ runner.dispose();
266
+ }
267
+ });
268
+
269
+ it('runs onTestFinished for dynamically skipped tests', async () => {
270
+ const calls: string[] = [];
271
+ const collector = getTestCollector();
272
+ const runner = getTestRunner();
273
+
274
+ try {
275
+ const collection = await collector.collect(() => {
276
+ harnessDescribe('Finished Skip Suite', () => {
277
+ afterEach(() => {
278
+ calls.push('afterEach');
279
+ });
280
+
281
+ harnessIt(
282
+ 'runs finished callback after skip',
283
+ (context: HarnessTestContext) => {
284
+ const { onTestFinished, skip } = getTaskContext(context);
285
+
286
+ onTestFinished(() => {
287
+ calls.push('onTestFinished');
288
+ });
289
+
290
+ calls.push('before-skip');
291
+ skip();
292
+ },
293
+ );
294
+ });
295
+ }, 'runtime/on-test-finished-skip.test.ts');
296
+
297
+ const result = await runner.run({
298
+ testSuite: collection.testSuite,
299
+ testFilePath: 'runtime/on-test-finished-skip.test.ts',
300
+ runner: 'android',
301
+ });
302
+
303
+ expect(result.suites[0].tests[0]).toMatchObject({ status: 'skipped' });
304
+ expect(calls).toEqual(['before-skip', 'afterEach', 'onTestFinished']);
305
+ } finally {
306
+ collector.dispose();
307
+ runner.dispose();
308
+ }
309
+ });
310
+
311
+ it('runs onTestFinished for failed tests', async () => {
312
+ const calls: string[] = [];
313
+ const collector = getTestCollector();
314
+ const runner = getTestRunner();
315
+
316
+ try {
317
+ const collection = await collector.collect(() => {
318
+ harnessDescribe('Finished Failure Suite', () => {
319
+ afterEach(() => {
320
+ calls.push('afterEach');
321
+ });
322
+
323
+ harnessIt(
324
+ 'runs finished callback after failure',
325
+ (context: HarnessTestContext) => {
326
+ const { onTestFinished } = getTaskContext(context);
327
+
328
+ onTestFinished(() => {
329
+ calls.push('onTestFinished');
330
+ });
331
+
332
+ calls.push('test');
333
+ throw new Error('expected failure');
334
+ },
335
+ );
336
+ });
337
+ }, 'runtime/on-test-finished-failure.test.ts');
338
+
339
+ const result = await runner.run({
340
+ testSuite: collection.testSuite,
341
+ testFilePath: 'runtime/on-test-finished-failure.test.ts',
342
+ runner: 'ios',
343
+ });
344
+
345
+ expect(result.suites[0].tests[0]).toMatchObject({ status: 'failed' });
346
+ expect(calls).toEqual(['test', 'afterEach', 'onTestFinished']);
347
+ } finally {
348
+ collector.dispose();
349
+ runner.dispose();
350
+ }
351
+ });
352
+
353
+ it('runs onTestFailed after afterEach for failed tests', async () => {
354
+ const calls: string[] = [];
355
+ const collector = getTestCollector();
356
+ const runner = getTestRunner();
357
+
358
+ try {
359
+ const collection = await collector.collect(() => {
360
+ harnessDescribe('Failed Hook Suite', () => {
361
+ afterEach(() => {
362
+ calls.push('afterEach');
363
+ });
364
+
365
+ harnessIt('runs failed callbacks', (context: HarnessTestContext) => {
366
+ const { onTestFailed } = getTaskContext(context);
367
+
368
+ onTestFailed(() => {
369
+ calls.push('onTestFailed:first');
370
+ });
371
+ onTestFailed(() => {
372
+ calls.push('onTestFailed:second');
373
+ });
374
+
375
+ calls.push('test');
376
+ throw new Error('expected failure');
377
+ });
378
+ });
379
+ }, 'runtime/on-test-failed-failure.test.ts');
380
+
381
+ const result = await runner.run({
382
+ testSuite: collection.testSuite,
383
+ testFilePath: 'runtime/on-test-failed-failure.test.ts',
384
+ runner: 'ios',
385
+ });
386
+
387
+ expect(result.suites[0].tests[0]).toMatchObject({ status: 'failed' });
388
+ expect(calls).toEqual([
389
+ 'test',
390
+ 'afterEach',
391
+ 'onTestFailed:second',
392
+ 'onTestFailed:first',
393
+ ]);
394
+ } finally {
395
+ collector.dispose();
396
+ runner.dispose();
397
+ }
398
+ });
399
+
400
+ it('does not run onTestFailed for dynamically skipped tests', async () => {
401
+ const calls: string[] = [];
402
+ const collector = getTestCollector();
403
+ const runner = getTestRunner();
404
+
405
+ try {
406
+ const collection = await collector.collect(() => {
407
+ harnessDescribe('Failed Skip Suite', () => {
408
+ afterEach(() => {
409
+ calls.push('afterEach');
410
+ });
411
+
412
+ harnessIt(
413
+ 'does not run failed callbacks on skip',
414
+ (context: HarnessTestContext) => {
415
+ const { onTestFailed, skip } = getTaskContext(context);
416
+
417
+ onTestFailed(() => {
418
+ calls.push('onTestFailed');
419
+ });
420
+
421
+ calls.push('before-skip');
422
+ skip();
423
+ },
424
+ );
425
+ });
426
+ }, 'runtime/on-test-failed-skip.test.ts');
427
+
428
+ const result = await runner.run({
429
+ testSuite: collection.testSuite,
430
+ testFilePath: 'runtime/on-test-failed-skip.test.ts',
431
+ runner: 'android',
432
+ });
433
+
434
+ expect(result.suites[0].tests[0]).toMatchObject({ status: 'skipped' });
435
+ expect(calls).toEqual(['before-skip', 'afterEach']);
436
+ } finally {
437
+ collector.dispose();
438
+ runner.dispose();
439
+ }
440
+ });
441
+
442
+ it('runs onTestFailed when afterEach fails', async () => {
443
+ const calls: string[] = [];
444
+ const collector = getTestCollector();
445
+ const runner = getTestRunner();
446
+
447
+ try {
448
+ const collection = await collector.collect(() => {
449
+ harnessDescribe('Failed AfterEach Suite', () => {
450
+ afterEach(() => {
451
+ calls.push('afterEach');
452
+ throw new Error('afterEach failure');
453
+ });
454
+
455
+ harnessIt(
456
+ 'runs failed callback after afterEach failure',
457
+ (context: HarnessTestContext) => {
458
+ const { onTestFailed } = getTaskContext(context);
459
+
460
+ onTestFailed(() => {
461
+ calls.push('onTestFailed');
462
+ });
463
+
464
+ calls.push('test');
465
+ },
466
+ );
467
+ });
468
+ }, 'runtime/on-test-failed-after-each.test.ts');
469
+
470
+ const result = await runner.run({
471
+ testSuite: collection.testSuite,
472
+ testFilePath: 'runtime/on-test-failed-after-each.test.ts',
473
+ runner: 'ios',
474
+ });
475
+
476
+ expect(result.suites[0].tests[0]).toMatchObject({ status: 'failed' });
477
+ expect(calls).toEqual(['test', 'afterEach', 'onTestFailed']);
478
+ } finally {
479
+ collector.dispose();
480
+ runner.dispose();
481
+ }
482
+ });
483
+ });
@@ -2,6 +2,7 @@ import type {
2
2
  TestCase,
3
3
  TestSuite,
4
4
  CollectionResult,
5
+ SuiteHookFn,
5
6
  } from '@react-native-harness/bridge';
6
7
  import type { TestFn } from './types.js';
7
8
  import { TestError } from './errors.js';
@@ -24,8 +25,8 @@ type RawTestSuite = {
24
25
  tests: RawTestCase[];
25
26
  suites: RawTestSuite[];
26
27
  hooks: {
27
- beforeAll: TestFn[];
28
- afterAll: TestFn[];
28
+ beforeAll: SuiteHookFn[];
29
+ afterAll: SuiteHookFn[];
29
30
  beforeEach: TestFn[];
30
31
  afterEach: TestFn[];
31
32
  };
@@ -316,7 +317,7 @@ export const test = Object.assign(
316
317
 
317
318
  export const it = test;
318
319
 
319
- export function beforeAll(fn: TestFn) {
320
+ export function beforeAll(fn: SuiteHookFn) {
320
321
  validateTestFunction(fn, 'beforeAll');
321
322
 
322
323
  const currentSuite = getCurrentSuite();
@@ -326,7 +327,7 @@ export function beforeAll(fn: TestFn) {
326
327
  currentSuite.hooks.beforeAll.push(fn);
327
328
  }
328
329
 
329
- export function afterAll(fn: TestFn) {
330
+ export function afterAll(fn: SuiteHookFn) {
330
331
  validateTestFunction(fn, 'afterAll');
331
332
 
332
333
  const currentSuite = getCurrentSuite();
@@ -2,9 +2,12 @@ import { EventEmitter } from '../utils/emitter.js';
2
2
  import {
3
3
  TestCollectorEvents,
4
4
  CollectionResult,
5
+ type HarnessTestContext,
5
6
  } from '@react-native-harness/bridge';
6
7
 
7
- export type TestFn = () => void | Promise<void>;
8
+ export type TestFn = (context: HarnessTestContext) => void | Promise<void>;
9
+
10
+ export type SuiteHookFn = () => void | Promise<void>;
8
11
 
9
12
  export type TestCollectorEventsEmitter = EventEmitter<TestCollectorEvents>;
10
13
 
@@ -1,5 +1,5 @@
1
1
  import { TestError } from './errors.js';
2
- import { TestFn } from './types.js';
2
+ import { TestFn, SuiteHookFn } from './types.js';
3
3
 
4
4
  export const validateTestName = (name: string, functionName: string): void => {
5
5
  if (!name || typeof name !== 'string' || name.trim() === '') {
@@ -10,7 +10,7 @@ export const validateTestName = (name: string, functionName: string): void => {
10
10
  };
11
11
 
12
12
  export const validateTestFunction = (
13
- fn: TestFn,
13
+ fn: TestFn | SuiteHookFn,
14
14
  functionName: string
15
15
  ): void => {
16
16
  if (typeof fn !== 'function') {
@@ -1,12 +1,33 @@
1
- import type { TestSuite } from '@react-native-harness/bridge';
1
+ import type { SuiteHookFn, TestFn, TestSuite } from '@react-native-harness/bridge';
2
+ import type { ActiveTestContext } from './types.js';
2
3
 
3
4
  export type HookType = 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll';
4
5
 
5
6
  const collectInheritedHooks = (
6
7
  suite: TestSuite,
7
- hookType: HookType
8
- ): (() => void | Promise<void>)[] => {
9
- const hooks: (() => void | Promise<void>)[] = [];
8
+ hookType: 'beforeEach' | 'afterEach'
9
+ ): TestFn[] => {
10
+ const hooks: TestFn[] = [];
11
+ const suiteChain: TestSuite[] = [];
12
+
13
+ let current: TestSuite | undefined = suite;
14
+ while (current) {
15
+ suiteChain.unshift(current);
16
+ current = current.parent;
17
+ }
18
+
19
+ for (const currentSuite of suiteChain) {
20
+ hooks.push(...currentSuite[hookType]);
21
+ }
22
+
23
+ return hooks;
24
+ };
25
+
26
+ const collectSuiteHooks = (
27
+ suite: TestSuite,
28
+ hookType: 'beforeAll' | 'afterAll'
29
+ ): SuiteHookFn[] => {
30
+ const hooks: SuiteHookFn[] = [];
10
31
  const suiteChain: TestSuite[] = [];
11
32
 
12
33
  // Collect all suites from current to root
@@ -16,23 +37,15 @@ const collectInheritedHooks = (
16
37
  currentSuite = currentSuite.parent;
17
38
  }
18
39
 
19
- if (hookType === 'beforeEach' || hookType === 'beforeAll') {
20
- // For beforeEach/beforeAll: run parent hooks first (reverse the chain)
40
+ if (hookType === 'beforeAll') {
41
+ // Run parent suite hooks before child suite hooks.
21
42
  for (let i = suiteChain.length - 1; i >= 0; i--) {
22
- if (hookType === 'beforeEach') {
23
- hooks.push(...suiteChain[i].beforeEach);
24
- } else {
25
- hooks.push(...suiteChain[i].beforeAll);
26
- }
43
+ hooks.push(...suiteChain[i].beforeAll);
27
44
  }
28
45
  } else {
29
- // For afterEach/afterAll: run child hooks first (use chain as-is)
46
+ // Run child suite hooks before parent suite hooks.
30
47
  for (const suiteInChain of suiteChain) {
31
- if (hookType === 'afterEach') {
32
- hooks.push(...suiteInChain.afterEach);
33
- } else {
34
- hooks.push(...suiteInChain.afterAll);
35
- }
48
+ hooks.push(...suiteInChain.afterAll);
36
49
  }
37
50
  }
38
51
 
@@ -41,11 +54,22 @@ const collectInheritedHooks = (
41
54
 
42
55
  export const runHooks = async (
43
56
  suite: TestSuite,
44
- hookType: HookType
57
+ hookType: HookType,
58
+ context?: ActiveTestContext,
45
59
  ): Promise<void> => {
60
+ if (hookType === 'beforeAll' || hookType === 'afterAll') {
61
+ const hooks = collectSuiteHooks(suite, hookType);
62
+
63
+ for (const hook of hooks) {
64
+ await hook();
65
+ }
66
+
67
+ return;
68
+ }
69
+
46
70
  const hooks = collectInheritedHooks(suite, hookType);
47
71
 
48
72
  for (const hook of hooks) {
49
- await hook();
73
+ await hook(context as ActiveTestContext);
50
74
  }
51
75
  };