@react-native-harness/bridge 1.5.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/binary.d.ts +16 -0
- package/dist/binary.d.ts.map +1 -0
- package/dist/binary.js +47 -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/link/channel.d.ts +45 -0
- package/dist/link/channel.d.ts.map +1 -0
- package/dist/link/channel.js +414 -0
- package/dist/link/envelope.d.ts +38 -0
- package/dist/link/envelope.d.ts.map +1 -0
- package/dist/link/envelope.js +90 -0
- package/dist/link/index.d.ts +22 -0
- package/dist/link/index.d.ts.map +1 -0
- package/dist/link/index.js +271 -0
- package/dist/link/ports.d.ts +31 -0
- package/dist/link/ports.d.ts.map +1 -0
- package/dist/link/ports.js +7 -0
- package/dist/link/scheduler.d.ts +21 -0
- package/dist/link/scheduler.d.ts.map +1 -0
- package/dist/link/scheduler.js +116 -0
- package/dist/link/standard-websocket.d.ts +36 -0
- package/dist/link/standard-websocket.d.ts.map +1 -0
- package/dist/link/standard-websocket.js +214 -0
- package/dist/link/streams.d.ts +4 -0
- package/dist/link/streams.d.ts.map +1 -0
- package/dist/link/streams.js +15 -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/heartbeat.d.ts +0 -13
- package/dist/heartbeat.d.ts.map +0 -1
- package/dist/heartbeat.js +0 -49
- 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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ConnectionCheckOptions, type PingableSocket } from './connection-check.js';
|
|
2
|
+
import { AppBridgeDisconnectedError } from './errors.js';
|
|
3
|
+
import { type JsBlockBudgetPolicyOptions } from './js-block-budget-policy.js';
|
|
4
|
+
import type { BridgeEvents } from './shared.js';
|
|
5
|
+
export type AppLivenessOptions = {
|
|
6
|
+
connectionCheck: ConnectionCheckOptions;
|
|
7
|
+
jsBlockBudget: JsBlockBudgetPolicyOptions;
|
|
8
|
+
jsProbeIntervalMs?: number;
|
|
9
|
+
};
|
|
10
|
+
export type AppLivenessOverrides = {
|
|
11
|
+
connectionCheck?: Partial<ConnectionCheckOptions>;
|
|
12
|
+
jsBlockBudget?: Partial<JsBlockBudgetPolicyOptions>;
|
|
13
|
+
jsProbeIntervalMs?: number;
|
|
14
|
+
};
|
|
15
|
+
export declare const DEFAULT_APP_LIVENESS_OPTIONS: AppLivenessOptions;
|
|
16
|
+
export declare const resolveAppLivenessOptions: (overrides?: AppLivenessOverrides) => AppLivenessOptions;
|
|
17
|
+
export type AppLiveness = {
|
|
18
|
+
handleEvent: (event: BridgeEvents) => void;
|
|
19
|
+
handleProbeAck: (id: number) => void;
|
|
20
|
+
dispose: () => void;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Watches one app connection with two independent checks: the connection
|
|
24
|
+
* check (is the process reachable?) and the JS watchdog (is the JS thread
|
|
25
|
+
* getting a turn?). See docs/adr/0001-app-liveness-detection.md.
|
|
26
|
+
*/
|
|
27
|
+
export declare const createAppLiveness: (options: {
|
|
28
|
+
socket: PingableSocket;
|
|
29
|
+
sendProbe: (id: number) => void;
|
|
30
|
+
onLost: (error: AppBridgeDisconnectedError) => void;
|
|
31
|
+
config: AppLivenessOptions;
|
|
32
|
+
}) => AppLiveness;
|
|
33
|
+
//# sourceMappingURL=app-liveness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-liveness.d.ts","sourceRoot":"","sources":["../src/app-liveness.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAGL,KAAK,0BAA0B,EAChC,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,eAAe,EAAE,sBAAsB,CAAC;IACxC,aAAa,EAAE,0BAA0B,CAAC;IAC1C,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,eAAe,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAClD,aAAa,CAAC,EAAE,OAAO,CAAC,0BAA0B,CAAC,CAAC;IACpD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,eAAO,MAAM,4BAA4B,EAAE,kBAG1C,CAAC;AASF,eAAO,MAAM,yBAAyB,GACpC,YAAW,oBAAyB,KACnC,kBAUD,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG;IACxB,WAAW,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS;IACzC,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,0BAA0B,KAAK,IAAI,CAAC;IACpD,MAAM,EAAE,kBAAkB,CAAC;CAC5B,KAAG,WAkCH,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createConnectionCheck, } from './connection-check.js';
|
|
2
|
+
import { AppBridgeDisconnectedError } from './errors.js';
|
|
3
|
+
import { createJsBlockBudgetPolicy, getIdleBudget, } from './js-block-budget-policy.js';
|
|
4
|
+
import { createJsWatchdog } from './js-watchdog.js';
|
|
5
|
+
export const DEFAULT_APP_LIVENESS_OPTIONS = {
|
|
6
|
+
connectionCheck: { intervalMs: 5_000, timeoutMs: 20_000 },
|
|
7
|
+
jsBlockBudget: { idleMs: 20_000, fileLoadMs: 60_000, graceMs: 5_000 },
|
|
8
|
+
};
|
|
9
|
+
// An override left `undefined` keeps the default instead of replacing it,
|
|
10
|
+
// which would turn a budget into `undefined` and disable it.
|
|
11
|
+
const definedOnly = (values = {}) => Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined));
|
|
12
|
+
export const resolveAppLivenessOptions = (overrides = {}) => ({
|
|
13
|
+
connectionCheck: {
|
|
14
|
+
...DEFAULT_APP_LIVENESS_OPTIONS.connectionCheck,
|
|
15
|
+
...definedOnly(overrides.connectionCheck),
|
|
16
|
+
},
|
|
17
|
+
jsBlockBudget: {
|
|
18
|
+
...DEFAULT_APP_LIVENESS_OPTIONS.jsBlockBudget,
|
|
19
|
+
...definedOnly(overrides.jsBlockBudget),
|
|
20
|
+
},
|
|
21
|
+
jsProbeIntervalMs: overrides.jsProbeIntervalMs,
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Watches one app connection with two independent checks: the connection
|
|
25
|
+
* check (is the process reachable?) and the JS watchdog (is the JS thread
|
|
26
|
+
* getting a turn?). See docs/adr/0001-app-liveness-detection.md.
|
|
27
|
+
*/
|
|
28
|
+
export const createAppLiveness = (options) => {
|
|
29
|
+
const connectionCheck = createConnectionCheck(options.socket, {
|
|
30
|
+
...options.config.connectionCheck,
|
|
31
|
+
onLost: () => {
|
|
32
|
+
options.onLost(new AppBridgeDisconnectedError('connection-lost'));
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
const policy = createJsBlockBudgetPolicy(options.config.jsBlockBudget);
|
|
36
|
+
const watchdog = createJsWatchdog({
|
|
37
|
+
sendProbe: options.sendProbe,
|
|
38
|
+
initialBudget: getIdleBudget(options.config.jsBlockBudget),
|
|
39
|
+
probeIntervalMs: options.config.jsProbeIntervalMs,
|
|
40
|
+
onUnresponsive: (budget) => {
|
|
41
|
+
options.onLost(new AppBridgeDisconnectedError('js-thread-unresponsive', budget.label));
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
handleEvent: (event) => {
|
|
46
|
+
const budget = policy.getBudgetForEvent(event);
|
|
47
|
+
if (budget) {
|
|
48
|
+
watchdog.setBlockBudget(budget);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
handleProbeAck: watchdog.notifyProbeAck,
|
|
52
|
+
dispose: () => {
|
|
53
|
+
connectionCheck.dispose();
|
|
54
|
+
watchdog.dispose();
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
};
|
package/dist/binary.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LinkConnection } from './link/index.js';
|
|
2
|
+
export type OutgoingBinary = {
|
|
3
|
+
channelId: string;
|
|
4
|
+
/** Settles once every byte is handed to the link, or rejects if the channel fails. */
|
|
5
|
+
written: Promise<void>;
|
|
6
|
+
abort: (reason?: string) => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Opens a link channel and starts writing `data` into it. The channel id travels in an
|
|
10
|
+
* RPC message; the peer reads the bytes with `receiveBinary`. The link sends the channel's
|
|
11
|
+
* OPEN before any later message, so the peer always knows the id by the time it asks.
|
|
12
|
+
*/
|
|
13
|
+
export declare const sendBinary: (link: LinkConnection, data: Uint8Array) => Promise<OutgoingBinary>;
|
|
14
|
+
/** Accepts the channel the peer opened with `sendBinary` and reads it to the end. */
|
|
15
|
+
export declare const receiveBinary: (link: LinkConnection, channelId: string) => Promise<Uint8Array>;
|
|
16
|
+
//# sourceMappingURL=binary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binary.d.ts","sourceRoot":"","sources":["../src/binary.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEnE,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,UAAU,GACrB,MAAM,cAAc,EACpB,MAAM,UAAU,KACf,OAAO,CAAC,cAAc,CAgBxB,CAAC;AA6BF,qFAAqF;AACrF,eAAO,MAAM,aAAa,GACxB,MAAM,cAAc,EACpB,WAAW,MAAM,KAChB,OAAO,CAAC,UAAU,CAOpB,CAAC"}
|
package/dist/binary.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opens a link channel and starts writing `data` into it. The channel id travels in an
|
|
3
|
+
* RPC message; the peer reads the bytes with `receiveBinary`. The link sends the channel's
|
|
4
|
+
* OPEN before any later message, so the peer always knows the id by the time it asks.
|
|
5
|
+
*/
|
|
6
|
+
export const sendBinary = async (link, data) => {
|
|
7
|
+
const channel = await link.openChannel({
|
|
8
|
+
signal: new AbortController().signal,
|
|
9
|
+
});
|
|
10
|
+
const written = (async () => {
|
|
11
|
+
const writer = channel.writable.getWriter();
|
|
12
|
+
await writer.write(data);
|
|
13
|
+
await writer.close();
|
|
14
|
+
})();
|
|
15
|
+
return {
|
|
16
|
+
channelId: channel.id,
|
|
17
|
+
written,
|
|
18
|
+
abort: (reason) => channel.abort(reason),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
const readAll = async (channel) => {
|
|
22
|
+
const reader = channel.readable.getReader();
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let size = 0;
|
|
25
|
+
for (;;) {
|
|
26
|
+
const { done, value } = await reader.read();
|
|
27
|
+
if (done) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
chunks.push(value);
|
|
31
|
+
size += value.length;
|
|
32
|
+
}
|
|
33
|
+
const bytes = new Uint8Array(size);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
for (const chunk of chunks) {
|
|
36
|
+
bytes.set(chunk, offset);
|
|
37
|
+
offset += chunk.length;
|
|
38
|
+
}
|
|
39
|
+
return bytes;
|
|
40
|
+
};
|
|
41
|
+
/** Accepts the channel the peer opened with `sendBinary` and reads it to the end. */
|
|
42
|
+
export const receiveBinary = async (link, channelId) => {
|
|
43
|
+
const channel = link.acceptChannel(channelId);
|
|
44
|
+
// Nothing flows back to the sender; ending this direction lets the channel free its slot.
|
|
45
|
+
void channel.writable.close().catch(() => undefined);
|
|
46
|
+
return readAll(channel);
|
|
47
|
+
};
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { createWebSocketClientTransport } from './websocket-client-transport.js';
|
|
1
|
+
import { type StandardWebSocket, type UplinkTransportConnection } from './link/index.js';
|
|
3
2
|
import type { DeviceDescriptor, BridgeEvents, FileReference, ImageSnapshotOptions, TestExecutionOptions, TestSuiteResult } from './shared.js';
|
|
4
3
|
export type HarnessCallbacks = {
|
|
5
4
|
runTests: (path: string, options: TestExecutionOptions) => Promise<TestSuiteResult>;
|
|
@@ -19,8 +18,11 @@ export type HarnessHandle = {
|
|
|
19
18
|
disconnect: () => void;
|
|
20
19
|
};
|
|
21
20
|
export type ConnectToHarnessOptions = {
|
|
22
|
-
transport
|
|
21
|
+
/** An already open link transport, used instead of dialing `url`. */
|
|
22
|
+
transport?: UplinkTransportConnection;
|
|
23
|
+
/** Creates the WebSocket to dial `url` with. Defaults to the global `WebSocket`. */
|
|
24
|
+
createSocket?: (url: string) => StandardWebSocket;
|
|
23
25
|
};
|
|
24
|
-
export {
|
|
26
|
+
export type { UplinkTransportConnection };
|
|
25
27
|
export declare const connectToHarness: (url: string, callbacks: HarnessCallbacks, options?: ConnectToHarnessOptions) => Promise<HarnessHandle>;
|
|
26
28
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,EAML,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC/B,MAAM,iBAAiB,CAAC;AAGzB,OAAO,KAAK,EAGV,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EAChB,MAAM,aAAa,CAAC;AAErB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACpF,gBAAgB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,WAAW,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAChD,SAAS,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,kBAAkB,EAAE,CAClB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KACxC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC5B,kBAAkB,EAAE,CAClB,UAAU,EAAE,aAAa,EACzB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,oBAAoB,EAC7B,MAAM,EAAE,MAAM,KACX,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,qEAAqE;IACrE,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC,oFAAoF;IACpF,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,iBAAiB,CAAC;CACnD,CAAC;AAEF,YAAY,EAAE,yBAAyB,EAAE,CAAC;AA2C1C,eAAO,MAAM,gBAAgB,GAC3B,KAAK,MAAM,EACX,WAAW,gBAAgB,EAC3B,UAAS,uBAA4B,KACpC,OAAO,CAAC,aAAa,CA6HvB,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -1,122 +1,129 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sendBinary } from './binary.js';
|
|
2
|
+
import { connectStandardWebSocket, createLink, globalClock, } from './link/index.js';
|
|
2
3
|
import { serializeBridgeMessage } from './protocol.js';
|
|
3
4
|
import { createRpcPeer } from './rpc-peer.js';
|
|
4
|
-
import { createRpcTransport, } from './transport.js';
|
|
5
|
-
import { createWebSocketClientTransport } from './websocket-client-transport.js';
|
|
6
|
-
export { createWebSocketClientTransport };
|
|
7
5
|
const noop = () => undefined;
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
6
|
+
const createConsoleLogger = () => {
|
|
7
|
+
const logger = {
|
|
8
|
+
debug: noop,
|
|
9
|
+
info: noop,
|
|
10
|
+
warn: (message) => console.warn(`[harness] ${message}`),
|
|
11
|
+
error: (message) => console.error(`[harness] ${message}`),
|
|
12
|
+
child: () => logger,
|
|
13
|
+
};
|
|
14
|
+
return logger;
|
|
15
|
+
};
|
|
16
|
+
// React Native has no `crypto.randomUUID`, so channel ids come from a counter. Ids only
|
|
17
|
+
// need to be unique among this connection's channels.
|
|
18
|
+
const createChannelIdGenerator = () => {
|
|
19
|
+
let nextId = 1;
|
|
20
|
+
return () => `app-${nextId++}`;
|
|
21
|
+
};
|
|
22
|
+
const dial = async (url, options) => {
|
|
23
|
+
if (options.transport) {
|
|
24
|
+
return options.transport;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return await connectStandardWebSocket({
|
|
28
|
+
url,
|
|
29
|
+
createSocket: options.createSocket,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const detail = error instanceof Error && error.message ? `: ${error.message}` : '';
|
|
34
|
+
throw new Error(`Failed to connect to Harness at ${url}${detail}`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export const connectToHarness = async (url, callbacks, options = {}) => {
|
|
38
|
+
const transport = await dial(url, options);
|
|
39
|
+
const link = createLink(transport, {
|
|
40
|
+
end: 'worker',
|
|
41
|
+
clock: globalClock,
|
|
42
|
+
logger: createConsoleLogger(),
|
|
43
|
+
generateId: createChannelIdGenerator(),
|
|
44
|
+
});
|
|
45
|
+
const rpc = createRpcPeer({
|
|
46
|
+
localMethods: callbacks,
|
|
47
|
+
transport: {
|
|
48
|
+
send: (message) => {
|
|
49
|
+
void link.write(message);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
11
53
|
let peerClosed = false;
|
|
12
|
-
let
|
|
13
|
-
let offError = noop;
|
|
54
|
+
let offMessage = noop;
|
|
14
55
|
let offClose = noop;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
offClose();
|
|
56
|
+
let offError = noop;
|
|
57
|
+
const getLinkClosedError = () => {
|
|
58
|
+
return new Error('Harness bridge link is closed');
|
|
19
59
|
};
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
60
|
+
const closePeer = (reason) => {
|
|
61
|
+
if (peerClosed) {
|
|
22
62
|
return;
|
|
23
63
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
64
|
+
peerClosed = true;
|
|
65
|
+
offMessage();
|
|
66
|
+
offClose();
|
|
67
|
+
offError();
|
|
68
|
+
rpc.close(reason);
|
|
27
69
|
};
|
|
28
|
-
const
|
|
29
|
-
|
|
70
|
+
const handleMessage = async (data) => {
|
|
71
|
+
const controlMessage = await rpc.handleMessage(data);
|
|
72
|
+
if (!controlMessage) {
|
|
30
73
|
return;
|
|
31
74
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
75
|
+
if (controlMessage.type === 'js-probe') {
|
|
76
|
+
void link.write(serializeBridgeMessage({ type: 'js-probe-ack', id: controlMessage.id }));
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
offMessage = link.onData((message) => {
|
|
80
|
+
void handleMessage(message).catch((error) => {
|
|
81
|
+
closePeer(error instanceof Error
|
|
82
|
+
? error
|
|
83
|
+
: new Error('Received invalid Harness bridge message'));
|
|
84
|
+
void link.close();
|
|
40
85
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
86
|
+
});
|
|
87
|
+
offError = link.onError((error) => {
|
|
88
|
+
closePeer(error);
|
|
89
|
+
});
|
|
90
|
+
offClose = link.onClose(() => {
|
|
91
|
+
closePeer(new Error('Harness connection closed'));
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
reportReady: (device) => {
|
|
95
|
+
if (link.closed) {
|
|
96
|
+
closePeer(getLinkClosedError());
|
|
46
97
|
return;
|
|
47
98
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
99
|
+
void link.write(serializeBridgeMessage({ type: 'ready', device }));
|
|
100
|
+
},
|
|
101
|
+
emitEvent: (event) => {
|
|
102
|
+
rpc.sendEvent(event);
|
|
103
|
+
},
|
|
104
|
+
transferScreenshot: async (data, metadata) => {
|
|
105
|
+
const binary = await sendBinary(link, data);
|
|
106
|
+
try {
|
|
107
|
+
const [file] = await Promise.all([
|
|
108
|
+
rpc.invoke('device.screenshot.receive', {
|
|
109
|
+
type: 'binary',
|
|
110
|
+
channelId: binary.channelId,
|
|
111
|
+
size: data.length,
|
|
112
|
+
mimeType: 'image/png',
|
|
113
|
+
}, metadata),
|
|
114
|
+
binary.written,
|
|
115
|
+
]);
|
|
116
|
+
return file;
|
|
61
117
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return;
|
|
118
|
+
catch (error) {
|
|
119
|
+
binary.abort();
|
|
120
|
+
throw error;
|
|
66
121
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
offRuntimeClose = transport.onClose((event) => {
|
|
77
|
-
closePeer(new Error(`Harness connection closed (code ${event.code}${event.reason ? `, reason: ${event.reason}` : ''})`));
|
|
78
|
-
});
|
|
79
|
-
offRuntimeError = transport.onError((error) => {
|
|
80
|
-
closePeer(error);
|
|
81
|
-
});
|
|
82
|
-
resolve({
|
|
83
|
-
reportReady: (device) => {
|
|
84
|
-
try {
|
|
85
|
-
if (transport.state !== 'open') {
|
|
86
|
-
throw getTransportNotOpenError();
|
|
87
|
-
}
|
|
88
|
-
transport.send(serializeBridgeMessage({ type: 'ready', device }));
|
|
89
|
-
}
|
|
90
|
-
catch (error) {
|
|
91
|
-
closePeer(error instanceof Error ? error : getTransportNotOpenError());
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
emitEvent: (event) => {
|
|
95
|
-
rpc.sendEvent(event);
|
|
96
|
-
},
|
|
97
|
-
transferScreenshot: async (data, metadata) => {
|
|
98
|
-
const transferId = generateTransferId();
|
|
99
|
-
transport.send(createBinaryFrame(transferId, data));
|
|
100
|
-
return rpc.invoke('device.screenshot.receive', { type: 'binary', transferId, size: data.length, mimeType: 'image/png' }, metadata);
|
|
101
|
-
},
|
|
102
|
-
matchImageSnapshot: (screenshot, testPath, options, runner) => rpc.invoke('test.matchImageSnapshot', screenshot, testPath, options, runner),
|
|
103
|
-
disconnect: () => {
|
|
104
|
-
closePeer(new Error('Harness connection closed by client'));
|
|
105
|
-
transport.close();
|
|
106
|
-
},
|
|
107
|
-
});
|
|
122
|
+
},
|
|
123
|
+
matchImageSnapshot: (screenshot, testPath, options, runner) => rpc.invoke('test.matchImageSnapshot', screenshot, testPath, options, runner),
|
|
124
|
+
disconnect: () => {
|
|
125
|
+
closePeer(new Error('Harness connection closed by client'));
|
|
126
|
+
void link.close();
|
|
127
|
+
},
|
|
108
128
|
};
|
|
109
|
-
|
|
110
|
-
const detail = error.message ? `: ${error.message}` : '';
|
|
111
|
-
fail(`Failed to connect to Harness at ${url}${detail}`);
|
|
112
|
-
};
|
|
113
|
-
const handleClose = (event) => {
|
|
114
|
-
fail(`Harness connection at ${url} closed before becoming ready (code ${event.code}${event.reason ? `, reason: ${event.reason}` : ''})`);
|
|
115
|
-
};
|
|
116
|
-
offOpen = transport.onOpen(handleOpen);
|
|
117
|
-
offError = transport.onError(handleError);
|
|
118
|
-
offClose = transport.onClose(handleClose);
|
|
119
|
-
if (transport.state === 'open') {
|
|
120
|
-
handleOpen();
|
|
121
|
-
}
|
|
122
|
-
});
|
|
129
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks that the app process is still reachable, using WebSocket protocol
|
|
3
|
+
* ping frames (RFC 6455). The app's native WebSocket answers them without
|
|
4
|
+
* involving JS (OkHttp on Android, SocketRocket on iOS, the network stack in
|
|
5
|
+
* browsers), so a blocked JS thread does not fail this check.
|
|
6
|
+
*/
|
|
7
|
+
export type PingableSocket = {
|
|
8
|
+
ping: () => void;
|
|
9
|
+
on: (event: 'pong', listener: () => void) => unknown;
|
|
10
|
+
off: (event: 'pong', listener: () => void) => unknown;
|
|
11
|
+
};
|
|
12
|
+
export type ConnectionCheckOptions = {
|
|
13
|
+
intervalMs: number;
|
|
14
|
+
timeoutMs: number;
|
|
15
|
+
};
|
|
16
|
+
export type ConnectionCheck = {
|
|
17
|
+
dispose: () => void;
|
|
18
|
+
};
|
|
19
|
+
export declare const createConnectionCheck: (socket: PingableSocket, options: ConnectionCheckOptions & {
|
|
20
|
+
onLost: () => void;
|
|
21
|
+
}) => ConnectionCheck;
|
|
22
|
+
//# sourceMappingURL=connection-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-check.d.ts","sourceRoot":"","sources":["../src/connection-check.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,OAAO,CAAC;IACrD,GAAG,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,KAAK,OAAO,CAAC;CACvD,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,QAAQ,cAAc,EACtB,SAAS,sBAAsB,GAAG;IAAE,MAAM,EAAE,MAAM,IAAI,CAAA;CAAE,KACvD,eAmCF,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const createConnectionCheck = (socket, options) => {
|
|
2
|
+
let lastPongAt = Date.now();
|
|
3
|
+
let disposed = false;
|
|
4
|
+
const handlePong = () => {
|
|
5
|
+
lastPongAt = Date.now();
|
|
6
|
+
};
|
|
7
|
+
socket.on('pong', handlePong);
|
|
8
|
+
const dispose = () => {
|
|
9
|
+
if (disposed) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
disposed = true;
|
|
13
|
+
clearInterval(interval);
|
|
14
|
+
socket.off('pong', handlePong);
|
|
15
|
+
};
|
|
16
|
+
const interval = setInterval(() => {
|
|
17
|
+
if (Date.now() - lastPongAt > options.timeoutMs) {
|
|
18
|
+
dispose();
|
|
19
|
+
options.onLost();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
socket.ping();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// The socket is closing; its close event ends the session.
|
|
27
|
+
}
|
|
28
|
+
}, options.intervalMs);
|
|
29
|
+
return { dispose };
|
|
30
|
+
};
|
package/dist/errors.d.ts
CHANGED
|
@@ -4,9 +4,13 @@ export declare class DeviceNotRespondingError extends HarnessError {
|
|
|
4
4
|
readonly args: unknown[];
|
|
5
5
|
constructor(functionName: string, args: unknown[]);
|
|
6
6
|
}
|
|
7
|
-
export type AppBridgeDisconnectedReason = 'app-disconnected' | 'app-replaced' | '
|
|
7
|
+
export type AppBridgeDisconnectedReason = 'app-disconnected' | 'app-replaced' | 'connection-lost' | 'js-thread-unresponsive' | 'socket-error' | 'bridge-disposed';
|
|
8
8
|
export declare class AppBridgeDisconnectedError extends HarnessError {
|
|
9
9
|
readonly reason: AppBridgeDisconnectedReason;
|
|
10
|
-
|
|
10
|
+
/** What the app was busy with, when known. Appended to the message. */
|
|
11
|
+
readonly detail?: string | undefined;
|
|
12
|
+
constructor(reason: AppBridgeDisconnectedReason,
|
|
13
|
+
/** What the app was busy with, when known. Appended to the message. */
|
|
14
|
+
detail?: string | undefined);
|
|
11
15
|
}
|
|
12
16
|
//# sourceMappingURL=errors.d.ts.map
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE3D,qBAAa,wBAAyB,SAAQ,YAAY;aAEtC,YAAY,EAAE,MAAM;aACpB,IAAI,EAAE,OAAO,EAAE;gBADf,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,OAAO,EAAE;CAKlC;AAED,MAAM,MAAM,2BAA2B,GACnC,kBAAkB,GAClB,cAAc,GACd,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE3D,qBAAa,wBAAyB,SAAQ,YAAY;aAEtC,YAAY,EAAE,MAAM;aACpB,IAAI,EAAE,OAAO,EAAE;gBADf,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,OAAO,EAAE;CAKlC;AAED,MAAM,MAAM,2BAA2B,GACnC,kBAAkB,GAClB,cAAc,GACd,iBAAiB,GACjB,wBAAwB,GACxB,cAAc,GACd,iBAAiB,CAAC;AAqBtB,qBAAa,0BAA2B,SAAQ,YAAY;aAExC,MAAM,EAAE,2BAA2B;IACnD,uEAAuE;aACvD,MAAM,CAAC,EAAE,MAAM;gBAFf,MAAM,EAAE,2BAA2B;IACnD,uEAAuE;IACvD,MAAM,CAAC,EAAE,MAAM,YAAA;CAUlC"}
|
package/dist/errors.js
CHANGED
|
@@ -13,8 +13,10 @@ const appBridgeDisconnectedMessage = (reason) => {
|
|
|
13
13
|
switch (reason) {
|
|
14
14
|
case 'app-replaced':
|
|
15
15
|
return 'The app bridge was replaced by a newer app connection. This can happen when the app reloads, restarts, or reconnects while a test file is still running.';
|
|
16
|
-
case '
|
|
17
|
-
return 'The app
|
|
16
|
+
case 'connection-lost':
|
|
17
|
+
return 'The app stopped answering WebSocket pings during test execution. The app process was likely killed, frozen, or lost its network connection. Check .harness/crash-reports for a crash report.';
|
|
18
|
+
case 'js-thread-unresponsive':
|
|
19
|
+
return "The app's JS thread was blocked for longer than allowed during test execution.";
|
|
18
20
|
case 'socket-error':
|
|
19
21
|
return 'The app bridge connection failed during test execution. This can happen if the app was killed, crashed, or the underlying WebSocket connection closed unexpectedly.';
|
|
20
22
|
case 'bridge-disposed':
|
|
@@ -25,9 +27,15 @@ const appBridgeDisconnectedMessage = (reason) => {
|
|
|
25
27
|
};
|
|
26
28
|
export class AppBridgeDisconnectedError extends HarnessError {
|
|
27
29
|
reason;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
detail;
|
|
31
|
+
constructor(reason,
|
|
32
|
+
/** What the app was busy with, when known. Appended to the message. */
|
|
33
|
+
detail) {
|
|
34
|
+
super(detail
|
|
35
|
+
? `${appBridgeDisconnectedMessage(reason)} It was busy with: ${detail}.`
|
|
36
|
+
: appBridgeDisconnectedMessage(reason));
|
|
30
37
|
this.reason = reason;
|
|
38
|
+
this.detail = detail;
|
|
31
39
|
this.name = 'AppBridgeDisconnectedError';
|
|
32
40
|
this.stack = `${this.name}: ${this.message}`;
|
|
33
41
|
}
|
package/dist/image-snapshot.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { FileReference, ImageSnapshotOptions } from './shared.js';
|
|
2
|
-
export declare const matchImageSnapshot: (screenshot: FileReference, testFilePath: string, options: ImageSnapshotOptions, platformName: string) => Promise<{
|
|
1
|
+
import type { FileReference, ImageSnapshotOptions, SnapshotUpdateState } from './shared.js';
|
|
2
|
+
export declare const matchImageSnapshot: (screenshot: FileReference, testFilePath: string, options: ImageSnapshotOptions, platformName: string, updateSnapshot: SnapshotUpdateState) => Promise<{
|
|
3
3
|
pass: boolean;
|
|
4
4
|
message: string;
|
|
5
5
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-snapshot.d.ts","sourceRoot":"","sources":["../src/image-snapshot.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"image-snapshot.d.ts","sourceRoot":"","sources":["../src/image-snapshot.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,aAAa,CAAC;AA2CrB,eAAO,MAAM,kBAAkB,GAC7B,YAAY,aAAa,EACzB,cAAc,MAAM,EACpB,SAAS,oBAAoB,EAC7B,cAAc,MAAM,EACpB,gBAAgB,mBAAmB;;;EAsIpC,CAAC"}
|
package/dist/image-snapshot.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { PNG } from 'pngjs';
|
|
6
6
|
const SNAPSHOT_DIR_NAME = '__image_snapshots__';
|
|
7
|
+
const DIFF_OUTPUT_DIR_NAME = '__diff_output__';
|
|
7
8
|
const DEFAULT_OPTIONS_FOR_PIXELMATCH = {
|
|
8
9
|
threshold: 0.1,
|
|
9
10
|
includeAA: false,
|
|
@@ -31,7 +32,7 @@ function maskRegions(data, imageWidth, regions) {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
|
-
export const matchImageSnapshot = async (screenshot, testFilePath, options, platformName) => {
|
|
35
|
+
export const matchImageSnapshot = async (screenshot, testFilePath, options, platformName, updateSnapshot) => {
|
|
35
36
|
const pixelmatchOptions = {
|
|
36
37
|
...DEFAULT_OPTIONS_FOR_PIXELMATCH,
|
|
37
38
|
...options,
|
|
@@ -47,13 +48,25 @@ export const matchImageSnapshot = async (screenshot, testFilePath, options, plat
|
|
|
47
48
|
// Create __image_snapshots__ directory in same directory as test file
|
|
48
49
|
const testDir = path.dirname(testFilePath);
|
|
49
50
|
const snapshotsDir = path.join(testDir, SNAPSHOT_DIR_NAME, platformName);
|
|
50
|
-
const
|
|
51
|
-
|
|
51
|
+
const snapshotPath = path.join(snapshotsDir, `${options.name}.png`);
|
|
52
|
+
// Failure artifacts live in their own folder so they can never overwrite
|
|
53
|
+
// a baseline of another snapshot (e.g. one named `<name>-diff`).
|
|
54
|
+
const diffOutputDir = path.join(snapshotsDir, DIFF_OUTPUT_DIR_NAME);
|
|
55
|
+
const failureImagePath = (image) => path.join(diffOutputDir, `${options.name}-${image}.png`);
|
|
56
|
+
const actualPath = failureImagePath('actual');
|
|
52
57
|
await fs.mkdir(snapshotsDir, { recursive: true });
|
|
53
58
|
try {
|
|
54
59
|
await fs.access(snapshotPath);
|
|
55
60
|
}
|
|
56
61
|
catch {
|
|
62
|
+
if (updateSnapshot === 'none') {
|
|
63
|
+
await fs.mkdir(diffOutputDir, { recursive: true });
|
|
64
|
+
await fs.writeFile(actualPath, receivedBuffer);
|
|
65
|
+
return {
|
|
66
|
+
pass: false,
|
|
67
|
+
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.`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
57
70
|
// First time - create snapshot
|
|
58
71
|
await fs.writeFile(snapshotPath, receivedBuffer);
|
|
59
72
|
return {
|
|
@@ -70,9 +83,11 @@ export const matchImageSnapshot = async (screenshot, testFilePath, options, plat
|
|
|
70
83
|
const { width, height } = img1;
|
|
71
84
|
const diff = new PNG({ width, height });
|
|
72
85
|
if (img1.width !== img2.width || img1.height !== img2.height) {
|
|
86
|
+
await fs.mkdir(diffOutputDir, { recursive: true });
|
|
87
|
+
await fs.writeFile(actualPath, receivedBuffer);
|
|
73
88
|
return {
|
|
74
89
|
pass: false,
|
|
75
|
-
message: `Images have different dimensions. Received image width: ${img1.width}, height: ${img1.height}. Snapshot image width: ${img2.width}, height: ${img2.height}.`,
|
|
90
|
+
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}.`,
|
|
76
91
|
};
|
|
77
92
|
}
|
|
78
93
|
if (options.ignoreRegions) {
|
|
@@ -116,11 +131,9 @@ export const matchImageSnapshot = async (screenshot, testFilePath, options, plat
|
|
|
116
131
|
}
|
|
117
132
|
// Save diff and actual images when test fails
|
|
118
133
|
if (!pass) {
|
|
119
|
-
|
|
120
|
-
const diffPath =
|
|
134
|
+
await fs.mkdir(diffOutputDir, { recursive: true });
|
|
135
|
+
const diffPath = failureImagePath('diff');
|
|
121
136
|
await fs.writeFile(diffPath, PNG.sync.write(diff));
|
|
122
|
-
const actualFileName = `${snapshotName.replace('.png', '')}-actual.png`;
|
|
123
|
-
const actualPath = path.join(snapshotsDir, actualFileName);
|
|
124
137
|
await fs.writeFile(actualPath, receivedBuffer);
|
|
125
138
|
message += ` Diff saved at ${diffPath}. Actual image saved at ${actualPath}.`;
|
|
126
139
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC"}
|
package/dist/index.js
CHANGED