Skip to content

Commit eae30ce

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/external-xctest
# Conflicts: # packages/platform-ios/src/xctest-agent.ts
2 parents eadbba0 + 7589bc7 commit eae30ce

59 files changed

Lines changed: 4102 additions & 3721 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
__default__: patch
3+
---
4+
5+
When `forwardClientLogs` is enabled, Harness continues attaching device `console` output to the active test result, while keeping the internal log-capture path simpler and easier to maintain.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
__default__: patch
3+
---
4+
5+
Harness now lets you build the iOS XCTest agent binary directly from the CLI, without starting a Harness test run. This makes it easier to prepare the agent artifact ahead of time for external injection workflows.

actions/shared/index.cjs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -633,8 +633,8 @@ function getErrorMap() {
633633

634634
// ../../node_modules/zod/dist/esm/v3/helpers/parseUtil.js
635635
var makeIssue = (params) => {
636-
const { data, path: path6, errorMaps, issueData } = params;
637-
const fullPath = [...path6, ...issueData.path || []];
636+
const { data, path: path7, errorMaps, issueData } = params;
637+
const fullPath = [...path7, ...issueData.path || []];
638638
const fullIssue = {
639639
...issueData,
640640
path: fullPath
@@ -750,11 +750,11 @@ var errorUtil;
750750

751751
// ../../node_modules/zod/dist/esm/v3/types.js
752752
var ParseInputLazyPath = class {
753-
constructor(parent, value, path6, key) {
753+
constructor(parent, value, path7, key) {
754754
this._cachedPath = [];
755755
this.parent = parent;
756756
this.data = value;
757-
this._path = path6;
757+
this._path = path7;
758758
this._key = key;
759759
}
760760
get path() {
@@ -4350,6 +4350,10 @@ var import_node_fs4 = __toESM(require("fs"), 1);
43504350
var import_node_path4 = __toESM(require("path"), 1);
43514351
var DEFAULT_ARTIFACT_ROOT = import_node_path4.default.join(process.cwd(), ".harness", "crash-reports");
43524352

4353+
// ../tools/dist/harness-artifacts.js
4354+
var import_node_fs5 = __toESM(require("fs"), 1);
4355+
var import_node_path5 = __toESM(require("path"), 1);
4356+
43534357
// ../plugins/dist/utils.js
43544358
var isHookTree = (value) => {
43554359
if (value == null || typeof value !== "object" || Array.isArray(value)) {
@@ -4396,6 +4400,7 @@ var RunnerSchema = external_exports.object({
43964400
name: external_exports.string().min(1, "Runner name is required").regex(/^[a-zA-Z0-9._-]+$/, "Runner name can only contain alphanumeric characters, dots, underscores, and hyphens"),
43974401
config: external_exports.record(external_exports.any()),
43984402
runner: external_exports.string(),
4403+
cli: external_exports.string().optional(),
43994404
platformId: external_exports.string()
44004405
});
44014406
var PluginSchema = external_exports.custom((value) => isHarnessPlugin(value), "Invalid Harness plugin");
@@ -4502,16 +4507,16 @@ var ConfigLoadError = class extends HarnessError {
45024507
};
45034508

45044509
// ../config/dist/reader.js
4505-
var import_node_path5 = __toESM(require("path"), 1);
4506-
var import_node_fs5 = __toESM(require("fs"), 1);
4510+
var import_node_path6 = __toESM(require("path"), 1);
4511+
var import_node_fs6 = __toESM(require("fs"), 1);
45074512
var import_node_module2 = require("module");
45084513
var import_meta = {};
45094514
var extensions = [".js", ".mjs", ".cjs", ".json"];
45104515
var importUp = async (dir, name) => {
4511-
const filePath = import_node_path5.default.join(dir, name);
4516+
const filePath = import_node_path6.default.join(dir, name);
45124517
for (const ext of extensions) {
45134518
const filePathWithExt = `${filePath}${ext}`;
4514-
if (import_node_fs5.default.existsSync(filePathWithExt)) {
4519+
if (import_node_fs6.default.existsSync(filePathWithExt)) {
45154520
let rawConfig;
45164521
try {
45174522
if (ext === ".mjs") {
@@ -4529,16 +4534,16 @@ var importUp = async (dir, name) => {
45294534
} catch (error) {
45304535
if (error instanceof ZodError) {
45314536
const validationErrors = error.errors.map((err) => {
4532-
const path6 = err.path.length > 0 ? ` at "${err.path.join(".")}"` : "";
4533-
return `${err.message}${path6}`;
4537+
const path7 = err.path.length > 0 ? ` at "${err.path.join(".")}"` : "";
4538+
return `${err.message}${path7}`;
45344539
});
45354540
throw new ConfigValidationError(filePathWithExt, validationErrors);
45364541
}
45374542
throw error;
45384543
}
45394544
}
45404545
}
4541-
const parentDir = import_node_path5.default.dirname(dir);
4546+
const parentDir = import_node_path6.default.dirname(dir);
45424547
if (parentDir === dir) {
45434548
throw new ConfigNotFoundError(dir);
45444549
}
@@ -4553,8 +4558,8 @@ var getConfig = async (dir) => {
45534558
};
45544559

45554560
// src/shared/index.ts
4556-
var import_node_path6 = __toESM(require("path"));
4557-
var import_node_fs6 = __toESM(require("fs"));
4561+
var import_node_path7 = __toESM(require("path"));
4562+
var import_node_fs7 = __toESM(require("fs"));
45584563
var getHostAndroidSystemImageArch = () => {
45594564
switch (process.arch) {
45604565
case "arm64":
@@ -4622,7 +4627,7 @@ var run = async () => {
46224627
if (!runnerInput) {
46234628
throw new Error("Runner input is required");
46244629
}
4625-
const projectRoot = projectRootInput ? import_node_path6.default.resolve(projectRootInput) : process.cwd();
4630+
const projectRoot = projectRootInput ? import_node_path7.default.resolve(projectRootInput) : process.cwd();
46264631
console.info(`Loading React Native Harness config from: ${projectRoot}`);
46274632
const { config, projectRoot: resolvedProjectRoot } = await getConfig(
46284633
projectRoot
@@ -4636,13 +4641,13 @@ var run = async () => {
46364641
throw new Error("GITHUB_OUTPUT environment variable is not set");
46374642
}
46384643
const resolvedRunner = getResolvedRunner(runner);
4639-
const relativeProjectRoot = import_node_path6.default.relative(process.cwd(), resolvedProjectRoot) || ".";
4644+
const relativeProjectRoot = import_node_path7.default.relative(process.cwd(), resolvedProjectRoot) || ".";
46404645
const output = `config=${JSON.stringify(
46414646
resolvedRunner
46424647
)}
46434648
projectRoot=${relativeProjectRoot}
46444649
`;
4645-
import_node_fs6.default.appendFileSync(githubOutput, output);
4650+
import_node_fs7.default.appendFileSync(githubOutput, output);
46464651
} catch (error) {
46474652
if (error instanceof Error) {
46484653
console.error(error.message);

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)