Skip to content

Commit 7eeab9a

Browse files
committed
Merge origin/main into codex/encapsulate-metro-startup
2 parents a3cb89a + ab5a226 commit 7eeab9a

32 files changed

Lines changed: 2119 additions & 141 deletions

actions/shared/index.cjs

Lines changed: 77 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4208,46 +4208,42 @@ var coerce = {
42084208
};
42094209
var NEVER = INVALID;
42104210

4211-
// ../config/dist/types.js
4212-
var DEFAULT_METRO_PORT = 8081;
4213-
var RunnerSchema = external_exports.object({
4214-
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"),
4215-
config: external_exports.record(external_exports.any()),
4216-
runner: external_exports.string(),
4217-
platformId: external_exports.string()
4218-
});
4219-
var ConfigSchema = external_exports.object({
4220-
entryPoint: external_exports.string().min(1, "Entry point is required"),
4221-
appRegistryComponentName: external_exports.string().min(1, "App registry component name is required"),
4222-
runners: external_exports.array(RunnerSchema).min(1, "At least one runner is required"),
4223-
defaultRunner: external_exports.string().optional(),
4224-
host: external_exports.string().min(1, "Host is required").optional(),
4225-
metroPort: external_exports.number().int("Metro port must be an integer").min(1, "Metro port must be at least 1").max(65535, "Metro port must be at most 65535").optional().default(DEFAULT_METRO_PORT),
4226-
webSocketPort: external_exports.number().optional().default(3001),
4227-
bridgeTimeout: external_exports.number().min(1e3, "Bridge timeout must be at least 1 second").default(6e4),
4228-
bundleStartTimeout: external_exports.number().min(1e3, "Bundle start timeout must be at least 1 second").default(15e3),
4229-
maxAppRestarts: external_exports.number().min(0, "Max app restarts must be at least 0").default(2),
4230-
resetEnvironmentBetweenTestFiles: external_exports.boolean().optional().default(true),
4231-
unstable__skipAlreadyIncludedModules: external_exports.boolean().optional().default(false),
4232-
unstable__enableMetroCache: external_exports.boolean().optional().default(false),
4233-
detectNativeCrashes: external_exports.boolean().optional().default(true),
4234-
crashDetectionInterval: external_exports.number().min(100, "Crash detection interval must be at least 100ms").default(500),
4235-
disableViewFlattening: external_exports.boolean().optional().default(false).describe("Disable view flattening in React Native. This will set collapsable={true} for all View components to ensure they are not flattened by the native layout engine."),
4236-
coverage: external_exports.object({
4237-
root: external_exports.string().optional().describe(`Root directory for coverage instrumentation in monorepo setups. Specifies the directory from which coverage data should be collected. Use ".." for create-react-native-library projects where tests run from example/ but source files are in parent directory. Passed to babel-plugin-istanbul's cwd option.`)
4238-
}).optional(),
4239-
forwardClientLogs: external_exports.boolean().optional().default(false).describe("Enable forwarding of console.log, console.warn, console.error, and other console method calls from the React Native app to the terminal. When enabled, all console output from your app will be displayed in the test runner terminal with styled level indicators (log, warn, error)."),
4240-
// Deprecated property - used for migration detection
4241-
include: external_exports.array(external_exports.string()).optional()
4242-
}).refine((config) => {
4243-
if (config.defaultRunner) {
4244-
return config.runners.some((runner) => runner.name === config.defaultRunner);
4211+
// ../plugins/dist/utils.js
4212+
var isHookTree = (value) => {
4213+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
4214+
return false;
4215+
}
4216+
for (const child of Object.values(value)) {
4217+
if (child === void 0) {
4218+
continue;
4219+
}
4220+
if (typeof child === "function") {
4221+
continue;
4222+
}
4223+
if (child == null || typeof child !== "object" || Array.isArray(child) || !isHookTree(child)) {
4224+
return false;
4225+
}
42454226
}
42464227
return true;
4247-
}, {
4248-
message: "Default runner must match one of the configured runner names",
4249-
path: ["defaultRunner"]
4250-
});
4228+
};
4229+
4230+
// ../plugins/dist/plugin.js
4231+
var isHarnessPlugin = (value) => {
4232+
if (value == null || typeof value !== "object" || Array.isArray(value)) {
4233+
return false;
4234+
}
4235+
const candidate = value;
4236+
if (typeof candidate.name !== "string" || candidate.name.length === 0) {
4237+
return false;
4238+
}
4239+
if (candidate.createState != null && typeof candidate.createState !== "function") {
4240+
return false;
4241+
}
4242+
if (candidate.hooks != null && !isHookTree(candidate.hooks)) {
4243+
return false;
4244+
}
4245+
return true;
4246+
};
42514247

