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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/client/factory.d.ts +2 -1
  2. package/dist/client/factory.d.ts.map +1 -1
  3. package/dist/client/factory.js +53 -59
  4. package/dist/client/store.d.ts +3 -3
  5. package/dist/client/store.d.ts.map +1 -1
  6. package/dist/client/store.js +7 -7
  7. package/dist/expect/matchers/toMatchImageSnapshot.d.ts +1 -1
  8. package/dist/expect/matchers/toMatchImageSnapshot.d.ts.map +1 -1
  9. package/dist/expect/matchers/toMatchImageSnapshot.js +4 -12
  10. package/dist/initialize.js +14 -5
  11. package/dist/jsx/jsx-dev-runtime.d.ts +2 -1
  12. package/dist/jsx/jsx-dev-runtime.d.ts.map +1 -1
  13. package/dist/jsx/jsx-dev-runtime.js +16 -7
  14. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  15. package/dist/waitFor.d.ts.map +1 -1
  16. package/dist/waitFor.js +5 -3
  17. package/out-tsc/vitest/src/client/factory.d.ts +2 -1
  18. package/out-tsc/vitest/src/client/factory.d.ts.map +1 -1
  19. package/out-tsc/vitest/src/client/store.d.ts +3 -3
  20. package/out-tsc/vitest/src/client/store.d.ts.map +1 -1
  21. package/out-tsc/vitest/src/expect/matchers/toMatchImageSnapshot.d.ts +1 -1
  22. package/out-tsc/vitest/src/expect/matchers/toMatchImageSnapshot.d.ts.map +1 -1
  23. package/out-tsc/vitest/src/jsx/jsx-dev-runtime.d.ts +2 -1
  24. package/out-tsc/vitest/src/jsx/jsx-dev-runtime.d.ts.map +1 -1
  25. package/out-tsc/vitest/src/waitFor.d.ts.map +1 -1
  26. package/out-tsc/vitest/tsconfig.spec.tsbuildinfo +1 -1
  27. package/package.json +2 -2
  28. package/src/client/factory.ts +63 -74
  29. package/src/client/store.ts +8 -8
  30. package/src/expect/matchers/toMatchImageSnapshot.ts +9 -23
  31. package/src/initialize.ts +14 -5
  32. package/src/jsx/jsx-dev-runtime.ts +34 -15
  33. package/src/waitFor.ts +8 -6
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@react-native-harness/runtime",
3
3
  "description": "The core test runtime that executes on React Native devices, providing Jest-compatible APIs (describe, it, expect) and managing test collection, execution, and result reporting in native environments.",
4
- "version": "1.2.0-rc.1",
4
+ "version": "1.2.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -47,7 +47,7 @@
47
47
  "react-native-url-polyfill": "^3.0.0",
48
48
  "use-sync-external-store": "^1.6.0",
49
49
  "zustand": "^5.0.5",
50
- "@react-native-harness/bridge": "1.2.0-rc.1"
50
+ "@react-native-harness/bridge": "1.2.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@types/chai": "^5.2.2"
@@ -4,7 +4,7 @@ import type {
4
4
  BundlerEvents,
5
5
  TestExecutionOptions,
6
6
  } from '@react-native-harness/bridge';
7
- import { getBridgeClient } from '@react-native-harness/bridge/client';
7
+ import { connectToHarness, type HarnessHandle } from '@react-native-harness/bridge/client';
8
8
  import { store } from '../ui/state.js';
9
9
  import { getTestRunner, TestRunner } from '../runner/index.js';
10
10
  import { getTestCollector, TestCollector } from '../collector/index.js';
@@ -14,92 +14,81 @@ import { getBundler, evaluateModule, Bundler } from '../bundler/index.js';
14
14
  import { markTestsAsSkippedByName } from '../filtering/index.js';
15
15
  import { setup } from '../render/setup.js';
16
16
  import { runSetupFiles } from './setup-files.js';
17
- import { setClient } from './store.js';
17
+ import { setHandle } from './store.js';
18
18
 
