@react-native-harness/runtime 1.0.0-alpha.2 → 1.0.0-alpha.21

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 (169) hide show
  1. package/README.md +23 -4
  2. package/assets/harness-module-system.js +73 -0
  3. package/dist/bundler/bundle.d.ts.map +1 -1
  4. package/dist/bundler/bundle.js +7 -2
  5. package/dist/bundler/errors.d.ts +5 -0
  6. package/dist/bundler/errors.d.ts.map +1 -1
  7. package/dist/bundler/errors.js +11 -0
  8. package/dist/bundler/evaluate.d.ts.map +1 -1
  9. package/dist/bundler/evaluate.js +7 -7
  10. package/dist/bundler/factory.d.ts +3 -0
  11. package/dist/bundler/factory.d.ts.map +1 -0
  12. package/dist/bundler/factory.js +36 -0
  13. package/dist/bundler/index.d.ts +2 -1
  14. package/dist/bundler/index.d.ts.map +1 -1
  15. package/dist/bundler/index.js +1 -1
  16. package/dist/bundler/types.d.ts +7 -0
  17. package/dist/bundler/types.d.ts.map +1 -0
  18. package/dist/bundler/types.js +1 -0
  19. package/dist/client/factory.d.ts.map +1 -1
  20. package/dist/client/factory.js +34 -6
  21. package/dist/client/getDeviceDescriptor.d.ts +1 -1
  22. package/dist/client/getDeviceDescriptor.d.ts.map +1 -1
  23. package/dist/client/getDeviceDescriptor.js +18 -6
  24. package/dist/client/getWSServer.d.ts.map +1 -1
  25. package/dist/client/getWSServer.js +2 -1
  26. package/dist/client/setup-files.d.ts +12 -0
  27. package/dist/client/setup-files.d.ts.map +1 -0
  28. package/dist/client/setup-files.js +60 -0
  29. package/dist/collector/functions.d.ts +1 -1
  30. package/dist/collector/functions.d.ts.map +1 -1
  31. package/dist/collector/functions.js +10 -2
  32. package/dist/collector/types.d.ts +1 -1
  33. package/dist/collector/types.d.ts.map +1 -1
  34. package/dist/constants.d.ts +0 -1
  35. package/dist/constants.d.ts.map +1 -1
  36. package/dist/constants.js +0 -1
  37. package/dist/disableHMRWhenReady.d.ts +2 -0
  38. package/dist/disableHMRWhenReady.d.ts.map +1 -0
  39. package/dist/disableHMRWhenReady.js +20 -0
  40. package/dist/entry-point.d.ts +2 -0
  41. package/dist/entry-point.d.ts.map +1 -0
  42. package/dist/entry-point.js +4 -0
  43. package/dist/expect/index.d.ts.map +1 -1
  44. package/dist/expect/index.js +2 -0
  45. package/dist/expect/setup.js +2 -0
  46. package/dist/filtering/index.d.ts +2 -0
  47. package/dist/filtering/index.d.ts.map +1 -0
  48. package/dist/filtering/index.js +1 -0
  49. package/dist/filtering/testNameFilter.d.ts +12 -0
  50. package/dist/filtering/testNameFilter.d.ts.map +1 -0
  51. package/dist/filtering/testNameFilter.js +56 -0
  52. package/dist/globals.d.ts +6 -2
  53. package/dist/globals.d.ts.map +1 -1
  54. package/dist/globals.js +7 -1
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +4 -0
  58. package/dist/initialize.js +14 -6
  59. package/dist/jest-mock.d.ts +2 -0
  60. package/dist/jest-mock.d.ts.map +1 -0
  61. package/dist/jest-mock.js +25 -0
  62. package/dist/mocker/index.d.ts +1 -1
  63. package/dist/mocker/index.d.ts.map +1 -1
  64. package/dist/mocker/index.js +1 -1
  65. package/dist/mocker/registry.d.ts +2 -3
  66. package/dist/mocker/registry.d.ts.map +1 -1
  67. package/dist/mocker/registry.js +25 -16
  68. package/dist/namespace.d.ts +18 -0
  69. package/dist/namespace.d.ts.map +1 -0
  70. package/dist/namespace.js +19 -0
  71. package/dist/polyfills.d.ts +11 -0
  72. package/dist/polyfills.d.ts.map +1 -0
  73. package/dist/polyfills.js +13 -0
  74. package/dist/render/ErrorBoundary.d.ts +17 -0
  75. package/dist/render/ErrorBoundary.d.ts.map +1 -0
  76. package/dist/render/ErrorBoundary.js +73 -0
  77. package/dist/render/TestComponentOverlay.d.ts +3 -0
  78. package/dist/render/TestComponentOverlay.d.ts.map +1 -0
  79. package/dist/render/TestComponentOverlay.js +36 -0
  80. package/dist/render/cleanup.d.ts +2 -0
  81. package/dist/render/cleanup.d.ts.map +1 -0
  82. package/dist/render/cleanup.js +6 -0
  83. package/dist/render/index.d.ts +6 -0
  84. package/dist/render/index.d.ts.map +1 -0
  85. package/dist/render/index.js +66 -0
  86. package/dist/render/setup.d.ts +2 -0
  87. package/dist/render/setup.d.ts.map +1 -0
  88. package/dist/render/setup.js +7 -0
  89. package/dist/render/types.d.ts +12 -0
  90. package/dist/render/types.d.ts.map +1 -0
  91. package/dist/render/types.js +1 -0
  92. package/dist/runner/errors.d.ts +4 -2
  93. package/dist/runner/errors.d.ts.map +1 -1
  94. package/dist/runner/errors.js +21 -3
  95. package/dist/runner/factory.d.ts.map +1 -1
  96. package/dist/runner/factory.js +6 -1
  97. package/dist/runner/runSuite.d.ts.map +1 -1
  98. package/dist/runner/runSuite.js +59 -7
  99. package/dist/symbolicate.d.ts +3 -0
  100. package/dist/symbolicate.d.ts.map +1 -0
  101. package/dist/symbolicate.js +19 -0
  102. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  103. package/dist/ui/ReadyScreen.d.ts.map +1 -1
  104. package/dist/ui/ReadyScreen.js +3 -10
  105. package/dist/ui/WrongEnvironmentScreen.d.ts.map +1 -1
  106. package/dist/ui/WrongEnvironmentScreen.js +2 -10
  107. package/dist/ui/state.d.ts +14 -1
  108. package/dist/ui/state.d.ts.map +1 -1
  109. package/dist/ui/state.js +22 -0
  110. package/dist/utils/emitter.d.ts.map +1 -1
  111. package/dist/waitFor.d.ts +21 -0
  112. package/dist/waitFor.d.ts.map +1 -0
  113. package/dist/waitFor.js +137 -0
  114. package/eslint.config.mjs +1 -7
  115. package/package.json +22 -14
  116. package/src/__tests__/collector.test.ts +55 -55
  117. package/src/__tests__/error-handling.test.ts +34 -34
  118. package/src/__tests__/expect.test.ts +13 -5
  119. package/src/__tests__/initialize.test.ts +24 -0
  120. package/src/bundler/bundle.ts +9 -2
  121. package/src/bundler/errors.ts +11 -0
  122. package/src/bundler/evaluate.ts +9 -9
  123. package/src/bundler/factory.ts +43 -0
  124. package/src/bundler/index.ts +2 -1
  125. package/src/bundler/types.ts +7 -0
  126. package/src/client/factory.ts +53 -11
  127. package/src/client/getDeviceDescriptor.ts +29 -8
  128. package/src/client/getWSServer.ts +2 -1
  129. package/src/client/setup-files.ts +81 -0
  130. package/src/collector/functions.ts +18 -2
  131. package/src/collector/types.ts +4 -1
  132. package/src/constants.ts +0 -1
  133. package/src/disableHMRWhenReady.ts +27 -0
  134. package/src/entry-point.ts +8 -0
  135. package/src/expect/index.ts +8 -2
  136. package/src/expect/setup.ts +3 -0
  137. package/src/filtering/index.ts +4 -0
  138. package/src/filtering/testNameFilter.ts +82 -0
  139. package/src/globals.ts +15 -2
  140. package/src/index.ts +4 -0
  141. package/src/initialize.ts +21 -8
  142. package/src/jest-mock.ts +32 -0
  143. package/src/mocker/index.ts +6 -1
  144. package/src/mocker/metro-require.d.ts +2 -0
  145. package/src/mocker/registry.ts +29 -18
  146. package/src/namespace.ts +41 -0
  147. package/src/polyfills.ts +14 -0
  148. package/src/react-native.d.ts +35 -6
  149. package/src/render/ErrorBoundary.tsx +108 -0
  150. package/src/render/TestComponentOverlay.tsx +47 -0
  151. package/src/render/cleanup.ts +7 -0
  152. package/src/render/index.ts +96 -0
  153. package/src/render/setup.ts +8 -0
  154. package/src/render/types.ts +11 -0
  155. package/src/runner/errors.ts +35 -5
  156. package/src/runner/factory.ts +8 -1
  157. package/src/runner/runSuite.ts +70 -9
  158. package/src/symbolicate.ts +24 -0
  159. package/src/ui/ReadyScreen.tsx +2 -12
  160. package/src/ui/WrongEnvironmentScreen.tsx +1 -19
  161. package/src/ui/state.ts +39 -0
  162. package/src/utils/emitter.ts +1 -0
  163. package/src/waitFor.ts +199 -0
  164. package/tsconfig.spec.json +7 -3
  165. package/tsconfig.tsbuildinfo +1 -1
  166. package/assets/logo.png +0 -0
  167. package/assets/moduleSystem.flow.js +0 -1062
  168. package/types/global.d.ts +0 -2
  169. package/types/index.d.ts +0 -1
