Skip to content

Commit 771ac83

Browse files
authored
refactor: simplify Harness architecture for safer evolution (#114)
## Summary Introduce a set of architectural refactorings that make Harness easier to understand, extend, and change safely without regressing existing behavior. ## Context This branch restructures the Jest, bridge, and runtime boundaries, replaces global-state and ad hoc coupling with explicit session/domain APIs, and adds coverage around the new abstractions. The goal is to make future changes easier to reason about and to reduce the risk of breaking previous behavior. ## Proposed Testing Scenario Review the refactor across the Harness session, bridge, and runtime layers, then run the Jest harness test suite and a representative end-to-end path to confirm behavior is unchanged.
1 parent a5570bf commit 771ac83

40 files changed

Lines changed: 2655 additions & 3356 deletions

packages/babel-preset/src/resolve-weak-plugin.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ const FUNCTIONS_REQUIRING_RESOLVE_WEAK = [
66
'unmock',
77
'requireActual',
88
] as const;
9+
type ResolveWeakFunctionName = (typeof FUNCTIONS_REQUIRING_RESOLVE_WEAK)[number];
10+
const isResolveWeakFunctionName = (
11+
value: string
12+
): value is ResolveWeakFunctionName =>
13+
FUNCTIONS_REQUIRING_RESOLVE_WEAK.includes(value as ResolveWeakFunctionName);
914

1015
const resolveWeakPlugin = ({
1116
types: t,
@@ -41,7 +46,7 @@ const resolveWeakPlugin = ({
4146
// Only transform if the function was imported from react-native-harness
4247
if (
4348
importedNames.has(functionName) &&
44-
FUNCTIONS_REQUIRING_RESOLVE_WEAK.includes(functionName as any)
49+
isResolveWeakFunctionName(functionName)
4550
) {
4651
const firstArg = node.arguments[0];
4752

packages/bridge/src/binary-transfer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function parseBinaryFrame(frame: Uint8Array): {
3232

3333
export class BinaryStore {
3434
private store = new Map<number, Uint8Array>();
35-
private timeouts = new Map<number, any>();
35+
private timeouts = new Map<number, ReturnType<typeof setTimeout>>();
3636
// 5 minutes timeout for binary data
3737
private readonly TIMEOUT_MS = 5 * 60 * 1000;
3838

packages/bridge/src/client.ts

Lines changed: 80 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,63 @@
1-
import { BirpcReturn, createBirpc } from 'birpc';
2-
import type { BridgeClientFunctions, BridgeServerFunctions } from './shared.js';
1+
import { createBirpc } from 'birpc';
32
import { deserialize, serialize } from './serializer.js';
4-
import { createBinaryFrame } from './binary-transfer.js';
3+
import { createBinaryFrame, generateTransferId } from './binary-transfer.js';
4+
import type {
5+
BridgeClientFunctions,
6+
BridgeServerFunctions,
7+
DeviceDescriptor,
8+
BridgeEvents,
9+
FileReference,
10+
ImageSnapshotOptions,
11+
TestExecutionOptions,
12+
TestSuiteResult,
13+
} from './shared.js';
514

6-
export type BridgeClient = {
7-
rpc: BirpcReturn<BridgeServerFunctions, BridgeClientFunctions>;
15+
// ---------------------------------------------------------------------------
16+
// Public types
17+
// ---------------------------------------------------------------------------
18+
19+
/** Handlers the app must implement for the CLI to call into. */
20+
export type HarnessCallbacks = {
21+
runTests: (path: string, options: TestExecutionOptions) => Promise<TestSuiteResult>;
22+
};
23+
24+
/** The app-side handle returned by connectToHarness. */
25+
export type HarnessHandle = {
26+
/** Call once when the app is initialised and ready to run tests. */
27+
reportReady: (device: DeviceDescriptor) => void;
28+
/** Forward a test or bundler event to the CLI. */
29+
emitEvent: (event: BridgeEvents) => void;
30+
/** Send a screenshot to the CLI and receive a file reference for snapshot comparison. */
31+
transferScreenshot: (
32+
data: Uint8Array,
33+
metadata: { width: number; height: number },
34+
) => Promise<FileReference>;
35+
/** Request an image snapshot comparison on the CLI. */
36+
matchImageSnapshot: (
37+
screenshot: FileReference,
38+
testPath: string,
39+
options: ImageSnapshotOptions,
40+
runner: string,
41+
) => Promise<{ pass: boolean; message: string }>;
842
disconnect: () => void;
9-
sendBinary: (transferId: number, data: Uint8Array) => void;
1043
};
1144

12-
const getBridgeClient = async (
45+
// ---------------------------------------------------------------------------
46+
// Factory
47+
// ---------------------------------------------------------------------------
48+
49+
/**
50+
* Connect the app to the CLI harness bridge.
51+
*
52+
* Pass the handlers the CLI can call (runTests). Returns a HarnessHandle
53+
* exposing the operations the app needs to drive a test run. The binary
54+
* transfer protocol and RPC wiring are fully encapsulated.
55+
*/
56+
export const connectToHarness = (
1357
url: string,
14-
handlers: BridgeClientFunctions,
15-
): Promise<BridgeClient> => {
16-
return new Promise((resolve, reject) => {
58+
callbacks: HarnessCallbacks,
59+
): Promise<HarnessHandle> =>
60+
new Promise((resolve, reject) => {
1761
const ws = new WebSocket(url);
1862
ws.binaryType = 'arraybuffer';
1963
let settled = false;
@@ -24,11 +68,8 @@ const getBridgeClient = async (
2468
ws.removeEventListener('close', handleClose);
2569
};
2670

27-
const rejectConnection = (message: string) => {
28-
if (settled) {
29-
return;
30-
}
31-
71+
const fail = (message: string) => {
72+
if (settled) return;
3273
settled = true;
3374
cleanup();
3475
reject(new Error(message));
@@ -39,56 +80,53 @@ const getBridgeClient = async (
3980
cleanup();
4081

4182
const rpc = createBirpc<BridgeServerFunctions, BridgeClientFunctions>(
42-
handlers,
83+
callbacks,
4384
{
4485
post: (data) => ws.send(data),
4586
on: (handler) => {
46-
ws.addEventListener('message', (event: any) => {
47-
if (typeof event.data === 'string') {
48-
handler(event.data);
49-
}
87+
ws.addEventListener('message', (event: MessageEvent<string | ArrayBuffer>) => {
88+
if (typeof event.data === 'string') handler(event.data);
5089
});
5190
},
5291
serialize,
5392
deserialize,
5493
},
5594
);
5695

57-
const client: BridgeClient = {
58-
rpc,
59-
disconnect: () => {
60-
ws.close();
61-
},
62-
sendBinary: (transferId: number, data: Uint8Array) => {
63-
const frame = createBinaryFrame(transferId, data);
64-
ws.send(frame);
96+
resolve({
97+
reportReady: (device) => void rpc.reportReady(device),
98+
emitEvent: (event) => void rpc.emitEvent(event.type, event),
99+
transferScreenshot: async (data, metadata) => {
100+
const transferId = generateTransferId();
101+
ws.send(createBinaryFrame(transferId, data));
102+
return rpc['device.screenshot.receive'](
103+
{ type: 'binary', transferId, size: data.length, mimeType: 'image/png' },
104+
metadata,
105+
);
65106
},
66-
};
67-
68-
resolve(client);
107+
matchImageSnapshot: (screenshot, testPath, options, runner) =>
108+
rpc['test.matchImageSnapshot'](screenshot, testPath, options, runner),
109+
disconnect: () => ws.close(),
110+
});
69111
};
70112

71113
const handleError = (event: Event & { message?: string }) => {
72-
const reason =
73-
typeof event.message === 'string' && event.message.length > 0
114+
const detail =
115+
typeof event.message === 'string' && event.message
74116
? `: ${event.message}`
75117
: '';
76-
77-
rejectConnection(
78-
`Failed to connect to the Harness bridge at ${url}${reason}`,
79-
);
118+
fail(`Failed to connect to Harness at ${url}${detail}`);
80119
};
81120

82121
const handleClose = (event: CloseEvent) => {
83-
rejectConnection(
84-
`Harness bridge connection to ${url} closed before it became ready (code ${event.code}${event.reason ? `, reason: ${event.reason}` : ''})`,
122+
fail(
123+
`Harness connection at ${url} closed before becoming ready (code ${event.code}${
124+
event.reason ? `, reason: ${event.reason}` : ''
125+
})`,
85126
);
86127
};
87128

88129
ws.addEventListener('open', handleOpen);
89130
ws.addEventListener('error', handleError);
90131
ws.addEventListener('close', handleClose);
91132
});
92-
};
93-
94-
export { getBridgeClient };

0 commit comments

Comments
 (0)