@react-native-harness/runtime 1.2.0 → 1.4.0-rc.1

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 (71) 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/functions.js +8 -0
  4. package/dist/collector/types.d.ts +3 -2
  5. package/dist/collector/types.d.ts.map +1 -1
  6. package/dist/collector/validation.d.ts +2 -2
  7. package/dist/collector/validation.d.ts.map +1 -1
  8. package/dist/device/index.d.ts +12 -0
  9. package/dist/device/index.d.ts.map +1 -0
  10. package/dist/device/index.js +62 -0
  11. package/dist/hmr.d.ts +2 -0
  12. package/dist/hmr.d.ts.map +1 -0
  13. package/dist/hmr.js +5 -0
  14. package/dist/logbox.d.ts +4 -0
  15. package/dist/logbox.d.ts.map +1 -0
  16. package/dist/logbox.js +18 -0
  17. package/dist/runner/hooks.d.ts +2 -1
  18. package/dist/runner/hooks.d.ts.map +1 -1
  19. package/dist/runner/hooks.js +27 -17
  20. package/dist/runner/runSuite.d.ts.map +1 -1
  21. package/dist/runner/runSuite.js +134 -35
  22. package/dist/runner/test-context.d.ts +16 -0
  23. package/dist/runner/test-context.d.ts.map +1 -0
  24. package/dist/runner/test-context.js +57 -0
  25. package/dist/runner/types.d.ts +2 -1
  26. package/dist/runner/types.d.ts.map +1 -1
  27. package/dist/test-utils/react-native-url-polyfill.d.ts +9 -0
  28. package/dist/test-utils/react-native-url-polyfill.d.ts.map +1 -0
  29. package/dist/test-utils/react-native-url-polyfill.js +1 -0
  30. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  31. package/out-tsc/vitest/src/__tests__/device.test.d.ts +2 -0
  32. package/out-tsc/vitest/src/__tests__/device.test.d.ts.map +1 -0
  33. package/out-tsc/vitest/src/__tests__/logbox.test.d.ts +2 -0
  34. package/out-tsc/vitest/src/__tests__/logbox.test.d.ts.map +1 -0
  35. package/out-tsc/vitest/src/__tests__/runner-context.test.d.ts +2 -0
  36. package/out-tsc/vitest/src/__tests__/runner-context.test.d.ts.map +1 -0
  37. package/out-tsc/vitest/src/collector/functions.d.ts +3 -3
  38. package/out-tsc/vitest/src/collector/functions.d.ts.map +1 -1
  39. package/out-tsc/vitest/src/collector/types.d.ts +3 -2
  40. package/out-tsc/vitest/src/collector/types.d.ts.map +1 -1
  41. package/out-tsc/vitest/src/collector/validation.d.ts +2 -2
  42. package/out-tsc/vitest/src/collector/validation.d.ts.map +1 -1
  43. package/out-tsc/vitest/src/device/index.d.ts +12 -0
  44. package/out-tsc/vitest/src/device/index.d.ts.map +1 -0
  45. package/out-tsc/vitest/src/hmr.d.ts +2 -0
  46. package/out-tsc/vitest/src/hmr.d.ts.map +1 -0
  47. package/out-tsc/vitest/src/logbox.d.ts +4 -0
  48. package/out-tsc/vitest/src/logbox.d.ts.map +1 -0
  49. package/out-tsc/vitest/src/runner/hooks.d.ts +2 -1
  50. package/out-tsc/vitest/src/runner/hooks.d.ts.map +1 -1
  51. package/out-tsc/vitest/src/runner/runSuite.d.ts.map +1 -1
  52. package/out-tsc/vitest/src/runner/test-context.d.ts +16 -0
  53. package/out-tsc/vitest/src/runner/test-context.d.ts.map +1 -0
  54. package/out-tsc/vitest/src/runner/types.d.ts +2 -1
  55. package/out-tsc/vitest/src/runner/types.d.ts.map +1 -1
  56. package/out-tsc/vitest/src/test-utils/react-native-url-polyfill.d.ts +9 -0
  57. package/out-tsc/vitest/src/test-utils/react-native-url-polyfill.d.ts.map +1 -0
  58. package/out-tsc/vitest/src/ui/state.d.ts +1 -1
  59. package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +1 -1
  60. package/out-tsc/vitest/vite.config.d.ts.map +1 -1
  61. package/package.json +2 -2
  62. package/src/__tests__/runner-context.test.ts +532 -0
  63. package/src/collector/functions.ts +14 -4
  64. package/src/collector/types.ts +4 -1
  65. package/src/collector/validation.ts +2 -2
  66. package/src/runner/hooks.ts +43 -19
  67. package/src/runner/runSuite.ts +178 -38
  68. package/src/runner/test-context.ts +84 -0
  69. package/src/runner/types.ts +3 -0
  70. package/src/test-utils/react-native-url-polyfill.ts +1 -0
  71. package/vite.config.ts +4 -0