package/src/globals.ts CHANGED
@@ -1,5 +1,18 @@
1
+ export type HarnessGlobal = {
2
+ appRegistryComponentName: string;
3
+ webSocketPort?: number;
4
+ };
5
+
1
6
  declare global {
2
- var RN_HARNESS: boolean | undefined;
7
+ var RN_HARNESS: HarnessGlobal | undefined;
3
8
  }
4
9
 
5
- export {};
10
+ export const getHarnessGlobal = (): HarnessGlobal => {
11
+ const harnessGlobal = global.RN_HARNESS;
12
+
13
+ if (!harnessGlobal) {
14
+ throw new Error('RN_HARNESS global is not set');
15
+ }
16
+
17
+ return harnessGlobal;
18
+ };
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import './polyfills.js';
1
2
  import './globals.d.ts';
2
3
 
3
4
  export { UI as ReactNativeHarness } from './ui/index.js';
@@ -5,3 +6,6 @@ export * from './spy/index.js';
5
6
  export * from './expect/index.js';
6
7
  export * from './collector/index.js';
7
8
  export * from './mocker/index.js';
9
+ export * from './namespace.js';
10
+ export * from './waitFor.js';
11
+ export * from './render/index.js';
package/src/initialize.ts CHANGED
@@ -1,22 +1,35 @@
1
1
  import { getDeviceDescriptor } from './client/getDeviceDescriptor.js';
