@react-native-harness/runtime 1.0.0-alpha.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 (229) hide show
  1. package/.babelrc.js +23 -0
  2. package/LICENSE +20 -0
  3. package/README.md +7 -0
  4. package/assets/logo.png +0 -0
  5. package/assets/moduleSystem.flow.js +1062 -0
  6. package/dist/bundler/bundle.d.ts +2 -0
  7. package/dist/bundler/bundle.d.ts.map +1 -0
  8. package/dist/bundler/bundle.js +16 -0
  9. package/dist/bundler/dev-server.d.ts +2 -0
  10. package/dist/bundler/dev-server.d.ts.map +1 -0
  11. package/dist/bundler/dev-server.js +5 -0
  12. package/dist/bundler/errors.d.ts +10 -0
  13. package/dist/bundler/errors.d.ts.map +1 -0
  14. package/dist/bundler/errors.js +18 -0
  15. package/dist/bundler/evaluate.d.ts +2 -0
  16. package/dist/bundler/evaluate.d.ts.map +1 -0
  17. package/dist/bundler/evaluate.js +18 -0
  18. package/dist/bundler/index.d.ts +3 -0
  19. package/dist/bundler/index.d.ts.map +1 -0
  20. package/dist/bundler/index.js +2 -0
  21. package/dist/client/factory.d.ts +2 -0
  22. package/dist/client/factory.d.ts.map +1 -0
  23. package/dist/client/factory.js +41 -0
  24. package/dist/client/getDeviceDescriptor.d.ts +8 -0
  25. package/dist/client/getDeviceDescriptor.d.ts.map +1 -0
  26. package/dist/client/getDeviceDescriptor.js +20 -0
  27. package/dist/client/getWSServer.d.ts +2 -0
  28. package/dist/client/getWSServer.d.ts.map +1 -0
  29. package/dist/client/getWSServer.js +7 -0
  30. package/dist/client/index.d.ts +2 -0
  31. package/dist/client/index.d.ts.map +1 -0
  32. package/dist/client/index.js +1 -0
  33. package/dist/collector/errors.d.ts +8 -0
  34. package/dist/collector/errors.d.ts.map +1 -0
  35. package/dist/collector/errors.js +20 -0
  36. package/dist/collector/factory.d.ts +3 -0
  37. package/dist/collector/factory.d.ts.map +1 -0
  38. package/dist/collector/factory.js +25 -0
  39. package/dist/collector/functions.d.ts +22 -0
  40. package/dist/collector/functions.d.ts.map +1 -0
  41. package/dist/collector/functions.js +271 -0
  42. package/dist/collector/index.d.ts +5 -0
  43. package/dist/collector/index.d.ts.map +1 -0
  44. package/dist/collector/index.js +3 -0
  45. package/dist/collector/types.d.ts +10 -0
  46. package/dist/collector/types.d.ts.map +1 -0
  47. package/dist/collector/types.js +1 -0
  48. package/dist/collector/validation.d.ts +4 -0
  49. package/dist/collector/validation.d.ts.map +1 -0
  50. package/dist/collector/validation.js +15 -0
  51. package/dist/constants.d.ts +3 -0
  52. package/dist/constants.d.ts.map +1 -0
  53. package/dist/constants.js +2 -0
  54. package/dist/errors.d.ts +6 -0
  55. package/dist/errors.d.ts.map +1 -0
  56. package/dist/errors.js +13 -0
  57. package/dist/expect/index.d.ts +9 -0
  58. package/dist/expect/index.d.ts.map +1 -0
  59. package/dist/expect/index.js +71 -0
  60. package/dist/expect/setup.d.ts +2 -0
  61. package/dist/expect/setup.d.ts.map +1 -0
  62. package/dist/expect/setup.js +5 -0
  63. package/dist/exports.d.ts +7 -0
  64. package/dist/exports.d.ts.map +1 -0
  65. package/dist/exports.js +6 -0
  66. package/dist/getEntryComponent.d.ts +6 -0
  67. package/dist/getEntryComponent.d.ts.map +1 -0
  68. package/dist/getEntryComponent.js +6 -0
  69. package/dist/globals.d.ts +5 -0
  70. package/dist/globals.d.ts.map +1 -0
  71. package/dist/globals.js +1 -0
  72. package/dist/index.d.ts +7 -0
  73. package/dist/index.d.ts.map +1 -0
  74. package/dist/index.js +6 -0
  75. package/dist/initialize.d.ts +2 -0
  76. package/dist/initialize.d.ts.map +1 -0
  77. package/dist/initialize.js +16 -0
  78. package/dist/logger.d.ts +6 -0
  79. package/dist/logger.d.ts.map +1 -0
  80. package/dist/logger.js +14 -0
  81. package/dist/mock.d.ts +15 -0
  82. package/dist/mock.d.ts.map +1 -0
  83. package/dist/mock.js +37 -0
  84. package/dist/mocker/index.d.ts +2 -0
  85. package/dist/mocker/index.d.ts.map +1 -0
  86. package/dist/mocker/index.js +1 -0
  87. package/dist/mocker/registry.d.ts +7 -0
  88. package/dist/mocker/registry.d.ts.map +1 -0
  89. package/dist/mocker/registry.js +41 -0
  90. package/dist/mocker/types.d.ts +6 -0
  91. package/dist/mocker/types.d.ts.map +1 -0
  92. package/dist/mocker/types.js +1 -0
  93. package/dist/module.d.ts +3 -0
  94. package/dist/module.d.ts.map +1 -0
  95. package/dist/module.js +19 -0
  96. package/dist/module.web.d.ts +2 -0
  97. package/dist/module.web.d.ts.map +1 -0
  98. package/dist/module.web.js +12 -0
  99. package/dist/rntl/client.d.ts +3 -0
  100. package/dist/rntl/client.d.ts.map +1 -0
  101. package/dist/rntl/client.js +8 -0
  102. package/dist/rntl/describe.d.ts +2 -0
  103. package/dist/rntl/describe.d.ts.map +1 -0
  104. package/dist/rntl/describe.js +1 -0
  105. package/dist/rntl/expect.d.ts +128 -0
  106. package/dist/rntl/expect.d.ts.map +1 -0
  107. package/dist/rntl/expect.js +670 -0
  108. package/dist/rntl/fn.d.ts +2 -0
  109. package/dist/rntl/fn.d.ts.map +1 -0
  110. package/dist/rntl/fn.js +1 -0
  111. package/dist/rntl/mock.d.ts +2 -0
  112. package/dist/rntl/mock.d.ts.map +1 -0
  113. package/dist/rntl/mock.js +1 -0
  114. package/dist/rntl/render.d.ts +4 -0
  115. package/dist/rntl/render.d.ts.map +1 -0
  116. package/dist/rntl/render.js +11 -0
  117. package/dist/rntl/screen.d.ts +45 -0
  118. package/dist/rntl/screen.d.ts.map +1 -0
  119. package/dist/rntl/screen.js +31 -0
  120. package/dist/rntl/spies.d.ts +45 -0
  121. package/dist/rntl/spies.d.ts.map +1 -0
  122. package/dist/rntl/spies.js +553 -0
  123. package/dist/rntl/userEvent.d.ts +22 -0
  124. package/dist/rntl/userEvent.d.ts.map +1 -0
  125. package/dist/rntl/userEvent.js +19 -0
  126. package/dist/runner/errors.d.ts +9 -0
  127. package/dist/runner/errors.d.ts.map +1 -0
  128. package/dist/runner/errors.js +23 -0
  129. package/dist/runner/factory.d.ts +3 -0
  130. package/dist/runner/factory.d.ts.map +1 -0
  131. package/dist/runner/factory.js +17 -0
  132. package/dist/runner/hooks.d.ts +4 -0
  133. package/dist/runner/hooks.d.ts.map +1 -0
  134. package/dist/runner/hooks.js +39 -0
  135. package/dist/runner/index.d.ts +4 -0
  136. package/dist/runner/index.d.ts.map +1 -0
  137. package/dist/runner/index.js +2 -0
  138. package/dist/runner/runSuite.d.ts +4 -0
  139. package/dist/runner/runSuite.d.ts.map +1 -0
  140. package/dist/runner/runSuite.js +147 -0
  141. package/dist/runner/types.d.ts +13 -0
  142. package/dist/runner/types.d.ts.map +1 -0
  143. package/dist/runner/types.js +1 -0
  144. package/dist/runner.d.ts +7 -0
  145. package/dist/runner.d.ts.map +1 -0
  146. package/dist/runner.js +201 -0
  147. package/dist/runtime.d.ts +2 -0
  148. package/dist/runtime.d.ts.map +1 -0
  149. package/dist/runtime.js +44 -0
  150. package/dist/spy/index.d.ts +2 -0
  151. package/dist/spy/index.d.ts.map +1 -0
  152. package/dist/spy/index.js +2 -0
  153. package/dist/state.d.ts +25 -0
  154. package/dist/state.d.ts.map +1 -0
  155. package/dist/state.js +37 -0
  156. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  157. package/dist/ui/ReadyScreen.d.ts +2 -0
  158. package/dist/ui/ReadyScreen.d.ts.map +1 -0
  159. package/dist/ui/ReadyScreen.js +110 -0
  160. package/dist/ui/UI.d.ts +13 -0
  161. package/dist/ui/UI.d.ts.map +1 -0
  162. package/dist/ui/UI.js +121 -0
  163. package/dist/ui/WrongEnvironmentScreen.d.ts +2 -0
  164. package/dist/ui/WrongEnvironmentScreen.d.ts.map +1 -0
  165. package/dist/ui/WrongEnvironmentScreen.js +87 -0
  166. package/dist/ui/index.d.ts +2 -0
  167. package/dist/ui/index.d.ts.map +1 -0
  168. package/dist/ui/index.js +3 -0
  169. package/dist/ui/state.d.ts +7 -0
  170. package/dist/ui/state.d.ts.map +1 -0
  171. package/dist/ui/state.js +6 -0
  172. package/dist/utils/dev-server.d.ts +2 -0
  173. package/dist/utils/dev-server.d.ts.map +1 -0
  174. package/dist/utils/dev-server.js +5 -0
  175. package/dist/utils/emitter.d.ts +16 -0
  176. package/dist/utils/emitter.d.ts.map +1 -0
  177. package/dist/utils/emitter.js +39 -0
  178. package/eslint.config.mjs +16 -0
  179. package/package.json +38 -0
  180. package/src/__tests__/collector.test.ts +553 -0
  181. package/src/__tests__/error-handling.test.ts +132 -0
  182. package/src/__tests__/expect.test.ts +619 -0
  183. package/src/__tests__/spy.test.ts +538 -0
  184. package/src/bundler/bundle.ts +19 -0
  185. package/src/bundler/errors.ts +16 -0
  186. package/src/bundler/evaluate.ts +25 -0
  187. package/src/bundler/index.ts +2 -0
  188. package/src/client/factory.ts +56 -0
  189. package/src/client/getDeviceDescriptor.ts +30 -0
  190. package/src/client/getWSServer.ts +9 -0
  191. package/src/client/index.ts +1 -0
  192. package/src/collector/errors.ts +27 -0
  193. package/src/collector/factory.ts +32 -0
  194. package/src/collector/functions.ts +376 -0
  195. package/src/collector/index.ts +12 -0
  196. package/src/collector/types.ts +15 -0
  197. package/src/collector/validation.ts +21 -0
  198. package/src/constants.ts +2 -0
  199. package/src/errors.ts +12 -0
  200. package/src/expect/index.ts +117 -0
  201. package/src/expect/setup.ts +10 -0
  202. package/src/globals.ts +5 -0
  203. package/src/index.ts +7 -0
  204. package/src/initialize.ts +22 -0
  205. package/src/mocker/index.ts +1 -0
  206. package/src/mocker/metro-require.d.ts +5 -0
  207. package/src/mocker/registry.ts +58 -0
  208. package/src/mocker/types.ts +6 -0
  209. package/src/react-native.d.ts +16 -0
  210. package/src/runner/errors.ts +31 -0
  211. package/src/runner/factory.ts +21 -0
  212. package/src/runner/hooks.ts +51 -0
  213. package/src/runner/index.ts +7 -0
  214. package/src/runner/runSuite.ts +201 -0
  215. package/src/runner/types.ts +19 -0
  216. package/src/spy/index.ts +2 -0
  217. package/src/ui/ReadyScreen.tsx +151 -0
  218. package/src/ui/WrongEnvironmentScreen.tsx +113 -0
  219. package/src/ui/index.ts +3 -0
  220. package/src/ui/state.ts +13 -0
  221. package/src/utils/dev-server.ts +6 -0
  222. package/src/utils/emitter.ts +64 -0
  223. package/tsconfig.json +16 -0
  224. package/tsconfig.lib.json +33 -0
  225. package/tsconfig.spec.json +30 -0
  226. package/tsconfig.tsbuildinfo +1 -0
  227. package/types/global.d.ts +2 -0
  228. package/types/index.d.ts +1 -0
  229. package/vite.config.ts +27 -0
