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