2
2
  import { getClient } from './client/index.js';
3
+ import { disableHMRWhenReady } from './disableHMRWhenReady.js';
4
+ import { setupJestMock } from './jest-mock.js';
3
5
 
4
6
  // Polyfill for EventTarget
5
7
  const Shim = require('event-target-shim');
6
8
  globalThis.Event = Shim.Event;
7
9
  globalThis.EventTarget = Shim.EventTarget;
8
10
 
11
+ // Setup jest mock to warn users about using Jest APIs
12
+ setupJestMock();
13
+
9
14
  // Turn off LogBox
10
15
  const { LogBox } = require('react-native');
11
16
  LogBox.ignoreAllLogs(true);
12
17
 
13
18
  // Turn off HMR
14
- const HMRClient = require('react-native/Libraries/Utilities/HMRClient');
15
- HMRClient.setup = () => {
16
- // No setup = no HMR
17
- };
19
+ const HMRClientModule = require('react-native/Libraries/Utilities/HMRClient');
20
+ const HMRClient =
21
+ 'default' in HMRClientModule ? HMRClientModule.default : HMRClientModule;
22
+
23
+ // Wait for HMRClient to be initialized
24
+ setTimeout(() => {
25
+ void disableHMRWhenReady(() => HMRClient.disable(), 50).then(() =>
26
+ getClient().then((client) =>
27
+ client.rpc.reportReady(getDeviceDescriptor())
28
+ )
29
+ );
30
+ });
18
31
 