@@ -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
  };
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ HarnessTaskContext,
2
3
  TestCase,
3
4
  TestResult,
4
5
  TestSuite,
@@ -11,7 +12,63 @@ import {
11
12
  import { flushExpectTestState } from '../expect/errors.js';
12
13
  import { runHooks } from './hooks.js';
13
14
  import { getTestExecutionError } from './errors.js';
14
- import { TestRunnerContext } from './types.js';
15
+ import { ActiveTestContext, TestRunnerContext } from './types.js';
16
+ import {
17
+ createTestContext,
18
+ createTestLifecycleState,
19
+ isSkipTestError,
20
+ runOnTestFailed,
21
+ runOnTestFinished,
22
+ } from './test-context.js';
23
+
24
+ const getAncestorTitles = (suite: TestSuite): string[] => {
25
+ const ancestorTitles: string[] = [];
26
+ let currentSuite = suite.parent;
27
+
28
+ while (currentSuite) {
29
+ if (currentSuite.name !== 'root') {
30
+ ancestorTitles.unshift(currentSuite.name);
31
+ }
32
+ currentSuite = currentSuite.parent;
33
+ }
34
+
35
+ if (suite.name !== 'root') {
36
+ ancestorTitles.push(suite.name);
37
+ }
38
+
39
+ return ancestorTitles;
40
+ };
41
+
42
+ const getFullName = (ancestorTitles: string[], testName: string): string =>
43
+ [...ancestorTitles, testName].join(' ');
44
+
45
+ const emitTestFinished = (
46
+ context: TestRunnerContext,
47
+ options: {
48
+ test: TestCase;
49
+ suite: TestSuite;
50
+ startedAt: number;
51
+ duration: number;
52
+ status: 'passed' | 'failed' | 'skipped' | 'todo';
53
+ error?: TestResult['error'];
54
+ },
55
+ ) => {
56
+ const ancestorTitles = getAncestorTitles(options.suite);
57
+
58
+ context.events.emit({
59
+ type: 'test-finished',
60
+ file: context.testFilePath,
61
+ suite: options.suite.name,
62
+ name: options.test.name,
63
+ ancestorTitles,
64
+ fullName: getFullName(ancestorTitles, options.test.name),
65
+ startedAt: options.startedAt,
66
+ declarationMode: options.test.declarationMode,
67
+ duration: options.duration,
68
+ error: options.error,
69
+ status: options.status,
70
+ });
71
+ };
15
72
 
16
73
  declare global {
17
74
  var HARNESS_TEST_PATH: string;
@@ -22,14 +79,40 @@ const runTest = async (
22
79
  suite: TestSuite,
23
80
  context: TestRunnerContext,
24
81
  ): Promise<TestResult> => {
25
- const startTime = Date.now();
82
+ const startedAt = Date.now();
83
+ const task: HarnessTaskContext = {
84
+ name: test.name,
85
+ type: 'test',
86
+ mode:
87
+ test.status === 'active'
88
+ ? 'run'
89
+ : test.status === 'skipped'
90
+ ? 'skip'
91
+ : 'todo',
92
+ file: {
93
+ name: context.testFilePath,
94
+ },
95
+ suite: {
96
+ name: suite.name,
97
+ },
98
+ };
99
+ const lifecycleState = createTestLifecycleState();
100
+ const activeTestContext: ActiveTestContext = createTestContext(
101
+ task,
102
+ lifecycleState,
103
+ );
26
104
 
27
105
  // Emit test-started event
106
+ const ancestorTitles = getAncestorTitles(suite);
28
107
  context.events.emit({
29
108
  type: 'test-started',
30
109
  name: test.name,
31
110
  suite: suite.name,
32
111
  file: context.testFilePath,
112
+ ancestorTitles,
113
+ fullName: getFullName(ancestorTitles, test.name),
114
+ startedAt,
115
+ declarationMode: test.declarationMode,
33
116
  });
34
117
 
35
118
  try {
@@ -38,14 +121,16 @@ const runTest = async (
38
121
  name: test.name,
39
122
  status: 'skipped' as const,
40
123
  duration: 0,
124
+ ancestorTitles,
125
+ fullName: getFullName(ancestorTitles, test.name),
126
+ startedAt,
127
+ declarationMode: test.declarationMode,
41
128
  };
42
129
 
43
- // Emit test-finished event
44
- context.events.emit({
45
- type: 'test-finished',
46
- name: test.name,
47
- suite: suite.name,
48
- file: context.testFilePath,
130
+ emitTestFinished(context, {
131
+ test,
132
+ suite,
133
+ startedAt,
49
134
  duration: 0,
50
135
  status: 'skipped',
51
136
  });
@@ -59,14 +144,16 @@ const runTest = async (
59
144
  name: test.name,
60
145
  status: 'todo' as const,
61
146
  duration: 0,
147
+ ancestorTitles,
148
+ fullName: getFullName(ancestorTitles, test.name),
149
+ startedAt,
150
+ declarationMode: test.declarationMode,
62
151
  };
63
152
 
64
- // Emit test-finished event
65
- context.events.emit({
66
- type: 'test-finished',
67
- name: test.name,
68
- suite: suite.name,
69
- file: context.testFilePath,
153
+ emitTestFinished(context, {
154
+ test,
155
+ suite,
156
+ startedAt,
70
157
  duration: 0,
71
158
  status: 'todo',
72
159
  });
@@ -78,61 +165,105 @@ const runTest = async (
78
165
  setCurrentExpectTestState(expectTestState);
79
166
 
80
167
  try {
81
- // Run all beforeEach hooks from the current suite and its parents
82
- await runHooks(suite, 'beforeEach');
83
-
84
- // Run the actual test
85
- await test.fn();
86
-
87
- // Run all afterEach hooks from the current suite and its parents
88
- await runHooks(suite, 'afterEach');
168
+ let didSkip = false;
169
+
170
+ try {
171
+ // Run all beforeEach hooks from the current suite and its parents
172
+ await runHooks(suite, 'beforeEach', activeTestContext);
173
+
174
+ // Run the actual test
175
+ await test.fn(activeTestContext);
176
+ } catch (error) {
177
+ if (!isSkipTestError(error)) {
178
+ throw error;
179
+ }
180
+
181
+ didSkip = true;
182
+ } finally {
183
+ // Run all afterEach hooks from the current suite and its parents
184
+ await runHooks(suite, 'afterEach', activeTestContext);
185
+ }
186
+
187
+ if (didSkip) {
188
+ const duration = Date.now() - startedAt;
189
+
190
+ await runOnTestFinished(lifecycleState);
191
+
192
+ const result = {
193
+ name: test.name,
194
+ status: 'skipped' as const,
195
+ duration,
196
+ ancestorTitles,
197
+ fullName: getFullName(ancestorTitles, test.name),
198
+ startedAt,
199
+ declarationMode: test.declarationMode,
200
+ };
201
+
202
+ emitTestFinished(context, {
203
+ test,
204
+ suite,
205
+ startedAt,
206
+ duration,
207
+ status: 'skipped',
208
+ });
209
+
210
+ return result;
211
+ }
89
212
 
90
213
  await flushExpectTestState(expectTestState);
214
+ await runOnTestFinished(lifecycleState);
91
215
  } finally {
92
216
  setCurrentExpectTestState(undefined);
93
217
  }
94
218
 
95
- const duration = Date.now() - startTime;
219
+ const duration = Date.now() - startedAt;
96
220
 
97
221
  const result = {
98
222
  name: test.name,
99
223
  status: 'passed' as const,
100
224
  duration,
225
+ ancestorTitles,
226
+ fullName: getFullName(ancestorTitles, test.name),
227
+ startedAt,
228
+ declarationMode: test.declarationMode,
101
229
  };
102
230
 
103
- // Emit test-finished event
104
- context.events.emit({
105
- type: 'test-finished',
106
- file: context.testFilePath,
107
- suite: suite.name,
108
- name: test.name,
231
+ emitTestFinished(context, {
232
+ test,
233
+ suite,
234
+ startedAt,
109
235
  duration,
110
236
  status: 'passed',
111
237
  });
112
238
 
113
239
  return result;
114
240
  } catch (error) {
241
+ await runOnTestFailed(lifecycleState);
242
+ await runOnTestFinished(lifecycleState);
243
+
115
244
  const testError = await getTestExecutionError(
116
245
  error,
117
246
  context.testFilePath,
118
247
  suite.name,
119
248
  test.name,
120
249
  );
121
- const duration = Date.now() - startTime;
250
+ const duration = Date.now() - startedAt;
122
251
 
123
252
  const result = {
124
253
  name: test.name,
125
254
  status: 'failed' as const,
126
255
  error: testError.toSerializedJSON(),
127
256
  duration,
257
+ ancestorTitles,
258
+ fullName: getFullName(ancestorTitles, test.name),
259
+ startedAt,
260
+ declarationMode: test.declarationMode,
128
261
  };
129
262
 
130
- // Emit test-finished event
131
- context.events.emit({
132
- type: 'test-finished',
133
- file: context.testFilePath,
134
- suite: suite.name,
135
- name: test.name,
263
+ emitTestFinished(context, {
264
+ test,
265
+ suite,
266
+ startedAt,
136
267
  duration,
137
268
  error: testError.toSerializedJSON(),
138
269
  status: 'failed',
@@ -157,10 +288,19 @@ export const runSuite = async (
157
288
 
158
289
  // Check if suite should be skipped or is todo
159
290
  if (suite.status === 'skipped') {
291
+ const testResults = await Promise.all(
292
+ suite.tests.map((test) => runTest({ ...test, status: 'skipped' }, suite, context)),
293
+ );
294
+ const suiteResults = await Promise.all(
295
+ suite.suites.map((childSuite) =>
296
+ runSuite({ ...childSuite, status: 'skipped' }, context),
297
+ ),
298
+ );
299
+
160
300
  const result = {
161
301
  name: suite.name,
162
- tests: [],
163
- suites: [],
302
+ tests: testResults,
303
+ suites: suiteResults,
164
304
  status: 'skipped' as const,
165
305
  duration: 0,
166
306
  };
@@ -0,0 +1,84 @@
1
+ import type { HarnessTaskContext } from '@react-native-harness/bridge';
2
+ import type { ActiveTestContext } from './types.js';
3
+
4
+ export type TestLifecycleState = {
5
+ onTestFailed: Array<() => void | Promise<void>>;
6
+ onTestFinished: Array<() => void | Promise<void>>;
7
+ };
8
+
9
+ export class SkipTestError extends Error {
10
+ note?: string;
11
+
12
+ constructor(note?: string) {
13
+ super(note ?? 'Test skipped');
14
+ this.name = 'SkipTestError';
15
+ this.note = note;
16
+ }
17
+ }
18
+
19
+ export const isSkipTestError = (error: unknown): error is SkipTestError => {
20
+ return error instanceof SkipTestError;
21
+ };
22
+
23
+ const createSkip = () => {
24
+ function skip(noteOrCondition?: boolean | string, note?: string): void {
25
+ if (typeof noteOrCondition === 'boolean') {
26
+ if (!noteOrCondition) {
27
+ return;
28
+ }
29
+
30
+ throw new SkipTestError(note);
31
+ }
32
+
33
+ throw new SkipTestError(noteOrCondition);
34
+ }
35
+
36
+ return skip as ActiveTestContext['skip'];
37
+ };
38
+
39
+ const createOnTestFinished = (state: TestLifecycleState) => {
40
+ return (fn: () => void | Promise<void>): void => {
41
+ state.onTestFinished.push(fn);
42
+ };
43
+ };
44
+
45
+ const createOnTestFailed = (state: TestLifecycleState) => {
46
+ return (fn: () => void | Promise<void>): void => {
47
+ state.onTestFailed.push(fn);
48
+ };
49
+ };
50
+
51
+ export const createTestLifecycleState = (): TestLifecycleState => {
52
+ return {
53
+ onTestFailed: [],
54
+ onTestFinished: [],
55
+ };
56
+ };
57
+
58
+ export const runOnTestFailed = async (
59
+ state: TestLifecycleState,
60
+ ): Promise<void> => {
61
+ for (let i = state.onTestFailed.length - 1; i >= 0; i--) {
62
+ await state.onTestFailed[i]();
63
+ }
64
+ };
65
+
66
+ export const runOnTestFinished = async (
67
+ state: TestLifecycleState,
68
+ ): Promise<void> => {
69
+ for (let i = state.onTestFinished.length - 1; i >= 0; i--) {
70
+ await state.onTestFinished[i]();
71
+ }
72
+ };
73
+
74
+ export const createTestContext = (
75
+ task: HarnessTaskContext,
76
+ state: TestLifecycleState,
77
+ ): ActiveTestContext => {
78
+ return {
79
+ task,
80
+ onTestFailed: createOnTestFailed(state),
81
+ onTestFinished: createOnTestFinished(state),
82
+ skip: createSkip(),
83
+ };
84
+ };
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from '../utils/emitter.js';
2
2
  import type {
3
+ HarnessTestContext,
3
4
  TestRunnerEvents,
4
5
  TestSuite,
5
6
  TestSuiteResult,
@@ -12,6 +13,8 @@ export type TestRunnerContext = {
12
13
  testFilePath: string;
13
14
  };
14
15
 
16
+ export type ActiveTestContext = HarnessTestContext;
17
+
15
18
  export type RunTestsOptions = {
16
19
  testSuite: TestSuite;
17
20
  testFilePath: string;
@@ -0,0 +1 @@
1
+ export const URL = globalThis.URL;
package/vite.config.ts CHANGED
@@ -22,6 +22,10 @@ export default defineConfig(() => ({
22
22
  alias: {
23
23
  '@vitest/spy': path.resolve(__dirname, 'node_modules/@vitest/spy'),
24
24
  '@vitest/expect': path.resolve(__dirname, 'node_modules/@vitest/expect'),
25
+ 'react-native-url-polyfill': path.resolve(
26
+ __dirname,
27
+ 'src/test-utils/react-native-url-polyfill.ts',
28
+ ),
25
29
  },
26
30
  },
27
31
  }));