@react-native-harness/bridge 1.3.0 → 1.4.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/__tests__/client.test.d.ts +2 -0
- package/dist/__tests__/client.test.d.ts.map +1 -0
- package/dist/__tests__/client.test.js +81 -0
- package/dist/__tests__/heartbeat.test.d.ts +2 -0
- package/dist/__tests__/heartbeat.test.d.ts.map +1 -0
- package/dist/__tests__/heartbeat.test.js +37 -0
- package/dist/__tests__/protocol.test.d.ts +2 -0
- package/dist/__tests__/protocol.test.d.ts.map +1 -0
- package/dist/__tests__/protocol.test.js +37 -0
- package/dist/__tests__/rpc-peer.test.d.ts +2 -0
- package/dist/__tests__/rpc-peer.test.d.ts.map +1 -0
- package/dist/__tests__/rpc-peer.test.js +144 -0
- package/dist/client.d.ts +8 -14
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +96 -42
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +23 -0
- package/dist/heartbeat.d.ts +13 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +49 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/protocol.d.ts +51 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +132 -0
- package/dist/rpc-peer.d.ts +28 -0
- package/dist/rpc-peer.d.ts.map +1 -0
- package/dist/rpc-peer.js +149 -0
- package/dist/server.d.ts +2 -20
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +123 -71
- package/dist/shared/test-collector.d.ts +2 -0
- package/dist/shared/test-collector.d.ts.map +1 -1
- package/dist/shared/test-runner.d.ts +23 -0
- package/dist/shared/test-runner.d.ts.map +1 -1
- package/dist/shared.d.ts +2 -2
- package/dist/shared.d.ts.map +1 -1
- package/dist/transport.d.ts +22 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +22 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/websocket-client-transport.d.ts +3 -0
- package/dist/websocket-client-transport.d.ts.map +1 -0
- package/dist/websocket-client-transport.js +45 -0
- package/dist/websocket-server-transport.d.ts +4 -0
- package/dist/websocket-server-transport.d.ts.map +1 -0
- package/dist/websocket-server-transport.js +43 -0
- package/eslint.config.mjs +5 -1
- package/package.json +6 -5
- package/src/__tests__/client.test.ts +99 -0
- package/src/__tests__/heartbeat.test.ts +47 -0
- package/src/__tests__/protocol.test.ts +51 -0
- package/src/__tests__/rpc-peer.test.ts +206 -0
- package/src/client.ts +147 -57
- package/src/errors.ts +32 -0
- package/src/heartbeat.ts +67 -0
- package/src/index.ts +1 -0
- package/src/protocol.ts +233 -0
- package/src/rpc-peer.ts +227 -0
- package/src/server.ts +181 -114
- package/src/shared/test-collector.ts +3 -0
- package/src/shared/test-runner.ts +24 -0
- package/src/shared.ts +2 -2
- package/src/transport.ts +47 -0
- package/src/websocket-client-transport.ts +85 -0
- package/src/websocket-server-transport.ts +54 -0
- package/vite.config.ts +18 -0
package/src/server.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { WebSocketServer, type WebSocket } from 'ws';
|
|
2
|
-
import { createBirpc, type BirpcReturn } from 'birpc';
|
|
3
2
|
import { EventEmitter } from 'node:events';
|
|
4
3
|
import type { Server as HttpServer } from 'node:http';
|
|
5
4
|
import type { Server as HttpsServer } from 'node:https';
|
|
@@ -9,9 +8,19 @@ import path from 'node:path';
|
|
|
9
8
|
import { randomUUID } from 'node:crypto';
|
|
10
9
|
import { logger } from '@react-native-harness/tools';
|
|
11
10
|
import { BinaryStore, parseBinaryFrame } from './binary-transfer.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
11
|
+
import {
|
|
12
|
+
AppBridgeDisconnectedError,
|
|
13
|
+
DeviceNotRespondingError,
|
|
14
|
+
} from './errors.js';
|
|
15
|
+
import { createHeartbeat } from './heartbeat.js';
|
|
14
16
|
import { matchImageSnapshot } from './image-snapshot.js';
|
|
17
|
+
import { serializeBridgeMessage } from './protocol.js';
|
|
18
|
+
import { createRpcPeer } from './rpc-peer.js';
|
|
19
|
+
import {
|
|
20
|
+
createRpcTransport,
|
|
21
|
+
type BridgeTransport,
|
|
22
|
+
} from './transport.js';
|
|
23
|
+
import { createNodeWebSocketTransport } from './websocket-server-transport.js';
|
|
15
24
|
import type {
|
|
16
25
|
BridgeServerFunctions,
|
|
17
26
|
BridgeClientFunctions,
|
|
@@ -24,29 +33,23 @@ import type {
|
|
|
24
33
|
TestSuiteResult,
|
|
25
34
|
} from './shared.js';
|
|
26
35
|
|
|
27
|
-
export {
|
|
36
|
+
export {
|
|
37
|
+
AppBridgeDisconnectedError,
|
|
38
|
+
DeviceNotRespondingError,
|
|
39
|
+
} from './errors.js';
|
|
28
40
|
|
|
29
41
|
const bridgeLogger = logger.child('bridge');
|
|
42
|
+
const noop = (): void => undefined;
|
|
30
43
|
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Public types
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Represents a single app session — one app launch to the next restart.
|
|
37
|
-
* Obtained via HarnessBridge.nextConnection().
|
|
38
|
-
*/
|
|
39
44
|
export type AppConnection = {
|
|
40
45
|
readonly device: DeviceDescriptor;
|
|
41
46
|
runTests: (path: string, options: TestExecutionOptions) => Promise<TestSuiteResult>;
|
|
47
|
+
resetEnvironment: () => Promise<void>;
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
export type HarnessBridgeEvents = {
|
|
45
|
-
/** Fired when the app connects and calls reportReady. */
|
|
46
51
|
connected: (connection: AppConnection) => void;
|
|
47
|
-
/** Fired when the app's WebSocket closes. */
|
|
48
52
|
disconnected: () => void;
|
|
49
|
-
/** Fired for every test/bundler event the app emits. */
|
|
50
53
|
event: (event: BridgeEvents) => void;
|
|
51
54
|
};
|
|
52
55
|
|
|
@@ -60,31 +63,15 @@ export type HarnessBridgeOptions = TransportOptions & {
|
|
|
60
63
|
context: HarnessContext;
|
|
61
64
|
};
|
|
62
65
|
|
|
63
|
-
/**
|
|
64
|
-
* The persistent CLI-side bridge. Spans the full test run regardless of how
|
|
65
|
-
* many times the app is restarted. Each restart produces a new AppConnection
|
|
66
|
-
* via nextConnection().
|
|
67
|
-
*/
|
|
68
66
|
export type HarnessBridge = {
|
|
69
|
-
/** The underlying WebSocket server, used to attach to Metro's HTTP server. */
|
|
70
67
|
readonly ws: WebSocketServer;
|
|
71
|
-
/** The currently active app connection, null if the app is not connected. */
|
|
72
68
|
readonly connection: AppConnection | null;
|
|
73
|
-
/**
|
|
74
|
-
* Resolves with the next AppConnection once the app connects and reports
|
|
75
|
-
* ready. Register this waiter before restarting the app so no ready signal
|
|
76
|
-
* is missed. Rejects if the supplied signal is aborted.
|
|
77
|
-
*/
|
|
78
69
|
nextConnection: (signal?: AbortSignal) => Promise<AppConnection>;
|
|
79
70
|
on: <T extends keyof HarnessBridgeEvents>(event: T, listener: HarnessBridgeEvents[T]) => void;
|
|
80
71
|
off: <T extends keyof HarnessBridgeEvents>(event: T, listener: HarnessBridgeEvents[T]) => void;
|
|
81
72
|
dispose: () => void;
|
|
82
73
|
};
|
|
83
74
|
|
|
84
|
-
// ---------------------------------------------------------------------------
|
|
85
|
-
// Helpers
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
|
|
88
75
|
const createWss = (transport: TransportOptions): Promise<WebSocketServer> => {
|
|
89
76
|
if ('port' in transport) {
|
|
90
77
|
return new Promise<WebSocketServer>((resolve) => {
|
|
@@ -94,6 +81,7 @@ const createWss = (transport: TransportOptions): Promise<WebSocketServer> => {
|
|
|
94
81
|
);
|
|
95
82
|
});
|
|
96
83
|
}
|
|
84
|
+
|
|
97
85
|
return Promise.resolve<WebSocketServer>(
|
|
98
86
|
new WebSocketServer(
|
|
99
87
|
'server' in transport
|
|
@@ -113,124 +101,194 @@ const receiveScreenshot = async (
|
|
|
113
101
|
`Binary data for transfer ${reference.transferId} not found or expired`,
|
|
114
102
|
);
|
|
115
103
|
}
|
|
104
|
+
|
|
116
105
|
binaryStore.delete(reference.transferId);
|
|
117
106
|
const file = path.join(os.tmpdir(), `harness-screenshot-${randomUUID()}.png`);
|
|
118
107
|
await fs.writeFile(file, data);
|
|
119
108
|
return { path: file };
|
|
120
109
|
};
|
|
121
110
|
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
// Factory
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
|
|
126
111
|
export const createHarnessBridge = async (
|
|
127
112
|
options: HarnessBridgeOptions,
|
|
128
113
|
): Promise<HarnessBridge> => {
|
|
129
|
-
const { timeout, context, ...
|
|
130
|
-
const wss = await createWss(
|
|
114
|
+
const { timeout, context, ...transportOptions } = options;
|
|
115
|
+
const wss = await createWss(transportOptions);
|
|
131
116
|
bridgeLogger.debug('bridge server ready');
|
|
132
117
|
|
|
133
118
|
const emitter = new EventEmitter();
|
|
134
119
|
let currentConnection: AppConnection | null = null;
|
|
120
|
+
let activeSession: { disconnect: (reason?: Error) => void; transport: BridgeTransport } | null =
|
|
121
|
+
null;
|
|
135
122
|
const connectionWaiters: Array<{
|
|
136
123
|
resolve: (c: AppConnection) => void;
|
|
137
124
|
reject: (e: unknown) => void;
|
|
138
125
|
}> = [];
|
|
139
126
|
|
|
140
127
|
wss.on('connection', (ws: WebSocket) => {
|
|
128
|
+
if (activeSession) {
|
|
129
|
+
bridgeLogger.info('replacing existing app connection with a newer client');
|
|
130
|
+
activeSession.disconnect(new AppBridgeDisconnectedError('app-replaced'));
|
|
131
|
+
}
|
|
132
|
+
|
|
141
133
|
bridgeLogger.debug('app connected');
|
|
134
|
+
const transport = createNodeWebSocketTransport(ws);
|
|
142
135
|
const binaryStore = new BinaryStore();
|
|
143
136
|
let readyConnection: AppConnection | null = null;
|
|
144
137
|
let disconnected = false;
|
|
138
|
+
let offMessage: () => void = noop;
|
|
139
|
+
let offClose: () => void = noop;
|
|
140
|
+
let offError: () => void = noop;
|
|
145
141
|
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
142
|
+
const closeTransport = () => {
|
|
143
|
+
if (transport.state === 'closing' || transport.state === 'closed') {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
transport.close(1012);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const rpc = createRpcPeer<
|
|
151
|
+
BridgeServerFunctions,
|
|
152
|
+
BridgeClientFunctions,
|
|
153
|
+
BridgeEvents
|
|
154
|
+
>({
|
|
155
|
+
localMethods: {
|
|
156
|
+
'device.screenshot.receive': (ref) => receiveScreenshot(binaryStore, ref),
|
|
157
|
+
'test.matchImageSnapshot': (screenshot, testPath, opts) =>
|
|
158
|
+
matchImageSnapshot(screenshot, testPath, opts, context.platform.name),
|
|
161
159
|
},
|
|
162
|
-
|
|
163
|
-
|
|
160
|
+
transport: createRpcTransport(transport),
|
|
161
|
+
onEvent: (event) => {
|
|
162
|
+
emitter.emit('event', event);
|
|
164
163
|
},
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
callTimeoutMs: (method) => method === 'runTests' ? undefined : timeout,
|
|
165
|
+
createTimeoutError: (functionName, args) => {
|
|
166
|
+
return new DeviceNotRespondingError(functionName, args) as unknown as Error;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
169
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
post: (data) => ws.send(data),
|
|
174
|
-
on: (handler) => {
|
|
175
|
-
ws.on(
|
|
176
|
-
'message',
|
|
177
|
-
(msg: Buffer | ArrayBuffer | Buffer[], isBinary: boolean) => {
|
|
178
|
-
if (isBinary) {
|
|
179
|
-
try {
|
|
180
|
-
const messageBuffer = Array.isArray(msg)
|
|
181
|
-
? Buffer.concat(msg)
|
|
182
|
-
: Buffer.isBuffer(msg)
|
|
183
|
-
? msg
|
|
184
|
-
: Buffer.from(msg);
|
|
185
|
-
const { transferId, data } = parseBinaryFrame(
|
|
186
|
-
new Uint8Array(messageBuffer),
|
|
187
|
-
);
|
|
188
|
-
binaryStore.add(transferId, data);
|
|
189
|
-
} catch (err) {
|
|
190
|
-
bridgeLogger.warn('failed to parse binary frame: %s', err);
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
handler(msg.toString());
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
);
|
|
197
|
-
},
|
|
198
|
-
serialize,
|
|
199
|
-
deserialize,
|
|
200
|
-
timeout,
|
|
201
|
-
onFunctionError: (error, functionName, args) => {
|
|
202
|
-
bridgeLogger.error(
|
|
203
|
-
'rpc function failed: %s args=%o',
|
|
204
|
-
functionName,
|
|
205
|
-
args,
|
|
206
|
-
);
|
|
207
|
-
throw error;
|
|
208
|
-
},
|
|
209
|
-
onTimeoutError: (fn, args) => {
|
|
210
|
-
throw new DeviceNotRespondingError(fn, args);
|
|
211
|
-
},
|
|
170
|
+
const heartbeat = createHeartbeat({
|
|
171
|
+
sendPing: (id) => {
|
|
172
|
+
transport.send(serializeBridgeMessage({ type: 'ping', id }));
|
|
212
173
|
},
|
|
213
|
-
|
|
174
|
+
onTimeout: () => {
|
|
175
|
+
bridgeLogger.warn('app heartbeat timed out');
|
|
176
|
+
disconnect(new AppBridgeDisconnectedError('heartbeat-timeout'));
|
|
177
|
+
},
|
|
178
|
+
});
|
|
214
179
|
|
|
215
180
|
const disconnect = (reason?: Error) => {
|
|
216
|
-
if (disconnected)
|
|
217
|
-
|
|
181
|
+
if (disconnected) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
218
184
|
|
|
185
|
+
disconnected = true;
|
|
186
|
+
offMessage();
|
|
187
|
+
offClose();
|
|
188
|
+
offError();
|
|
219
189
|
bridgeLogger.debug('app disconnected');
|
|
190
|
+
heartbeat.dispose();
|
|
220
191
|
binaryStore.dispose();
|
|
192
|
+
|
|
193
|
+
if (activeSession?.transport === transport) {
|
|
194
|
+
activeSession = null;
|
|
195
|
+
}
|
|
196
|
+
|
|
221
197
|
if (currentConnection === readyConnection) {
|
|
222
198
|
currentConnection = null;
|
|
223
199
|
}
|
|
224
|
-
|
|
225
|
-
|
|
200
|
+
|
|
201
|
+
rpc.close(reason ?? new AppBridgeDisconnectedError('app-disconnected'));
|
|
202
|
+
closeTransport();
|
|
203
|
+
|
|
204
|
+
if (readyConnection) {
|
|
205
|
+
emitter.emit('disconnected');
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
activeSession = { disconnect, transport };
|
|
210
|
+
|
|
211
|
+
const handleControlMessage = async (message: string) => {
|
|
212
|
+
const controlMessage = await rpc.handleMessage(message);
|
|
213
|
+
|
|
214
|
+
if (!controlMessage) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
switch (controlMessage.type) {
|
|
219
|
+
case 'ready': {
|
|
220
|
+
if (readyConnection) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const conn: AppConnection = {
|
|
225
|
+
device: controlMessage.device,
|
|
226
|
+
runTests: (testPath, opts) => rpc.invoke('runTests', testPath, opts),
|
|
227
|
+
resetEnvironment: () => rpc.invoke('resetEnvironment'),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
readyConnection = conn;
|
|
231
|
+
currentConnection = conn;
|
|
232
|
+
bridgeLogger.debug(
|
|
233
|
+
'app ready: platform=%s model=%s',
|
|
234
|
+
controlMessage.device.platform,
|
|
235
|
+
controlMessage.device.model,
|
|
236
|
+
);
|
|
237
|
+
emitter.emit('connected', conn);
|
|
238
|
+
|
|
239
|
+
for (const { resolve } of connectionWaiters.splice(0)) {
|
|
240
|
+
resolve(conn);
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
case 'ping': {
|
|
245
|
+
transport.send(
|
|
246
|
+
serializeBridgeMessage({ type: 'pong', id: controlMessage.id }),
|
|
247
|
+
);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
case 'pong': {
|
|
251
|
+
heartbeat.notifyPong(controlMessage.id);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
226
255
|
};
|
|
227
256
|
|
|
228
|
-
|
|
257
|
+
const handleBinaryMessage = (message: Uint8Array) => {
|
|
258
|
+
try {
|
|
259
|
+
const { transferId, data } = parseBinaryFrame(message);
|
|
260
|
+
binaryStore.add(transferId, data);
|
|
261
|
+
} catch (error) {
|
|
262
|
+
bridgeLogger.warn('failed to parse binary frame: %s', error);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
offMessage = transport.onMessage((message) => {
|
|
267
|
+
if (typeof message !== 'string') {
|
|
268
|
+
handleBinaryMessage(message);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
void handleControlMessage(message).catch((error) => {
|
|
273
|
+
bridgeLogger.warn('failed to handle bridge message: %s', error);
|
|
274
|
+
disconnect(
|
|
275
|
+
error instanceof Error
|
|
276
|
+
? error
|
|
277
|
+
: new Error('Received invalid app bridge message'),
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
offClose = transport.onClose(() => {
|
|
229
283
|
disconnect();
|
|
230
284
|
});
|
|
231
285
|
|
|
232
|
-
|
|
233
|
-
disconnect(
|
|
286
|
+
offError = transport.onError((error) => {
|
|
287
|
+
disconnect(
|
|
288
|
+
error instanceof Error
|
|
289
|
+
? error
|
|
290
|
+
: new AppBridgeDisconnectedError('socket-error'),
|
|
291
|
+
);
|
|
234
292
|
});
|
|
235
293
|
});
|
|
236
294
|
|
|
@@ -247,12 +305,11 @@ export const createHarnessBridge = async (
|
|
|
247
305
|
signal.reason ?? new DOMException('Aborted', 'AbortError'),
|
|
248
306
|
);
|
|
249
307
|
}
|
|
250
|
-
|
|
251
|
-
// startup between startAttempt and waitForReady), return it immediately
|
|
252
|
-
// rather than waiting for a second reportReady that will never come.
|
|
308
|
+
|
|
253
309
|
if (currentConnection) {
|
|
254
310
|
return Promise.resolve(currentConnection);
|
|
255
311
|
}
|
|
312
|
+
|
|
256
313
|
return new Promise((resolve, reject) => {
|
|
257
314
|
const entry = { resolve, reject };
|
|
258
315
|
connectionWaiters.push(entry);
|
|
@@ -260,7 +317,10 @@ export const createHarnessBridge = async (
|
|
|
260
317
|
'abort',
|
|
261
318
|
() => {
|
|
262
319
|
const idx = connectionWaiters.indexOf(entry);
|
|
263
|
-
if (idx !== -1)
|
|
320
|
+
if (idx !== -1) {
|
|
321
|
+
connectionWaiters.splice(idx, 1);
|
|
322
|
+
}
|
|
323
|
+
|
|
264
324
|
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
265
325
|
},
|
|
266
326
|
{ once: true },
|
|
@@ -271,10 +331,17 @@ export const createHarnessBridge = async (
|
|
|
271
331
|
off: (event, listener) => emitter.off(event, listener),
|
|
272
332
|
dispose: () => {
|
|
273
333
|
bridgeLogger.debug('disposing bridge');
|
|
334
|
+
|
|
274
335
|
for (const { reject } of connectionWaiters.splice(0)) {
|
|
275
|
-
reject(new
|
|
336
|
+
reject(new AppBridgeDisconnectedError('bridge-disposed'));
|
|
276
337
|
}
|
|
277
|
-
|
|
338
|
+
|
|
339
|
+
activeSession?.disconnect(new AppBridgeDisconnectedError('bridge-disposed'));
|
|
340
|
+
|
|
341
|
+
for (const client of wss.clients) {
|
|
342
|
+
client.terminate();
|
|
343
|
+
}
|
|
344
|
+
|
|
278
345
|
wss.close();
|
|
279
346
|
emitter.removeAllListeners();
|
|
280
347
|
},
|
|
@@ -2,6 +2,8 @@ import type { HarnessTestContext } from './test-context.js';
|
|
|
2
2
|
|
|
3
3
|
export type TestStatus = 'active' | 'skipped' | 'todo';
|
|
4
4
|
|
|
5
|
+
export type TestDeclarationMode = 'only' | 'skip' | 'todo';
|
|
6
|
+
|
|
5
7
|
export type TestFn = (context: HarnessTestContext) => void | Promise<void>;
|
|
6
8
|
|
|
7
9
|
export type SuiteHookFn = () => void | Promise<void>;
|
|
@@ -10,6 +12,7 @@ export type TestCase = {
|
|
|
10
12
|
name: string;
|
|
11
13
|
fn: TestFn;
|
|
12
14
|
status: TestStatus;
|
|
15
|
+
declarationMode?: TestDeclarationMode;
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
export type TestSuite = {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { TestDeclarationMode } from './test-collector.js';
|
|
2
|
+
|
|
1
3
|
export type CodeFrame = {
|
|
2
4
|
content: string;
|
|
3
5
|
location?: {
|
|
@@ -13,6 +15,16 @@ export type SerializedError = {
|
|
|
13
15
|
name: string;
|
|
14
16
|
message: string;
|
|
15
17
|
codeFrame?: CodeFrame;
|
|
18
|
+
diagnostics?: {
|
|
19
|
+
pendingPromises?: {
|
|
20
|
+
total: number;
|
|
21
|
+
items: Array<{
|
|
22
|
+
id: number;
|
|
23
|
+
createdAt: number;
|
|
24
|
+
stack?: string;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
16
28
|
};
|
|
17
29
|
|
|
18
30
|
export type TestRunnerFileStartedEvent = {
|
|
@@ -37,6 +49,10 @@ export type TestRunnerTestStartedEvent = {
|
|
|
37
49
|
name: string;
|
|
38
50
|
suite: string;
|
|
39
51
|
file: string;
|
|
52
|
+
ancestorTitles: string[];
|
|
53
|
+
fullName: string;
|
|
54
|
+
startedAt: number;
|
|
55
|
+
declarationMode?: TestDeclarationMode;
|
|
40
56
|
};
|
|
41
57
|
|
|
42
58
|
export type TestRunnerTestFinishedEvent = {
|
|
@@ -44,6 +60,10 @@ export type TestRunnerTestFinishedEvent = {
|
|
|
44
60
|
name: string;
|
|
45
61
|
suite: string;
|
|
46
62
|
file: string;
|
|
63
|
+
ancestorTitles: string[];
|
|
64
|
+
fullName: string;
|
|
65
|
+
startedAt: number;
|
|
66
|
+
declarationMode?: TestDeclarationMode;
|
|
47
67
|
duration: number;
|
|
48
68
|
error?: SerializedError;
|
|
49
69
|
status: TestResultStatus;
|
|
@@ -71,6 +91,10 @@ export type TestResult = {
|
|
|
71
91
|
status: TestResultStatus;
|
|
72
92
|
error?: SerializedError;
|
|
73
93
|
duration: number;
|
|
94
|
+
ancestorTitles?: string[];
|
|
95
|
+
fullName?: string;
|
|
96
|
+
startedAt?: number;
|
|
97
|
+
declarationMode?: TestDeclarationMode;
|
|
74
98
|
};
|
|
75
99
|
|
|
76
100
|
export type TestSuiteResult = {
|
package/src/shared.ts
CHANGED
|
@@ -134,6 +134,7 @@ export type TestExecutionOptions = {
|
|
|
134
134
|
testNamePattern?: string;
|
|
135
135
|
setupFiles?: string[];
|
|
136
136
|
setupFilesAfterEnv?: string[];
|
|
137
|
+
testTimeout?: number;
|
|
137
138
|
runner: string;
|
|
138
139
|
};
|
|
139
140
|
|
|
@@ -142,6 +143,7 @@ export type BridgeClientFunctions = {
|
|
|
142
143
|
path: string,
|
|
143
144
|
options: TestExecutionOptions
|
|
144
145
|
) => Promise<TestSuiteResult>;
|
|
146
|
+
resetEnvironment: () => Promise<void>;
|
|
145
147
|
};
|
|
146
148
|
|
|
147
149
|
export type BinaryDataReference = {
|
|
@@ -154,8 +156,6 @@ export type BinaryDataReference = {
|
|
|
154
156
|
export type ScreenshotData = BinaryDataReference;
|
|
155
157
|
|
|
156
158
|
export type BridgeServerFunctions = {
|
|
157
|
-
reportReady: (device: DeviceDescriptor) => void;
|
|
158
|
-
emitEvent: (event: BridgeEvents['type'], data: BridgeEvents) => void;
|
|
159
159
|
'device.screenshot.receive': (
|
|
160
160
|
reference: BinaryDataReference,
|
|
161
161
|
metadata: { width: number; height: number }
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type BridgeTransportState = 'connecting' | 'open' | 'closing' | 'closed';
|
|
2
|
+
|
|
3
|
+
export type BridgeTransportMessage = string | Uint8Array;
|
|
4
|
+
|
|
5
|
+
export type BridgeTransportCloseEvent = {
|
|
6
|
+
code: number;
|
|
7
|
+
reason: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type BridgeTransport = {
|
|
11
|
+
readonly state: BridgeTransportState;
|
|
12
|
+
send: (message: BridgeTransportMessage) => void;
|
|
13
|
+
close: (code?: number, reason?: string) => void;
|
|
14
|
+
onOpen: (listener: () => void) => () => void;
|
|
15
|
+
onMessage: (listener: (message: BridgeTransportMessage) => void) => () => void;
|
|
16
|
+
onClose: (listener: (event: BridgeTransportCloseEvent) => void) => () => void;
|
|
17
|
+
onError: (listener: (error: Error) => void) => () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type RpcTransport = {
|
|
21
|
+
readonly state: BridgeTransportState;
|
|
22
|
+
send: (message: string) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const toTransportState = (readyState: number): BridgeTransportState => {
|
|
26
|
+
switch (readyState) {
|
|
27
|
+
case 0:
|
|
28
|
+
return 'connecting';
|
|
29
|
+
case 1:
|
|
30
|
+
return 'open';
|
|
31
|
+
case 2:
|
|
32
|
+
return 'closing';
|
|
33
|
+
default:
|
|
34
|
+
return 'closed';
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const createRpcTransport = (transport: BridgeTransport): RpcTransport => {
|
|
39
|
+
return {
|
|
40
|
+
get state() {
|
|
41
|
+
return transport.state;
|
|
42
|
+
},
|
|
43
|
+
send: (message) => {
|
|
44
|
+
transport.send(message);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
toTransportState,
|
|
3
|
+
type BridgeTransport,
|
|
4
|
+
} from './transport.js';
|
|
5
|
+
|
|
6
|
+
type BrowserWebSocketLike = {
|
|
7
|
+
readonly readyState: number;
|
|
8
|
+
binaryType: BinaryType;
|
|
9
|
+
send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
|
|
10
|
+
close: (code?: number, reason?: string) => void;
|
|
11
|
+
addEventListener: {
|
|
12
|
+
(type: 'open', listener: () => void): void;
|
|
13
|
+
(
|
|
14
|
+
type: 'message',
|
|
15
|
+
listener: (event: MessageEvent<string | ArrayBuffer>) => void,
|
|
16
|
+
): void;
|
|
17
|
+
(type: 'close', listener: (event: CloseEvent) => void): void;
|
|
18
|
+
(
|
|
19
|
+
type: 'error',
|
|
20
|
+
listener: (event: Event & { message?: string }) => void,
|
|
21
|
+
): void;
|
|
22
|
+
};
|
|
23
|
+
removeEventListener: {
|
|
24
|
+
(type: 'open', listener: () => void): void;
|
|
25
|
+
(
|
|
26
|
+
type: 'message',
|
|
27
|
+
listener: (event: MessageEvent<string | ArrayBuffer>) => void,
|
|
28
|
+
): void;
|
|
29
|
+
(type: 'close', listener: (event: CloseEvent) => void): void;
|
|
30
|
+
(
|
|
31
|
+
type: 'error',
|
|
32
|
+
listener: (event: Event & { message?: string }) => void,
|
|
33
|
+
): void;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const createWebSocketClientTransport = (url: string): BridgeTransport => {
|
|
38
|
+
const socket = new WebSocket(url) as BrowserWebSocketLike;
|
|
39
|
+
socket.binaryType = 'arraybuffer';
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
get state() {
|
|
43
|
+
return toTransportState(socket.readyState);
|
|
44
|
+
},
|
|
45
|
+
send: (message) => {
|
|
46
|
+
socket.send(message);
|
|
47
|
+
},
|
|
48
|
+
close: (code, reason) => {
|
|
49
|
+
socket.close(code, reason);
|
|
50
|
+
},
|
|
51
|
+
onOpen: (listener) => {
|
|
52
|
+
socket.addEventListener('open', listener);
|
|
53
|
+
return () => socket.removeEventListener('open', listener);
|
|
54
|
+
},
|
|
55
|
+
onMessage: (listener) => {
|
|
56
|
+
const handleMessage = (event: MessageEvent<string | ArrayBuffer>) => {
|
|
57
|
+
if (typeof event.data === 'string') {
|
|
58
|
+
listener(event.data);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
listener(new Uint8Array(event.data));
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
socket.addEventListener('message', handleMessage);
|
|
66
|
+
return () => socket.removeEventListener('message', handleMessage);
|
|
67
|
+
},
|
|
68
|
+
onClose: (listener) => {
|
|
69
|
+
const handleClose = (event: CloseEvent) => {
|
|
70
|
+
listener({ code: event.code, reason: event.reason });
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
socket.addEventListener('close', handleClose);
|
|
74
|
+
return () => socket.removeEventListener('close', handleClose);
|
|
75
|
+
},
|
|
76
|
+
onError: (listener) => {
|
|
77
|
+
const handleError = (event: Event & { message?: string }) => {
|
|
78
|
+
listener(new Error(event.message || 'Harness connection error'));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
socket.addEventListener('error', handleError);
|
|
82
|
+
return () => socket.removeEventListener('error', handleError);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
};
|