@react-native-harness/bridge 1.0.0-alpha.20 → 1.0.0-alpha.22

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.
package/src/server.ts CHANGED
@@ -2,23 +2,37 @@ import { WebSocketServer, type WebSocket } from 'ws';
2
2
  import { type BirpcGroup, createBirpcGroup } from 'birpc';
3
3
  import { logger } from '@react-native-harness/tools';
4
4
  import { EventEmitter } from 'node:events';
5
+ import fs from 'node:fs/promises';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { randomUUID } from 'node:crypto';
9
+ import { BinaryStore, parseBinaryFrame } from './binary-transfer.js';
5
10
  import type {
6
11
  BridgeServerFunctions,
7
12
  BridgeClientFunctions,
8
13
  DeviceDescriptor,
9
14
  BridgeEvents,
15
+ ImageSnapshotOptions,
16
+ HarnessContext,
17
+ BinaryDataReference,
18
+ FileReference,
10
19
  } from './shared.js';
11
20
  import { deserialize, serialize } from './serializer.js';
12
21
  import { DeviceNotRespondingError } from './errors.js';
22
+ import { matchImageSnapshot } from './image-snapshot.js';
23
+
24
+ export { DeviceNotRespondingError } from './errors.js';
13
25
 
14
26
  export type BridgeServerOptions = {
15
27
  port: number;
16
28
  timeout?: number;
29
+ context: HarnessContext;
17
30
  };
18
31
 
19
32
  export type BridgeServerEvents = {
20
33
  ready: (device: DeviceDescriptor) => void;
21
34
  event: (event: BridgeEvents) => void;
35
+ disconnect: () => void;
22
36
  };
23
37
 
