@deeeed/metamask-harness 0.42.0 → 0.44.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/CHANGELOG.md +42 -0
- package/README.md +5 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +363 -55
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +23 -10
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +2 -0
- package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +296 -25
- package/adapters/mobile/bridge-runtime/lib/config.cjs +14 -4
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +4 -2
- package/adapters/mobile/reload-app.mjs +99 -1
- package/adapters/mobile/start-console-forwarder.sh +5 -1
- package/dist/adapters/extension/browser-cdp.js +174 -0
- package/dist/adapters/extension/network-observer.js +19 -110
- package/dist/adapters/extension/performance-observer.js +75 -0
- package/dist/adapters/mobile/frame-metrics.js +45 -0
- package/dist/adapters/mobile/performance-observer.js +43 -0
- package/dist/adapters/mobile/prepare.js +12 -0
- package/dist/adapters/performance/cdp-trace.js +342 -0
- package/dist/adapters/performance/js-task-metrics.js +35 -0
- package/dist/adapters.js +39 -5
- package/dist/artifact-files.js +92 -0
- package/dist/async.js +19 -0
- package/dist/commands/call.js +63 -4
- package/dist/commands/run-engine.js +265 -139
- package/dist/commands/run-report.js +68 -0
- package/dist/commands/run.js +67 -3
- package/dist/execution-provenance.js +342 -0
- package/dist/network-observation.js +59 -47
- package/dist/performance-observation.js +465 -0
- package/dist/run-diagnostics.js +36 -11
- package/dist/runner.js +44 -13
- package/docs/PERFORMANCE-CAPTURE.md +33 -0
- package/docs/RECIPES.md +7 -0
- package/library/actions/mobile/perps/performance-capture.mjs +570 -189
- package/library/actions/mobile/perps/perps.mjs +122 -0
- package/library/actions/mobile/platform/bridge.mjs +43 -8
- package/library/actions/mobile/platform/native-session.mjs +1 -1
- package/library/actions/mobile/wallet/lock.mjs +1 -4
- package/library/actions/mobile/wallet/select_account.mjs +129 -17
- package/library/manifests/extension.action-manifest.json +85 -0
- package/library/manifests/mobile.action-manifest.json +125 -3
- package/library/recipes/mobile/perps/performance.recipe.json +73 -47
- package/package.json +1 -1
- package/scripts/site-contrast.mjs +43 -27
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.44.0 - 2026-08-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Remove `cold_disk_cache` from the `perps.performance` lifecycle enum because disk hydration is a cache source within a lifecycle, not a standalone lifecycle.
|
|
10
|
+
- Await promise-returning Mobile `press-test-id` and `set-input` bridge handlers so recipe nodes finish only after the in-app operation settles.
|
|
11
|
+
- Keep Mobile broker commands available while Hermes installs the in-app bridge after a target handoff, instead of reconnecting the inspector until actions time out.
|
|
12
|
+
- Honor `launch --build` when a healthy Mobile runtime is already connected; the explicit native build can no longer return a false pass without invoking the build leaf.
|
|
13
|
+
- Let resident Homepage and short-resume performance recipes finish from the ordered visible-content sequence without requiring a new live takeover.
|
|
14
|
+
- Make Mobile reload stay on the pinned device, fail before an active Android Hermes sampler can abort the app, and fail if the process exits or a replacement target never appears; reload never relaunches the app.
|
|
15
|
+
- Bind run evidence to the executable Harness source, build, bin, package, library, and adapter inputs plus task-local command/config helpers, and reject input drift or symlink swaps.
|
|
16
|
+
- Persist recovery and mutation lists in recipe packages, include a redacted run-scoped application log, reject failed recovery before retrying, and prove lifecycle continuity with native process IDs, including opaque runtimes when requested.
|
|
17
|
+
- Refuse stale Mobile source under every heal policy unless an authored `app.lifecycle restart` reloads it, and invalidate run evidence if recipe, library, or product provenance drifts after preparation.
|
|
18
|
+
- Require Homepage demand, committed UI, resolved content, and live-current frame records before the canonical Mobile Perps performance recipe can pass.
|
|
19
|
+
- Fail a Mobile recipe or action without implicitly restarting Metro or the app; process restarts remain explicit lifecycle actions.
|
|
20
|
+
- Keep one Mobile inspector connection open while React Native replays its console buffer, avoiding repeated `Runtime.enable` requests that can stall Metro after unlock.
|
|
21
|
+
- Allow `metamask.wallet.lock` to use the same visible Android controls on development and opaque clients.
|
|
22
|
+
- Keep account-selection proof attached across the temporary Hermes/CDP stall caused by the account switch itself.
|
|
23
|
+
- Add `metamask.perps.ensure_mode` so recipes can reach Lite or Pro through the visible mode control without inheriting prior device state.
|
|
24
|
+
- Preserve Homepage boundary source and content-variant metadata in performance summaries, and bind recipe manifests to the exact product checkout commit.
|
|
25
|
+
- Accept only structured, generation-coherent Perps live-stream records as performance proof, and keep Mobile wallet and mode probes inside one absolute action deadline.
|
|
26
|
+
- Keep the latest automatic Mobile HUD update in the CDP broker and retry it until the in-app bridge applies it when the pinned Hermes target returns, without restarting the app.
|
|
27
|
+
- Keep the Perps performance recipe's empty-account setup read-only and allow its setup-only prime capture to contain no performance records.
|
|
28
|
+
- Redact quoted JSON credential fields before persisting run-scoped application logs.
|
|
29
|
+
|
|
30
|
+
## 0.43.0 - 2026-08-21
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Add platform-neutral `app.performance_capture` and `app.performance_assert` actions that attribute Mobile or Extension CDP trace samples to exact recipe-node boundaries and emit JSON plus self-contained HTML evidence.
|
|
35
|
+
- Mobile native UI FPS requires a development client that reports `unstable_frameRecordingEnabled: true`; unpatched clients report partial or unavailable native coverage. Extension uses Chromium CDP and needs no client patch.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Bound performance capture duration, discard raw samples after node attribution, correct iOS frame completion timestamps, fail closed on unresolved Extension renderers, and reset CDP collector state after trace failures.
|
|
40
|
+
- Scope Mobile Hermes broker ownership by checkout runtime and Metro port so a forwarder from a prior port cannot intercept bridge, HUD, or observation commands after a slot port change.
|
|
41
|
+
- Preserve explicit Mobile `--device` pins when slot defaults load so HUD and live-adapter child processes cannot target another device.
|
|
42
|
+
- Resume broker commands on the next Hermes session generation without losing the pinned logical device.
|
|
43
|
+
- Keep Hermes session recovery inside the caller's RPC deadline, pass only the remaining time to the recovered command, and classify broker connection or request timeouts so transient HUD updates do not abort a recipe.
|
|
44
|
+
- Accept React Native trace response chunks up to 16 MiB while keeping broker requests capped at 8 MiB, and never retry the non-idempotent `Tracing.end` command.
|
|
45
|
+
- Limit Mobile UI captures to clock and native-frame categories; keep browser timeline and JavaScript task categories on Extension.
|
|
46
|
+
|
|
5
47
|
## 0.42.0 - 2026-08-20
|
|
6
48
|
|
|
7
49
|
### Added
|
package/README.md
CHANGED
|
@@ -116,6 +116,11 @@ explicit `app.network_capture` and `app.network_assert` nodes for filtered
|
|
|
116
116
|
windows and machine-checked request expectations. See
|
|
117
117
|
[Recipe-scoped network capture](docs/NETWORK-CAPTURE.md).
|
|
118
118
|
|
|
119
|
+
Mobile and Extension recipes can bracket explicit UI-smoothness windows with
|
|
120
|
+
`app.performance_capture` and verify their evidence with
|
|
121
|
+
`app.performance_assert`. Capture is never automatic. See
|
|
122
|
+
[UI smoothness capture](docs/PERFORMANCE-CAPTURE.md).
|
|
123
|
+
|
|
119
124
|
## Discover and prove
|
|
120
125
|
|
|
121
126
|
```bash
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
|
|
19
19
|
const fs = require('node:fs');
|
|
20
20
|
const path = require('node:path');
|
|
21
|
+
const { execFileSync } = require('node:child_process');
|
|
21
22
|
const { loadPort } = require('./lib/config.cjs');
|
|
22
23
|
const {
|
|
23
24
|
discoverTarget,
|
|
24
|
-
rankRuntimeCandidates,
|
|
25
25
|
} = require('./lib/target-discovery.cjs');
|
|
26
26
|
const { createWSClient } = require('./lib/ws-client.cjs');
|
|
27
27
|
const {
|
|
@@ -37,6 +37,126 @@ const {
|
|
|
37
37
|
} = require('./lib/bridge-errors.cjs');
|
|
38
38
|
const { cdpEval, cdpEvalAsync } = require('./lib/cdp-eval.cjs');
|
|
39
39
|
const { buildArmSnippet, buildCollectSnippet } = require('./lib/issue-capture.cjs');
|
|
40
|
+
const APPLY_HUD_UPDATE_FUNCTION = `function(step) {
|
|
41
|
+
const bridge = this.__AGENTIC__;
|
|
42
|
+
if (step === null) {
|
|
43
|
+
if (typeof bridge?.hideStep !== 'function') return false;
|
|
44
|
+
setTimeout(() => bridge.hideStep(), 0);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
if (typeof bridge?.showStep !== 'function') return false;
|
|
48
|
+
setTimeout(() => bridge.showStep(step), 0);
|
|
49
|
+
return true;
|
|
50
|
+
}`;
|
|
51
|
+
|
|
52
|
+
function mobileToolPath(tool) {
|
|
53
|
+
return process.env[`MM_HARNESS_${tool.toUpperCase()}_PATH`] || tool;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function bootedIosDevice(deviceName) {
|
|
57
|
+
if (!deviceName) return null;
|
|
58
|
+
try {
|
|
59
|
+
const devices = JSON.parse(
|
|
60
|
+
execFileSync('xcrun', ['simctl', 'list', 'devices', 'available', '-j'], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
const device = Object.values(devices.devices || {})
|
|
65
|
+
.flat()
|
|
66
|
+
.find(
|
|
67
|
+
(candidate) =>
|
|
68
|
+
(candidate?.name === deviceName || candidate?.udid === deviceName) &&
|
|
69
|
+
candidate?.state === 'Booted',
|
|
70
|
+
);
|
|
71
|
+
return device?.udid ? device : null;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function tapVisibleIosAccessibilityTarget(testId, deviceName, platform) {
|
|
78
|
+
if (platform !== 'ios') return null;
|
|
79
|
+
const device = bootedIosDevice(deviceName);
|
|
80
|
+
if (!device) return null;
|
|
81
|
+
try {
|
|
82
|
+
const idb = mobileToolPath('idb');
|
|
83
|
+
const elements = JSON.parse(
|
|
84
|
+
execFileSync(
|
|
85
|
+
idb,
|
|
86
|
+
['ui', 'describe-all', '--udid', device.udid, '--json'],
|
|
87
|
+
{ encoding: 'utf8' },
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
const windowFrame = elements
|
|
91
|
+
.map((element) => element?.frame)
|
|
92
|
+
.filter(
|
|
93
|
+
(frame) =>
|
|
94
|
+
frame &&
|
|
95
|
+
frame.x === 0 &&
|
|
96
|
+
frame.y === 0 &&
|
|
97
|
+
frame.width > 0 &&
|
|
98
|
+
frame.height > 0,
|
|
99
|
+
)
|
|
100
|
+
.sort(
|
|
101
|
+
(first, second) =>
|
|
102
|
+
second.width * second.height - first.width * first.height,
|
|
103
|
+
)[0];
|
|
104
|
+
if (!windowFrame) return null;
|
|
105
|
+
const target = elements.find((element) => {
|
|
106
|
+
const frame = element?.frame;
|
|
107
|
+
return (
|
|
108
|
+
element?.AXUniqueId === testId &&
|
|
109
|
+
element?.enabled !== false &&
|
|
110
|
+
element?.hittable !== false &&
|
|
111
|
+
frame &&
|
|
112
|
+
frame.width > 0 &&
|
|
113
|
+
frame.height > 0 &&
|
|
114
|
+
frame.x >= 0 &&
|
|
115
|
+
frame.y >= 0 &&
|
|
116
|
+
frame.x + frame.width <= windowFrame.width &&
|
|
117
|
+
frame.y + frame.height <= windowFrame.height
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
if (!target) return null;
|
|
121
|
+
const x = Math.round(target.frame.x + target.frame.width / 2);
|
|
122
|
+
const y = Math.round(target.frame.y + target.frame.height / 2);
|
|
123
|
+
execFileSync(
|
|
124
|
+
idb,
|
|
125
|
+
['ui', 'tap', String(x), String(y), '--udid', device.udid],
|
|
126
|
+
{ encoding: 'utf8' },
|
|
127
|
+
);
|
|
128
|
+
return { ok: true, testId, deviceName, provider: 'idb-accessibility' };
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function applyHudUpdate(client, step) {
|
|
135
|
+
const globalObject = await client.send('Runtime.evaluate', {
|
|
136
|
+
expression: 'globalThis',
|
|
137
|
+
returnByValue: false,
|
|
138
|
+
awaitPromise: false,
|
|
139
|
+
});
|
|
140
|
+
const objectId = globalObject?.result?.objectId;
|
|
141
|
+
if (!objectId) {
|
|
142
|
+
throw new Error('Mobile HUD runtime did not expose globalThis');
|
|
143
|
+
}
|
|
144
|
+
const result = await client.send('Runtime.callFunctionOn', {
|
|
145
|
+
objectId,
|
|
146
|
+
functionDeclaration: APPLY_HUD_UPDATE_FUNCTION,
|
|
147
|
+
arguments: [{ value: step }],
|
|
148
|
+
returnByValue: true,
|
|
149
|
+
awaitPromise: false,
|
|
150
|
+
});
|
|
151
|
+
if (result?.exceptionDetails) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
result.exceptionDetails.exception?.description ||
|
|
154
|
+
result.exceptionDetails.text ||
|
|
155
|
+
'Mobile HUD update failed',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return result?.result?.value === true;
|
|
159
|
+
}
|
|
40
160
|
|
|
41
161
|
function parsePerformanceConsoleEvent(params) {
|
|
42
162
|
const markers = ['[PerpsPerf] ', '[HomepagePerf] '];
|
|
@@ -98,7 +218,7 @@ async function setInput(client, testId, value, { deviceName } = {}, redact = fal
|
|
|
98
218
|
throw new Error('Usage: set-input <testId> <value>');
|
|
99
219
|
}
|
|
100
220
|
const expr = `(function() {
|
|
101
|
-
if (globalThis.__AGENTIC__?.setInput) return globalThis.__AGENTIC__.setInput(${JSON.stringify(testId)}, ${JSON.stringify(value)});
|
|
221
|
+
if (globalThis.__AGENTIC__?.setInput) return Promise.resolve(globalThis.__AGENTIC__.setInput(${JSON.stringify(testId)}, ${JSON.stringify(value)}));
|
|
102
222
|
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
103
223
|
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
104
224
|
var renderers = hook.renderers;
|
|
@@ -134,7 +254,7 @@ async function setInput(client, testId, value, { deviceName } = {}, redact = fal
|
|
|
134
254
|
})()`;
|
|
135
255
|
let result;
|
|
136
256
|
try {
|
|
137
|
-
result = await
|
|
257
|
+
result = await cdpEvalAsync(client, expr);
|
|
138
258
|
} catch (error) {
|
|
139
259
|
if (redact) {
|
|
140
260
|
throw new Error(`Secret input could not be applied for testID ${testId}.`);
|
|
@@ -152,6 +272,26 @@ async function setInput(client, testId, value, { deviceName } = {}, redact = fal
|
|
|
152
272
|
};
|
|
153
273
|
}
|
|
154
274
|
|
|
275
|
+
function parseHudStep(args) {
|
|
276
|
+
const raw = args.join(' ');
|
|
277
|
+
let step;
|
|
278
|
+
try {
|
|
279
|
+
step = JSON.parse(raw);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
throw new Error(`show-step-json requires a JSON step payload: ${error.message}`);
|
|
282
|
+
}
|
|
283
|
+
if (!step || typeof step !== 'object' || Array.isArray(step)) {
|
|
284
|
+
throw new Error('show-step-json requires a JSON object step payload');
|
|
285
|
+
}
|
|
286
|
+
if (typeof step.intent !== 'string' || !step.intent.trim()) {
|
|
287
|
+
throw new Error('show-step-json requires step.intent');
|
|
288
|
+
}
|
|
289
|
+
if (step.id !== undefined && typeof step.id !== 'string') {
|
|
290
|
+
throw new Error('show-step-json step.id must be a string when provided');
|
|
291
|
+
}
|
|
292
|
+
return step;
|
|
293
|
+
}
|
|
294
|
+
|
|
155
295
|
const COMMANDS = {
|
|
156
296
|
async navigate(client, args, { deviceName, platform } = {}) {
|
|
157
297
|
const routeName = ROUTE_ALIASES[args[0]] || args[0];
|
|
@@ -301,14 +441,15 @@ const COMMANDS = {
|
|
|
301
441
|
return await cdpEval(client, `globalThis.__AGENTIC__?.switchAccount(${JSON.stringify(address)})`);
|
|
302
442
|
},
|
|
303
443
|
|
|
304
|
-
async 'press-test-id'(client, args, { deviceName } = {}) {
|
|
444
|
+
async 'press-test-id'(client, args, { deviceName, platform } = {}) {
|
|
305
445
|
const testId = args[0];
|
|
306
446
|
if (!testId) {
|
|
307
447
|
throw new Error('Usage: press-test-id <testId>');
|
|
308
448
|
}
|
|
309
|
-
// Try
|
|
449
|
+
// Try the app bridge first. Native accessibility remains an iOS fallback
|
|
450
|
+
// for controls whose React handler cannot be invoked directly.
|
|
310
451
|
const expr = `(function() {
|
|
311
|
-
if (globalThis.__AGENTIC__?.pressTestId) return globalThis.__AGENTIC__.pressTestId(${JSON.stringify(testId)});
|
|
452
|
+
if (globalThis.__AGENTIC__?.pressTestId) return Promise.resolve(globalThis.__AGENTIC__.pressTestId(${JSON.stringify(testId)}));
|
|
312
453
|
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
313
454
|
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
314
455
|
var renderers = hook.renderers;
|
|
@@ -331,10 +472,72 @@ const COMMANDS = {
|
|
|
331
472
|
}
|
|
332
473
|
return { ok: false, error: 'No component with testID=' + ${JSON.stringify(testId)} + ' found or no onPress' };
|
|
333
474
|
})()`;
|
|
334
|
-
|
|
475
|
+
let result;
|
|
476
|
+
let bridgeError;
|
|
477
|
+
try {
|
|
478
|
+
result = await cdpEvalAsync(client, expr);
|
|
479
|
+
if (result?.ok !== false) {
|
|
480
|
+
return { ...result, testId, deviceName };
|
|
481
|
+
}
|
|
482
|
+
} catch (error) {
|
|
483
|
+
bridgeError = error;
|
|
484
|
+
}
|
|
485
|
+
const nativeResult = tapVisibleIosAccessibilityTarget(
|
|
486
|
+
testId,
|
|
487
|
+
deviceName,
|
|
488
|
+
platform,
|
|
489
|
+
);
|
|
490
|
+
if (nativeResult) return nativeResult;
|
|
491
|
+
if (bridgeError) throw bridgeError;
|
|
335
492
|
return { ...result, testId, deviceName };
|
|
336
493
|
},
|
|
337
494
|
|
|
495
|
+
async 'key-press'(_client, args, { deviceName, platform } = {}) {
|
|
496
|
+
const requestedKey = String(args[0] || 'Enter').toLowerCase();
|
|
497
|
+
const isEnter = requestedKey === 'enter' || requestedKey === 'return';
|
|
498
|
+
const isBack = requestedKey === 'escape' || requestedKey === 'back';
|
|
499
|
+
if (!isEnter && !isBack) {
|
|
500
|
+
throw new Error(`Unsupported Mobile key ${JSON.stringify(args[0])}`);
|
|
501
|
+
}
|
|
502
|
+
if (platform === 'android') {
|
|
503
|
+
const serial = process.env.ADB_SERIAL || process.env.ANDROID_SERIAL;
|
|
504
|
+
if (!serial) throw new Error('key-press requires a selected Android device');
|
|
505
|
+
const keyCode = isEnter ? '66' : '4';
|
|
506
|
+
execFileSync(
|
|
507
|
+
mobileToolPath('adb'),
|
|
508
|
+
['-s', serial, 'shell', 'input', 'keyevent', keyCode],
|
|
509
|
+
{ encoding: 'utf8' },
|
|
510
|
+
);
|
|
511
|
+
return {
|
|
512
|
+
ok: true,
|
|
513
|
+
key: args[0] || 'Enter',
|
|
514
|
+
keyCode,
|
|
515
|
+
deviceName,
|
|
516
|
+
provider: 'adb-key',
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
if (platform !== 'ios') {
|
|
520
|
+
throw new Error(`key-press requires a selected Mobile platform`);
|
|
521
|
+
}
|
|
522
|
+
const device = bootedIosDevice(deviceName);
|
|
523
|
+
if (!device) {
|
|
524
|
+
throw new Error(`No booted simulator named ${JSON.stringify(deviceName)}`);
|
|
525
|
+
}
|
|
526
|
+
const keyCode = isEnter ? '40' : '41';
|
|
527
|
+
execFileSync(
|
|
528
|
+
mobileToolPath('idb'),
|
|
529
|
+
['ui', 'key', keyCode, '--udid', device.udid],
|
|
530
|
+
{ encoding: 'utf8' },
|
|
531
|
+
);
|
|
532
|
+
return {
|
|
533
|
+
ok: true,
|
|
534
|
+
key: args[0] || 'Enter',
|
|
535
|
+
keyCode,
|
|
536
|
+
deviceName,
|
|
537
|
+
provider: 'idb-key',
|
|
538
|
+
};
|
|
539
|
+
},
|
|
540
|
+
|
|
338
541
|
async 'long-press-test-id'(client, args, { deviceName } = {}) {
|
|
339
542
|
const testId = args[0];
|
|
340
543
|
if (!testId) {
|
|
@@ -407,6 +610,9 @@ const COMMANDS = {
|
|
|
407
610
|
// selector fiber. For into-view requests the scroll container is commonly
|
|
408
611
|
// an ancestor (for example, a Wallet Home section row), so use the
|
|
409
612
|
// ancestor-aware bridge fallback below instead.
|
|
613
|
+
if (${intoView} && globalThis.__AGENTIC__?.scrollIntoView) {
|
|
614
|
+
return globalThis.__AGENTIC__.scrollIntoView(${JSON.stringify(testId)}, ${animated});
|
|
615
|
+
}
|
|
410
616
|
if (!${intoView} && globalThis.__AGENTIC__?.scrollView) return globalThis.__AGENTIC__.scrollView(${optsJson});
|
|
411
617
|
var hook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
412
618
|
if (!hook) return { ok: false, error: 'No React DevTools hook' };
|
|
@@ -428,14 +634,56 @@ const COMMANDS = {
|
|
|
428
634
|
}
|
|
429
635
|
return false;
|
|
430
636
|
}
|
|
637
|
+
function findMeasurable(fiber) {
|
|
638
|
+
if (!fiber) return null;
|
|
639
|
+
var sn = fiber.stateNode;
|
|
640
|
+
if (sn && typeof sn.measureLayout === 'function') return sn;
|
|
641
|
+
return findMeasurable(fiber.child);
|
|
642
|
+
}
|
|
431
643
|
function tryScrollNear(anchor) {
|
|
432
644
|
if (!opts.intoView && tryScroll(anchor, false)) return true;
|
|
645
|
+
var target = opts.intoView ? findMeasurable(anchor) : null;
|
|
433
646
|
var current = anchor ? anchor.return : null;
|
|
434
647
|
while (current) {
|
|
435
648
|
var sn = current.stateNode;
|
|
649
|
+
var currentProps = current.memoizedProps;
|
|
436
650
|
if (sn) {
|
|
437
|
-
if (typeof sn.scrollTo === 'function'
|
|
438
|
-
|
|
651
|
+
if (typeof sn.scrollTo === 'function' && !(currentProps && currentProps.horizontal === true)) {
|
|
652
|
+
if (target) {
|
|
653
|
+
return new Promise(function(resolve) {
|
|
654
|
+
target.measureLayout(
|
|
655
|
+
sn,
|
|
656
|
+
function(x, y) {
|
|
657
|
+
sn.scrollTo({ y: y, animated: opts.animated });
|
|
658
|
+
resolve({ ok: true, testId: opts.testId, measuredOffset: y, animated: opts.animated });
|
|
659
|
+
},
|
|
660
|
+
function() {
|
|
661
|
+
resolve({ ok: false, error: 'Unable to measure testID=' + opts.testId + ' relative to its scroll container' });
|
|
662
|
+
}
|
|
663
|
+
);
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
sn.scrollTo({ y: opts.offset, animated: opts.animated });
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
if (typeof sn.scrollToOffset === 'function' && !(currentProps && currentProps.horizontal === true)) {
|
|
670
|
+
if (target) {
|
|
671
|
+
return new Promise(function(resolve) {
|
|
672
|
+
target.measureLayout(
|
|
673
|
+
sn,
|
|
674
|
+
function(x, y) {
|
|
675
|
+
sn.scrollToOffset({ offset: y, animated: opts.animated });
|
|
676
|
+
resolve({ ok: true, testId: opts.testId, measuredOffset: y, animated: opts.animated });
|
|
677
|
+
},
|
|
678
|
+
function() {
|
|
679
|
+
resolve({ ok: false, error: 'Unable to measure testID=' + opts.testId + ' relative to its scroll container' });
|
|
680
|
+
}
|
|
681
|
+
);
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
sn.scrollToOffset({ offset: opts.offset, animated: opts.animated });
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
439
687
|
}
|
|
440
688
|
current = current.return;
|
|
441
689
|
}
|
|
@@ -444,7 +692,23 @@ const COMMANDS = {
|
|
|
444
692
|
function findTestId(fiber) {
|
|
445
693
|
if (!fiber) return null;
|
|
446
694
|
var props = fiber.memoizedProps;
|
|
447
|
-
if (props && props.testID === opts.testId)
|
|
695
|
+
if (props && props.testID === opts.testId) {
|
|
696
|
+
var ancestor = fiber;
|
|
697
|
+
var inactive = false;
|
|
698
|
+
while (ancestor) {
|
|
699
|
+
var ancestorProps = ancestor.memoizedProps;
|
|
700
|
+
var ancestorStyle = ancestorProps && ancestorProps.style;
|
|
701
|
+
if (
|
|
702
|
+
(ancestorProps && ancestorProps.activityState === 0) ||
|
|
703
|
+
(ancestorStyle && ancestorStyle.display === 'none')
|
|
704
|
+
) {
|
|
705
|
+
inactive = true;
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
ancestor = ancestor.return;
|
|
709
|
+
}
|
|
710
|
+
if (!inactive) return fiber;
|
|
711
|
+
}
|
|
448
712
|
return findTestId(fiber.child) || findTestId(fiber.sibling);
|
|
449
713
|
}
|
|
450
714
|
for (var [id] of renderers) {
|
|
@@ -460,11 +724,16 @@ const COMMANDS = {
|
|
|
460
724
|
scrolled = tryScroll(r.current);
|
|
461
725
|
}
|
|
462
726
|
});
|
|
463
|
-
if (scrolled)
|
|
727
|
+
if (scrolled) {
|
|
728
|
+
if (typeof scrolled.then === 'function') return scrolled;
|
|
729
|
+
return { ok: true, testId: opts.testId, offset: opts.offset, animated: opts.animated };
|
|
730
|
+
}
|
|
464
731
|
}
|
|
465
732
|
return { ok: false, error: opts.testId ? 'No scrollable near testID=' + opts.testId : 'No scrollable found' };
|
|
466
733
|
})()`;
|
|
467
|
-
const result =
|
|
734
|
+
const result = intoView
|
|
735
|
+
? await cdpEvalAsync(client, expr)
|
|
736
|
+
: await cdpEval(client, expr);
|
|
468
737
|
return { ...result, deviceName };
|
|
469
738
|
},
|
|
470
739
|
|
|
@@ -842,34 +1111,33 @@ const COMMANDS = {
|
|
|
842
1111
|
},
|
|
843
1112
|
|
|
844
1113
|
async 'show-step-json'(client, args) {
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
try {
|
|
848
|
-
step = JSON.parse(raw);
|
|
849
|
-
} catch (error) {
|
|
850
|
-
throw new Error(`show-step-json requires a JSON step payload: ${error.message}`);
|
|
851
|
-
}
|
|
852
|
-
if (!step || typeof step !== 'object' || Array.isArray(step)) {
|
|
853
|
-
throw new Error('show-step-json requires a JSON object step payload');
|
|
854
|
-
}
|
|
855
|
-
if (typeof step.intent !== 'string' || !step.intent.trim()) {
|
|
856
|
-
throw new Error('show-step-json requires step.intent');
|
|
857
|
-
}
|
|
858
|
-
// HUD derives status/progress from step.id when status is omitted; a
|
|
859
|
-
// non-string id makes statusForStep throw during render. Reject at the boundary.
|
|
860
|
-
if (step.id !== undefined && typeof step.id !== 'string') {
|
|
861
|
-
throw new Error('show-step-json step.id must be a string when provided');
|
|
862
|
-
}
|
|
863
|
-
const payload = JSON.stringify(step);
|
|
864
|
-
await cdpEval(client, `globalThis.__AGENTIC__?.showStep && globalThis.__AGENTIC__.showStep(${payload})`);
|
|
1114
|
+
const step = parseHudStep(args);
|
|
1115
|
+
await applyHudUpdate(client, step);
|
|
865
1116
|
return { ok: true };
|
|
866
1117
|
},
|
|
867
1118
|
|
|
1119
|
+
async 'show-step-json-deferred'(client, args) {
|
|
1120
|
+
const step = parseHudStep(args);
|
|
1121
|
+
if (typeof client.control === 'function') {
|
|
1122
|
+
return client.control('hud-update', { step });
|
|
1123
|
+
}
|
|
1124
|
+
await applyHudUpdate(client, step);
|
|
1125
|
+
return { ok: true, status: 'applied' };
|
|
1126
|
+
},
|
|
1127
|
+
|
|
868
1128
|
async 'hide-step'(client) {
|
|
869
|
-
await
|
|
1129
|
+
await applyHudUpdate(client, null);
|
|
870
1130
|
return { ok: true };
|
|
871
1131
|
},
|
|
872
1132
|
|
|
1133
|
+
async 'hide-step-deferred'(client) {
|
|
1134
|
+
if (typeof client.control === 'function') {
|
|
1135
|
+
return client.control('hud-update', { step: null });
|
|
1136
|
+
}
|
|
1137
|
+
await applyHudUpdate(client, null);
|
|
1138
|
+
return { ok: true, status: 'applied' };
|
|
1139
|
+
},
|
|
1140
|
+
|
|
873
1141
|
async 'profiler-start'(client) {
|
|
874
1142
|
// Hermes CDP exposes the sampling profiler via the Profiler domain.
|
|
875
1143
|
// Output of Profiler.stop is a Chrome-compatible .cpuprofile object.
|
|
@@ -961,7 +1229,6 @@ const BRIDGE_LOCK_FILE = path.join(
|
|
|
961
1229
|
process.env.RECIPE_RUNTIME_DIR || path.join('temp', 'recipe', 'runtime'),
|
|
962
1230
|
'cdp-bridge.lock',
|
|
963
1231
|
);
|
|
964
|
-
const BROKER_SOCKET_FILE = brokerSocketPath(path.dirname(BRIDGE_LOCK_FILE));
|
|
965
1232
|
const BRIDGE_LOCK_OWNER_PID = /^\d+$/.test(
|
|
966
1233
|
process.env.CDP_BRIDGE_LOCK_OWNER_PID || '',
|
|
967
1234
|
)
|
|
@@ -1063,30 +1330,70 @@ Environment:
|
|
|
1063
1330
|
}
|
|
1064
1331
|
|
|
1065
1332
|
const port = loadPort();
|
|
1333
|
+
const brokerSocketFile = brokerSocketPath(
|
|
1334
|
+
path.dirname(BRIDGE_LOCK_FILE),
|
|
1335
|
+
port,
|
|
1336
|
+
);
|
|
1066
1337
|
const timeout = Number.parseInt(process.env.CDP_TIMEOUT || '5000', 10);
|
|
1067
|
-
const brokerAvailable = fs.existsSync(
|
|
1338
|
+
const brokerAvailable = fs.existsSync(brokerSocketFile);
|
|
1068
1339
|
if (!brokerAvailable) acquireBridgeLock();
|
|
1069
1340
|
|
|
1070
|
-
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
const
|
|
1075
|
-
return
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1341
|
+
function selectedTargetPin() {
|
|
1342
|
+
const platform = String(process.env.MM_HARNESS_EXPLICIT_PLATFORM || '').toLowerCase();
|
|
1343
|
+
const androidPin =
|
|
1344
|
+
process.env.ANDROID_TARGET_DEVICE_NAME || process.env.ANDROID_DEVICE || '';
|
|
1345
|
+
const iosPin = process.env.IOS_SIMULATOR || '';
|
|
1346
|
+
return String(
|
|
1347
|
+
platform === 'android'
|
|
1348
|
+
? androidPin
|
|
1349
|
+
: platform === 'ios'
|
|
1350
|
+
? iosPin
|
|
1351
|
+
: androidPin || iosPin,
|
|
1352
|
+
).trim();
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
async function brokerTargets({ retainIdentity = false } = {}) {
|
|
1356
|
+
const discoveryClient = await createBrokerClient(
|
|
1357
|
+
brokerSocketFile,
|
|
1358
|
+
'',
|
|
1359
|
+
timeout,
|
|
1360
|
+
);
|
|
1361
|
+
try {
|
|
1362
|
+
const targets = await discoveryClient.control(
|
|
1363
|
+
retainIdentity ? 'resolve-targets' : 'list-targets',
|
|
1364
|
+
retainIdentity ? { nameIncludes: selectedTargetPin() } : {},
|
|
1365
|
+
timeout,
|
|
1366
|
+
);
|
|
1367
|
+
return (Array.isArray(targets) ? targets : []).flatMap((target) => {
|
|
1368
|
+
const deviceId = String(target?.deviceId || '');
|
|
1369
|
+
return deviceId
|
|
1370
|
+
? [{ wsUrl: deviceId, deviceName: String(target?.name || '') }]
|
|
1371
|
+
: [];
|
|
1372
|
+
});
|
|
1373
|
+
} finally {
|
|
1374
|
+
discoveryClient.close();
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function selectBrokerTarget(targets) {
|
|
1379
|
+
const pin = selectedTargetPin();
|
|
1380
|
+
const candidates = pin
|
|
1381
|
+
? targets.filter((target) =>
|
|
1382
|
+
target.deviceName.toLowerCase().includes(pin.toLowerCase()),
|
|
1383
|
+
)
|
|
1384
|
+
: targets;
|
|
1385
|
+
if (candidates.length !== 1) {
|
|
1386
|
+
throw new Error(
|
|
1387
|
+
`Mobile CDP broker target selection requires one target; found ${candidates.length}${pin ? ` for ${JSON.stringify(pin)}` : ''}.`,
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
return candidates[0];
|
|
1084
1391
|
}
|
|
1085
1392
|
|
|
1086
1393
|
async function clientFor(wsUrl) {
|
|
1087
1394
|
if (brokerAvailable) {
|
|
1088
1395
|
return createBrokerClient(
|
|
1089
|
-
|
|
1396
|
+
brokerSocketFile,
|
|
1090
1397
|
deviceIdFromUrl(wsUrl),
|
|
1091
1398
|
timeout,
|
|
1092
1399
|
);
|
|
@@ -1125,15 +1432,16 @@ Environment:
|
|
|
1125
1432
|
return;
|
|
1126
1433
|
}
|
|
1127
1434
|
|
|
1128
|
-
const { wsUrl, deviceName } =
|
|
1129
|
-
|
|
1130
|
-
|
|
1435
|
+
const { wsUrl, deviceName } = brokerAvailable
|
|
1436
|
+
? selectBrokerTarget(await brokerTargets({ retainIdentity: true }))
|
|
1437
|
+
: await discoverTarget(port, { probe: true });
|
|
1131
1438
|
if (!wsUrl) throw new Error('No broker-owned Hermes target is available');
|
|
1132
1439
|
const client = await clientFor(wsUrl);
|
|
1133
1440
|
|
|
1134
1441
|
try {
|
|
1135
|
-
|
|
1136
|
-
|
|
1442
|
+
const platform = command.endsWith('-deferred')
|
|
1443
|
+
? ''
|
|
1444
|
+
: await cdpEval(client, 'globalThis.__AGENTIC__?.platform') || '';
|
|
1137
1445
|
const result = await handler(client, args.slice(1), { deviceName, platform });
|
|
1138
1446
|
console.log(JSON.stringify(result, null, 2));
|
|
1139
1447
|
} finally {
|