42524248
// ../tools/dist/logger.js
42534249
var import_node_util2 = __toESM(require("util"), 1);
@@ -4342,6 +4338,49 @@ var import_node_fs4 = __toESM(require("fs"), 1);
43424338
var import_node_path4 = __toESM(require("path"), 1);
43434339
var DEFAULT_ARTIFACT_ROOT = import_node_path4.default.join(process.cwd(), ".harness", "crash-reports");
43444340

4341+
// ../config/dist/types.js
4342+
var DEFAULT_METRO_PORT = 8081;
4343+
var RunnerSchema = external_exports.object({
4344+
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"),
4345+
config: external_exports.record(external_exports.any()),
4346+
runner: external_exports.string(),
4347+
platformId: external_exports.string()
4348+
});
4349+
var PluginSchema = external_exports.custom((value) => isHarnessPlugin(value), "Invalid Harness plugin");
4350+
var ConfigSchema = external_exports.object({
4351+
entryPoint: external_exports.string().min(1, "Entry point is required"),
4352+
appRegistryComponentName: external_exports.string().min(1, "App registry component name is required"),
4353+
runners: external_exports.array(RunnerSchema).min(1, "At least one runner is required"),
4354+
plugins: external_exports.array(PluginSchema).optional().default([]),
4355+
defaultRunner: external_exports.string().optional(),
4356+
host: external_exports.string().min(1, "Host is required").optional(),
4357+
metroPort: external_exports.number().int("Metro port must be an integer").min(1, "Metro port must be at least 1").max(65535, "Metro port must be at most 65535").optional().default(DEFAULT_METRO_PORT),
4358+
webSocketPort: external_exports.number().optional().default(3001),
4359+
bridgeTimeout: external_exports.number().min(1e3, "Bridge timeout must be at least 1 second").default(6e4),
4360+
bundleStartTimeout: external_exports.number().min(1e3, "Bundle start timeout must be at least 1 second").default(15e3),
4361+
maxAppRestarts: external_exports.number().min(0, "Max app restarts must be at least 0").default(2),
4362+
resetEnvironmentBetweenTestFiles: external_exports.boolean().optional().default(true),
4363+
unstable__skipAlreadyIncludedModules: external_exports.boolean().optional().default(false),
4364+
unstable__enableMetroCache: external_exports.boolean().optional().default(false),
4365+
detectNativeCrashes: external_exports.boolean().optional().default(true),
4366+
crashDetectionInterval: external_exports.number().min(100, "Crash detection interval must be at least 100ms").default(500),
4367+
disableViewFlattening: external_exports.boolean().optional().default(false).describe("Disable view flattening in React Native. This will set collapsable={true} for all View components to ensure they are not flattened by the native layout engine."),
4368+
coverage: external_exports.object({
4369+
root: external_exports.string().optional().describe(`Root directory for coverage instrumentation in monorepo setups. Specifies the directory from which coverage data should be collected. Use ".." for create-react-native-library projects where tests run from example/ but source files are in parent directory. Passed to babel-plugin-istanbul's cwd option.`)
4370+
}).optional(),
4371+
forwardClientLogs: external_exports.boolean().optional().default(false).describe("Enable forwarding of console.log, console.warn, console.error, and other console method calls from the React Native app to the terminal. When enabled, all console output from your app will be displayed in the test runner terminal with styled level indicators (log, warn, error)."),
4372+
// Deprecated property - used for migration detection
4373+
include: external_exports.array(external_exports.string()).optional()
4374+
}).refine((config) => {
4375+
if (config.defaultRunner) {
4376+
return config.runners.some((runner) => runner.name === config.defaultRunner);
4377+
}
4378+
return true;
4379+
}, {
4380+
message: "Default runner must match one of the configured runner names",
4381+
path: ["defaultRunner"]
4382+
});
4383+
43454384
// ../config/dist/errors.js
43464385
var ConfigValidationError = class extends HarnessError {
43474386
filePath;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
const logWithDetails = (hookName, getDetails) => {
2+
return (ctx) => {
3+
const details = getDetails?.(ctx);
4+
const suffix = details ? ` ${details}` : '';
5+
ctx.logger.info(`${hookName}${suffix}`);
6+
};
7+
};
8+
9+
export const harnessLoggingPlugin = () => ({
10+
name: 'playground-logging',
11+
hooks: {
12+
harness: {
13+
beforeCreation: logWithDetails(
14+
'harness.beforeCreation',
15+
(ctx) => `runner=${ctx.runner.name}`
16+
),
17+
beforeDispose: logWithDetails(
18+
'harness.beforeDispose',
19+
(ctx) => `reason=${ctx.reason ?? 'normal'}`
20+
),
21+
},
22+
run: {
23+
started: logWithDetails(
24+
'run.started',
25+
(ctx) => `runId=${ctx.runId} files=${ctx.testFiles.length}`
26+
),
27+
finished: logWithDetails(
28+
'run.finished',
29+
(ctx) =>
30+
`runId=${ctx.runId} status=${ctx.status} duration=${ctx.duration}ms`
31+
),
32+
},
33+
runtime: {
34+
ready: logWithDetails(
35+
'runtime.ready',
36+
(ctx) => `runId=${ctx.runId} device=${ctx.device.platform}`
37+
),
38+
disconnected: logWithDetails(
39+
'runtime.disconnected',
40+
(ctx) => `runId=${ctx.runId} reason=${ctx.reason ?? 'unknown'}`
41+
),
42+
},
43+
metro: {
44+
initialized: logWithDetails(
45+
'metro.initialized',
46+
(ctx) => `runId=${ctx.runId} port=${ctx.port}`
47+
),
48+
bundleStarted: logWithDetails(
49+
'metro.bundleStarted',
50+
(ctx) => `runId=${ctx.runId} target=${ctx.target} file=${ctx.file}`
51+
),
52+
bundleFinished: logWithDetails(
53+
'metro.bundleFinished',
54+
(ctx) =>
55+
`runId=${ctx.runId} target=${ctx.target} file=${ctx.file} duration=${ctx.duration}ms`
56+
),
57+
bundleFailed: logWithDetails(
58+
'metro.bundleFailed',
59+
(ctx) =>
60+
`runId=${ctx.runId} target=${ctx.target} file=${ctx.file} error=${ctx.error}`
61+
),
62+
clientLog: logWithDetails(
63+
'metro.clientLog',
64+
(ctx) => `runId=${ctx.runId} level=${ctx.level}`
65+
),
66+
},
67+
app: {
68+
started: logWithDetails(
69+
'app.started',
70+
(ctx) => `runId=${ctx.runId} testFile=${ctx.testFile ?? 'n/a'}`
71+
),
72+
exited: logWithDetails(
73+
'app.exited',
74+
(ctx) => `runId=${ctx.runId} testFile=${ctx.testFile ?? 'n/a'}`
75+
),
76+
possibleCrash: logWithDetails(
77+
'app.possibleCrash',
78+
(ctx) => `runId=${ctx.runId} testFile=${ctx.testFile ?? 'n/a'}`
79+
),
80+
},
81+
collection: {
82+
started: logWithDetails(
83+
'collection.started',
84+
(ctx) => `runId=${ctx.runId} file=${ctx.file}`
85+
),
86+
finished: logWithDetails(
87+
'collection.finished',
88+
(ctx) =>
89+
`runId=${ctx.runId} file=${ctx.file} totalTests=${ctx.totalTests}`
90+
),
91+
},
92+
testFile: {
93+
started: logWithDetails(
94+
'testFile.started',
95+
(ctx) => `runId=${ctx.runId} file=${ctx.file}`
96+
),
97+
finished: logWithDetails(
98+
'testFile.finished',
99+
(ctx) =>
100+
`runId=${ctx.runId} file=${ctx.file} status=${ctx.status} duration=${ctx.duration}ms`
101+
),
102+
},
103+
suite: {
104+
started: logWithDetails(
105+
'suite.started',
106+
(ctx) => `runId=${ctx.runId} suite=${ctx.name}`
107+
),
108+
finished: logWithDetails(
109+
'suite.finished',
110+
(ctx) =>
111+
`runId=${ctx.runId} suite=${ctx.name} status=${ctx.status} duration=${ctx.duration}ms`
112+
),
113+
},
114+
test: {
115+
started: logWithDetails(
116+
'test.started',
117+
(ctx) => `runId=${ctx.runId} suite=${ctx.suite} test=${ctx.name}`
118+
),
119+
finished: logWithDetails(
120+
'test.finished',
121+
(ctx) =>
122+
`runId=${ctx.runId} suite=${ctx.suite} test=${ctx.name} status=${ctx.status} duration=${ctx.duration}ms`
123+
),
124+
},
125+
},
126+
});

apps/playground/rn-harness.config.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ import {
1717
chromium,
1818
chrome,
1919
} from '@react-native-harness/platform-web';
20+
import { harnessLoggingPlugin } from './harness-logging-plugin.mjs';
2021

2122
export default {
2223
entryPoint: './index.js',
2324
appRegistryComponentName: 'HarnessPlayground',
25+
plugins: [harnessLoggingPlugin()],
2426

2527
runners: [
2628
androidPlatform({
@@ -75,7 +77,7 @@ export default {
7577
}),
7678
applePlatform({
7779
name: 'ios',
78-
device: appleSimulator('iPhone 16 Pro', '18.6'),
80+
device: appleSimulator('iPhone 17 Pro', '26.2'),
7981
bundleId: 'com.harnessplayground',
8082
}),
8183
applePlatform({

packages/bridge/src/shared/test-collector.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type TestCollectionFinishedEvent = {
3636
type: 'collection-finished';
3737
file: string;
3838
duration: number;
39+
totalTests: number;
3940
};
4041

4142
export type TestCollectorEvents =

packages/config/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
}
1616
},
1717
"dependencies": {
18+
"@react-native-harness/plugins": "workspace:*",
1819
"@react-native-harness/tools": "workspace:*",
1920
"tslib": "^2.3.0",
2021
"zod": "^3.25.67"

packages/config/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { z } from 'zod';
2+
import type { HarnessPlugin } from '@react-native-harness/plugins';
3+
import { isHarnessPlugin } from '@react-native-harness/plugins';
24

35
export const DEFAULT_METRO_PORT = 8081;
46

@@ -15,13 +17,21 @@ const RunnerSchema = z.object({
1517
platformId: z.string(),
1618
});
1719

20+
type AnyHarnessPlugin = HarnessPlugin<any, any, any>;
21+
22+
const PluginSchema = z.custom<AnyHarnessPlugin>(
23+
(value) => isHarnessPlugin(value),
24+
'Invalid Harness plugin'
25+
);
26+
1827
export const ConfigSchema = z
1928
.object({
2029
entryPoint: z.string().min(1, 'Entry point is required'),
2130
appRegistryComponentName: z
2231
.string()
2332
.min(1, 'App registry component name is required'),
2433
runners: z.array(RunnerSchema).min(1, 'At least one runner is required'),
34+
plugins: z.array(PluginSchema).optional().default([]),
2535
defaultRunner: z.string().optional(),
2636
host: z.string().min(1, 'Host is required').optional(),
2737
metroPort: z

packages/config/tsconfig.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
{
77
"path": "../tools"
88
},
9+
{
10+
"path": "../plugins"
11+
},
912
{
1013
"path": "./tsconfig.lib.json"
1114
}

packages/config/tsconfig.lib.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"references": [
1414
{
1515
"path": "../tools/tsconfig.lib.json"
16+
},
17+
{
18+
"path": "../plugins/tsconfig.lib.json"
1619
}
1720
]
1821
}

packages/jest/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"@react-native-harness/bridge": "workspace:*",
3838
"@react-native-harness/bundler-metro": "workspace:*",
3939
"@react-native-harness/config": "workspace:*",
40+
"@react-native-harness/plugins": "workspace:*",
4041
"@react-native-harness/platforms": "workspace:*",
4142
"@react-native-harness/tools": "workspace:*",
4243
"chalk": "^4.1.2",

0 commit comments

Comments
 (0)