19
- // Initialize the client
20
- void getClient().then((client) =>
21
- client.rpc.reportReady(getDeviceDescriptor())
22
- );
32
+ // Re-throw fatal errors
33
+ ErrorUtils.setGlobalHandler((error) => {
34
+ throw error;
35
+ });
@@ -0,0 +1,32 @@
1
+ // Mock jest global to warn users about using Jest APIs in Harness tests
2
+ export const setupJestMock = (): void => {
3
+ function throwError(): never {
4
+ throw new Error(
5
+ `Jest globals are not available in Harness tests. Import from 'react-native-harness' instead (e.g., import { harness } from 'react-native-harness'; harness.fn())`
6
+ );
7
+ }
8
+
9
+ const jestMock = new Proxy(
10
+ {},
11
+ {
12
+ get() {
13
+ throwError();
14
+ },
15
+ set() {
16
+ throwError();
17
+ },
18
+ has() {
19
+ throwError();
20
+ },
21
+ ownKeys() {
22
+ throwError();
23
+ },
24
+ }
25
+ );
26
+
27
+ Object.defineProperty(globalThis, 'jest', {
28
+ value: jestMock,
29
+ writable: false,
30
+ configurable: false,
31
+ });
32
+ };
@@ -1 +1,6 @@
1
- export { mock, requireActual, clearMocks } from './registry.js';
1
+ export {
2
+ mock,
3
+ requireActual,
4
+ unmock,
5
+ resetModules,
6
+ } from './registry.js';
@@ -2,4 +2,6 @@ import type { Require } from './types.js';
2
2
 
3
3
  declare global {
4
4
  var __r: Require;
5
+ var __resetAllModules: () => void;
6
+ var __resetModule: (moduleId: number) => void;
5
7
  }
@@ -1,36 +1,28 @@
1
1
  import { ModuleFactory, ModuleId, Require } from './types.js';
2
2
 
3
+ const modulesCache = new Map<number, unknown>();
3
4
  const mockRegistry = new Map<number, ModuleFactory>();
4
- const mockCache = new Map<number, unknown>();
5
5
 
6
6
  const originalRequire = global.__r;
7
7
 
8
8
  export const mock = (moduleId: string, factory: ModuleFactory): void => {
9
- mockCache.delete(moduleId as unknown as ModuleId);
9
+ modulesCache.delete(moduleId as unknown as ModuleId);
10
10
  mockRegistry.set(moduleId as unknown as ModuleId, factory);
11
11
  };
12
12
 
