@react-native-harness/bridge 1.1.0 → 1.2.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/binary-transfer.d.ts.map +1 -1
- package/dist/client.d.ts +29 -7
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +66 -61
- package/dist/events.d.ts +45 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +1 -0
- package/dist/platform-bridge.d.ts +4 -0
- package/dist/platform-bridge.d.ts.map +1 -0
- package/dist/platform-bridge.js +25 -0
- package/dist/server.d.ts +41 -21
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +128 -110
- package/dist/shared.d.ts +1 -1
- package/dist/shared.d.ts.map +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/binary-transfer.ts +1 -1
- package/src/client.ts +80 -42
- package/src/server.ts +202 -175
- package/src/shared.ts +1 -4
|
@@ -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,
|
|
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 {
|
|
2
|
-
|
|
3
|
-
export type
|
|
4
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
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":"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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
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/events.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type ExecutionStartedEvent = {
|
|
2
|
+
type: 'execution-started';
|
|
3
|
+
path: string;
|
|
4
|
+
numberOfTests: number;
|
|
5
|
+
};
|
|
6
|
+
export type ExecutionFinishedEvent = {
|
|
7
|
+
type: 'execution-finished';
|
|
8
|
+
path: string;
|
|
9
|
+
duration: number;
|
|
10
|
+
};
|
|
11
|
+
export type SuiteStartedEvent = {
|
|
12
|
+
type: 'suite-started';
|
|
13
|
+
path: string;
|
|
14
|
+
name: string;
|
|
15
|
+
};
|
|
16
|
+
export type SuiteFinishedEvent = {
|
|
17
|
+
type: 'suite-finished';
|
|
18
|
+
path: string;
|
|
19
|
+
name: string;
|
|
20
|
+
duration: number;
|
|
21
|
+
reason: 'passed' | 'failed' | 'skipped' | 'todo';
|
|
22
|
+
};
|
|
23
|
+
export type TestStartedEvent = {
|
|
24
|
+
type: 'test-started';
|
|
25
|
+
path: string;
|
|
26
|
+
suite: string;
|
|
27
|
+
name: string;
|
|
28
|
+
};
|
|
29
|
+
export type TestFinishedEvent = {
|
|
30
|
+
type: 'test-finished';
|
|
31
|
+
path: string;
|
|
32
|
+
suite: string;
|
|
33
|
+
name: string;
|
|
34
|
+
duration: number;
|
|
35
|
+
reason: 'passed' | 'failed' | 'skipped' | 'todo';
|
|
36
|
+
};
|
|
37
|
+
export type BridgeEvents = {
|
|
38
|
+
'execution-started': ExecutionStartedEvent;
|
|
39
|
+
'execution-finished': ExecutionFinishedEvent;
|
|
40
|
+
'suite-started': SuiteStartedEvent;
|
|
41
|
+
'suite-finished': SuiteFinishedEvent;
|
|
42
|
+
'test-started': TestStartedEvent;
|
|
43
|
+
'test-finished': TestFinishedEvent;
|
|
44
|
+
};
|
|
45
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,eAAe,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,eAAe,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,mBAAmB,EAAE,qBAAqB,CAAC;IAC3C,oBAAoB,EAAE,sBAAsB,CAAC;IAC7C,eAAe,EAAE,iBAAiB,CAAC;IACnC,gBAAgB,EAAE,kBAAkB,CAAC;IACrC,cAAc,EAAE,gBAAgB,CAAC;IACjC,eAAe,EAAE,iBAAiB,CAAC;CACpC,CAAC"}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HarnessPlatformRunner } from '@react-native-harness/platforms';
|
|
2
|
+
import type { BridgeServerFunctions } from './shared.js';
|
|
3
|
+
export declare const createPlatformBridgeFunctions: (platformRunner: HarnessPlatformRunner) => Partial<BridgeServerFunctions>;
|
|
4
|
+
//# sourceMappingURL=platform-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"platform-bridge.d.ts","sourceRoot":"","sources":["../src/platform-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,qBAAqB,EACtB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEzD,eAAO,MAAM,6BAA6B,GACxC,gBAAgB,qBAAqB,KACpC,OAAO,CAAC,qBAAqB,CAwB/B,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const createPlatformBridgeFunctions = (platformRunner) => {
|
|
2
|
+
return {
|
|
3
|
+
'platform.actions.tap': async (x, y) => {
|
|
4
|
+
await platformRunner.actions.tap(x, y);
|
|
5
|
+
},
|
|
6
|
+
'platform.actions.inputText': async (text) => {
|
|
7
|
+
await platformRunner.actions.inputText(text);
|
|
8
|
+
},
|
|
9
|
+
'platform.actions.tapElement': async (element) => {
|
|
10
|
+
await platformRunner.actions.tapElement(element);
|
|
11
|
+
},
|
|
12
|
+
'platform.actions.screenshot': async () => {
|
|
13
|
+
return await platformRunner.actions.screenshot();
|
|
14
|
+
},
|
|
15
|
+
'platform.queries.getUiHierarchy': async () => {
|
|
16
|
+
return await platformRunner.queries.getUiHierarchy();
|
|
17
|
+
},
|
|
18
|
+
'platform.queries.findByTestId': async (testId) => {
|
|
19
|
+
return await platformRunner.queries.findByTestId(testId);
|
|
20
|
+
},
|
|
21
|
+
'platform.queries.findAllByTestId': async (testId) => {
|
|
22
|
+
return await platformRunner.queries.findAllByTestId(testId);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
};
|
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 {
|
|
4
|
+
import type { DeviceDescriptor, BridgeEvents, HarnessContext, TestExecutionOptions, TestSuiteResult } from './shared.js';
|
|
6
5
|
export { DeviceNotRespondingError } from './errors.js';
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export type
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
55
|
+
export declare const createHarnessBridge: (options: HarnessBridgeOptions) => Promise<HarnessBridge>;
|
|
36
56
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAkB,MAAM,IAAI,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,CAyIvB,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -1,133 +1,151 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
|
-
import {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
const
|
|
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('
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
49
|
+
bridgeLogger.debug('app connected');
|
|
50
|
+
const binaryStore = new BinaryStore();
|
|
51
|
+
const serverFunctions = {
|
|
52
|
+
reportReady: (device) => {
|
|
53
|
+
const conn = {
|
|
54
|
+
device,
|
|
55
|
+
runTests: (testPath, opts) => rpc.runTests(testPath, opts),
|
|
56
|
+
};
|
|
57
|
+
currentConnection = conn;
|
|
58
|
+
bridgeLogger.debug('app ready: platform=%s model=%s', device.platform, device.model);
|
|
59
|
+
emitter.emit('connected', conn);
|
|
60
|
+
for (const { resolve } of connectionWaiters.splice(0))
|
|
61
|
+
resolve(conn);
|
|
62
|
+
},
|
|
63
|
+
emitEvent: (_, data) => {
|
|
64
|
+
emitter.emit('event', data);
|
|
65
|
+
},
|
|
66
|
+
'device.screenshot.receive': (ref) => receiveScreenshot(binaryStore, ref),
|
|
67
|
+
'test.matchImageSnapshot': (screenshot, testPath, opts) => matchImageSnapshot(screenshot, testPath, opts, context.platform.name),
|
|
68
|
+
};
|
|
69
|
+
const rpc = createBirpc(serverFunctions, {
|
|
70
|
+
post: (data) => ws.send(data),
|
|
71
|
+
on: (handler) => {
|
|
72
|
+
ws.on('message', (msg, isBinary) => {
|
|
73
|
+
if (isBinary) {
|
|
74
|
+
try {
|
|
75
|
+
const messageBuffer = Array.isArray(msg)
|
|
76
|
+
? Buffer.concat(msg)
|
|
77
|
+
: Buffer.isBuffer(msg)
|
|
78
|
+
? msg
|
|
79
|
+
: Buffer.from(msg);
|
|
80
|
+
const { transferId, data } = parseBinaryFrame(new Uint8Array(messageBuffer));
|
|
81
|
+
binaryStore.add(transferId, data);
|
|
106
82
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
83
|
+
catch (err) {
|
|
84
|
+
bridgeLogger.warn('failed to parse binary frame: %s', err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
handler(msg.toString());
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
serialize,
|
|
93
|
+
deserialize,
|
|
94
|
+
timeout,
|
|
95
|
+
onFunctionError: (error, functionName, args) => {
|
|
96
|
+
bridgeLogger.error('rpc function failed: %s args=%o', functionName, args);
|
|
97
|
+
throw error;
|
|
98
|
+
},
|
|
99
|
+
onTimeoutError: (fn, args) => {
|
|
100
|
+
throw new DeviceNotRespondingError(fn, args);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
ws.on('close', () => {
|
|
104
|
+
bridgeLogger.debug('app disconnected');
|
|
105
|
+
binaryStore.dispose();
|
|
106
|
+
currentConnection = null;
|
|
107
|
+
emitter.emit('disconnected');
|
|
114
108
|
});
|
|
115
109
|
});
|
|
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
110
|
return {
|
|
126
|
-
ws
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
111
|
+
get ws() {
|
|
112
|
+
return wss;
|
|
113
|
+
},
|
|
114
|
+
get connection() {
|
|
115
|
+
return currentConnection;
|
|
116
|
+
},
|
|
117
|
+
nextConnection: (signal) => {
|
|
118
|
+
if (signal?.aborted) {
|
|
119
|
+
return Promise.reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
120
|
+
}
|
|
121
|
+
// If the app already connected before this call (e.g. fast simulator
|
|
122
|
+
// startup between startAttempt and waitForReady), return it immediately
|
|
123
|
+
// rather than waiting for a second reportReady that will never come.
|
|
124
|
+
if (currentConnection) {
|
|
125
|
+
return Promise.resolve(currentConnection);
|
|
126
|
+
}
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const entry = { resolve, reject };
|
|
129
|
+
connectionWaiters.push(entry);
|
|
130
|
+
signal?.addEventListener('abort', () => {
|
|
131
|
+
const idx = connectionWaiters.indexOf(entry);
|
|
132
|
+
if (idx !== -1)
|
|
133
|
+
connectionWaiters.splice(idx, 1);
|
|
134
|
+
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
135
|
+
}, { once: true });
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
on: (event, listener) => emitter.on(event, listener),
|
|
139
|
+
off: (event, listener) => emitter.off(event, listener),
|
|
140
|
+
dispose: () => {
|
|
141
|
+
bridgeLogger.debug('disposing bridge');
|
|
142
|
+
for (const { reject } of connectionWaiters.splice(0)) {
|
|
143
|
+
reject(new Error('Bridge disposed'));
|
|
144
|
+
}
|
|
145
|
+
for (const client of wss.clients)
|
|
146
|
+
client.terminate();
|
|
147
|
+
wss.close();
|
|
148
|
+
emitter.removeAllListeners();
|
|
149
|
+
},
|
|
132
150
|
};
|
|
133
151
|
};
|