@react-native-harness/bridge 1.6.0 → 1.7.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.
- package/dist/app-liveness.d.ts +33 -0
- package/dist/app-liveness.d.ts.map +1 -0
- package/dist/app-liveness.js +57 -0
- package/dist/client.d.ts +6 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +112 -105
- package/dist/connection-check.d.ts +22 -0
- package/dist/connection-check.d.ts.map +1 -0
- package/dist/connection-check.js +30 -0
- package/dist/errors.d.ts +6 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +12 -4
- package/dist/image-snapshot.d.ts +2 -2
- package/dist/image-snapshot.d.ts.map +1 -1
- package/dist/image-snapshot.js +21 -8
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/js-block-budget-policy.d.ts +31 -0
- package/dist/js-block-budget-policy.d.ts.map +1 -0
- package/dist/js-block-budget-policy.js +60 -0
- package/dist/js-watchdog.d.ts +31 -0
- package/dist/js-watchdog.d.ts.map +1 -0
- package/dist/js-watchdog.js +46 -0
- package/dist/protocol.d.ts +10 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +2 -2
- package/dist/rpc-peer.d.ts +3 -1
- package/dist/rpc-peer.d.ts.map +1 -1
- package/dist/rpc-peer.js +2 -2
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +71 -56
- package/dist/shared/test-runner.d.ts +11 -0
- package/dist/shared/test-runner.d.ts.map +1 -1
- package/dist/shared.d.ts +5 -1
- package/dist/shared.d.ts.map +1 -1
- package/package.json +4 -3
- package/src/app-liveness.ts +103 -0
- package/src/binary.ts +74 -0
- package/src/client.ts +172 -170
- package/src/connection-check.ts +60 -0
- package/src/errors.ts +16 -5
- package/src/image-snapshot.ts +31 -9
- package/src/index.ts +0 -2
- package/src/js-block-budget-policy.ts +90 -0
- package/src/js-watchdog.ts +78 -0
- package/src/link/README.md +43 -0
- package/src/link/channel.ts +473 -0
- package/src/link/envelope.ts +110 -0
- package/src/link/index.ts +321 -0
- package/src/link/ports.ts +42 -0
- package/src/link/scheduler.ts +132 -0
- package/src/link/standard-websocket.ts +251 -0
- package/src/link/streams.ts +34 -0
- package/src/protocol.ts +14 -9
- package/src/rpc-peer.ts +6 -3
- package/src/server.ts +99 -67
- package/src/shared/test-runner.ts +12 -0
- package/src/shared.ts +6 -1
- package/dist/binary-transfer.d.ts +0 -17
- package/dist/binary-transfer.d.ts.map +0 -1
- package/dist/binary-transfer.js +0 -60
- package/dist/events.d.ts +0 -45
- package/dist/events.d.ts.map +0 -1
- package/dist/events.js +0 -1
- package/dist/heartbeat.d.ts +0 -13
- package/dist/heartbeat.d.ts.map +0 -1
- package/dist/heartbeat.js +0 -49
- package/dist/platform-bridge.d.ts +0 -4
- package/dist/platform-bridge.d.ts.map +0 -1
- package/dist/platform-bridge.js +0 -25
- package/dist/transport.d.ts +0 -22
- package/dist/transport.d.ts.map +0 -1
- package/dist/transport.js +0 -22
- package/dist/websocket-client-transport.d.ts +0 -3
- package/dist/websocket-client-transport.d.ts.map +0 -1
- package/dist/websocket-client-transport.js +0 -45
- package/dist/websocket-server-transport.d.ts +0 -4
- package/dist/websocket-server-transport.d.ts.map +0 -1
- package/dist/websocket-server-transport.js +0 -43
- package/src/binary-transfer.ts +0 -79
- package/src/heartbeat.ts +0 -67
- package/src/transport.ts +0 -47
- package/src/websocket-client-transport.ts +0 -85
- package/src/websocket-server-transport.ts +0 -54
package/src/image-snapshot.ts
CHANGED
|
@@ -3,11 +3,16 @@ import { ssim } from 'ssim.js';
|
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { PNG } from 'pngjs';
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
FileReference,
|
|
8
|
+
ImageSnapshotOptions,
|
|
9
|
+
SnapshotUpdateState,
|
|
10
|
+
} from './shared.js';
|
|
7
11
|
|
|
8
12
|
type PixelmatchOptions = Parameters<typeof pixelmatch>[5];
|
|
9
13
|
|
|
10
14
|
const SNAPSHOT_DIR_NAME = '__image_snapshots__';
|
|
15
|
+
const DIFF_OUTPUT_DIR_NAME = '__diff_output__';
|
|
11
16
|
const DEFAULT_OPTIONS_FOR_PIXELMATCH: PixelmatchOptions = {
|
|
12
17
|
threshold: 0.1,
|
|
13
18
|
includeAA: false,
|
|
@@ -49,7 +54,8 @@ export const matchImageSnapshot = async (
|
|
|
49
54
|
screenshot: FileReference,
|
|
50
55
|
testFilePath: string,
|
|
51
56
|
options: ImageSnapshotOptions,
|
|
52
|
-
platformName: string
|
|
57
|
+
platformName: string,
|
|
58
|
+
updateSnapshot: SnapshotUpdateState
|
|
53
59
|
) => {
|
|
54
60
|
const pixelmatchOptions = {
|
|
55
61
|
...DEFAULT_OPTIONS_FOR_PIXELMATCH,
|
|
@@ -69,14 +75,28 @@ export const matchImageSnapshot = async (
|
|
|
69
75
|
const testDir = path.dirname(testFilePath);
|
|
70
76
|
const snapshotsDir = path.join(testDir, SNAPSHOT_DIR_NAME, platformName);
|
|
71
77
|
|
|
72
|
-
const
|
|
73
|
-
|
|
78
|
+
const snapshotPath = path.join(snapshotsDir, `${options.name}.png`);
|
|
79
|
+
// Failure artifacts live in their own folder so they can never overwrite
|
|
80
|
+
// a baseline of another snapshot (e.g. one named `<name>-diff`).
|
|
81
|
+
const diffOutputDir = path.join(snapshotsDir, DIFF_OUTPUT_DIR_NAME);
|
|
82
|
+
const failureImagePath = (image: 'actual' | 'diff'): string =>
|
|
83
|
+
path.join(diffOutputDir, `${options.name}-${image}.png`);
|
|
84
|
+
const actualPath = failureImagePath('actual');
|
|
74
85
|
|
|
75
86
|
await fs.mkdir(snapshotsDir, { recursive: true });
|
|
76
87
|
|
|
77
88
|
try {
|
|
78
89
|
await fs.access(snapshotPath);
|
|
79
90
|
} catch {
|
|
91
|
+
if (updateSnapshot === 'none') {
|
|
92
|
+
await fs.mkdir(diffOutputDir, { recursive: true });
|
|
93
|
+
await fs.writeFile(actualPath, receivedBuffer);
|
|
94
|
+
return {
|
|
95
|
+
pass: false,
|
|
96
|
+
message: `New snapshot was not written: ${snapshotPath} does not exist, and missing snapshots are not written in CI mode. Received image saved at ${actualPath}; commit it as ${snapshotPath}, or run without CI mode (--ci=false) on a machine with this device and commit the snapshot it writes.`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
// First time - create snapshot
|
|
81
101
|
await fs.writeFile(snapshotPath, receivedBuffer);
|
|
82
102
|
return {
|
|
@@ -95,9 +115,12 @@ export const matchImageSnapshot = async (
|
|
|
95
115
|
const diff = new PNG({ width, height });
|
|
96
116
|
|
|
97
117
|
if (img1.width !== img2.width || img1.height !== img2.height) {
|
|
118
|
+
await fs.mkdir(diffOutputDir, { recursive: true });
|
|
119
|
+
await fs.writeFile(actualPath, receivedBuffer);
|
|
120
|
+
|
|
98
121
|
return {
|
|
99
122
|
pass: false,
|
|
100
|
-
message: `Images have different dimensions. Received image width: ${img1.width}, height: ${img1.height}. Snapshot image width: ${img2.width}, height: ${img2.height}.`,
|
|
123
|
+
message: `Images have different dimensions. Received image width: ${img1.width}, height: ${img1.height}. Snapshot image width: ${img2.width}, height: ${img2.height}. Actual image saved at ${actualPath}.`,
|
|
101
124
|
};
|
|
102
125
|
}
|
|
103
126
|
|
|
@@ -152,12 +175,11 @@ export const matchImageSnapshot = async (
|
|
|
152
175
|
|
|
153
176
|
// Save diff and actual images when test fails
|
|
154
177
|
if (!pass) {
|
|
155
|
-
|
|
156
|
-
|
|
178
|
+
await fs.mkdir(diffOutputDir, { recursive: true });
|
|
179
|
+
|
|
180
|
+
const diffPath = failureImagePath('diff');
|
|
157
181
|
await fs.writeFile(diffPath, PNG.sync.write(diff));
|
|
158
182
|
|
|
159
|
-
const actualFileName = `${snapshotName.replace('.png', '')}-actual.png`;
|
|
160
|
-
const actualPath = path.join(snapshotsDir, actualFileName);
|
|
161
183
|
await fs.writeFile(actualPath, receivedBuffer);
|
|
162
184
|
|
|
163
185
|
message += ` Diff saved at ${diffPath}. Actual image saved at ${actualPath}.`;
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { JsBlockBudget } from './js-watchdog.js';
|
|
2
|
+
import type { BridgeEvents } from './shared.js';
|
|
3
|
+
|
|
4
|
+
export type JsBlockBudgetPolicyOptions = {
|
|
5
|
+
/** Budget outside any test, hook or file load. */
|
|
6
|
+
idleMs: number;
|
|
7
|
+
/** Budget while a setup file or test file loads (module code and `describe` bodies). */
|
|
8
|
+
fileLoadMs: number;
|
|
9
|
+
/** Added to a test's or hook's own timeout. */
|
|
10
|
+
graceMs: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const IDLE_BUDGET_LABEL = 'between test phases';
|
|
14
|
+
|
|
15
|
+
export const getIdleBudget = (
|
|
16
|
+
options: JsBlockBudgetPolicyOptions,
|
|
17
|
+
): JsBlockBudget => ({ ms: options.idleMs, label: IDLE_BUDGET_LABEL });
|
|
18
|
+
|
|
19
|
+
export type JsBlockBudgetPolicy = {
|
|
20
|
+
/**
|
|
21
|
+
* Returns the block budget that applies from this event on, or `null` for
|
|
22
|
+
* events that do not change it.
|
|
23
|
+
*/
|
|
24
|
+
getBudgetForEvent: (event: BridgeEvents) => JsBlockBudget | null;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Maps runner events to block budgets. Budgets come from timeouts the user
|
|
29
|
+
* already configures, so long work within them never fails a run:
|
|
30
|
+
*
|
|
31
|
+
* - loading a setup file or test file: `fileLoadMs`
|
|
32
|
+
* - a test, including its `beforeEach`/`afterEach` hooks: its timeout + grace
|
|
33
|
+
* - a suite's `beforeAll`/`afterAll` hooks: the hook timeout + grace, from
|
|
34
|
+
* `suite-started` until `suite-finished`, outside the suite's tests
|
|
35
|
+
* - anything else: `idleMs`
|
|
36
|
+
*/
|
|
37
|
+
export const createJsBlockBudgetPolicy = (
|
|
38
|
+
options: JsBlockBudgetPolicyOptions,
|
|
39
|
+
): JsBlockBudgetPolicy => {
|
|
40
|
+
const idle = getIdleBudget(options);
|
|
41
|
+
// Hook budgets of the suites currently running, innermost last.
|
|
42
|
+
const suites: JsBlockBudget[] = [];
|
|
43
|
+
const outsideTests = () => suites[suites.length - 1] ?? idle;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
getBudgetForEvent: (event) => {
|
|
47
|
+
switch (event.type) {
|
|
48
|
+
case 'setup-file-bundling-finished':
|
|
49
|
+
return {
|
|
50
|
+
ms: options.fileLoadMs,
|
|
51
|
+
label: `loading setup file ${event.file}`,
|
|
52
|
+
};
|
|
53
|
+
case 'collection-started':
|
|
54
|
+
return { ms: options.fileLoadMs, label: `loading ${event.file}` };
|
|
55
|
+
case 'setup-file-bundling-failed':
|
|
56
|
+
case 'collection-finished':
|
|
57
|
+
case 'file-started':
|
|
58
|
+
case 'file-finished':
|
|
59
|
+
suites.length = 0;
|
|
60
|
+
return idle;
|
|
61
|
+
case 'suite-started':
|
|
62
|
+
suites.push(
|
|
63
|
+
event.timeout === undefined
|
|
64
|
+
? outsideTests()
|
|
65
|
+
: {
|
|
66
|
+
ms: event.timeout + options.graceMs,
|
|
67
|
+
label: `beforeAll/afterAll hooks of suite "${event.name}" (${event.timeout}ms timeout + ${options.graceMs}ms grace)`,
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
return outsideTests();
|
|
71
|
+
case 'suite-finished':
|
|
72
|
+
suites.pop();
|
|
73
|
+
return outsideTests();
|
|
74
|
+
case 'test-started':
|
|
75
|
+
if (event.timeout === undefined) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
ms: event.timeout + options.graceMs,
|
|
81
|
+
label: `test "${event.fullName}" (${event.timeout}ms timeout + ${options.graceMs}ms grace)`,
|
|
82
|
+
};
|
|
83
|
+
case 'test-finished':
|
|
84
|
+
return outsideTests();
|
|
85
|
+
default:
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The longest the app's JS thread may go without answering a `js-probe`, and
|
|
3
|
+
* a label naming what it is busy with, used in the failure message.
|
|
4
|
+
*/
|
|
5
|
+
export type JsBlockBudget = {
|
|
6
|
+
ms: number;
|
|
7
|
+
label: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type JsWatchdog = {
|
|
11
|
+
notifyProbeAck: (id: number) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Replaces the current budget. The new budget covers silence from the
|
|
14
|
+
* later of the last acknowledged probe and the change itself: a block that
|
|
15
|
+
* fitted the previous budget is never judged against a smaller one.
|
|
16
|
+
*/
|
|
17
|
+
setBlockBudget: (budget: JsBlockBudget) => void;
|
|
18
|
+
dispose: () => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const DEFAULT_JS_PROBE_INTERVAL_MS = 1_000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Checks that the app's JS thread keeps getting a turn to run. The server
|
|
25
|
+
* sends `js-probe` messages, which only JS can answer, and fails once the
|
|
26
|
+
* thread has been silent for longer than the current block budget.
|
|
27
|
+
*/
|
|
28
|
+
export const createJsWatchdog = (options: {
|
|
29
|
+
sendProbe: (id: number) => void;
|
|
30
|
+
onUnresponsive: (budget: JsBlockBudget) => void;
|
|
31
|
+
initialBudget: JsBlockBudget;
|
|
32
|
+
probeIntervalMs?: number;
|
|
33
|
+
}): JsWatchdog => {
|
|
34
|
+
let budget = options.initialBudget;
|
|
35
|
+
let lastAckAt = Date.now();
|
|
36
|
+
let budgetSetAt = lastAckAt;
|
|
37
|
+
let nextProbeId = 1;
|
|
38
|
+
let pendingProbeId: number | null = null;
|
|
39
|
+
let disposed = false;
|
|
40
|
+
|
|
41
|
+
const dispose = () => {
|
|
42
|
+
if (disposed) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
disposed = true;
|
|
47
|
+
clearInterval(interval);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const interval = setInterval(() => {
|
|
51
|
+
if (Date.now() - Math.max(lastAckAt, budgetSetAt) > budget.ms) {
|
|
52
|
+
dispose();
|
|
53
|
+
options.onUnresponsive(budget);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (pendingProbeId === null) {
|
|
58
|
+
pendingProbeId = nextProbeId++;
|
|
59
|
+
options.sendProbe(pendingProbeId);
|
|
60
|
+
}
|
|
61
|
+
}, options.probeIntervalMs ?? DEFAULT_JS_PROBE_INTERVAL_MS);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
notifyProbeAck: (id) => {
|
|
65
|
+
if (id !== pendingProbeId) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pendingProbeId = null;
|
|
70
|
+
lastAckAt = Date.now();
|
|
71
|
+
},
|
|
72
|
+
setBlockBudget: (next) => {
|
|
73
|
+
budget = next;
|
|
74
|
+
budgetSetAt = Date.now();
|
|
75
|
+
},
|
|
76
|
+
dispose,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Link
|
|
2
|
+
|
|
3
|
+
A copy of the link from [simlock](https://github.com/callstackincubator/simlock)
|
|
4
|
+
(`src/link/`, commit `b730ef37536b51ebdb37d076e7592954c32d0904`), plus the
|
|
5
|
+
standard `WebSocket` transport from simlock's browser link check.
|
|
6
|
+
|
|
7
|
+
The link wraps one WebSocket and carries two kinds of traffic:
|
|
8
|
+
|
|
9
|
+
- **Text messages** (`write` / `onData`): the bridge's JSON RPC, events, and
|
|
10
|
+
heartbeat ping/pong. They go in the priority lane, so they are never stuck
|
|
11
|
+
behind a large transfer.
|
|
12
|
+
- **Byte channels** (`openChannel` / `acceptChannel`): raw bytes as
|
|
13
|
+
`ReadableStream` / `WritableStream`, in 64 KiB frames with per-channel credit.
|
|
14
|
+
The bridge uses them for screenshots (`../binary.ts`).
|
|
15
|
+
|
|
16
|
+
See simlock's `src/link/AGENTS.md` for the wire format, limits and rules.
|
|
17
|
+
|
|
18
|
+
## Harness changes
|
|
19
|
+
|
|
20
|
+
Keep the files close to upstream so they are easy to sync. Changes are marked
|
|
21
|
+
with `harness:` comments:
|
|
22
|
+
|
|
23
|
+
- `ports.ts` holds the port types the link imports from simlock's `src/ports`,
|
|
24
|
+
plus `globalClock`.
|
|
25
|
+
- `streams.ts` uses the global WHATWG streams when present and the
|
|
26
|
+
`web-streams-polyfill` ponyfill otherwise (React Native). It never installs
|
|
27
|
+
globals.
|
|
28
|
+
- `envelope.ts` encodes channel ids as ASCII instead of using
|
|
29
|
+
`TextEncoder`/`TextDecoder`.
|
|
30
|
+
- `standard-websocket.ts` treats a missing `bufferedAmount` as `0`, since React
|
|
31
|
+
Native's `WebSocket` never sets it.
|
|
32
|
+
- `__tests__/` holds upstream's link tests. The in-memory pair and `FakeClock`
|
|
33
|
+
come from simlock's ports, and the WebSocket pair uses `ws` with
|
|
34
|
+
`StandardWebSocketConnection` on both ends.
|
|
35
|
+
|
|
36
|
+
## React Native notes
|
|
37
|
+
|
|
38
|
+
- React Native's `WebSocket` base64-encodes binary frames on the way to the
|
|
39
|
+
native module. The protocol is binary, but stock React Native still pays for
|
|
40
|
+
that encoding inside the socket.
|
|
41
|
+
- With no `bufferedAmount`, the link cannot see the socket's send buffer, so
|
|
42
|
+
only per-channel credit (4 MiB) limits how much is in flight.
|
|
43
|
+
- There is no `crypto.randomUUID`, so the client passes `generateId`.
|