@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,58 @@
1
+ import { ModuleFactory, ModuleId, Require } from './types.js';
2
+
3
+ const mockRegistry = new Map<number, ModuleFactory>();
4
+ const mockCache = new Map<number, unknown>();
5
+
6
+ const originalRequire = global.__r;
7
+
8
+ export const mock = (moduleId: ModuleId, factory: ModuleFactory): void => {
9
+ mockCache.delete(moduleId);
10
+ mockRegistry.set(moduleId, factory);
11
+ };
12
+
13
+ export const clearMocks = (): void => {
14
+ mockRegistry.clear();
15
+ mockCache.clear();
16
+ };
17
+
18
+ export const getMockRegistry = (): Map<number, ModuleFactory> => {
19
+ return mockRegistry;
20
+ };
21
+
22
+ export const getMockImplementation = (moduleId: number): unknown | null => {
23
+ if (mockCache.has(moduleId)) {
24
+ return mockCache.get(moduleId);
25
+ }
26
+
27
+ const factory = mockRegistry.get(moduleId);
28
+ if (!factory) {
29
+ return null;
30
+ }
31
+
32
+ const implementation = factory();
33
+ mockCache.set(moduleId, implementation);
34
+ return implementation;
35
+ };
36
+
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ export const requireActual = <T = any>(moduleId: string): T =>
39
+ // babel plugin will transform 'moduleId' to a number
40
+ originalRequire(moduleId as unknown as ModuleId) as T;
41
+
42
+ const mockRequire = (moduleId: string) => {
43
+ // babel plugin will transform 'moduleId' to a number
44
+ const mockedModule = getMockImplementation(moduleId as unknown as ModuleId);
45
+
46
+ if (mockedModule) {
47
+ return mockedModule;
48
+ }
49
+
50
+ return originalRequire(moduleId as unknown as ModuleId);
51
+ };
52
+
53
+ Object.setPrototypeOf(mockRequire, Object.getPrototypeOf(originalRequire));
54
+ Object.defineProperties(
55
+ mockRequire,
56
+ Object.getOwnPropertyDescriptors(originalRequire)
57
+ );
58
+ global.__r = mockRequire as unknown as Require;
@@ -0,0 +1,6 @@
1
+ export type ModuleId = number;
2
+ export type Require = {
3
+ (moduleId: number): unknown;
4
+ };
5
+
6
+ export type ModuleFactory = () => unknown;
@@ -0,0 +1,16 @@
1
+ declare module 'react-native/Libraries/Core/Devtools/getDevServer' {
2
+ export type DevServerInfo = {
3
+ url: string;
4
+ fullBundleUrl?: string;
5
+ bundleLoadedFromServer: boolean;
6
+ };
7
+ export default function getDevServer(): DevServerInfo;
8
+ }
9
+
10
+ declare global {
11
+ var __r:
12
+ | {
13
+ (moduleId: number): unknown;
14
+ }
15
+ | undefined;
16
+ }
@@ -0,0 +1,31 @@
1
+ import type { SerializedError } from '@react-native-harness/bridge';
2
+
3
+ export class TestExecutionError extends Error {
4
+ file: string;
5
+ suite: string;
6
+ test: string;
7
+
8
+ constructor(error: unknown, file: string, suite: string, test: string) {
9
+ super('Test execution error');
10
+ this.name = 'TestExecutionError';
11
+ this.file = file;
12
+ this.suite = suite;
13
+ this.test = test;
14
+ this.cause = error;
15
+ }
16
+
17
+ toSerializedJSON(): SerializedError {
18
+ const causeName =
19
+ this.cause instanceof Error ? this.cause.name : 'Unknown name';
20
+ const causeMessage =
21
+ this.cause instanceof Error ? this.cause.message : 'Unknown message';
22
+ const causeStack =
23
+ this.cause instanceof Error ? this.cause.stack : undefined;
24
+
25
+ return {
26
+ name: causeName,
27
+ message: causeMessage,
28
+ stack: causeStack,
29
+ };
30
+ }
31
+ }
@@ -0,0 +1,21 @@
1
+ import type { TestRunnerEvents } from '@react-native-harness/bridge';
2
+ import { getEmitter } from '../utils/emitter.js';
3
+ import { runSuite } from './runSuite.js';
4
+ import { TestRunner } from './types.js';
5
+
6
+ export const getTestRunner = (): TestRunner => {
7
+ const events = getEmitter<TestRunnerEvents>();
8
+
9
+ return {
10
+ events,
11
+ run: async (testSuite, testFilePath) => {
12
+ return runSuite(testSuite, {
13
+ events,
14
+ testFilePath,
15
+ });
16
+ },
17
+ dispose: () => {
18
+ events.clearAllListeners();
19
+ },
20
+ };
21
+ };
@@ -0,0 +1,51 @@
1
+ import type { TestSuite } from '@react-native-harness/bridge';
2
+
3
+ export type HookType = 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll';
4
+
5
+ const collectInheritedHooks = (
6
+ suite: TestSuite,
7
+ hookType: HookType
8
+ ): (() => void | Promise<void>)[] => {
9
+ const hooks: (() => void | Promise<void>)[] = [];
10
+ const suiteChain: TestSuite[] = [];
11
+
12
+ // Collect all suites from current to root
13
+ let currentSuite: TestSuite | undefined = suite;
14
+ while (currentSuite) {
15
+ suiteChain.push(currentSuite);
16
+ currentSuite = currentSuite.parent;
17
+ }
18
+
19
+ if (hookType === 'beforeEach' || hookType === 'beforeAll') {
20
+ // For beforeEach/beforeAll: run parent hooks first (reverse the chain)
21
+ 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
+ }
27
+ }
28
+ } else {
29
+ // For afterEach/afterAll: run child hooks first (use chain as-is)
30
+ for (const suiteInChain of suiteChain) {
31
+ if (hookType === 'afterEach') {
32
+ hooks.push(...suiteInChain.afterEach);
33
+ } else {
34
+ hooks.push(...suiteInChain.afterAll);
35
+ }
36
+ }
37
+ }
38
+
39
+ return hooks;
40
+ };
41
+
42
+ export const runHooks = async (
43
+ suite: TestSuite,
44
+ hookType: HookType
45
+ ): Promise<void> => {
46
+ const hooks = collectInheritedHooks(suite, hookType);
47
+
48
+ for (const hook of hooks) {
49
+ await hook();
50
+ }
51
+ };
@@ -0,0 +1,7 @@
1
+ export type {
2
+ TestRunnerEventsEmitter,
3
+ TestRunner,
4
+ TestRunnerContext,
5
+ } from './types.js';
6
+ export { TestExecutionError } from './errors.js';
7
+ export { getTestRunner } from './factory.js';
@@ -0,0 +1,201 @@
1
+ import type {
2
+ TestCase,
3
+ TestResult,
4
+ TestSuite,
5
+ TestSuiteResult,
6
+ } from '@react-native-harness/bridge';
7
+ import { runHooks } from './hooks.js';
8
+ import { TestExecutionError } from './errors.js';
9
+ import { TestRunnerContext } from './types.js';
10
+
11
+ const runTest = async (
12
+ test: TestCase,
13
+ suite: TestSuite,
14
+ context: TestRunnerContext
15
+ ): Promise<TestResult> => {
16
+ const startTime = Date.now();
17
+
18
+ // Emit test-started event
19
+ context.events.emit({
20
+ type: 'test-started',
21
+ name: test.name,
22
+ suite: suite.name,
23
+ file: context.testFilePath,
24
+ });
25
+
26
+ try {
27
+ if (test.status === 'skipped') {
28
+ const result = {
29
+ name: test.name,
30
+ status: 'skipped' as const,
31
+ duration: 0,
32
+ };
33
+
34
+ // Emit test-finished event
35
+ context.events.emit({
36
+ type: 'test-finished',
37
+ name: test.name,
38
+ suite: suite.name,
39
+ file: context.testFilePath,
40
+ duration: 0,
41
+ status: 'skipped',
42
+ });
43
+
44
+ return result;
45
+ }
46
+
47
+ if (test.status === 'todo') {
48
+ console.log(`- ${test.name} (todo)`);
49
+ const result = {
50
+ name: test.name,
51
+ status: 'todo' as const,
52
+ duration: 0,
53
+ };
54
+
55
+ // Emit test-finished event
56
+ context.events.emit({
57
+ type: 'test-finished',
58
+ name: test.name,
59
+ suite: suite.name,
60
+ file: context.testFilePath,
61
+ duration: 0,
62
+ status: 'todo',
63
+ });
64
+
65
+ return result;
66
+ }
67
+
68
+ // Run all beforeEach hooks from the current suite and its parents
69
+ await runHooks(suite, 'beforeEach');
70
+
71
+ // Run the actual test
72
+ await test.fn();
73
+
74
+ // Run all afterEach hooks from the current suite and its parents
75
+ await runHooks(suite, 'afterEach');
76
+
77
+ const duration = Date.now() - startTime;
78
+
79
+ const result = {
80
+ name: test.name,
81
+ status: 'passed' as const,
82
+ duration,
83
+ };
84
+
85
+ // Emit test-finished event
86
+ context.events.emit({
87
+ type: 'test-finished',
88
+ file: context.testFilePath,
89
+ suite: suite.name,
90
+ name: test.name,
91
+ duration,
92
+ status: 'passed',
93
+ });
94
+
95
+ return result;
96
+ } catch (error) {
97
+ const testError = new TestExecutionError(
98
+ error,
99
+ context.testFilePath,
100
+ suite.name,
101
+ test.name
102
+ );
103
+ const duration = Date.now() - startTime;
104
+
105
+ const result = {
106
+ name: test.name,
107
+ status: 'failed' as const,
108
+ error: testError.toSerializedJSON(),
109
+ duration,
110
+ };
111
+
112
+ // Emit test-finished event
113
+ context.events.emit({
114
+ type: 'test-finished',
115
+ file: context.testFilePath,
116
+ suite: suite.name,
117
+ name: test.name,
118
+ duration,
119
+ error: testError.toSerializedJSON(),
120
+ status: 'failed',
121
+ });
122
+
123
+ return result;
124
+ }
125
+ };
126
+
127
+ export const runSuite = async (
128
+ suite: TestSuite,
129
+ context: TestRunnerContext
130
+ ): Promise<TestSuiteResult> => {
131
+ const startTime = Date.now();
132
+
133
+ // Emit suite-started event
134
+ context.events.emit({
135
+ type: 'suite-started',
136
+ name: suite.name,
137
+ file: context.testFilePath,
138
+ });
139
+
140
+ const testResults: TestResult[] = [];
141
+ const suiteResults: TestSuiteResult[] = [];
142
+
143
+ // Run beforeAll hooks
144
+ await runHooks(suite, 'beforeAll');
145
+
146
+ // Run all tests in the current suite
147
+ for (const test of suite.tests) {
148
+ const result = await runTest(test, suite, context);
149
+ testResults.push(result);
150
+ }
151
+
152
+ // Run all child suites
153
+ for (const childSuite of suite.suites) {
154
+ const result = await runSuite(childSuite, context);
155
+ suiteResults.push(result);
156
+ }
157
+
158
+ // Run afterAll hooks
159
+ await runHooks(suite, 'afterAll');
160
+
161
+ const duration = Date.now() - startTime;
162
+
163
+ // Determine overall suite status
164
+ let status: 'passed' | 'failed' | 'skipped' | 'todo' = 'passed';
165
+
166
+ // Check if any tests or child suites failed
167
+ const hasFailedTests = testResults.some(
168
+ (result) => result.status === 'failed'
169
+ );
170
+ const hasFailedSuites = suiteResults.some(
171
+ (result) => result.status === 'failed'
172
+ );
173
+
174
+ if (hasFailedTests || hasFailedSuites) {
175
+ status = 'failed';
176
+ } else if (
177
+ (testResults.every((result) => result.status === 'skipped') &&
178
+ suiteResults.every((result) => result.status === 'skipped') &&
179
+ testResults.length > 0) ||
180
+ suiteResults.length > 0
181
+ ) {
182
+ status = 'skipped';
183
+ }
184
+
185
+ // Emit suite-finished event
186
+ context.events.emit({
187
+ type: 'suite-finished',
188
+ file: context.testFilePath,
189
+ name: suite.name,
190
+ duration,
191
+ status,
192
+ });
193
+
194
+ return {
195
+ name: suite.name,
196
+ tests: testResults,
197
+ suites: suiteResults,
198
+ status,
199
+ duration,
200
+ };
201
+ };
@@ -0,0 +1,19 @@
1
+ import { EventEmitter } from '../utils/emitter.js';
2
+ import type {
3
+ TestRunnerEvents,
4
+ TestSuite,
5
+ TestSuiteResult,
6
+ } from '@react-native-harness/bridge';
7
+
8
+ export type TestRunnerEventsEmitter = EventEmitter<TestRunnerEvents>;
9
+
10
+ export type TestRunnerContext = {
11
+ events: TestRunnerEventsEmitter;
12
+ testFilePath: string;
13
+ };
14
+
15
+ export type TestRunner = {
16
+ events: TestRunnerEventsEmitter;
17
+ run: (testSuite: TestSuite, testFilePath: string) => Promise<TestSuiteResult>;
18
+ dispose: () => void;
19
+ };
@@ -0,0 +1,2 @@
1
+ // @vitest/spy is available as a no-dependency package, so we can re-use it in React Native Harness!
2
+ export * from '@vitest/spy';
@@ -0,0 +1,151 @@
1
+ import {
2
+ View,
3
+ Text,
4
+ Image,
5
+ StyleSheet,
6
+ ActivityIndicator,
7
+ StatusBar,
8
+ Platform,
9
+ } from 'react-native';
10
+ import { LOGO_IMAGE } from '../constants.js';
11
+ import { useRunnerStatus } from './state.js';
12
+
13
+ require('../initialize.js');
14
+
15
+ export const ReadyScreen = () => {
16
+ const status = useRunnerStatus();
17
+
18
+ return (
19
+ <View style={styles.container}>
20
+ <View style={styles.contentContainer}>
21
+ <View style={styles.logoContainer}>
22
+ <Image style={styles.logo} source={LOGO_IMAGE} />
23
+ </View>
24
+ <Text style={styles.title}>React Native Harness</Text>
25
+
26
+ {status === 'idle' ? (
27
+ <View style={styles.statusIndicator}>
28
+ <View style={styles.statusDot} />
29
+ <Text style={styles.statusText}>Idle</Text>
30
+ </View>
31
+ ) : status === 'loading' ? (
32
+ <View style={styles.loadingContainer}>
33
+ <ActivityIndicator
34
+ size="small"
35
+ color="#3b82f6"
36
+ style={styles.loadingSpinner}
37
+ />
38
+ <Text style={styles.loadingText}>Loading...</Text>
39
+ </View>
40
+ ) : (
41
+ <View style={styles.statusIndicator}>
42
+ <View style={styles.statusDot} />
43
+ <Text style={styles.statusText}>Running...</Text>
44
+ </View>
45
+ )}
46
+ </View>
47
+ </View>
48
+ );
49
+ };
50
+
51
+ const styles = StyleSheet.create({
52
+ container: {
53
+ flex: 1,
54
+ justifyContent: 'center',
55
+ alignItems: 'center',
56
+ backgroundColor: '#0a1628',
57
+ position: 'relative',
58
+ },
59
+ safeArea: {
60
+ flex: 1,
61
+ backgroundColor: 'transparent',
62
+ paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
63
+ },
64
+ contentContainer: {
65
+ alignItems: 'center',
66
+ padding: 24,
67
+ borderRadius: 24,
68
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
69
+ borderWidth: 1,
70
+ borderColor: 'rgba(59, 130, 246, 0.2)',
71
+ shadowColor: '#000',
72
+ shadowOffset: {
73
+ width: 0,
74
+ height: 20,
75
+ },
76
+ shadowOpacity: 0.3,
77
+ shadowRadius: 30,
78
+ maxWidth: 350,
79
+ },
80
+ logoContainer: {
81
+ marginBottom: 12,
82
+ },
83
+ logo: {
84
+ width: 128,
85
+ height: 128,
86
+ },
87
+ title: {
88
+ fontSize: 28,
89
+ fontWeight: '700',
90
+ color: '#38bdf8',
91
+ textAlign: 'center',
92
+ letterSpacing: 1,
93
+ marginBottom: 8,
94
+ },
95
+ subtitle: {
96
+ fontSize: 16,
97
+ fontWeight: '400',
98
+ color: 'rgba(255, 255, 255, 0.7)',
99
+ textAlign: 'center',
100
+ letterSpacing: 0.5,
101
+ marginBottom: 24,
102
+ },
103
+ statusIndicator: {
104
+ flexDirection: 'row',
105
+ alignItems: 'center',
106
+ backgroundColor: 'rgba(34, 211, 238, 0.15)',
107
+ paddingHorizontal: 16,
108
+ paddingVertical: 8,
109
+ height: 36,
110
+ borderRadius: 20,
111
+ borderWidth: 1,
112
+ borderColor: 'rgba(34, 211, 238, 0.4)',
113
+ },
114
+ statusDot: {
115
+ width: 8,
116
+ height: 8,
117
+ borderRadius: 4,
118
+ backgroundColor: '#22d3ee',
119
+ marginRight: 8,
120
+ shadowColor: '#22d3ee',
121
+ shadowOffset: { width: 0, height: 0 },
122
+ shadowOpacity: 0.8,
123
+ shadowRadius: 4,
124
+ },
125
+ statusText: {
126
+ fontSize: 14,
127
+ fontWeight: '500',
128
+ color: '#22d3ee',
129
+ letterSpacing: 0.5,
130
+ },
131
+ loadingContainer: {
132
+ flexDirection: 'row',
133
+ alignItems: 'center',
134
+ backgroundColor: 'rgba(59, 130, 246, 0.15)',
135
+ paddingHorizontal: 16,
136
+ paddingVertical: 8,
137
+ height: 36,
138
+ borderRadius: 20,
139
+ borderWidth: 1,
140
+ borderColor: 'rgba(59, 130, 246, 0.4)',
141
+ },
142
+ loadingSpinner: {
143
+ marginRight: 8,
144
+ },
145
+ loadingText: {
146
+ fontSize: 14,
147
+ fontWeight: '500',
148
+ color: '#3b82f6',
149
+ letterSpacing: 0.5,
150
+ },
151
+ });
@@ -0,0 +1,113 @@
1
+ import {
2
+ View,
3
+ Text,
4
+ Image,
5
+ StyleSheet,
6
+ Platform,
7
+ StatusBar,
8
+ } from 'react-native';
9
+ import { LOGO_IMAGE } from '../constants.js';
10
+
11
+ export const WrongEnvironmentScreen = () => {
12
+ return (
13
+ <View style={styles.container}>
14
+ <View style={styles.contentContainer}>
15
+ <View style={styles.logoContainer}>
16
+ <Image style={styles.logo} source={LOGO_IMAGE} />
17
+ </View>
18
+ <Text style={styles.title}>React Native Harness</Text>
19
+
20
+ <View style={styles.errorIndicator}>
21
+ <View style={styles.errorDot} />
22
+ <Text style={styles.errorText}>Environment Error</Text>
23
+ </View>
24
+ <Text style={styles.submessage}>
25
+ Please double-check that you followed the installation documentation
26
+ carefully.
27
+ </Text>
28
+ </View>
29
+ </View>
30
+ );
31
+ };
32
+
33
+ const styles = StyleSheet.create({
34
+ container: {
35
+ flex: 1,
36
+ justifyContent: 'center',
37
+ alignItems: 'center',
38
+ backgroundColor: '#0a1628',
39
+ position: 'relative',
40
+ },
41
+ safeArea: {
42
+ flex: 1,
43
+ backgroundColor: 'transparent',
44
+ paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0,
45
+ },
46
+ contentContainer: {
47
+ alignItems: 'center',
48
+ padding: 24,
49
+ borderRadius: 24,
50
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
51
+ borderWidth: 1,
52
+ borderColor: 'rgba(239, 68, 68, 0.2)',
53
+ shadowColor: '#000',
54
+ shadowOffset: {
55
+ width: 0,
56
+ height: 20,
57
+ },
58
+ shadowOpacity: 0.3,
59
+ shadowRadius: 30,
60
+ maxWidth: 350,
61
+ },
62
+ logoContainer: {
63
+ marginBottom: 12,
64
+ },
65
+ logo: {
66
+ width: 128,
67
+ height: 128,
68
+ },
69
+ title: {
70
+ fontSize: 28,
71
+ fontWeight: '700',
72
+ color: '#38bdf8',
73
+ textAlign: 'center',
74
+ letterSpacing: 1,
75
+ marginBottom: 8,
76
+ },
77
+ errorIndicator: {
78
+ flexDirection: 'row',
79
+ alignItems: 'center',
80
+ backgroundColor: 'rgba(239, 68, 68, 0.15)',
81
+ paddingHorizontal: 16,
82
+ paddingVertical: 8,
83
+ height: 36,
84
+ borderRadius: 20,
85
+ borderWidth: 1,
86
+ borderColor: 'rgba(239, 68, 68, 0.4)',
87
+ marginBottom: 24,
88
+ },
89
+ errorDot: {
90
+ width: 8,
91
+ height: 8,
92
+ borderRadius: 4,
93
+ backgroundColor: '#ef4444',
94
+ marginRight: 8,
95
+ shadowColor: '#ef4444',
96
+ shadowOffset: { width: 0, height: 0 },
97
+ shadowOpacity: 0.8,
98
+ shadowRadius: 4,
99
+ },
100
+ errorText: {
101
+ fontSize: 14,
102
+ fontWeight: '500',
103
+ color: '#ef4444',
104
+ letterSpacing: 0.5,
105
+ },
106
+ submessage: {
107
+ fontSize: 14,
108
+ fontWeight: '400',
109
+ color: 'rgba(255, 255, 255, 0.6)',
110
+ textAlign: 'center',
111
+ letterSpacing: 0.5,
112
+ },
113
+ });
@@ -0,0 +1,3 @@
1
+ export const UI = global.RN_HARNESS
2
+ ? require('./ReadyScreen').ReadyScreen
3
+ : require('./WrongEnvironmentScreen').WrongEnvironmentScreen;
@@ -0,0 +1,13 @@
1
+ import { create, useStore } from 'zustand/react';
2
+
3
+ export type RunnerState = {
4
+ status: 'loading' | 'idle' | 'running';
5
+ setStatus: (status: 'loading' | 'idle' | 'running') => void;
6
+ };
7
+
8
+ export const store = create<RunnerState>((set) => ({
9
+ status: 'loading',
10
+ setStatus: (status) => set({ status }),
11
+ }));
12
+
13
+ export const useRunnerStatus = () => useStore(store, (state) => state.status);