@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/dist/binary-transfer.d.ts +17 -0
- package/dist/binary-transfer.d.ts.map +1 -0
- package/dist/binary-transfer.js +60 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +9 -1
- package/dist/image-snapshot.d.ts.map +1 -1
- package/dist/image-snapshot.js +58 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/server.d.ts +5 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +49 -4
- package/dist/shared.d.ts +85 -2
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +8 -2
- package/src/binary-transfer.ts +79 -0
- package/src/client.ts +10 -1
- package/src/image-snapshot.ts +170 -0
- package/src/index.ts +1 -0
- package/src/server.ts +88 -12
- package/src/shared.ts +92 -2
- package/tsconfig.json +3 -0
- package/tsconfig.lib.json +3 -0
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(
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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