@@ -0,0 +1,27 @@
1
+ export type TestErrorCode =
2
+ | 'CONTEXT_NOT_INITIALIZED'
3
+ | 'OUTSIDE_DESCRIBE_BLOCK'
4
+ | 'INVALID_TEST_NAME'
5
+ | 'DUPLICATE_TEST_NAME'
6
+ | 'INVALID_FUNCTION';
7
+
8
+ export class TestError extends Error {
9
+ constructor(
10
+ public code: TestErrorCode,
11
+ public functionName: string,
12
+ public context?: Record<string, unknown>
13
+ ) {
14
+ const baseMessages: Record<TestErrorCode, string> = {
15
+ CONTEXT_NOT_INITIALIZED:
16
+ 'Test context not initialized. Call collectTests() first.',
17
+ OUTSIDE_DESCRIBE_BLOCK: `${functionName}() must be called within a describe() block`,
18
+ INVALID_TEST_NAME: `${functionName}() requires a non-empty string name`,
19
+ DUPLICATE_TEST_NAME: `Duplicate test name "${context?.name}" in suite "${context?.suiteName}"`,
20
+ INVALID_FUNCTION: `${functionName}() requires a function as the second parameter`,
21
+ };
22
+
23
+ const message = baseMessages[code] || `Unknown error in ${functionName}()`;
24
+ super(message);
25
+ this.name = 'TestError';
26
+ }
27
+ }
@@ -0,0 +1,32 @@
1
+ import type { TestCollectorEvents } from '@react-native-harness/bridge';
2
+ import { getEmitter } from '../utils/emitter.js';
3
+ import { collectTests } from './functions.js';
4
+ import { TestCollector } from './types.js';
5
+
6
+ export const getTestCollector = (): TestCollector => {
7
+ const events = getEmitter<TestCollectorEvents>();
8
+
9
+ return {
10
+ events,
11
+ collect: async (fn, testFilePath) => {
12
+ const start = Date.now();
13
+ events.emit({
14
+ type: 'collection-started',
15
+ file: testFilePath,
16
+ });
17
+
18
+ const result = await collectTests(fn);
19
+
20
+ events.emit({
21
+ type: 'collection-finished',
22
+ file: testFilePath,
23
+ duration: Date.now() - start,
24
+ });
25
+
26
+ return result;
27
+ },
28
+ dispose: () => {
29
+ events.clearAllListeners();
30
+ },
31
+ };
32
+ };
@@ -0,0 +1,376 @@
1
+ import type {
2
+ TestCase,
3
+ TestSuite,
4
+ CollectionResult,
5
+ } from '@react-native-harness/bridge';
6
+ import type { TestFn } from './types.js';
7
+ import { TestError } from './errors.js';
8
+ import { validateTestName, validateTestFunction } from './validation.js';
9
+
10
+ type TestStatus = 'active' | 'skipped' | 'todo';
11
+
12
+ type RawTestCase = {
13
+ name: string;
14
+ fn: TestFn;
15
+ options: {
16
+ only?: boolean;
17
+ skip?: boolean;
18
+ todo?: boolean;
19
+ };
20
+ };
21
+
22
+ type RawTestSuite = {
23
+ name: string;
24
+ tests: RawTestCase[];
25
+ suites: RawTestSuite[];
26
+ hooks: {
27
+ beforeAll: TestFn[];
28
+ afterAll: TestFn[];
29
+ beforeEach: TestFn[];
30
+ afterEach: TestFn[];
31
+ };
32
+ options: {
33
+ only?: boolean;
34
+ skip?: boolean;
35
+ };
36
+ };
37
+
38
+ // Computation functions for two-phase approach
39
+ const computeTestStatus = (
40
+ test: RawTestCase,
41
+ suiteContext: { hasFocusedTests: boolean }
42
+ ): TestStatus => {
43
+ if (test.options.todo) return 'todo';
44
+ if (test.options.skip) return 'skipped';
45
+ if (test.options.only) return 'active';
46
+ if (suiteContext.hasFocusedTests) return 'skipped';
47
+ return 'active';
48
+ };
49
+
50
+ const computeSuiteStatus = (
51
+ suite: RawTestSuite,
52
+ parentContext: { hasFocusedChildren: boolean }
53
+ ): TestStatus => {
54
+ if (suite.options.skip) return 'skipped';
55
+ if (suite.options.only) return 'active';
56
+ if (parentContext.hasFocusedChildren) return 'skipped';
57
+ return 'active';
58
+ };
59
+
60
+ const convertRawTestCaseToTestCase = (
61
+ rawTest: RawTestCase,
62
+ suiteContext: { hasFocusedTests: boolean }
63
+ ): TestCase => {
64
+ return {
65
+ name: rawTest.name,
66
+ fn: rawTest.fn,
67
+ status: computeTestStatus(rawTest, suiteContext),
68
+ };
69
+ };
70
+
71
+ const convertRawTestSuiteToTestSuite = (
72
+ rawSuite: RawTestSuite,
73
+ parentContext: { hasFocusedChildren: boolean } = {
74
+ hasFocusedChildren: false,
75
+ },
76
+ parentSuite?: TestSuite
77
+ ): TestSuite => {
78
+ // Validate duplicate test names within this suite
79
+ const testNames = new Set<string>();
80
+ for (const test of rawSuite.tests) {
81
+ if (testNames.has(test.name)) {
82
+ throw new TestError('DUPLICATE_TEST_NAME', 'test', {
83
+ name: test.name,
84
+ suiteName: rawSuite.name,
85
+ });
86
+ }
87
+ testNames.add(test.name);
88
+ }
89
+
90
+ // Check if this suite has focused tests
91
+ const hasFocusedTests = rawSuite.tests.some((test) => test.options.only);
92
+
93
+ // Check if this suite has focused children
94
+ const hasFocusedChildren = rawSuite.suites.some(
95
+ (suite) =>
96
+ suite.options.only || suite.tests.some((test) => test.options.only)
97
+ );
98
+
99
+ // Convert tests
100
+ const tests = rawSuite.tests.map((test) =>
101
+ convertRawTestCaseToTestCase(test, { hasFocusedTests })
102
+ );
103
+
104
+ // Create the suite first so we can reference it when converting children
105
+ const suite: TestSuite = {
106
+ name: rawSuite.name,
107
+ tests,
108
+ suites: [],
109
+ parent: parentSuite,
110
+ beforeAll: rawSuite.hooks.beforeAll,
111
+ afterAll: rawSuite.hooks.afterAll,
112
+ beforeEach: rawSuite.hooks.beforeEach,
113
+ afterEach: rawSuite.hooks.afterEach,
114
+ status: computeSuiteStatus(rawSuite, parentContext),
115
+ _hasFocused: hasFocusedTests || hasFocusedChildren || rawSuite.options.only,
116
+ };
117
+
118
+ // Convert child suites with this suite as their parent
119
+ suite.suites = rawSuite.suites.map((childSuite) =>
120
+ convertRawTestSuiteToTestSuite(childSuite, { hasFocusedChildren }, suite)
121
+ );
122
+
123
+ return suite;
124
+ };
125
+
126
+ type TestContext = {
127
+ rootSuite: RawTestSuite;
128
+ currentSuite: RawTestSuite | null;
129
+ };
130
+
131
+ let currentContext: TestContext | null = null;
132
+
133
+ const clearState = (): TestContext => {
134
+ const rootSuite = createRawSuite('root');
135
+ return {
136
+ rootSuite,
137
+ currentSuite: rootSuite,
138
+ };
139
+ };
140
+
141
+ const getCurrentSuite = (): RawTestSuite | null => {
142
+ if (!currentContext) {
143
+ throw new TestError('CONTEXT_NOT_INITIALIZED', 'getCurrentSuite');
144
+ }
145
+ return currentContext.currentSuite;
146
+ };
147
+
148
+ const getRootSuite = (): RawTestSuite => {
149
+ if (!currentContext) {
150
+ throw new TestError('CONTEXT_NOT_INITIALIZED', 'getRootSuite');
151
+ }
152
+ return currentContext.rootSuite;
153
+ };
154
+
155
+ const setCurrentSuite = (suite: RawTestSuite | null): void => {
156
+ if (!currentContext) {
157
+ throw new TestError('CONTEXT_NOT_INITIALIZED', 'setCurrentSuite');
158
+ }
159
+ currentContext.currentSuite = suite;
160
+ };
161
+
162
+ const createRawSuite = (
163
+ name: string,
164
+ options: { only?: boolean; skip?: boolean } = {}
165
+ ): RawTestSuite => {
166
+ return {
167
+ name,
168
+ tests: [],
169
+ suites: [],
170
+ hooks: {
171
+ beforeAll: [],
172
+ afterAll: [],
173
+ beforeEach: [],
174
+ afterEach: [],
175
+ },
176
+ options,
177
+ };
178
+ };
179
+
180
+ export const describe = Object.assign(
181
+ (name: string, fn: () => void) => {
182
+ validateTestName(name, 'describe');
183
+ validateTestFunction(fn, 'describe');
184
+
185
+ const suite = createRawSuite(name);
186
+ const previousSuite = getCurrentSuite();
187
+ setCurrentSuite(suite);
188
+
189
+ try {
190
+ fn();
191
+ } finally {
192
+ setCurrentSuite(previousSuite);
193
+ }
194
+
195
+ // Add the suite to its parent
196
+ if (previousSuite) {
197
+ previousSuite.suites.push(suite);
198
+ } else {
199
+ getRootSuite().suites.push(suite);
200
+ }
201
+ },
202
+ {
203
+ skip: (name: string, fn: () => void) => {
204
+ validateTestName(name, 'describe.skip');
205
+ validateTestFunction(fn, 'describe.skip');
206
+
207
+ const suite = createRawSuite(name, { skip: true });
208
+ const previousSuite = getCurrentSuite();
209
+ setCurrentSuite(suite);
210
+
211
+ try {
212
+ fn();
213
+ } finally {
214
+ setCurrentSuite(previousSuite);
215
+ }
216
+
217
+ // Add the suite to its parent
218
+ if (previousSuite) {
219
+ previousSuite.suites.push(suite);
220
+ } else {
221
+ getRootSuite().suites.push(suite);
222
+ }
223
+ },
224
+ only: (name: string, fn: () => void) => {
225
+ validateTestName(name, 'describe.only');
226
+ validateTestFunction(fn, 'describe.only');
227
+
228
+ const suite = createRawSuite(name, { only: true });
229
+ const previousSuite = getCurrentSuite();
230
+ setCurrentSuite(suite);
231
+
232
+ try {
233
+ fn();
234
+ } finally {
235
+ setCurrentSuite(previousSuite);
236
+ }
237
+
238
+ // Add the suite to its parent
239
+ if (previousSuite) {
240
+ previousSuite.suites.push(suite);
241
+ } else {
242
+ getRootSuite().suites.push(suite);
243
+ }
244
+ },
245
+ }
246
+ );
247
+
248
+ export const test = Object.assign(
249
+ (name: string, fn: TestFn) => {
250
+ validateTestName(name, 'test');
251
+ validateTestFunction(fn, 'test');
252
+
253
+ const currentSuite = getCurrentSuite();
254
+ if (!currentSuite) {
255
+ throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'test');
256
+ }
257
+
258
+ // Add test with default options
259
+ currentSuite.tests.push({ name, fn, options: {} });
260
+ },
261
+ {
262
+ skip: (name: string, fn: TestFn) => {
263
+ validateTestName(name, 'test.skip');
264
+ validateTestFunction(fn, 'test.skip');
265
+
266
+ const currentSuite = getCurrentSuite();
267
+ if (!currentSuite) {
268
+ throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'test.skip');
269
+ }
270
+
271
+ currentSuite.tests.push({ name, fn, options: { skip: true } });
272
+ },
273
+ only: (name: string, fn: TestFn) => {
274
+ validateTestName(name, 'test.only');
275
+ validateTestFunction(fn, 'test.only');
276
+
277
+ const currentSuite = getCurrentSuite();
278
+ if (!currentSuite) {
279
+ throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'test.only');
280
+ }
281
+
282
+ currentSuite.tests.push({ name, fn, options: { only: true } });
283
+ },
284
+ todo: (name: string) => {
285
+ validateTestName(name, 'test.todo');
286
+
287
+ const currentSuite = getCurrentSuite();
288
+ if (!currentSuite) {
289
+ throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'test.todo');
290
+ }
291
+
292
+ currentSuite.tests.push({
293
+ name,
294
+ fn: () => {
295
+ // Empty function for todo tests
296
+ },
297
+ options: { todo: true },
298
+ });
299
+ },
300
+ }
301
+ );
302
+
303
+ export const it = test;
304
+
305
+ export function beforeAll(fn: TestFn) {
306
+ validateTestFunction(fn, 'beforeAll');
307
+
308
+ const currentSuite = getCurrentSuite();
309
+ if (!currentSuite) {
310
+ throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'beforeAll');
311
+ }
312
+ currentSuite.hooks.beforeAll.push(fn);
313
+ }
314
+
315
+ export function afterAll(fn: TestFn) {
316
+ validateTestFunction(fn, 'afterAll');
317
+
318
+ const currentSuite = getCurrentSuite();
319
+ if (!currentSuite) {
320
+ throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'afterAll');
321
+ }
322
+ currentSuite.hooks.afterAll.push(fn);
323
+ }
324
+
325
+ export function beforeEach(fn: TestFn) {
326
+ validateTestFunction(fn, 'beforeEach');
327
+
328
+ const currentSuite = getCurrentSuite();
329
+ if (!currentSuite) {
330
+ throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'beforeEach');
331
+ }
332
+ currentSuite.hooks.beforeEach.push(fn);
333
+ }
334
+
335
+ export function afterEach(fn: TestFn) {
336
+ validateTestFunction(fn, 'afterEach');
337
+
338
+ const currentSuite = getCurrentSuite();
339
+ if (!currentSuite) {
340
+ throw new TestError('OUTSIDE_DESCRIBE_BLOCK', 'afterEach');
341
+ }
342
+ currentSuite.hooks.afterEach.push(fn);
343
+ }
344
+
345
+ /**
346
+ * Recursively counts the total number of tests that will actually be executed.
347
+ * Only counts active tests since skipped and todo tests are not executed.
348
+ */
349
+ const countTests = (suite: TestSuite): number => {
350
+ let count = suite.tests.filter((test) => test.status === 'active').length;
351
+
352
+ for (const childSuite of suite.suites) {
353
+ count += countTests(childSuite);
354
+ }
355
+
356
+ return count;
357
+ };
358
+
359
+ export const collectTests = (fn: () => void): CollectionResult => {
360
+ currentContext = clearState();
361
+
362
+ try {
363
+ fn();
364
+
365
+ // Convert raw structure to final structure using computation phase
366
+ const testSuite = convertRawTestSuiteToTestSuite(getRootSuite());
367
+ const totalTests = countTests(testSuite);
368
+
369
+ return {
370
+ testSuite,
371
+ totalTests,
372
+ };
373
+ } finally {
374
+ currentContext = null;
375
+ }
376
+ };
@@ -0,0 +1,12 @@
1
+ export {
2
+ describe,
3
+ test,
4
+ it,
5
+ beforeAll,
6
+ afterAll,
7
+ beforeEach,
8
+ afterEach,
9
+ } from './functions.js';
10
+ export { TestError, type TestErrorCode } from './errors.js';
11
+ export type { TestCollector, TestCollectorEventsEmitter } from './types.js';
12
+ export { getTestCollector } from './factory.js';
@@ -0,0 +1,15 @@
1
+ import { EventEmitter } from '../utils/emitter.js';
2
+ import {
3
+ TestCollectorEvents,
4
+ CollectionResult,
5
+ } from '@react-native-harness/bridge';
6
+
7
+ export type TestFn = () => void | Promise<void>;
8
+
9
+ export type TestCollectorEventsEmitter = EventEmitter<TestCollectorEvents>;
10
+
11
+ export type TestCollector = {
12
+ events: TestCollectorEventsEmitter;
13
+ collect: (fn: () => void, testFilePath: string) => Promise<CollectionResult>;
14
+ dispose: () => void;
15
+ };
@@ -0,0 +1,21 @@
1
+ import { TestError } from './errors.js';
2
+ import { TestFn } from './types.js';
3
+
4
+ export const validateTestName = (name: string, functionName: string): void => {
5
+ if (!name || typeof name !== 'string' || name.trim() === '') {
6
+ throw new TestError('INVALID_TEST_NAME', functionName, {
7
+ name,
8
+ });
9
+ }
10
+ };
11
+
12
+ export const validateTestFunction = (
13
+ fn: TestFn,
14
+ functionName: string
15
+ ): void => {
16
+ if (typeof fn !== 'function') {
17
+ throw new TestError('INVALID_FUNCTION', functionName, {
18
+ functionType: typeof fn,
19
+ });
20
+ }
21
+ };
@@ -0,0 +1,2 @@
1
+ export const LOGO_IMAGE = require('../assets/logo.png');
2
+ export const WS_SERVER_PORT = 3001;
package/src/errors.ts ADDED
@@ -0,0 +1,12 @@
1
+ export class EnvironmentError extends Error {
2
+ constructor(
3
+ public readonly context: string,
4
+ public readonly details?: string
5
+ ) {
6
+ const message = details
7
+ ? `Environment error in ${context}: ${details}`
8
+ : `Environment error: ${context}`;
9
+ super(message);
10
+ this.name = 'EnvironmentError';
11
+ }
12
+ }
@@ -0,0 +1,117 @@
1
+ import type { Assertion, ExpectStatic, MatcherState } from '@vitest/expect';
2
+ import {
3
+ addCustomEqualityTesters,
4
+ ASYMMETRIC_MATCHERS_OBJECT,
5
+ customMatchers,
6
+ getState,
7
+ GLOBAL_EXPECT,
8
+ setState,
9
+ } from '@vitest/expect';
10
+ import * as chai from 'chai';
11
+
12
+ // Setup additional matchers
13
+ import './setup.js';
14
+
15
+ export function createExpect(): ExpectStatic {
16
+ const expect = ((value: any, message?: string): Assertion => {
17
+ const { assertionCalls } = getState(expect);
18
+ setState({ assertionCalls: assertionCalls + 1 }, expect);
19
+ return chai.expect(value, message) as unknown as Assertion;
20
+ }) as ExpectStatic;
21
+ Object.assign(expect, chai.expect);
22
+ Object.assign(expect, (globalThis as any)[ASYMMETRIC_MATCHERS_OBJECT]);
23
+
24
+ expect.getState = () => getState<MatcherState>(expect);
25
+ expect.setState = (state) => setState(state as Partial<MatcherState>, expect);
26
+
27
+ // @ts-expect-error global is not typed
28
+ const globalState = getState(globalThis[GLOBAL_EXPECT]) || {};
29
+
30
+ setState<MatcherState>(
31
+ {
32
+ // this should also add "snapshotState" that is added conditionally
33
+ ...globalState,
34
+ assertionCalls: 0,
35
+ isExpectingAssertions: false,
36
+ isExpectingAssertionsError: null,
37
+ expectedAssertionsNumber: null,
38
+ expectedAssertionsNumberErrorGen: null,
39
+ },
40
+ expect
41
+ );
42
+
43
+ // @ts-expect-error untyped
44
+ expect.extend = (matchers) => chai.expect.extend(expect, matchers);
45
+ // @ts-expect-error untyped
46
+ expect.addEqualityTesters = (customTesters) =>
47
+ addCustomEqualityTesters(customTesters);
48
+
49
+ // @ts-expect-error untyped
50
+ expect.soft = (...args) => {
51
+ // @ts-expect-error private soft access
52
+ return expect(...args).withContext({ soft: true }) as Assertion;
53
+ };
54
+
55
+ // @ts-expect-error untyped
56
+ expect.unreachable = (message?: string) => {
57
+ chai.assert.fail(
58
+ `expected${message ? ` "${message}" ` : ' '}not to be reached`
59
+ );
60
+ };
61
+
62
+ function assertions(expected: number) {
63
+ const errorGen = () =>
64
+ new Error(
65
+ `expected number of assertions to be ${expected}, but got ${
66
+ expect.getState().assertionCalls
67
+ }`
68
+ );
69
+ if (Error.captureStackTrace) {
70
+ Error.captureStackTrace(errorGen(), assertions);
71
+ }
72
+
73
+ expect.setState({
74
+ expectedAssertionsNumber: expected,
75
+ expectedAssertionsNumberErrorGen: errorGen,
76
+ });
77
+ }
78
+
79
+ function hasAssertions() {
80
+ const error = new Error('expected any number of assertion, but got none');
81
+ if (Error.captureStackTrace) {
82
+ Error.captureStackTrace(error, hasAssertions);
83
+ }
84
+
85
+ expect.setState({
86
+ isExpectingAssertions: true,
87
+ isExpectingAssertionsError: error,
88
+ });
89
+ }
90
+
91
+ chai.util.addMethod(expect, 'assertions', assertions);
92
+ chai.util.addMethod(expect, 'hasAssertions', hasAssertions);
93
+
94
+ expect.extend(customMatchers);
95
+
96
+ return expect;
97
+ }
98
+
99
+ const globalExpect: ExpectStatic = createExpect();
100
+
101
+ Object.defineProperty(globalThis, GLOBAL_EXPECT, {
102
+ value: globalExpect,
103
+ writable: true,
104
+ configurable: true,
105
+ });
106
+
107
+ export { assert, should } from 'chai';
108
+ export { chai, globalExpect as expect };
109
+
110
+ export type {
111
+ Assertion,
112
+ AsymmetricMatchersContaining,
113
+ DeeplyAllowMatchers,
114
+ ExpectStatic,
115
+ JestAssertion,
116
+ Matchers,
117
+ } from '@vitest/expect';
@@ -0,0 +1,10 @@
1
+ import {
2
+ JestAsymmetricMatchers,
3
+ JestChaiExpect,
4
+ JestExtend,
5
+ } from '@vitest/expect';
6
+ import * as chai from 'chai';
7
+
8
+ chai.use(JestExtend);
9
+ chai.use(JestChaiExpect);
10
+ chai.use(JestAsymmetricMatchers);
package/src/globals.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare global {
2
+ var RN_HARNESS: boolean | undefined;
3
+ }
4
+
5
+ export {};
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ import './globals.d.ts';
2
+
3
+ export { UI as ReactNativeHarness } from './ui/index.js';
4
+ export * from './spy/index.js';
5
+ export * from './expect/index.js';
6
+ export * from './collector/index.js';
7
+ export * from './mocker/index.js';
@@ -0,0 +1,22 @@
1
+ import { getDeviceDescriptor } from './client/getDeviceDescriptor.js';
2
+ import { getClient } from './client/index.js';
3
+
4
+ // Polyfill for EventTarget
5
+ const Shim = require('event-target-shim');
6
+ globalThis.Event = Shim.Event;
7
+ globalThis.EventTarget = Shim.EventTarget;
8
+
9
+ // Turn off LogBox
10
+ const { LogBox } = require('react-native');
11
+ LogBox.ignoreAllLogs(true);
12
+
13
+ // Turn off HMR
14
+ const HMRClient = require('react-native/Libraries/Utilities/HMRClient');
15
+ HMRClient.setup = () => {
16
+ // No setup = no HMR
17
+ };
18
+
19
+ // Initialize the client
20
+ void getClient().then((client) =>
21
+ client.rpc.reportReady(getDeviceDescriptor())
22
+ );
@@ -0,0 +1 @@
1
+ export { mock, requireActual, clearMocks } from './registry.js';
@@ -0,0 +1,5 @@
1
+ import type { Require } from './types.js';
2
+
3
+ declare global {
4
+ var __r: Require;
5
+ }