@deeeed/metamask-harness 0.15.0 → 0.15.2
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 +12 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/bin/mm-harness +7 -0
- package/dist/adapters.js +9 -2
- package/dist/commands/doctor.js +15 -7
- package/docs/live-adapter-contract.md +1 -1
- package/library/manifests/mobile.action-manifest.json +28 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.15.2 - 2026-07-13
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Mobile `ui.press text=<visible text>` now invokes the app bridge's text-content press path, while `test_id`, `testID`, and `selector` continue to use exact testID matching; ambiguous target combinations fail before bridge execution.
|
|
10
|
+
|
|
11
|
+
## 0.15.1 - 2026-07-13
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `doctor --fix` recovery `nextActions` (numbered fixture choices, overlay install, runtime-context retry) now embed the resolved `--adapter` and absolute `--target`, shell-quoted, so each command works verbatim from any cwd. They are also prefixed with the resolved `bin/mm-harness` executable path, so they work in task-local installs where `mm-harness` is not on PATH.
|
|
16
|
+
|
|
5
17
|
## 0.15.0 - 2026-07-13
|
|
6
18
|
|
|
7
19
|
### Changed
|
|
@@ -254,6 +254,21 @@ const COMMANDS = {
|
|
|
254
254
|
return { ...result, testId, deviceName };
|
|
255
255
|
},
|
|
256
256
|
|
|
257
|
+
async 'press-text'(client, args, { deviceName } = {}) {
|
|
258
|
+
const text = args[0];
|
|
259
|
+
if (!text) {
|
|
260
|
+
throw new Error('Usage: press-text <text>');
|
|
261
|
+
}
|
|
262
|
+
const expr = `(function() {
|
|
263
|
+
if (typeof globalThis.__AGENTIC__?.pressText !== 'function') {
|
|
264
|
+
return { ok: false, error: 'The app bridge does not expose pressText' };
|
|
265
|
+
}
|
|
266
|
+
return globalThis.__AGENTIC__.pressText(${JSON.stringify(text)});
|
|
267
|
+
})()`;
|
|
268
|
+
const result = await cdpEval(client, expr);
|
|
269
|
+
return { ...result, text, deviceName };
|
|
270
|
+
},
|
|
271
|
+
|
|
257
272
|
async 'scroll-view'(client, args, { deviceName } = {}) {
|
|
258
273
|
let testId;
|
|
259
274
|
let offset = 300;
|
|
@@ -678,6 +693,7 @@ Commands:
|
|
|
678
693
|
get-selected-account Get the currently selected account
|
|
679
694
|
switch-account <address> Switch to account by address
|
|
680
695
|
press-test-id <testId> Press a component by its testID prop
|
|
696
|
+
press-text <text> Press a component containing visible text
|
|
681
697
|
scroll-view [--test-id <id>] [--offset <n>] [--animated]
|
|
682
698
|
Scroll a ScrollView/FlatList
|
|
683
699
|
set-input <testId> <value> Set text input value by testID (calls onChangeText)
|
package/bin/mm-harness
CHANGED
|
@@ -50,6 +50,13 @@ if [ -n "${MM_HARNESS_BIN:-}" ]; then
|
|
|
50
50
|
fi
|
|
51
51
|
fi
|
|
52
52
|
|
|
53
|
+
# Internal executable identity (not part of the public env surface): the CLI
|
|
54
|
+
# embeds this in emitted recovery commands so they run verbatim even when
|
|
55
|
+
# mm-harness is not on PATH (task-local installs). Set AFTER the MM_HARNESS_BIN
|
|
56
|
+
# delegation above so the final executable entered always overwrites any
|
|
57
|
+
# inherited/stale value with its own resolved self path.
|
|
58
|
+
export MM_HARNESS_EXECUTABLE="$SCRIPT_DIR/$(basename "$SOURCE")"
|
|
59
|
+
|
|
53
60
|
ENTRY_TS="$RUNNER_DIR/src/mm-harness-cli.ts"
|
|
54
61
|
ENTRY_DIST="$RUNNER_DIR/dist/mm-harness-cli.js"
|
|
55
62
|
|
package/dist/adapters.js
CHANGED
|
@@ -287,8 +287,15 @@ async function handleMobileNavigate(payload, context) {
|
|
|
287
287
|
return isRecord(live.result) ? { ...live.result, liveAdapter: live.script } : live.result;
|
|
288
288
|
}
|
|
289
289
|
async function handleMobilePress(payload, context) {
|
|
290
|
-
const
|
|
291
|
-
|
|
290
|
+
const identifiers = ["test_id", "testID", "selector"].map((field) => optionalScalarText(payload[field], `ui.press.${field}`)).filter((value) => value !== void 0);
|
|
291
|
+
const text = optionalScalarText(payload.text, "ui.press.text");
|
|
292
|
+
if (identifiers.length + Number(text !== void 0) !== 1) {
|
|
293
|
+
throw new Error("ui.press requires exactly one of: test_id, testID, selector, text.");
|
|
294
|
+
}
|
|
295
|
+
const target = text ?? identifiers[0];
|
|
296
|
+
if (target.trim() === "") throw new Error("ui.press target must be non-empty.");
|
|
297
|
+
const input = mobileUiInput(context, "press", payload);
|
|
298
|
+
return text !== void 0 ? bridgeCommand(input, ["press-text", target]) : bridgeCommand(input, ["press-test-id", target]);
|
|
292
299
|
}
|
|
293
300
|
async function handleMobileSetInput(payload, context) {
|
|
294
301
|
const input = mobileUiInput(context, "set_input", payload);
|
package/dist/commands/doctor.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { color } from "../cli-color.js";
|
|
6
6
|
import { createDoctorReport, fixtureSummary, renderRuntimeContext, requiredDoctorCheckSummary } from "../doctor.js";
|
|
7
|
-
import { detectAdapter } from "../harness.js";
|
|
7
|
+
import { detectAdapter, resolveRuntimeContextPath } from "../harness.js";
|
|
8
8
|
import { assertAdapter, runnerDir } from "../paths.js";
|
|
9
9
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
10
10
|
import { ensureMobilePerpsEnvironment, mobilePerpsEnvironment } from "../adapters/mobile/perps-env.js";
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
applyRuntimeDirOption,
|
|
28
28
|
optionFlag,
|
|
29
29
|
optionString,
|
|
30
|
+
shellQuote,
|
|
30
31
|
targetPath
|
|
31
32
|
} from "./parse-args.js";
|
|
32
33
|
function applyDoctorRuntimePorts(options) {
|
|
@@ -95,7 +96,7 @@ async function handleDoctor({ options }) {
|
|
|
95
96
|
lock.release();
|
|
96
97
|
}
|
|
97
98
|
const result2 = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
|
|
98
|
-
const nextActions = doctorFixNextActions(failed, result2.fixture);
|
|
99
|
+
const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target);
|
|
99
100
|
const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
|
|
100
101
|
if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, nextActions }, null, 2));
|
|
101
102
|
else {
|
|
@@ -215,15 +216,21 @@ function doctorRuntimeCheck(runtime, error, expectLive) {
|
|
|
215
216
|
detail: runtime?.reasons.join(" | ") || "Runtime readiness probe returned no result."
|
|
216
217
|
};
|
|
217
218
|
}
|
|
218
|
-
function doctorFixNextActions(failed, fixture) {
|
|
219
|
+
function doctorFixNextActions(failed, fixture, adapter, target) {
|
|
219
220
|
const actions = [];
|
|
221
|
+
const executable = process.env.MM_HARNESS_EXECUTABLE ? shellQuote(process.env.MM_HARNESS_EXECUTABLE) : "mm-harness";
|
|
222
|
+
const scope = `--adapter ${adapter} --target ${shellQuote(target)}`;
|
|
220
223
|
if (failed.includes("wallet-fixture")) {
|
|
221
224
|
const force = fixture.status === "invalid" || fixture.status === "incomplete" ? " --force" : "";
|
|
222
|
-
actions.push(`1)
|
|
223
|
-
actions.push(`2)
|
|
225
|
+
actions.push(`1) ${executable} fixtures init --from <path>${force} ${scope} # existing team/test fixture`);
|
|
226
|
+
actions.push(`2) ${executable} fixtures init --dev${force} ${scope} # disposable public test wallet; never real funds`);
|
|
224
227
|
}
|
|
225
|
-
if (failed.includes("runtime-context"))
|
|
226
|
-
|
|
228
|
+
if (failed.includes("runtime-context")) {
|
|
229
|
+
actions.push(
|
|
230
|
+
`inspect ${shellQuote(resolveRuntimeContextPath(target))}, then retry ${executable} doctor --fix ${scope}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (failed.includes("overlay")) actions.push(`${executable} install ${scope}`);
|
|
227
234
|
return actions;
|
|
228
235
|
}
|
|
229
236
|
function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
|
|
@@ -389,6 +396,7 @@ function readTextSnapshot(file) {
|
|
|
389
396
|
}
|
|
390
397
|
}
|
|
391
398
|
export {
|
|
399
|
+
doctorFixNextActions,
|
|
392
400
|
ensureExtensionPerpsBuildFlag,
|
|
393
401
|
handleDoctor
|
|
394
402
|
};
|
|
@@ -185,4 +185,4 @@ ui.screenshot
|
|
|
185
185
|
|
|
186
186
|
Navigation supports a small discoverable `page` alias set (`home`, `perps`, `perps-market`) plus raw React Navigation route/params fallback. Check the action manifest before hardcoding routes.
|
|
187
187
|
|
|
188
|
-
Read-only position checks use `Engine.context.PerpsController.getPositions()` through Hermes CDP. State-changing Perps actions use supported controller APIs (`placeOrder`, `closePositions`) through the same app bridge rather than mutating Redux/React/local storage.
|
|
188
|
+
Read-only position checks use `Engine.context.PerpsController.getPositions()` through Hermes CDP. State-changing Perps actions use supported controller APIs (`placeOrder`, `closePositions`) through the same app bridge rather than mutating Redux/React/local storage. Mobile `ui.press` uses `press-test-id` for `test_id`/`testID`/`selector` and `press-text` for visible `text`; scrolling uses `scroll-view`. Screenshot capture uses `xcrun simctl io <simulator> screenshot` for iOS simulator proof.
|
|
@@ -156,13 +156,38 @@
|
|
|
156
156
|
]
|
|
157
157
|
},
|
|
158
158
|
"ui.press": {
|
|
159
|
-
"description": "Press/
|
|
159
|
+
"description": "Press a React Native component by exact testID/test_id, a legacy selector identifier, or contained visible text.",
|
|
160
|
+
"schema": {
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {
|
|
163
|
+
"action": { "const": "ui.press" },
|
|
164
|
+
"test_id": { "type": "string", "minLength": 1 },
|
|
165
|
+
"testID": { "type": "string", "minLength": 1 },
|
|
166
|
+
"selector": { "type": "string", "minLength": 1 },
|
|
167
|
+
"text": { "type": "string", "minLength": 1 }
|
|
168
|
+
},
|
|
169
|
+
"oneOf": [
|
|
170
|
+
{ "required": ["test_id"] },
|
|
171
|
+
{ "required": ["testID"] },
|
|
172
|
+
{ "required": ["selector"] },
|
|
173
|
+
{ "required": ["text"] }
|
|
174
|
+
]
|
|
175
|
+
},
|
|
160
176
|
"examples": [
|
|
161
177
|
{
|
|
178
|
+
"description": "Press by stable React Native testID",
|
|
179
|
+
"node": {
|
|
180
|
+
"action": "ui.press",
|
|
181
|
+
"test_id": "perps-tab",
|
|
182
|
+
"intent": "Open Perps through its stable testID"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"description": "Press a component that is reachable only by visible text",
|
|
162
187
|
"node": {
|
|
163
188
|
"action": "ui.press",
|
|
164
|
-
"
|
|
165
|
-
"intent": "Press
|
|
189
|
+
"text": "Ethereum",
|
|
190
|
+
"intent": "Press the component containing the visible Ethereum label"
|
|
166
191
|
}
|
|
167
192
|
}
|
|
168
193
|
]
|