24
38
  export type BridgeServer = {
@@ -42,6 +56,7 @@ export type BridgeServer = {
42
56
  export const getBridgeServer = async ({
43
57
  port,
44
58
  timeout,
59
+ context,
45
60
  }: BridgeServerOptions): Promise<BridgeServer> => {
46
61
  const wss = await new Promise<WebSocketServer>((resolve) => {
47
62
  const server = new WebSocketServer({ port, host: '0.0.0.0' }, () => {
@@ -50,19 +65,65 @@ export const getBridgeServer = async ({
50
65
  });
51
66
  const emitter = new EventEmitter();
52
67
  const clients = new Set<WebSocket>();
68
+ const binaryStore = new BinaryStore();
69
+
70
+ const baseFunctions: BridgeServerFunctions = {
71
+ reportReady: (device) => {
72
+ emitter.emit('ready', device);
73
+ },
74
+ emitEvent: (_, data) => {
75
+ emitter.emit('event', data);
76
+ },
77
+ 'device.screenshot.receive': async (
78
+ reference: BinaryDataReference,
79
+ metadata: { width: number; height: number }
80
+ ) => {
81
+ const data = binaryStore.get(reference.transferId);
82
+ if (!data) {
83
+ throw new Error(
84
+ `Binary data for transfer ${reference.transferId} not found or expired`
85
+ );
86
+ }
87
+
88
+ // Clean up from store
89
+ binaryStore.delete(reference.transferId);
90
+
91
+ // Write to temp file
92
+ const tempFile = path.join(
93
+ os.tmpdir(),
94
+ `harness-screenshot-${randomUUID()}.png`
95
+ );
96
+ await fs.writeFile(tempFile, data);
97
+
98
+ return {
99
+ path: tempFile,
100
+ width: metadata.width,
101
+ height: metadata.height,
102
+ };
103
+ },
104
+ 'test.matchImageSnapshot': async (
105
+ screenshot: FileReference,
106
+ testPath: string,
107
+ options: ImageSnapshotOptions
108
+ ) => {
109
+ return await matchImageSnapshot(
110
+ screenshot,
111
+ testPath,
112
+ options,
113
+ context.platform.name
114
+ );
115
+ },
116
+ };
53
117
 
54
118
  const group = createBirpcGroup<BridgeClientFunctions, BridgeServerFunctions>(
55
- {
56
- reportReady: (device) => {
57
- emitter.emit('ready', device);
58
- },
59
- emitEvent: (_, data) => {
60
- emitter.emit('event', data);
61
- },
62
- } satisfies BridgeServerFunctions,
119
+ baseFunctions,
63
120
  [],
64
121
  {
65
122
  timeout,
123
+ onFunctionError: (error, functionName, args) => {
124
+ console.error('Function error', error, functionName, args);
125
+ throw error;
126
+ },
66
127
  onTimeoutError(functionName, args) {
67
128
  throw new DeviceNotRespondingError(functionName, args);
68
129
  },
@@ -76,16 +137,30 @@ export const getBridgeServer = async ({
76
137
 
77
138
  // TODO: Remove channel when connection is closed.
78
139
  clients.delete(ws);
140
+ emitter.emit('disconnect');
79
141
  });
80
142
 
81
143
  group.updateChannels((channels) => {
82
144
  channels.push({
83
145
  post: (data) => ws.send(data),
84
146
  on: (handler) => {
85
- ws.on('message', (event: Buffer | ArrayBuffer | Buffer[]) => {
86
- const message = event.toString();
87
- handler(message);
88
- });
147
+ ws.on(
148
+ 'message',
149
+ (event: Buffer | ArrayBuffer | Buffer[], isBinary: boolean) => {
150
+ if (isBinary) {
151
+ const uint8Array = new Uint8Array(event as any);
152
+ try {
153
+ const { transferId, data } = parseBinaryFrame(uint8Array);
154
+ binaryStore.add(transferId, data);
155
+ return;
156
+ } catch (error) {
157
+ logger.warn('Failed to parse binary frame', error);
158
+ }
159
+ }
160
+ const message = event.toString();
161
+ handler(message);
162
+ }
163
+ );
89
164
  },
90
165
  serialize,
91
166
  deserialize,
@@ -96,6 +171,7 @@ export const getBridgeServer = async ({
96
171
  const dispose = () => {
97
172
  wss.close();
98
173
  emitter.removeAllListeners();
174
+ binaryStore.dispose();
99
175
  };
100
176
 
101
177
  return {
package/src/shared.ts CHANGED
@@ -4,6 +4,73 @@ import type {
4
4
  } from './shared/test-runner.js';
5
5
  import type { TestCollectorEvents } from './shared/test-collector.js';
6
6
  import type { BundlerEvents } from './shared/bundler.js';
7
+ import type { HarnessPlatform } from '@react-native-harness/platforms';
8
+
9
+ export type FileReference = {
10
+ path: string;
11
+ };
12
+
13
+ export type ImageSnapshotOptions = {
14
+ /**
15
+ * The name of the snapshot. This is required and must be unique within the test.
16
+ */
17
+ name: string;
18
+ /**
19
+ * Comparison algorithm to use.
20
+ * @default 'pixelmatch'
21
+ */
22
+ comparisonMethod?: 'pixelmatch' | 'ssim';
23
+ /**
24
+ * Matching threshold for pixelmatch, ranges from 0 to 1. Smaller values make the comparison more sensitive.
25
+ * @default 0.1
26
+ */
27
+ threshold?: number;
28
+ /**
29
+ * Threshold for test failure.
30
+ */
31
+ failureThreshold?: number;
32
+ /**
33
+ * Type of failure threshold.
34
+ * @default 'pixel'
35
+ */
36
+ failureThresholdType?: 'pixel' | 'percent';
37
+ /**
38
+ * Minimum similarity score for SSIM comparison (0-1).
39
+ * @default 0.95
40
+ */
41
+ ssimThreshold?: number;
42
+ /**
43
+ * Regions to ignore during comparison.
44
+ */
45
+ ignoreRegions?: Array<{
46
+ x: number;
47
+ y: number;
48
+ width: number;
49
+ height: number;
50
+ }>;
51
+ /**
52
+ * If true, disables detecting and ignoring anti-aliased pixels.
53
+ * @default false
54
+ */
55
+ includeAA?: boolean;
56
+ /**
57
+ * Blending factor of unchanged pixels in the diff output.
58
+ * Ranges from 0 for pure white to 1 for original brightness
59
+ * @default 0.1
60
+ */
61
+ alpha?: number;
62
+ /**
63
+ * The color of differing pixels in the diff output.
64
+ * @default [255, 0, 0]
65
+ */
66
+ diffColor?: [number, number, number];
67
+ /**
68
+ * An alternative color to use for dark on light differences to differentiate between "added" and "removed" parts.
69
+ * If not provided, all differing pixels use the color specified by `diffColor`.
70
+ * @default null
71
+ */
72
+ diffColorAlt?: [number, number, number];
73
+ };
7
74
 
8
75
  export type {
9
76
  TestCollectorEvents,
@@ -36,7 +103,6 @@ export type {
36
103
  SetupFileBundlingFailedEvent,
37
104
  BundlerEvents,
38
105
  } from './shared/bundler.js';
39
- export { DeviceNotRespondingError } from './errors.js';
40
106
 
41
107
  export type DeviceDescriptor = {
42
108
  platform: 'ios' | 'android' | 'vega';
@@ -60,19 +126,43 @@ export type TestExecutionOptions = {
60
126
  testNamePattern?: string;
61
127
  setupFiles?: string[];
62
128
  setupFilesAfterEnv?: string[];
129
+ runner: string;
63
130
  };
64
131
 
65
132
  export type BridgeClientFunctions = {
66
133
  runTests: (
67
134
  path: string,
68
- options?: TestExecutionOptions
135
+ options: TestExecutionOptions
69
136
  ) => Promise<TestSuiteResult>;
70
137
  };
71
138
 
139
+ export type BinaryDataReference = {
140
+ type: 'binary';
141
+ transferId: number;
142
+ size: number;
143
+ mimeType: 'image/png';
144
+ };
145
+
146
+ export type ScreenshotData = BinaryDataReference;
147
+
72
148
  export type BridgeServerFunctions = {
73
149
  reportReady: (device: DeviceDescriptor) => void;
74
150
  emitEvent: <TEvent extends BridgeEvents>(
75
151
  event: TEvent['type'],
76
152
  data: TEvent
77
153
  ) => void;
154
+ 'device.screenshot.receive': (
155
+ reference: BinaryDataReference,
156
+ metadata: { width: number; height: number }
157
+ ) => Promise<FileReference>;
158
+ 'test.matchImageSnapshot': (
159
+ screenshot: FileReference,
160
+ testPath: string,
161
+ options: ImageSnapshotOptions,
162
+ runner: string
163
+ ) => Promise<{ pass: boolean; message: string }>;
164
+ };
165
+
166
+ export type HarnessContext = {
167
+ platform: HarnessPlatform;
78
168
  };
package/tsconfig.json CHANGED
@@ -6,6 +6,9 @@
6
6
  {
7
7
  "path": "../tools"
8
8
  },
9
+ {
10
+ "path": "../platforms"
11
+ },
9
12
  {
10
13
  "path": "./tsconfig.lib.json"
11
14
  }
package/tsconfig.lib.json CHANGED
@@ -14,6 +14,9 @@
14
14
  "references": [
15
15
  {
16
16
  "path": "../tools/tsconfig.lib.json"
17
+ },
18
+ {
19
+ "path": "../platforms/tsconfig.lib.json"
17
20
  }
18
21
  ]
19
22
  }