19
- export const getClient = async () => {
20
- const client = await getBridgeClient(getWSServer(), {
21
- runTests: async () => {
22
- throw new Error('Not implemented');
23
- },
24
- });
25
-
26
- setClient(client);
27
-
28
- client.rpc.$functions.runTests = async (
29
- path: string,
30
- options: TestExecutionOptions
31
- ) => {
32
- if (store.getState().status === 'running') {
33
- throw new Error('Already running tests');
34
- }
35
-
36
- store.getState().setStatus('running');
19
+ export const getClient = async (): Promise<HarnessHandle> => {
20
+ const handle = await connectToHarness(getWSServer(), {
21
+ runTests: async (path: string, options: TestExecutionOptions) => {
22
+ if (store.getState().status === 'running') {
23
+ throw new Error('Already running tests');
24
+ }
37
25
 
38
- let collector: TestCollector | null = null;
39
- let runner: TestRunner | null = null;
40
- let events: EventEmitter<
41
- TestRunnerEvents | TestCollectorEvents | BundlerEvents
42
- > | null = null;
43
- let bundler: Bundler | null = null;
26
+ store.getState().setStatus('running');
44
27
 
45
- try {
46
- collector = getTestCollector();
47
- runner = getTestRunner();
48
- bundler = getBundler();
49
- events = combineEventEmitters(
50
- collector.events,
51
- runner.events,
52
- bundler.events
53
- );
28
+ let collector: TestCollector | null = null;
29
+ let runner: TestRunner | null = null;
30
+ let events: EventEmitter<
31
+ TestRunnerEvents | TestCollectorEvents | BundlerEvents
32
+ > | null = null;
33
+ let bundler: Bundler | null = null;
54
34
 
55
- events.addListener((event) => {
56
- client.rpc.emitEvent(event.type, event);
57
- });
35
+ try {
36
+ collector = getTestCollector();
37
+ runner = getTestRunner();
38
+ bundler = getBundler();
39
+ events = combineEventEmitters(
40
+ collector.events,
41
+ runner.events,
42
+ bundler.events,
43
+ );
58
44
 
59
- await runSetupFiles({
60
- setupFiles: options.setupFiles ?? [],
61
- setupFilesAfterEnv: [],
62
- events: events as EventEmitter<BundlerEvents>,
63
- bundler: bundler as Bundler,
64
- evaluateModule,
65
- });
45
+ events.addListener((event) => {
46
+ handle.emitEvent(event);
47
+ });
66
48
 
67
- const moduleJs = await bundler.getModule(path);
68
- const collectionResult = await collector.collect(async () => {
69
49
  await runSetupFiles({
70
- setupFiles: [],
71
- setupFilesAfterEnv: options.setupFilesAfterEnv ?? [],
50
+ setupFiles: options.setupFiles ?? [],
51
+ setupFilesAfterEnv: [],
72
52
  events: events as EventEmitter<BundlerEvents>,
73
53
  bundler: bundler as Bundler,
74
54
  evaluateModule,
75
55
  });
76
56
 
77
- // Setup automatic cleanup for rendered components
78
- setup();
79
- evaluateModule(moduleJs, path);
80
- }, path);
57
+ const moduleJs = await bundler.getModule(path);
58
+ const collectionResult = await collector.collect(async () => {
59
+ await runSetupFiles({
60
+ setupFiles: [],
61
+ setupFilesAfterEnv: options.setupFilesAfterEnv ?? [],
62
+ events: events as EventEmitter<BundlerEvents>,
63
+ bundler: bundler as Bundler,
64
+ evaluateModule,
65
+ });
66
+
67
+ setup();
68
+ evaluateModule(moduleJs, path);
69
+ }, path);
81
70
 
82
- // Apply test name pattern by marking non-matching tests as skipped
83
- const processedTestSuite = options.testNamePattern
84
- ? markTestsAsSkippedByName(
85
- collectionResult.testSuite,
86
- options.testNamePattern
87
- )
88
- : collectionResult.testSuite;
71
+ const processedTestSuite = options.testNamePattern
72
+ ? markTestsAsSkippedByName(
73
+ collectionResult.testSuite,
74
+ options.testNamePattern,
75
+ )
76
+ : collectionResult.testSuite;
89
77
 
90
- const result = await runner.run({
91
- testSuite: processedTestSuite,
92
- testFilePath: path,
93
- runner: options.runner,
94
- });
95
- return result;
96
- } finally {
97
- collector?.dispose();
98
- runner?.dispose();
99
- events?.clearAllListeners();
100
- store.getState().setStatus('idle');
101
- }
102
- };
78
+ return await runner.run({
79
+ testSuite: processedTestSuite,
80
+ testFilePath: path,
81
+ runner: options.runner,
82
+ });
83
+ } finally {
84
+ collector?.dispose();
85
+ runner?.dispose();
86
+ events?.clearAllListeners();
87
+ store.getState().setStatus('idle');
88
+ }
89
+ },
90
+ });
103
91
 
104
- return client;
92
+ setHandle(handle);
93
+ return handle;
105
94
  };
@@ -1,16 +1,16 @@
1
- import type { BridgeClient } from '@react-native-harness/bridge/client';
1
+ import type { HarnessHandle } from '@react-native-harness/bridge/client';
2
2
 
3
- let clientInstance: BridgeClient | null = null;
3
+ let handle: HarnessHandle | null = null;
4
4
 
5
- export const setClient = (client: BridgeClient): void => {
6
- clientInstance = client;
5
+ export const setHandle = (h: HarnessHandle): void => {
6
+ handle = h;
7
7
  };
8
8
 
9
- export const getClientInstance = (): BridgeClient => {
10
- if (!clientInstance) {
9
+ export const getHandle = (): HarnessHandle => {
10
+ if (!handle) {
11
11
  throw new Error(
12
- 'Bridge client not initialized. This should not happen in normal operation.'
12
+ 'Harness not connected. This should not happen in normal operation.'
13
13
  );
14
14
  }
15
- return clientInstance;
15
+ return handle;
16
16
  };
@@ -1,9 +1,6 @@
1
- import { getClientInstance } from '../../client/store.js';
1
+ import { getHandle } from '../../client/store.js';
2
2
  import type { MatcherState } from '@vitest/expect';
3
- import {
4
- type ImageSnapshotOptions,
5
- generateTransferId,
6
- } from '@react-native-harness/bridge';
3
+ import type { ImageSnapshotOptions } from '@react-native-harness/bridge';
7
4
  import { getHarnessContext } from '../../runner/index.js';
8
5
 
9
6
  type ScreenshotResult = {
@@ -17,30 +14,19 @@ export async function toMatchImageSnapshot(
17
14
  received: ScreenshotResult,
18
15
  options: ImageSnapshotOptions
19
16
  ): Promise<{ pass: boolean; message: () => string }> {
20
- const client = getClientInstance();
17
+ const handle = getHandle();
21
18
  const context = getHarnessContext();
22
19
 
23
- const transferId = generateTransferId();
24
- client.sendBinary(transferId, received.data);
20
+ const screenshotFile = await handle.transferScreenshot(received.data, {
21
+ width: received.width,
22
+ height: received.height,
23
+ });
25
24
 
26
- const screenshotFile = await client.rpc['device.screenshot.receive'](
27
- {
28
- type: 'binary',
29
- transferId,
30
- size: received.data.length,
31
- mimeType: 'image/png',
32
- },
33
- {
34
- width: received.width,
35
- height: received.height,
36
- }
37
- );
38
-
39
- const result = await client.rpc['test.matchImageSnapshot'](
25
+ const result = await handle.matchImageSnapshot(
40
26
  screenshotFile,
41
27
  context.testFilePath,
42
28
  options,
43
- context.runner
29
+ context.runner,
44
30
  );
45
31
 
46
32
  return {
package/src/initialize.ts CHANGED
@@ -3,10 +3,19 @@ import { getClient } from './client/index.js';
3
3
  import { disableHMRWhenReady } from './disableHMRWhenReady.js';
4
4
  import { setupJestMock } from './jest-mock.js';
5
5
 
6
- // Polyfill for EventTarget
6
+ // Polyfill for EventTarget on runtimes that don't ship one (RN's JSC).
7
+ // Do NOT overwrite when a native ctor already exists (RN Web / browsers):
8
+ // Safari's EventTarget.dispatchEvent() does an internal brand check and
9
+ // rejects polyfill instances with a TypeError, which breaks any
10
+ // DOM-event-driven flow in the page — most visibly DRM (FairPlay) via
11
+ // libraries that re-dispatch synthetic `encrypted` events.
7
12
  const Shim = require('event-target-shim');
8
- globalThis.Event = Shim.Event;
9
- globalThis.EventTarget = Shim.EventTarget;
13
+ if (typeof globalThis.Event !== 'function') {
14
+ globalThis.Event = Shim.Event;
15
+ }
16
+ if (typeof globalThis.EventTarget !== 'function') {
17
+ globalThis.EventTarget = Shim.EventTarget;
18
+ }
10
19
 
11
20
  // Setup jest mock to warn users about using Jest APIs
12
21
  setupJestMock();
@@ -25,10 +34,10 @@ setTimeout(() => {
25
34
  void (async () => {
26
35
  try {
27
36
  await disableHMRWhenReady(() => HMRClient.disable(), 50);
28
- const client = await getClient();
37
+ const handle = await getClient();
29
38
 
30
39
  const deviceDescriptor = getDeviceDescriptor();
31
- await client.rpc.reportReady(deviceDescriptor);
40
+ handle.reportReady(deviceDescriptor);
32
41
  } catch (error) {
33
42
  console.error('Failed to initialize React Native Harness', error);
34
43
  }
@@ -1,26 +1,45 @@
1
1
  import * as ReactJSXRuntimeDev from 'react/jsx-dev-runtime';
2
2
 
3
+ type NamedElementType = {
4
+ displayName?: string;
5
+ name?: string;
6
+ };
7
+
8
+ const isNamedElementType = (value: unknown): value is NamedElementType =>
9
+ (typeof value === 'function' ||
10
+ (typeof value === 'object' && value !== null)) &&
11
+ ('displayName' in value || 'name' in value);
12
+
13
+ const isPropsObject = (value: unknown): value is Record<string, unknown> =>
14
+ typeof value === 'object' && value !== null;
15
+
3
16
  export const Fragment = ReactJSXRuntimeDev.Fragment;
4
17
 
5
- export function jsxDEV(
6
- type: any,
7
- props: any,
8
- key: any,
9
- isStaticChildren: any,
10
- source: any,
11
- self: any
12
- ) {
13
- if (
14
- type &&
15
- (type.displayName === 'View' || type.name === 'View') &&
16
- props &&
18
+ export function jsxDEV(...args: Parameters<typeof ReactJSXRuntimeDev.jsxDEV>) {
19
+ const [type, props, key, isStaticChildren, source, self] = args;
20
+ const isViewType =
21
+ isNamedElementType(type) &&
22
+ (type.displayName === 'View' || type.name === 'View');
23
+ const nextProps =
24
+ isViewType &&
25
+ isPropsObject(props) &&
17
26
  props.collapsable === undefined
18
- ) {
19
- props = { ...props, collapsable: true };
27
+ ? { ...props, collapsable: true }
28
+ : props;
29
+
30
+ if (isViewType && isPropsObject(props) && props.collapsable === undefined) {
31
+ return ReactJSXRuntimeDev.jsxDEV(
32
+ type,
33
+ nextProps,
34
+ key,
35
+ isStaticChildren,
36
+ source,
37
+ self
38
+ );
20
39
  }
21
40
  return ReactJSXRuntimeDev.jsxDEV(
22
41
  type,
23
- props,
42
+ nextProps,
24
43
  key,
25
44
  isStaticChildren,
26
45
  source,
package/src/waitFor.ts CHANGED
@@ -114,6 +114,12 @@ export interface WaitUntilOptions
114
114
 
115
115
  type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T;
116
116
 
117
+ const isPromiseLike = <T>(value: T | PromiseLike<T>): value is PromiseLike<T> =>
118
+ value !== null &&
119
+ typeof value === 'object' &&
120
+ 'then' in value &&
121
+ typeof value.then === 'function';
122
+
117
123
  export function waitUntil<T>(
118
124
  callback: WaitUntilCallback<T>,
119
125
  options: number | WaitUntilOptions = {}
@@ -164,12 +170,8 @@ export function waitUntil<T>(
164
170
  }
165
171
  try {
166
172
  const result = callback();
167
- if (
168
- result !== null &&
169
- typeof result === 'object' &&
170
- typeof (result as any).then === 'function'
171
- ) {
172
- const thenable = result as PromiseLike<T>;
173
+ if (isPromiseLike(result)) {
174
+ const thenable = result;
173
175
  promiseStatus = 'pending';
174
176
  thenable.then(
175
177
  (resolvedValue) => {