13
- export const clearMocks = (): void => {
14
- mockRegistry.clear();
15
- mockCache.clear();
16
- };
17
-
18
- export const getMockRegistry = (): Map<number, ModuleFactory> => {
19
- return mockRegistry;
13
+ const isModuleMocked = (moduleId: number): boolean => {
14
+ return mockRegistry.has(moduleId);
20
15
  };
21
16
 
22
- export const getMockImplementation = (moduleId: number): unknown | null => {
23
- if (mockCache.has(moduleId)) {
24
- return mockCache.get(moduleId);
25
- }
26
-
17
+ const getMockImplementation = (moduleId: number): unknown | null => {
27
18
  const factory = mockRegistry.get(moduleId);
19
+
28
20
  if (!factory) {
29
21
  return null;
30
22
  }
31
23
 
32
24
  const implementation = factory();
33
- mockCache.set(moduleId, implementation);
25
+ modulesCache.set(moduleId, implementation);
34
26
  return implementation;
35
27
  };
36
28
 
@@ -39,15 +31,34 @@ export const requireActual = <T = any>(moduleId: string): T =>
39
31
  // babel plugin will transform 'moduleId' to a number
40
32
  originalRequire(moduleId as unknown as ModuleId) as T;
41
33
 
34
+ export const unmock = (moduleId: string) => {
35
+ mockRegistry.delete(moduleId as unknown as ModuleId);
36
+ modulesCache.delete(moduleId as unknown as ModuleId);
37
+ };
38
+
39
+ export const resetModules = (): void => {
40
+ modulesCache.clear();
41
+ mockRegistry.clear();
42
+ };
43
+
42
44
  const mockRequire = (moduleId: string) => {
43
45
  // babel plugin will transform 'moduleId' to a number
44
- const mockedModule = getMockImplementation(moduleId as unknown as ModuleId);
46
+ const moduleIdNumber = moduleId as unknown as ModuleId;
47
+ const cachedModule = modulesCache.get(moduleIdNumber);
48
+
49
+ if (cachedModule) {
50
+ return cachedModule;
51
+ }
45
52
 
46
- if (mockedModule) {
53
+ if (isModuleMocked(moduleIdNumber)) {
54
+ const mockedModule = getMockImplementation(moduleIdNumber);
55
+ modulesCache.set(moduleIdNumber, mockedModule);
47
56
  return mockedModule;
48
57
  }
49
58
 
50
- return originalRequire(moduleId as unknown as ModuleId);
59
+ const originalModule = originalRequire(moduleIdNumber);
60
+ modulesCache.set(moduleIdNumber, originalModule);
61
+ return originalModule;
51
62
  };
52
63
 
53
64
  Object.setPrototypeOf(mockRequire, Object.getPrototypeOf(originalRequire));
@@ -0,0 +1,41 @@
1
+ import {
2
+ spyOn,
3
+ fn,
4
+ clearAllMocks,
5
+ resetAllMocks,
6
+ restoreAllMocks,
7
+ } from './spy/index.js';
8
+ import { mock, unmock, requireActual, resetModules } from './mocker/index.js';
9
+ import { waitFor, waitUntil } from './waitFor.js';
10
+
11
+ export type HarnessNamespace = {
12
+ spyOn: typeof spyOn;
13
+ fn: typeof fn;
14
+ mock: typeof mock;
15
+ unmock: typeof unmock;
16
+ requireActual: typeof requireActual;
17
+ clearAllMocks: typeof clearAllMocks;
18
+ resetAllMocks: typeof resetAllMocks;
19
+ restoreAllMocks: typeof restoreAllMocks;
20
+ resetModules: typeof resetModules;
21
+ waitFor: typeof waitFor;
22
+ waitUntil: typeof waitUntil;
23
+ };
24
+
25
+ const createHarnessNamespace = (): HarnessNamespace => {
26
+ return {
27
+ spyOn,
28
+ fn,
29
+ mock,
30
+ unmock,
31
+ requireActual,
32
+ clearAllMocks,
33
+ resetAllMocks,
34
+ restoreAllMocks,
35
+ resetModules,
36
+ waitFor,
37
+ waitUntil,
38
+ };
39
+ };
40
+
41
+ export const harness = createHarnessNamespace();
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Polyfills for ES2022+ features not supported by JSC (JavaScriptCore).
3
+ *
4
+ * JSC, used in React Native when Hermes is disabled, doesn't support
5
+ * Object.hasOwn (ES2022). This causes runtime errors when @vitest/expect
6
+ * v4.x initializes.
7
+ *
8
+ * This polyfill must be loaded before any code that uses Object.hasOwn.
9
+ */
10
+
11
+ if (typeof Object.hasOwn !== 'function') {
12
+ Object.hasOwn = (obj: object, prop: PropertyKey): boolean =>
13
+ Object.prototype.hasOwnProperty.call(obj, prop);
14
+ }
@@ -7,10 +7,39 @@ declare module 'react-native/Libraries/Core/Devtools/getDevServer' {
7
7
  export default function getDevServer(): DevServerInfo;
8
8
  }
9
9
 
10
- declare global {
11
- var __r:
12
- | {
13
- (moduleId: number): unknown;
14
- }
15
- | undefined;
10
+ declare module 'react-native/Libraries/Core/Devtools/symbolicateStackTrace' {
11
+ import { StackFrame } from 'react-native/Libraries/Core/Devtools/parseErrorStack';
12
+
13
+ export type CodeFrame = Readonly<{
14
+ content: string;
15
+ location:
16
+ | {
17
+ row: number;
18
+ column: number;
19
+ [key: string]: unknown;
20
+ }
21
+ | null
22
+ | undefined;
23
+ fileName: string;
24
+ }>;
25
+
26
+ export type SymbolicatedStackTrace = Readonly<{
27
+ stack: ReadonlyArray<StackFrame>;
28
+ codeFrame: CodeFrame | null | undefined;
29
+ }>;
30
+
31
+ export default function symbolicateStackTrace(
32
+ stack: ReadonlyArray<StackFrame>,
33
+ extraData?: unknown
34
+ ): Promise<SymbolicatedStackTrace>;
35
+ }
36
+
37
+ declare module 'react-native/Libraries/Core/Devtools/parseErrorStack' {
38
+ export type StackFrame = {
39
+ column: number | null | undefined;
40
+ file: string | null | undefined;
41
+ lineNumber: number | null | undefined;
42
+ methodName: string;
43
+ };
44
+ export default function parseErrorStack(errorStack?: string): StackFrame[];
16
45
  }
@@ -0,0 +1,108 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet, ScrollView } from 'react-native';
3
+
4
+ type ErrorBoundaryProps = {
5
+ children: React.ReactNode;
6
+ };
7
+
8
+ type ErrorBoundaryState = {
9
+ hasError: boolean;
10
+ error: Error | null;
11
+ };
12
+
13
+ export class ErrorBoundary extends React.Component<
14
+ ErrorBoundaryProps,
15
+ ErrorBoundaryState
16
+ > {
17
+ constructor(props: ErrorBoundaryProps) {
18
+ super(props);
19
+ this.state = { hasError: false, error: null };
20
+ }
21
+
22
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
23
+ return { hasError: true, error };
24
+ }
25
+
26
+ override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
27
+ console.error('Error caught by ErrorBoundary:', error, errorInfo);
28
+ }
29
+
30
+ override componentDidUpdate(prevProps: ErrorBoundaryProps): void {
31
+ // Reset error state when children change (new component rendered)
32
+ if (prevProps.children !== this.props.children && this.state.hasError) {
33
+ this.setState({ hasError: false, error: null });
34
+ }
35
+ }
36
+
37
+ override render(): React.ReactNode {
38
+ if (this.state.hasError && this.state.error) {
39
+ return (
40
+ <View style={styles.errorContainer}>
41
+ <View style={styles.errorContent}>
42
+ <Text style={styles.errorTitle}>Component Error</Text>
43
+ <Text style={styles.errorSubtitle}>
44
+ The rendered component threw an error:
45
+ </Text>
46
+ <ScrollView style={styles.errorScrollView}>
47
+ <Text style={styles.errorMessage}>
48
+ {this.state.error.message}
49
+ </Text>
50
+ {this.state.error.stack && (
51
+ <Text style={styles.errorStack}>{this.state.error.stack}</Text>
52
+ )}
53
+ </ScrollView>
54
+ </View>
55
+ </View>
56
+ );
57
+ }
58
+
59
+ return this.props.children;
60
+ }
61
+ }
62
+
63
+ const styles = StyleSheet.create({
64
+ errorContainer: {
65
+ flex: 1,
66
+ backgroundColor: 'rgba(220, 38, 38, 0.1)',
67
+ justifyContent: 'center',
68
+ alignItems: 'center',
69
+ padding: 20,
70
+ },
71
+ errorContent: {
72
+ backgroundColor: '#1f2937',
73
+ borderRadius: 12,
74
+ padding: 20,
75
+ maxWidth: 500,
76
+ width: '100%',
77
+ maxHeight: '80%',
78
+ borderWidth: 2,
79
+ borderColor: '#dc2626',
80
+ },
81
+ errorTitle: {
82
+ fontSize: 24,
83
+ fontWeight: '700',
84
+ color: '#dc2626',
85
+ marginBottom: 8,
86
+ },
87
+ errorSubtitle: {
88
+ fontSize: 14,
89
+ color: '#9ca3af',
90
+ marginBottom: 16,
91
+ },
92
+ errorScrollView: {
93
+ maxHeight: 400,
94
+ },
95
+ errorMessage: {
96
+ fontSize: 16,
97
+ fontWeight: '600',
98
+ color: '#fca5a5',
99
+ marginBottom: 12,
100
+ fontFamily: 'Courier',
101
+ },
102
+ errorStack: {
103
+ fontSize: 12,
104
+ color: '#d1d5db',
105
+ fontFamily: 'Courier',
106
+ lineHeight: 18,
107
+ },
108
+ });
@@ -0,0 +1,47 @@
1
+ import React, { useEffect } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import { useRenderedElement } from '../ui/state.js';
4
+ import { store } from '../ui/state.js';
5
+ import { ErrorBoundary } from './ErrorBoundary.js';
6
+
7
+ export const TestComponentOverlay = (): React.ReactElement | null => {
8
+ const { element, key } = useRenderedElement();
9
+
10
+ useEffect(() => {
11
+ // Call onRenderCallback when element changes
12
+ const callback = store.getState().onRenderCallback;
13
+
14
+ if (callback) {
15
+ callback();
16
+ store.getState().setOnRenderCallback(null);
17
+ }
18
+ }, [element]);
19
+
20
+ if (!element) {
21
+ return null;
22
+ }
23
+
24
+ const handleLayout = (): void => {
25
+ const callback = store.getState().onLayoutCallback;
26
+
27
+ if (callback) {
28
+ callback();
29
+ // Clear the callback after calling it
30
+ store.getState().setOnLayoutCallback(null);
31
+ }
32
+ };
33
+
34
+ return (
35
+ <View key={key} style={styles.overlay} onLayout={handleLayout}>
36
+ <ErrorBoundary>{element}</ErrorBoundary>
37
+ </View>
38
+ );
39
+ };
40
+
41
+ const styles = StyleSheet.create({
42
+ overlay: {
43
+ ...StyleSheet.absoluteFillObject,
44
+ backgroundColor: '#0a1628',
45
+ zIndex: 1000,
46
+ },
47
+ });
@@ -0,0 +1,7 @@
1
+ import { store } from '../ui/state.js';
2
+
3
+ export const cleanup = (): void => {
4
+ store.getState().setRenderedElement(null);
5
+ store.getState().setOnLayoutCallback(null);
6
+ store.getState().setOnRenderCallback(null);
7
+ };
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import { store } from '../ui/state.js';
3
+ import type { RenderResult, RenderOptions } from './types.js';
4
+
5
+ const wrapElement = (
6
+ element: React.ReactElement,
7
+ wrapper?: React.ComponentType<{ children: React.ReactNode }>
8
+ ): React.ReactElement => {
9
+ if (!wrapper) {
10
+ return element;
11
+ }
12
+ return React.createElement(wrapper, { children: element });
13
+ };
14
+
15
+ export const render = async (
16
+ element: React.ReactElement,
17
+ options: RenderOptions = {}
18
+ ): Promise<RenderResult> => {
19
+ const { timeout = 1000, wrapper } = options;
20
+
21
+ // If an element is already rendered, unmount it first
22
+ if (store.getState().renderedElement !== null) {
23
+ store.getState().setRenderedElement(null);
24
+ store.getState().setOnLayoutCallback(null);
25
+ store.getState().setOnRenderCallback(null);
26
+ }
27
+
28
+ // Create a promise that resolves when the element is laid out
29
+ const layoutPromise = new Promise<void>((resolve, reject) => {
30
+ const timeoutId = setTimeout(() => {
31
+ store.getState().setOnLayoutCallback(null);
32
+ reject(
33
+ new Error(`Render timeout: Element did not mount within ${timeout}ms`)
34
+ );
35
+ }, timeout);
36
+
37
+ store.getState().setOnLayoutCallback(() => {
38
+ clearTimeout(timeoutId);
39
+ resolve();
40
+ });
41
+ });
42
+
43
+ // Wrap and set the element in state (key is generated automatically)
44
+ const wrappedElement = wrapElement(element, wrapper);
45
+ store.getState().setRenderedElement(wrappedElement);
46
+
47
+ // Wait for layout
48
+ await layoutPromise;
49
+
50
+ const rerender = async (newElement: React.ReactElement): Promise<void> => {
51
+ if (store.getState().renderedElement === null) {
52
+ throw new Error('No element is currently rendered. Call render() first.');
53
+ }
54
+
55
+ // Create a promise that resolves when the element is re-rendered
56
+ const renderPromise = new Promise<void>((resolve, reject) => {
57
+ const timeoutId = setTimeout(() => {
58
+ store.getState().setOnRenderCallback(null);
59
+ reject(
60
+ new Error(
61
+ `Rerender timeout: Element did not update within ${timeout}ms`
62
+ )
63
+ );
64
+ }, timeout);
65
+
66
+ store.getState().setOnRenderCallback(() => {
67
+ clearTimeout(timeoutId);
68
+ resolve();
69
+ });
70
+ });
71
+
72
+ const wrappedNewElement = wrapElement(newElement, wrapper);
73
+ store.getState().updateRenderedElement(wrappedNewElement);
74
+
75
+ // Wait for render
76
+ await renderPromise;
77
+ };
78
+
79
+ const unmount = (): void => {
80
+ if (store.getState().renderedElement === null) {
81
+ return;
82
+ }
83
+
84
+ store.getState().setRenderedElement(null);
85
+ store.getState().setOnLayoutCallback(null);
86
+ store.getState().setOnRenderCallback(null);
87
+ };
88
+
89
+ return {
90
+ rerender,
91
+ unmount,
92
+ };
93
+ };
94
+
95
+ export { cleanup } from './cleanup.js';
96
+ export type { RenderResult, RenderOptions } from './types.js';
@@ -0,0 +1,8 @@
1
+ import { afterEach } from '../collector/functions.js';
2
+ import { cleanup } from './cleanup.js';
3
+
4
+ export const setup = () => {
5
+ afterEach(() => {
6
+ cleanup();
7
+ });
8
+ };
@@ -0,0 +1,11 @@
1
+ import type React from 'react';
2
+
3
+ export type RenderResult = {
4
+ rerender: (element: React.ReactElement) => Promise<void>;
5
+ unmount: () => void;
6
+ };
7
+
8
+ export type RenderOptions = {
9
+ timeout?: number;
10
+ wrapper?: React.ComponentType<{ children: React.ReactNode }>;
11
+ };
@@ -1,17 +1,26 @@
1
- import type { SerializedError } from '@react-native-harness/bridge';
1
+ import type { SerializedError, CodeFrame } from '@react-native-harness/bridge';
2
+ import { getCodeFrame } from '../symbolicate.js';
2
3
 
3
4
  export class TestExecutionError extends Error {
4
5
  file: string;
5
6
  suite: string;
6
7
  test: string;
8
+ codeFrame?: CodeFrame;
7
9
 
8
- constructor(error: unknown, file: string, suite: string, test: string) {
10
+ constructor(
11
+ error: unknown,
12
+ file: string,
13
+ suite: string,
14
+ test: string,
15
+ codeFrame?: CodeFrame
16
+ ) {
9
17
  super('Test execution error');
10
18
  this.name = 'TestExecutionError';
11
19
  this.file = file;
12
20
  this.suite = suite;
13
21
  this.test = test;
14
22
  this.cause = error;
23
+ this.codeFrame = codeFrame;
15
24
  }
16
25
 
17
26
  toSerializedJSON(): SerializedError {
@@ -19,13 +28,34 @@ export class TestExecutionError extends Error {
19
28
  this.cause instanceof Error ? this.cause.name : 'Unknown name';
20
29
  const causeMessage =
21
30
  this.cause instanceof Error ? this.cause.message : 'Unknown message';
22
- const causeStack =
23
- this.cause instanceof Error ? this.cause.stack : undefined;
31
+ const causeCodeFrame = this.codeFrame;
24
32
 
25
33
  return {
26
34
  name: causeName,
27
35
  message: causeMessage,
28
- stack: causeStack,
36
+ codeFrame: causeCodeFrame,
29
37
  };
30
38
  }
31
39
  }
40
+
41
+ export const getTestExecutionError = async (
42
+ error: unknown,
43
+ file: string,
44
+ suite: string,
45
+ test: string
46
+ ): Promise<TestExecutionError> => {
47
+ try {
48
+ if (error instanceof Error) {
49
+ const codeFrame = await getCodeFrame(error);
50
+
51
+ if (codeFrame) {
52
+ return new TestExecutionError(error, file, suite, test, codeFrame);
53
+ }
54
+ }
55
+
56
+ return new TestExecutionError(error, file, suite, test);
57
+ } catch (error) {
58
+ // If the stack cannot be symbolicated, return the original error
59
+ return new TestExecutionError(error, file, suite, test);
60
+ }
61
+ };
@@ -9,10 +9,17 @@ export const getTestRunner = (): TestRunner => {
9
9
  return {
10
10
  events,
11
11
  run: async (testSuite, testFilePath) => {
12
- return runSuite(testSuite, {
12
+ const result = await runSuite(testSuite, {
13
13
  events,
14
14
  testFilePath,
15
15
  });
16
+
17
+ // If coverage is enabled, there will be a global variable called __coverage__
18
+ if ('__coverage__' in global && !!global.__coverage__) {
19
+ result.coverage = global.__coverage__;
20
+ }
21
+
22
+ return result;
16
23
  },
17
24
  dispose: () => {
18
25
  events.clearAllListeners();