@react-native-harness/bridge 1.2.0-rc.1 → 1.3.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.
@@ -1 +1 @@
1
- {"version":3,"file":"binary-transfer.d.ts","sourceRoot":"","sources":["../src/binary-transfer.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,UAAU,GACf,UAAU,CAcZ;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;CAClB,CAMA;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,QAAQ,CAA0B;IAE1C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiB;IAE5C,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI;IAS/C,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAI/C,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IASnC,OAAO,IAAI,IAAI;CAOhB;AAGD,wBAAgB,kBAAkB,IAAI,MAAM,CAQ3C"}
1
+ {"version":3,"file":"binary-transfer.d.ts","sourceRoot":"","sources":["../src/binary-transfer.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,IAAI,CAAC;AAE7B,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,UAAU,GACf,UAAU,CAcZ;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,UAAU,CAAC;CAClB,CAMA;AAED,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,QAAQ,CAAoD;IAEpE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiB;IAE5C,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI;IAS/C,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAI/C,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IASnC,OAAO,IAAI,IAAI;CAOhB;AAGD,wBAAgB,kBAAkB,IAAI,MAAM,CAQ3C"}
package/dist/client.d.ts CHANGED
@@ -1,10 +1,32 @@
1
- import { BirpcReturn } from 'birpc';
2
- import type { BridgeClientFunctions, BridgeServerFunctions } from './shared.js';
3
- export type BridgeClient = {
4
- rpc: BirpcReturn<BridgeServerFunctions, BridgeClientFunctions>;
1
+ import type { DeviceDescriptor, BridgeEvents, FileReference, ImageSnapshotOptions, TestExecutionOptions, TestSuiteResult } from './shared.js';
2
+ /** Handlers the app must implement for the CLI to call into. */
3
+ export type HarnessCallbacks = {
4
+ runTests: (path: string, options: TestExecutionOptions) => Promise<TestSuiteResult>;
5
+ };
6
+ /** The app-side handle returned by connectToHarness. */
7
+ export type HarnessHandle = {
8
+ /** Call once when the app is initialised and ready to run tests. */
9
+ reportReady: (device: DeviceDescriptor) => void;
10
+ /** Forward a test or bundler event to the CLI. */
11
+ emitEvent: (event: BridgeEvents) => void;
12
+ /** Send a screenshot to the CLI and receive a file reference for snapshot comparison. */
13
+ transferScreenshot: (data: Uint8Array, metadata: {
14
+ width: number;
15
+ height: number;
16
+ }) => Promise<FileReference>;
17
+ /** Request an image snapshot comparison on the CLI. */
18
+ matchImageSnapshot: (screenshot: FileReference, testPath: string, options: ImageSnapshotOptions, runner: string) => Promise<{
19
+ pass: boolean;
20
+ message: string;
21
+ }>;
5
22
  disconnect: () => void;
6
- sendBinary: (transferId: number, data: Uint8Array) => void;
7
23
  };
8
- declare const getBridgeClient: (url: string, handlers: BridgeClientFunctions) => Promise<BridgeClient>;
9
- export { getBridgeClient };
24
+ /**
25
+ * Connect the app to the CLI harness bridge.
26
+ *
27
+ * Pass the handlers the CLI can call (runTests). Returns a HarnessHandle
28
+ * exposing the operations the app needs to drive a test run. The binary
29
+ * transfer protocol and RPC wiring are fully encapsulated.
30
+ */
31
+ export declare const connectToHarness: (url: string, callbacks: HarnessCallbacks) => Promise<HarnessHandle>;
10
32
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,MAAM,OAAO,CAAC;AACjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAIhF,MAAM,MAAM,YAAY,GAAG;IACzB,GAAG,EAAE,WAAW,CAAC,qBAAqB,EAAE,qBAAqB,CAAC,CAAC;IAC/D,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,UAAU,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;CAC5D,CAAC;AAEF,QAAA,MAAM,eAAe,GACnB,KAAK,MAAM,EACX,UAAU,qBAAqB,KAC9B,OAAO,CAAC,YAAY,CA6EtB,CAAC;AAEF,OAAO,EAAE,eAAe,EAAE,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAGV,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EAChB,MAAM,aAAa,CAAC;AAMrB,gEAAgE;AAChE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;CACrF,CAAC;AAEF,wDAAwD;AACxD,MAAM,MAAM,aAAa,GAAG;IAC1B,oEAAoE;IACpE,WAAW,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAChD,kDAAkD;IAClD,SAAS,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,yFAAyF;IACzF,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,uDAAuD;IACvD,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;AAMF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAC3B,KAAK,MAAM,EACX,WAAW,gBAAgB,KAC1B,OAAO,CAAC,aAAa,CAyEpB,CAAC"}
package/dist/client.js CHANGED
@@ -1,63 +1,68 @@
1
1
  import { createBirpc } from 'birpc';
2
2
  import { deserialize, serialize } from './serializer.js';
3
- import { createBinaryFrame } from './binary-transfer.js';
4
- const getBridgeClient = async (url, handlers) => {
5
- return new Promise((resolve, reject) => {
6
- const ws = new WebSocket(url);
7
- ws.binaryType = 'arraybuffer';
8
- let settled = false;
9
- const cleanup = () => {
10
- ws.removeEventListener('open', handleOpen);
11
- ws.removeEventListener('error', handleError);
12
- ws.removeEventListener('close', handleClose);
13
- };
14
- const rejectConnection = (message) => {
15
- if (settled) {
16
- return;
17
- }
18
- settled = true;
19
- cleanup();
20
- reject(new Error(message));
21
- };
22
- const handleOpen = () => {
23
- settled = true;
24
- cleanup();
25
- const rpc = createBirpc(handlers, {
26
- post: (data) => ws.send(data),
27
- on: (handler) => {
28
- ws.addEventListener('message', (event) => {
29
- if (typeof event.data === 'string') {
30
- handler(event.data);
31
- }
32
- });
33
- },
34
- serialize,
35
- deserialize,
36
- });
37
- const client = {
38
- rpc,
39
- disconnect: () => {
40
- ws.close();
41
- },
42
- sendBinary: (transferId, data) => {
43
- const frame = createBinaryFrame(transferId, data);
44
- ws.send(frame);
45
- },
46
- };
47
- resolve(client);
48
- };
49
- const handleError = (event) => {
50
- const reason = typeof event.message === 'string' && event.message.length > 0
51
- ? `: ${event.message}`
52
- : '';
53
- rejectConnection(`Failed to connect to the Harness bridge at ${url}${reason}`);
54
- };
55
- const handleClose = (event) => {
56
- rejectConnection(`Harness bridge connection to ${url} closed before it became ready (code ${event.code}${event.reason ? `, reason: ${event.reason}` : ''})`);
57
- };
58
- ws.addEventListener('open', handleOpen);
59
- ws.addEventListener('error', handleError);
60
- ws.addEventListener('close', handleClose);
61
- });
62
- };
63
- export { getBridgeClient };
3
+ import { createBinaryFrame, generateTransferId } from './binary-transfer.js';
4
+ // ---------------------------------------------------------------------------
5
+ // Factory
6
+ // ---------------------------------------------------------------------------
7
+ /**
8
+ * Connect the app to the CLI harness bridge.
9
+ *
10
+ * Pass the handlers the CLI can call (runTests). Returns a HarnessHandle
11
+ * exposing the operations the app needs to drive a test run. The binary
12
+ * transfer protocol and RPC wiring are fully encapsulated.
13
+ */
14
+ export const connectToHarness = (url, callbacks) => new Promise((resolve, reject) => {
15
+ const ws = new WebSocket(url);
16
+ ws.binaryType = 'arraybuffer';
17
+ let settled = false;
18
+ const cleanup = () => {
19
+ ws.removeEventListener('open', handleOpen);
20
+ ws.removeEventListener('error', handleError);
21
+ ws.removeEventListener('close', handleClose);
22
+ };
23
+ const fail = (message) => {
24
+ if (settled)
25
+ return;
26
+ settled = true;
27
+ cleanup();
28
+ reject(new Error(message));
29
+ };
30
+ const handleOpen = () => {
31
+ settled = true;
32
+ cleanup();
33
+ const rpc = createBirpc(callbacks, {
34
+ post: (data) => ws.send(data),
35
+ on: (handler) => {
36
+ ws.addEventListener('message', (event) => {
37
+ if (typeof event.data === 'string')
38
+ handler(event.data);
39
+ });
40
+ },
41
+ serialize,
42
+ deserialize,
43
+ });
44
+ resolve({
45
+ reportReady: (device) => void rpc.reportReady(device),
46
+ emitEvent: (event) => void rpc.emitEvent(event.type, event),
47
+ transferScreenshot: async (data, metadata) => {
48
+ const transferId = generateTransferId();
49
+ ws.send(createBinaryFrame(transferId, data));
50
+ return rpc['device.screenshot.receive']({ type: 'binary', transferId, size: data.length, mimeType: 'image/png' }, metadata);
51
+ },
52
+ matchImageSnapshot: (screenshot, testPath, options, runner) => rpc['test.matchImageSnapshot'](screenshot, testPath, options, runner),
53
+ disconnect: () => ws.close(),
54
+ });
55
+ };
56
+ const handleError = (event) => {
57
+ const detail = typeof event.message === 'string' && event.message
58
+ ? `: ${event.message}`
59
+ : '';
60
+ fail(`Failed to connect to Harness at ${url}${detail}`);
61
+ };
62
+ const handleClose = (event) => {
63
+ fail(`Harness connection at ${url} closed before becoming ready (code ${event.code}${event.reason ? `, reason: ${event.reason}` : ''})`);
64
+ };
65
+ ws.addEventListener('open', handleOpen);
66
+ ws.addEventListener('error', handleError);
67
+ ws.addEventListener('close', handleClose);
68
+ });
package/dist/server.d.ts CHANGED
@@ -1,36 +1,56 @@
1
1
  import { WebSocketServer } from 'ws';
2
- import { type BirpcGroup } from 'birpc';
3
2
  import type { Server as HttpServer } from 'node:http';
4
3
  import type { Server as HttpsServer } from 'node:https';
5
- import type { BridgeServerFunctions, BridgeClientFunctions, DeviceDescriptor, BridgeEvents, HarnessContext } from './shared.js';
4
+ import type { DeviceDescriptor, BridgeEvents, HarnessContext, TestExecutionOptions, TestSuiteResult } from './shared.js';
6
5
  export { DeviceNotRespondingError } from './errors.js';
7
- type BridgeServerStandaloneOptions = {
6
+ /**
7
+ * Represents a single app session — one app launch to the next restart.
8
+ * Obtained via HarnessBridge.nextConnection().
9
+ */
10
+ export type AppConnection = {
11
+ readonly device: DeviceDescriptor;
12
+ runTests: (path: string, options: TestExecutionOptions) => Promise<TestSuiteResult>;
13
+ };
14
+ export type HarnessBridgeEvents = {
15
+ /** Fired when the app connects and calls reportReady. */
16
+ connected: (connection: AppConnection) => void;
17
+ /** Fired when the app's WebSocket closes. */
18
+ disconnected: () => void;
19
+ /** Fired for every test/bundler event the app emits. */
20
+ event: (event: BridgeEvents) => void;
21
+ };
22
+ type TransportOptions = {
23
+ noServer: true;
24
+ } | {
8
25
  port: number;
9
26
  host?: string;
10
- };
11
- type BridgeServerAttachedOptions = {
27
+ } | {
12
28
  server: HttpServer | HttpsServer;
13
29
  path?: string;
14
30
  };
15
- type BridgeServerNoServerOptions = {
16
- noServer: true;
17
- };
18
- export type BridgeServerOptions = (BridgeServerStandaloneOptions | BridgeServerAttachedOptions | BridgeServerNoServerOptions) & {
31
+ export type HarnessBridgeOptions = TransportOptions & {
19
32
  timeout?: number;
20
33
  context: HarnessContext;
21
34
  };
22
- export type BridgeServerEvents = {
23
- ready: (device: DeviceDescriptor) => void;
24
- event: (event: BridgeEvents) => void;
25
- disconnect: () => void;
26
- };
27
- export type BridgeServer = {
28
- ws: WebSocketServer;
29
- rpc: BirpcGroup<BridgeClientFunctions, BridgeServerFunctions>;
30
- on: <T extends keyof BridgeServerEvents>(event: T, listener: BridgeServerEvents[T]) => void;
31
- once: <T extends keyof BridgeServerEvents>(event: T, listener: BridgeServerEvents[T]) => void;
32
- off: <T extends keyof BridgeServerEvents>(event: T, listener: BridgeServerEvents[T]) => void;
35
+ /**
36
+ * The persistent CLI-side bridge. Spans the full test run regardless of how
37
+ * many times the app is restarted. Each restart produces a new AppConnection
38
+ * via nextConnection().
39
+ */
40
+ export type HarnessBridge = {
41
+ /** The underlying WebSocket server, used to attach to Metro's HTTP server. */
42
+ readonly ws: WebSocketServer;
43
+ /** The currently active app connection, null if the app is not connected. */
44
+ readonly connection: AppConnection | null;
45
+ /**
46
+ * Resolves with the next AppConnection once the app connects and reports
47
+ * ready. Register this waiter before restarting the app so no ready signal
48
+ * is missed. Rejects if the supplied signal is aborted.
49
+ */
50
+ nextConnection: (signal?: AbortSignal) => Promise<AppConnection>;
51
+ on: <T extends keyof HarnessBridgeEvents>(event: T, listener: HarnessBridgeEvents[T]) => void;
52
+ off: <T extends keyof HarnessBridgeEvents>(event: T, listener: HarnessBridgeEvents[T]) => void;
33
53
  dispose: () => void;
34
54
  };
35
- export declare const getBridgeServer: ({ timeout, context, ...transport }: BridgeServerOptions) => Promise<BridgeServer>;
55
+ export declare const createHarnessBridge: (options: HarnessBridgeOptions) => Promise<HarnessBridge>;
36
56
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAkB,MAAM,IAAI,CAAC;AACrD,OAAO,EAAE,KAAK,UAAU,EAAoB,MAAM,OAAO,CAAC;AAG1D,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,IAAI,WAAW,EAAE,MAAM,YAAY,CAAC;AAMxD,OAAO,KAAK,EACV,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,YAAY,EAEZ,cAAc,EAGf,MAAM,aAAa,CAAC;AAKrB,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAGvD,KAAK,6BAA6B,GAAG;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,2BAA2B,GAAG;IACjC,MAAM,EAAE,UAAU,GAAG,WAAW,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,2BAA2B,GAAG;IACjC,QAAQ,EAAE,IAAI,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,CAC9B,6BAA6B,GAC7B,2BAA2B,GAC3B,2BAA2B,CAC9B,GAAG;IACF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC1C,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACrC,UAAU,EAAE,MAAM,IAAI,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,EAAE,EAAE,eAAe,CAAC;IACpB,GAAG,EAAE,UAAU,CAAC,qBAAqB,EAAE,qBAAqB,CAAC,CAAC;IAC9D,EAAE,EAAE,CAAC,CAAC,SAAS,MAAM,kBAAkB,EACrC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,KAC5B,IAAI,CAAC;IACV,IAAI,EAAE,CAAC,CAAC,SAAS,MAAM,kBAAkB,EACvC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,KAC5B,IAAI,CAAC;IACV,GAAG,EAAE,CAAC,CAAC,SAAS,MAAM,kBAAkB,EACtC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,kBAAkB,CAAC,CAAC,CAAC,KAC5B,IAAI,CAAC;IACV,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,eAAe,GAAU,oCAInC,mBAAmB,KAAG,OAAO,CAAC,YAAY,CA8J5C,CAAC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAkB,MAAM,IAAI,CAAC;AAGrD,OAAO,KAAK,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,KAAK,EAAE,MAAM,IAAI,WAAW,EAAE,MAAM,YAAY,CAAC;AAUxD,OAAO,KAAK,EAGV,gBAAgB,EAChB,YAAY,EAGZ,cAAc,EACd,oBAAoB,EACpB,eAAe,EAChB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAQvD;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;CACrF,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,yDAAyD;IACzD,SAAS,EAAE,CAAC,UAAU,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C,6CAA6C;IAC7C,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,wDAAwD;IACxD,KAAK,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CACtC,CAAC;AAEF,KAAK,gBAAgB,GACjB;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,MAAM,EAAE,UAAU,GAAG,WAAW,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,GAAG;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;CACzB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,EAAE,eAAe,CAAC;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,UAAU,EAAE,aAAa,GAAG,IAAI,CAAC;IAC1C;;;;OAIG;IACH,cAAc,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC;IACjE,EAAE,EAAE,CAAC,CAAC,SAAS,MAAM,mBAAmB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAC9F,GAAG,EAAE,CAAC,CAAC,SAAS,MAAM,mBAAmB,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAC/F,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AA4CF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,oBAAoB,KAC5B,OAAO,CAAC,aAAa,CA0JvB,CAAC"}
package/dist/server.js CHANGED
@@ -1,133 +1,166 @@
1
1
  import { WebSocketServer } from 'ws';
2
- import { createBirpcGroup } from 'birpc';
3
- import { logger } from '@react-native-harness/tools';
2
+ import { createBirpc } from 'birpc';
4
3
  import { EventEmitter } from 'node:events';
5
4
  import fs from 'node:fs/promises';
6
5
  import os from 'node:os';
7
6
  import path from 'node:path';
8
7
  import { randomUUID } from 'node:crypto';
8
+ import { logger } from '@react-native-harness/tools';
9
9
  import { BinaryStore, parseBinaryFrame } from './binary-transfer.js';
10
10
  import { deserialize, serialize } from './serializer.js';
11
11
  import { DeviceNotRespondingError } from './errors.js';
12
12
  import { matchImageSnapshot } from './image-snapshot.js';
13
13
  export { DeviceNotRespondingError } from './errors.js';
14
14
  const bridgeLogger = logger.child('bridge');
15
- export const getBridgeServer = async ({ timeout, context, ...transport }) => {
16
- const wss = 'port' in transport
17
- ? await new Promise((resolve) => {
18
- const server = new WebSocketServer({
19
- port: transport.port,
20
- host: transport.host ?? '0.0.0.0',
21
- }, () => {
22
- resolve(server);
23
- });
24
- })
25
- : new WebSocketServer('server' in transport
26
- ? {
27
- server: transport.server,
28
- path: transport.path,
29
- }
30
- : {
31
- noServer: true,
32
- });
15
+ // ---------------------------------------------------------------------------
16
+ // Helpers
17
+ // ---------------------------------------------------------------------------
18
+ const createWss = (transport) => {
33
19
  if ('port' in transport) {
34
- bridgeLogger.debug('bridge server listening on port %d', transport.port);
35
- }
36
- else if ('server' in transport) {
37
- bridgeLogger.debug('bridge server attached to existing HTTP server at path %s', transport.path ?? '/');
20
+ return new Promise((resolve) => {
21
+ const wss = new WebSocketServer({ port: transport.port, host: transport.host ?? '0.0.0.0' }, () => resolve(wss));
22
+ });
38
23
  }
39
- else {
40
- bridgeLogger.debug('bridge server created in noServer mode');
24
+ return Promise.resolve(new WebSocketServer('server' in transport
25
+ ? { server: transport.server, path: transport.path }
26
+ : { noServer: true }));
27
+ };
28
+ const receiveScreenshot = async (binaryStore, reference) => {
29
+ const data = binaryStore.get(reference.transferId);
30
+ if (!data) {
31
+ throw new Error(`Binary data for transfer ${reference.transferId} not found or expired`);
41
32
  }
33
+ binaryStore.delete(reference.transferId);
34
+ const file = path.join(os.tmpdir(), `harness-screenshot-${randomUUID()}.png`);
35
+ await fs.writeFile(file, data);
36
+ return { path: file };
37
+ };
38
+ // ---------------------------------------------------------------------------
39
+ // Factory
40
+ // ---------------------------------------------------------------------------
41
+ export const createHarnessBridge = async (options) => {
42
+ const { timeout, context, ...transport } = options;
43
+ const wss = await createWss(transport);
44
+ bridgeLogger.debug('bridge server ready');
42
45
  const emitter = new EventEmitter();
43
- const clients = new Set();
44
- const binaryStore = new BinaryStore();
45
- const baseFunctions = {
46
- reportReady: (device) => {
47
- emitter.emit('ready', device);
48
- },
49
- emitEvent: (_, data) => {
50
- emitter.emit('event', data);
51
- },
52
- 'device.screenshot.receive': async (reference, metadata) => {
53
- const data = binaryStore.get(reference.transferId);
54
- if (!data) {
55
- throw new Error(`Binary data for transfer ${reference.transferId} not found or expired`);
56
- }
57
- // Clean up from store
58
- binaryStore.delete(reference.transferId);
59
- // Write to temp file
60
- const tempFile = path.join(os.tmpdir(), `harness-screenshot-${randomUUID()}.png`);
61
- await fs.writeFile(tempFile, data);
62
- return {
63
- path: tempFile,
64
- width: metadata.width,
65
- height: metadata.height,
66
- };
67
- },
68
- 'test.matchImageSnapshot': async (screenshot, testPath, options) => {
69
- return await matchImageSnapshot(screenshot, testPath, options, context.platform.name);
70
- },
71
- };
72
- const group = createBirpcGroup(baseFunctions, [], {
73
- timeout,
74
- onFunctionError: (error, functionName, args) => {
75
- bridgeLogger.error('rpc function failed: %s args=%o', functionName, args);
76
- bridgeLogger.error(error);
77
- throw error;
78
- },
79
- onTimeoutError(functionName, args) {
80
- throw new DeviceNotRespondingError(functionName, args);
81
- },
82
- });
46
+ let currentConnection = null;
47
+ const connectionWaiters = [];
83
48
  wss.on('connection', (ws) => {
84
- bridgeLogger.debug('client connected');
49
+ bridgeLogger.debug('app connected');
50
+ const binaryStore = new BinaryStore();
51
+ let readyConnection = null;
52
+ let disconnected = false;
53
+ const serverFunctions = {
54
+ reportReady: (device) => {
55
+ const conn = {
56
+ device,
57
+ runTests: (testPath, opts) => rpc.runTests(testPath, opts),
58
+ };
59
+ readyConnection = conn;
60
+ currentConnection = conn;
61
+ bridgeLogger.debug('app ready: platform=%s model=%s', device.platform, device.model);
62
+ emitter.emit('connected', conn);
63
+ for (const { resolve } of connectionWaiters.splice(0))
64
+ resolve(conn);
65
+ },
66
+ emitEvent: (_, data) => {
67
+ emitter.emit('event', data);
68
+ },
69
+ 'device.screenshot.receive': (ref) => receiveScreenshot(binaryStore, ref),
70
+ 'test.matchImageSnapshot': (screenshot, testPath, opts) => matchImageSnapshot(screenshot, testPath, opts, context.platform.name),
71
+ };
72
+ const rpc = createBirpc(serverFunctions, {
73
+ post: (data) => ws.send(data),
74
+ on: (handler) => {
75
+ ws.on('message', (msg, isBinary) => {
76
+ if (isBinary) {
77
+ try {
78
+ const messageBuffer = Array.isArray(msg)
79
+ ? Buffer.concat(msg)
80
+ : Buffer.isBuffer(msg)
81
+ ? msg
82
+ : Buffer.from(msg);
83
+ const { transferId, data } = parseBinaryFrame(new Uint8Array(messageBuffer));
84
+ binaryStore.add(transferId, data);
85
+ }
86
+ catch (err) {
87
+ bridgeLogger.warn('failed to parse binary frame: %s', err);
88
+ }
89
+ }
90
+ else {
91
+ handler(msg.toString());
92
+ }
93
+ });
94
+ },
95
+ serialize,
96
+ deserialize,
97
+ timeout,
98
+ onFunctionError: (error, functionName, args) => {
99
+ bridgeLogger.error('rpc function failed: %s args=%o', functionName, args);
100
+ throw error;
101
+ },
102
+ onTimeoutError: (fn, args) => {
103
+ throw new DeviceNotRespondingError(fn, args);
104
+ },
105
+ });
106
+ const disconnect = (reason) => {
107
+ if (disconnected)
108
+ return;
109
+ disconnected = true;
110
+ bridgeLogger.debug('app disconnected');
111
+ binaryStore.dispose();
112
+ if (currentConnection === readyConnection) {
113
+ currentConnection = null;
114
+ }
115
+ rpc.$close(reason ?? new Error('App bridge disconnected'));
116
+ emitter.emit('disconnected');
117
+ };
85
118
  ws.on('close', () => {
86
- bridgeLogger.debug('client disconnected');
87
- // TODO: Remove channel when connection is closed.
88
- clients.delete(ws);
89
- emitter.emit('disconnect');
119
+ disconnect();
90
120
  });
91
- group.updateChannels((channels) => {
92
- channels.push({
93
- post: (data) => ws.send(data),
94
- on: (handler) => {
95
- ws.on('message', (event, isBinary) => {
96
- if (isBinary) {
97
- const uint8Array = new Uint8Array(event);
98
- try {
99
- const { transferId, data } = parseBinaryFrame(uint8Array);
100
- binaryStore.add(transferId, data);
101
- return;
102
- }
103
- catch (error) {
104
- bridgeLogger.warn('failed to parse binary frame', error);
105
- }
106
- }
107
- const message = event.toString();
108
- handler(message);
109
- });
110
- },
111
- serialize,
112
- deserialize,
113
- });
121
+ ws.on('error', (error) => {
122
+ disconnect(error instanceof Error ? error : new Error('App bridge socket error'));
114
123
  });
115
124
  });
116
- const dispose = () => {
117
- bridgeLogger.debug('disposing bridge server');
118
- for (const client of wss.clients) {
119
- client.terminate();
120
- }
121
- wss.close();
122
- emitter.removeAllListeners();
123
- binaryStore.dispose();
124
- };
125
125
  return {
126
- ws: wss,
127
- rpc: group,
128
- on: emitter.on.bind(emitter),
129
- once: emitter.once.bind(emitter),
130
- off: emitter.off.bind(emitter),
131
- dispose,
126
+ get ws() {
127
+ return wss;
128
+ },
129
+ get connection() {
130
+ return currentConnection;
131
+ },
132
+ nextConnection: (signal) => {
133
+ if (signal?.aborted) {
134
+ return Promise.reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
135
+ }
136
+ // If the app already connected before this call (e.g. fast simulator
137
+ // startup between startAttempt and waitForReady), return it immediately
138
+ // rather than waiting for a second reportReady that will never come.
139
+ if (currentConnection) {
140
+ return Promise.resolve(currentConnection);
141
+ }
142
+ return new Promise((resolve, reject) => {
143
+ const entry = { resolve, reject };
144
+ connectionWaiters.push(entry);
145
+ signal?.addEventListener('abort', () => {
146
+ const idx = connectionWaiters.indexOf(entry);
147
+ if (idx !== -1)
148
+ connectionWaiters.splice(idx, 1);
149
+ reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
150
+ }, { once: true });
151
+ });
152
+ },
153
+ on: (event, listener) => emitter.on(event, listener),
154
+ off: (event, listener) => emitter.off(event, listener),
155
+ dispose: () => {
156
+ bridgeLogger.debug('disposing bridge');
157
+ for (const { reject } of connectionWaiters.splice(0)) {
158
+ reject(new Error('Bridge disposed'));
159
+ }
160
+ for (const client of wss.clients)
161
+ client.terminate();
162
+ wss.close();
163
+ emitter.removeAllListeners();
164
+ },
132
165
  };
133
166
  };
@@ -1,5 +1,7 @@
1
+ import type { HarnessTestContext } from './test-context.js';
1
2
  export type TestStatus = 'active' | 'skipped' | 'todo';
2
- export type TestFn = () => void | Promise<void>;
3
+ export type TestFn = (context: HarnessTestContext) => void | Promise<void>;
4
+ export type SuiteHookFn = () => void | Promise<void>;
3
5
  export type TestCase = {
4
6
  name: string;
5
7
  fn: TestFn;
@@ -10,8 +12,8 @@ export type TestSuite = {
10
12
  tests: TestCase[];
11
13
  suites: TestSuite[];
12
14
  parent?: TestSuite;
13
- beforeAll: TestFn[];
14
- afterAll: TestFn[];
15
+ beforeAll: SuiteHookFn[];
16
+ afterAll: SuiteHookFn[];
15
17
  beforeEach: TestFn[];
16
18
  afterEach: TestFn[];
17
19
  status?: TestStatus;
@@ -1 +1 @@
1
- {"version":3,"file":"test-collector.d.ts","sourceRoot":"","sources":["../../src/shared/test-collector.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAEvD,MAAM,MAAM,MAAM,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAEhD,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,SAAS,CAAC;IACrB,uFAAuF;IACvF,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC3B,0BAA0B,GAC1B,2BAA2B,CAAC"}
1
+ {"version":3,"file":"test-collector.d.ts","sourceRoot":"","sources":["../../src/shared/test-collector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;AAEvD,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE3E,MAAM,MAAM,WAAW,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAErD,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,UAAU,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,SAAS,EAAE,WAAW,EAAE,CAAC;IACzB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,SAAS,CAAC;IACrB,uFAAuF;IACvF,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GAAG;IACvC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,qBAAqB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC3B,0BAA0B,GAC1B,2BAA2B,CAAC"}
@@ -0,0 +1,21 @@
1
+ export type HarnessTaskContext = {
2
+ name: string;
3
+ type: 'test';
4
+ mode: 'run' | 'skip' | 'todo';
5
+ file: {
6
+ name: string;
7
+ };
8
+ suite: {
9
+ name: string;
10
+ };
11
+ };
12
+ export type HarnessTestContext = {
13
+ task: HarnessTaskContext;
14
+ onTestFailed: (fn: () => void | Promise<void>) => void;
15
+ onTestFinished: (fn: () => void | Promise<void>) => void;
16
+ skip: {
17
+ (note?: string): never;
18
+ (condition: boolean, note?: string): void;
19
+ };
20
+ };
21
+ //# sourceMappingURL=test-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test-context.d.ts","sourceRoot":"","sources":["../../src/shared/test-context.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;IAC9B,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,kBAAkB,CAAC;IACzB,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IACvD,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;IACzD,IAAI,EAAE;QACJ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;QACvB,CAAC,SAAS,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3C,CAAC;CACH,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
package/dist/shared.d.ts CHANGED
@@ -67,7 +67,8 @@ export type ImageSnapshotOptions = {
67
67
  */
68
68
  diffColorAlt?: [number, number, number];
69
69
  };
70
- export type { TestCollectorEvents, TestCollectionStartedEvent, TestCollectionFinishedEvent, TestSuite, TestCase, CollectionResult, } from './shared/test-collector.js';
70
+ export type { TestCollectorEvents, TestCollectionStartedEvent, TestCollectionFinishedEvent, TestSuite, TestCase, CollectionResult, TestFn, SuiteHookFn, } from './shared/test-collector.js';
71
+ export type { HarnessTaskContext, HarnessTestContext, } from './shared/test-context.js';
71
72
  export type { TestRunnerEvents, TestRunnerFileStartedEvent, TestRunnerFileFinishedEvent, TestRunnerSuiteStartedEvent, TestRunnerTestStartedEvent, TestRunnerTestFinishedEvent, TestRunnerSuiteFinishedEvent, TestSuiteResult, TestResult, TestResultStatus, SerializedError, CodeFrame, } from './shared/test-runner.js';
72
73
  export type { ModuleBundlingStartedEvent, ModuleBundlingFinishedEvent, ModuleBundlingFailedEvent, SetupFileBundlingStartedEvent, SetupFileBundlingFinishedEvent, SetupFileBundlingFailedEvent, BundlerEvents, } from './shared/bundler.js';
73
74
  export type DeviceDescriptor = {
@@ -100,7 +101,7 @@ export type BinaryDataReference = {
100
101
  export type ScreenshotData = BinaryDataReference;
101
102
  export type BridgeServerFunctions = {
102
103
  reportReady: (device: DeviceDescriptor) => void;
103
- emitEvent: <TEvent extends BridgeEvents>(event: TEvent['type'], data: TEvent) => void;
104
+ emitEvent: (event: BridgeEvents['type'], data: BridgeEvents) => void;
104
105
  'device.screenshot.receive': (reference: BinaryDataReference, metadata: {
105
106
  width: number;
106
107
  height: number;