@deeeed/metamask-harness 0.74.0 → 0.76.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 +39 -1
- package/README.md +1 -1
- package/adapters/shared/ensure-runner-deps.sh +15 -3
- package/adapters/shared/reap-checkout-metros.sh +9 -0
- package/dist/action-values.js +15 -0
- package/dist/adapters/core/actions.js +20 -0
- package/dist/adapters/core/live-script-env.js +36 -0
- package/dist/adapters/core/overlay.js +34 -0
- package/dist/adapters/core/readiness.js +8 -0
- package/dist/adapters/core/run.js +54 -0
- package/dist/adapters/core/surface.js +12 -1
- package/dist/adapters/extension/action-inputs.js +206 -0
- package/dist/adapters/extension/actions.js +51 -0
- package/dist/adapters/extension/console-capture.js +13 -1
- package/dist/adapters/extension/doctor-checks.js +135 -0
- package/dist/adapters/extension/fixtures.js +103 -0
- package/dist/adapters/extension/launch.js +315 -0
- package/dist/adapters/extension/launched-browser.js +51 -0
- package/dist/adapters/extension/overlay.js +55 -0
- package/dist/adapters/extension/readiness.js +32 -0
- package/dist/adapters/extension/reload.js +40 -0
- package/dist/adapters/extension/run.js +105 -0
- package/dist/adapters/extension/surface.js +31 -3
- package/dist/adapters/harness-freshness.js +4 -2
- package/dist/adapters/mobile/actions.js +54 -0
- package/dist/adapters/mobile/devices.js +21 -0
- package/dist/adapters/mobile/doctor-checks.js +40 -0
- package/dist/adapters/mobile/fixtures.js +141 -0
- package/dist/adapters/mobile/launch.js +192 -0
- package/dist/adapters/mobile/lifecycle.js +300 -0
- package/dist/adapters/mobile/overlay.js +27 -0
- package/dist/adapters/mobile/prepare-target.js +38 -0
- package/dist/adapters/mobile/readiness.js +39 -0
- package/dist/adapters/mobile/reload.js +15 -0
- package/dist/adapters/mobile/run.js +156 -0
- package/dist/adapters/mobile/runtime-identity.js +34 -0
- package/dist/adapters/mobile/surface.js +39 -3
- package/dist/adapters/mobile/ui.js +701 -0
- package/dist/adapters/performance/cdp-trace.js +24 -6
- package/dist/adapters/performance/device-platform.js +10 -0
- package/dist/adapters/registry.js +1 -1
- package/dist/adapters/terminal/actions.js +43 -0
- package/dist/adapters/terminal/console-capture.js +14 -1
- package/dist/adapters/terminal/doctor-checks.js +22 -0
- package/dist/adapters/terminal/overlay.js +18 -0
- package/dist/adapters/terminal/readiness.js +11 -0
- package/dist/adapters/terminal/run.js +30 -0
- package/dist/adapters/terminal/surface.js +21 -3
- package/dist/adapters.js +32 -824
- package/dist/cli-commands.js +1 -0
- package/dist/cli.js +3 -1
- package/dist/command-contract.js +3 -0
- package/dist/commands/call.js +2 -5
- package/dist/commands/debug.js +6 -25
- package/dist/commands/device-target.js +2 -1
- package/dist/commands/doctor.js +26 -137
- package/dist/commands/fixtures.js +13 -217
- package/dist/commands/launch/device.js +2 -6
- package/dist/commands/launch/extension.js +3 -303
- package/dist/commands/launch/index.js +1 -1
- package/dist/commands/launch/mobile.js +2 -190
- package/dist/commands/launch/quote.js +7 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/parse-args.js +9 -4
- package/dist/commands/pin.js +226 -0
- package/dist/commands/prepare.js +23 -53
- package/dist/commands/provision.js +3 -7
- package/dist/commands/reload.js +8 -47
- package/dist/commands/run-engine.js +38 -138
- package/dist/commands/run-report.js +4 -48
- package/dist/commands/run.js +6 -48
- package/dist/commands/runtime-decision.js +5 -27
- package/dist/commands/self-test.js +1 -3
- package/dist/commands/status.js +20 -28
- package/dist/commands/stop.js +1 -1
- package/dist/doctor.js +12 -120
- package/dist/execution-provenance.js +3 -2
- package/dist/harness-pin.js +10 -0
- package/dist/harness.js +31 -100
- package/dist/heal-bounds.js +3 -2
- package/dist/live-adapter-contract.js +8 -34
- package/dist/manifest.js +3 -5
- package/dist/metamask-action-validation.js +5 -196
- package/dist/mm-harness-cli.js +30 -4
- package/dist/network-observation.js +10 -3
- package/dist/paths.js +8 -2
- package/dist/performance-observation.js +15 -22
- package/dist/pr-body/render.js +3 -1
- package/dist/recording-target.js +4 -39
- package/dist/run-diagnostics.js +21 -60
- package/dist/run-recording.js +5 -4
- package/dist/runner.js +20 -368
- package/dist/runtime-context.js +12 -17
- package/docs/CONTRIBUTING.md +29 -7
- package/package.json +2 -1
- package/scripts/completions.sh +2 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { optionFlag, optionString, parsePort } from "../../commands/parse-args.js";
|
|
2
|
+
import { ensureExtensionPerpsBuildFlag, extensionBrowserDoctorCheck, extensionBrowserResolutionDoctorCheck } from "./doctor-checks.js";
|
|
3
|
+
import { readExtensionPinnedFlags } from "./feature-flags.js";
|
|
4
|
+
import { extensionProductConfigBlock } from "./product-config.js";
|
|
5
|
+
const extensionReadiness = {
|
|
6
|
+
compatibilityMode(_target, shape) {
|
|
7
|
+
if (!shape.extensionProject) return "unsupported/no bridge";
|
|
8
|
+
if (shape.extensionRuntime || shape.injectedHarness) return "bridge present";
|
|
9
|
+
return "bridge injectable";
|
|
10
|
+
},
|
|
11
|
+
checks: (target) => [extensionBrowserDoctorCheck(target), extensionBrowserResolutionDoctorCheck(target)],
|
|
12
|
+
captureProviders: ["cdp"],
|
|
13
|
+
fixes: [{ id: "extension-perps-build", apply: ensureExtensionPerpsBuildFlag }],
|
|
14
|
+
runtimeBlock: (target, runtime) => runtime?.reasonCode === "product-config-required" ? { id: "extension-product-config", userAction: extensionProductConfigBlock(target)?.userAction } : void 0,
|
|
15
|
+
pinnedFlags: readExtensionPinnedFlags,
|
|
16
|
+
statusRuntime: true
|
|
17
|
+
};
|
|
18
|
+
async function extensionRuntimeDecision(target, options) {
|
|
19
|
+
const cdpPortRaw = optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
|
|
20
|
+
const cdpPort = cdpPortRaw === void 0 ? void 0 : parsePort(cdpPortRaw, "runtime-decision --cdp-port must be a port.");
|
|
21
|
+
const { decideExtensionReadiness } = await import("./runtime-decision.js");
|
|
22
|
+
const report = await decideExtensionReadiness(target, {
|
|
23
|
+
cdpPort,
|
|
24
|
+
watchLog: optionString(options, "watchLog"),
|
|
25
|
+
record: optionFlag(options, "record")
|
|
26
|
+
});
|
|
27
|
+
return { report, line: `${report.decision}${report.clean ? " (clean)" : ""} ${report.reasonCode} \u2014 ${report.reasons[0] ?? ""}` };
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
extensionReadiness,
|
|
31
|
+
extensionRuntimeDecision
|
|
32
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { EXIT, spawnScript, usageOut } from "../../commands/shared.js";
|
|
3
|
+
import { runnerDir } from "../../paths.js";
|
|
4
|
+
function reloadExtension(target, json) {
|
|
5
|
+
const adapter = "extension";
|
|
6
|
+
if (!process.env.CDP_PORT) {
|
|
7
|
+
return usageOut(
|
|
8
|
+
json,
|
|
9
|
+
"reload",
|
|
10
|
+
"the Extension CDP port could not be resolved.",
|
|
11
|
+
"mm-harness launch"
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
const script = path.join(runnerDir, "adapters/extension/reattach.sh");
|
|
15
|
+
const args = ["--target", target, "--cdp-port", process.env.CDP_PORT];
|
|
16
|
+
if (process.env.WATCHER_PORT) {
|
|
17
|
+
args.push("--watcher-port", process.env.WATCHER_PORT);
|
|
18
|
+
}
|
|
19
|
+
const result = spawnScript(script, args, target, json);
|
|
20
|
+
if (json) {
|
|
21
|
+
console.log(
|
|
22
|
+
JSON.stringify(
|
|
23
|
+
{
|
|
24
|
+
ok: result.status === 0,
|
|
25
|
+
adapter,
|
|
26
|
+
method: "cdp-reattach",
|
|
27
|
+
command: "reload",
|
|
28
|
+
cdpPort: Number(process.env.CDP_PORT),
|
|
29
|
+
...result.status === 0 ? {} : { error: result.output.slice(-4e3) || "Extension reload failed." }
|
|
30
|
+
},
|
|
31
|
+
null,
|
|
32
|
+
2
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return result.status === 0 ? EXIT.ok : EXIT.runtime;
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
reloadExtension
|
|
40
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { optionString } from "../../commands/parse-args.js";
|
|
2
|
+
import { resolveExtensionBrowserPid } from "../../recording-target.js";
|
|
3
|
+
import {
|
|
4
|
+
ensureExtensionConsoleCapture,
|
|
5
|
+
extensionCaptureFiles,
|
|
6
|
+
extensionCdpPort,
|
|
7
|
+
verifyExtensionConsoleControl
|
|
8
|
+
} from "./console-capture.js";
|
|
9
|
+
import { launchedBrowser } from "./launched-browser.js";
|
|
10
|
+
const extensionRun = {
|
|
11
|
+
async prepareRuntime(projectRoot, runtimeOptions) {
|
|
12
|
+
if (runtimeOptions.skipExtensionRuntimePrepare === true) return;
|
|
13
|
+
const { releaseArtifactState, runtimeDistCheck } = await import("./runtime-decision.js");
|
|
14
|
+
const artifact = releaseArtifactState(projectRoot);
|
|
15
|
+
const runtimeSnapshot = runtimeDistCheck(projectRoot, artifact);
|
|
16
|
+
if (artifact.status !== "none") {
|
|
17
|
+
if (artifact.status === "valid" && runtimeSnapshot.status === "fresh") return;
|
|
18
|
+
throw new Error(
|
|
19
|
+
"The loaded release artifact identity is invalid or stale. Next: rerun runtime-launch with the same artifact source."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
const { prepareExtensionRuntime } = await import("./runtime.js");
|
|
23
|
+
await prepareExtensionRuntime({
|
|
24
|
+
projectRoot,
|
|
25
|
+
cdpPort: runtimeOptions.cdpPort,
|
|
26
|
+
slot: runtimeOptions.slot,
|
|
27
|
+
launchExistingDist: runtimeOptions.launchExistingDist === true,
|
|
28
|
+
validationRuntimeDir: runtimeOptions.validationRuntimeDir,
|
|
29
|
+
pageMode: "home-or-sidepanel"
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
// The proof runtime is checked on the run's own ports: explicit flags win over
|
|
33
|
+
// the slot's for the check, and the slot's come back afterwards.
|
|
34
|
+
runtimeCheck({ target, options, heal, state, json, onRecovery }) {
|
|
35
|
+
return async () => {
|
|
36
|
+
const cliOptions = options;
|
|
37
|
+
const previousCdpPort = process.env.CDP_PORT;
|
|
38
|
+
const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
39
|
+
const previousWatcherPort = process.env.WATCHER_PORT;
|
|
40
|
+
const explicitCdpPort = optionString(cliOptions, "cdpPort");
|
|
41
|
+
const explicitWatcherPort = optionString(cliOptions, "watcherPort") ?? optionString(cliOptions, "metroPort");
|
|
42
|
+
if (explicitCdpPort) {
|
|
43
|
+
process.env.CDP_PORT = explicitCdpPort;
|
|
44
|
+
process.env.RECIPE_CDP_PORT = explicitCdpPort;
|
|
45
|
+
}
|
|
46
|
+
if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
|
|
47
|
+
try {
|
|
48
|
+
const { ensureExtensionProofRuntime } = await import("../../commands/run-engine.js");
|
|
49
|
+
return await ensureExtensionProofRuntime(target, heal, state, json, { onRecovery });
|
|
50
|
+
} finally {
|
|
51
|
+
restoreEnv("CDP_PORT", previousCdpPort);
|
|
52
|
+
restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
|
|
53
|
+
restoreEnv("WATCHER_PORT", previousWatcherPort);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
launchedBrowser: (target, artifactsDir, cdpPort) => launchedBrowser(target, artifactsDir, cdpPort)
|
|
58
|
+
};
|
|
59
|
+
const extensionRecording = {
|
|
60
|
+
target: (context) => resolveExtensionRecordingTarget(context),
|
|
61
|
+
framed: {
|
|
62
|
+
browserPid: (projectRoot, artifactsDir, cdpPort) => resolveExtensionBrowserPid(projectRoot, artifactsDir, cdpPort)
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
async function resolveExtensionRecordingTarget(context) {
|
|
66
|
+
const pid = resolveExtensionBrowserPid(
|
|
67
|
+
context.projectRoot,
|
|
68
|
+
context.artifactsDir,
|
|
69
|
+
context.env.CDP_PORT ?? context.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT
|
|
70
|
+
);
|
|
71
|
+
if (!pid) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"Could not resolve the extension browser PID for --record-video. Ensure the slot browser is running and CDP_PORT is set."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return { kind: "pid", pid };
|
|
77
|
+
}
|
|
78
|
+
const extensionDiagnostics = {
|
|
79
|
+
console: {
|
|
80
|
+
start: (projectRoot) => ensureExtensionConsoleCapture(projectRoot),
|
|
81
|
+
files: (projectRoot) => extensionCaptureFiles(projectRoot),
|
|
82
|
+
cdpPort: () => extensionCdpPort(),
|
|
83
|
+
verifyControl: (capture) => verifyExtensionConsoleControl(capture)
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const extensionObservation = {
|
|
87
|
+
network: {
|
|
88
|
+
backend: async (_target, env, artifactsDir) => (await import("../../network-observation.js")).createExtensionNetworkBackend(env, artifactsDir),
|
|
89
|
+
actions: true
|
|
90
|
+
},
|
|
91
|
+
performance: {
|
|
92
|
+
backend: async (target, env, ports) => (await import("../../performance-observation.js")).createExtensionPerformanceObserver(target, env, ports),
|
|
93
|
+
platform: () => "extension"
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
function restoreEnv(key, value) {
|
|
97
|
+
if (value === void 0) delete process.env[key];
|
|
98
|
+
else process.env[key] = value;
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
extensionDiagnostics,
|
|
102
|
+
extensionObservation,
|
|
103
|
+
extensionRecording,
|
|
104
|
+
extensionRun
|
|
105
|
+
};
|
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
import { decideExtensionReadiness } from "./runtime-decision.js";
|
|
11
11
|
import { extensionProductConfigBlock } from "./product-config.js";
|
|
12
12
|
import { adapterRegistry } from "../registry.js";
|
|
13
|
+
import { extensionActions } from "./actions.js";
|
|
14
|
+
import { extensionHarness, extensionPrepare, extensionRuntimeContext } from "./overlay.js";
|
|
15
|
+
import { extensionReadiness, extensionRuntimeDecision } from "./readiness.js";
|
|
16
|
+
import { extensionDiagnostics, extensionObservation, extensionRecording, extensionRun } from "./run.js";
|
|
13
17
|
function watcherStatus(buildLog) {
|
|
14
18
|
if (buildLog === "ok") return "up";
|
|
15
19
|
if (buildLog === "no-watch") return "down";
|
|
@@ -82,7 +86,8 @@ const extensionSurface = {
|
|
|
82
86
|
spawnSync("bash", [stopViewers, "--target", target], { stdio: "ignore", timeout: 5e3 });
|
|
83
87
|
const summary = stopped.signalled > 0 || stopped.removedMarkers > 0 ? `stopped Extension runtime (${stopped.signalled} process${stopped.signalled === 1 ? "" : "es"}, ${stopped.removedMarkers} stale marker${stopped.removedMarkers === 1 ? "" : "s"}) for ${target}` : `Extension runtime not running for ${target} \u2014 nothing to stop`;
|
|
84
88
|
return { kind: "stopped", status: 0, summary, signalled: stopped.signalled };
|
|
85
|
-
}
|
|
89
|
+
},
|
|
90
|
+
afterStop: (target) => `mm-harness launch --target ${shellQuote(target)}`
|
|
86
91
|
},
|
|
87
92
|
logSources(target) {
|
|
88
93
|
return [
|
|
@@ -103,10 +108,33 @@ const extensionSurface = {
|
|
|
103
108
|
launch: async (context) => {
|
|
104
109
|
const [{ runDeviceLaunch }, { extensionLaunchProfile }] = await Promise.all([
|
|
105
110
|
import("../../commands/launch/device.js"),
|
|
106
|
-
import("
|
|
111
|
+
import("./launch.js")
|
|
107
112
|
]);
|
|
108
113
|
return runDeviceLaunch(context, extensionLaunchProfile);
|
|
109
|
-
}
|
|
114
|
+
},
|
|
115
|
+
actions: extensionActions,
|
|
116
|
+
harness: extensionHarness,
|
|
117
|
+
runtimeContext: extensionRuntimeContext,
|
|
118
|
+
readiness: extensionReadiness,
|
|
119
|
+
fixtures: {
|
|
120
|
+
operations: ["init", "sync", "set", "reset", "generate", "finalize"],
|
|
121
|
+
apply: async (context) => (await import("./fixtures.js")).applyExtensionFixture(context)
|
|
122
|
+
},
|
|
123
|
+
prepare: extensionPrepare,
|
|
124
|
+
debug: {
|
|
125
|
+
refuse: ({ devMenu }) => devMenu ? { message: "--dev-menu is mobile-only (RN developer menu). Use --worker on the extension.", userAction: "mm-harness debug --worker" } : void 0,
|
|
126
|
+
args: ({ worker }) => [
|
|
127
|
+
...process.env.CDP_PORT ? ["--cdp-port", process.env.CDP_PORT] : [],
|
|
128
|
+
"--target",
|
|
129
|
+
worker ? "worker" : "page"
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
reload: async (target, json) => (await import("./reload.js")).reloadExtension(target, json),
|
|
133
|
+
runtimeDecision: extensionRuntimeDecision,
|
|
134
|
+
run: extensionRun,
|
|
135
|
+
recording: extensionRecording,
|
|
136
|
+
diagnostics: extensionDiagnostics,
|
|
137
|
+
observation: extensionObservation
|
|
110
138
|
};
|
|
111
139
|
adapterRegistry.register(extensionSurface);
|
|
112
140
|
export {
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { recipeHarnessPath, runnerDir } from "../paths.js";
|
|
6
|
+
import { getAdapterSurface } from "./surface.js";
|
|
6
7
|
async function ensureHarnessFresh(target, adapter) {
|
|
7
8
|
if (!fs.existsSync(path.join(target, "package.json")) || !hasCheckoutHead(target)) return;
|
|
8
9
|
let current;
|
|
@@ -17,9 +18,10 @@ async function ensureHarnessFresh(target, adapter) {
|
|
|
17
18
|
const installed = readInstalledFingerprint(manifestPath);
|
|
18
19
|
if (installed === current) return;
|
|
19
20
|
const reason = installed === null ? "harness not installed" : "runner changed";
|
|
20
|
-
const
|
|
21
|
+
const install = getAdapterSurface(adapter).harness.install;
|
|
22
|
+
const injector = path.resolve(runnerDir, install.entry);
|
|
21
23
|
try {
|
|
22
|
-
execFileSync(
|
|
24
|
+
execFileSync(install.node ? process.execPath : "bash", [injector, "--target", target], {
|
|
23
25
|
stdio: ["ignore", "ignore", "inherit"]
|
|
24
26
|
});
|
|
25
27
|
console.error(`\u2192 refreshed ${adapter} harness overlay (${reason})`);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { manifestPath } from "../../paths.js";
|
|
2
|
+
const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
|
|
3
|
+
"ui.press",
|
|
4
|
+
"ui.key_press",
|
|
5
|
+
"ui.set_input",
|
|
6
|
+
"ui.scroll",
|
|
7
|
+
"ui.swipe",
|
|
8
|
+
"ui.pan",
|
|
9
|
+
"ui.drag",
|
|
10
|
+
"ui.long_press",
|
|
11
|
+
"ui.wait_for",
|
|
12
|
+
"ui.screenshot",
|
|
13
|
+
"ui.capture_surface"
|
|
14
|
+
]);
|
|
15
|
+
const mobileActions = {
|
|
16
|
+
manifestPath: () => manifestPath("mobile"),
|
|
17
|
+
semantic: [
|
|
18
|
+
"app.network_capture",
|
|
19
|
+
"app.network_assert",
|
|
20
|
+
"app.performance_capture",
|
|
21
|
+
"app.performance_assert",
|
|
22
|
+
"metamask.swap_bridge.submit_transaction",
|
|
23
|
+
"metamask.perps.close_positions",
|
|
24
|
+
"metamask.perps.start_state",
|
|
25
|
+
"metamask.perps.clear_performance_caches",
|
|
26
|
+
"metamask.perps.measure_homepage_visible",
|
|
27
|
+
"metamask.perps.prepare_local_snapshot_endpoint"
|
|
28
|
+
],
|
|
29
|
+
cdpTarget: {
|
|
30
|
+
transport: "react-native-debug-bridge",
|
|
31
|
+
probePath: "json/list",
|
|
32
|
+
watcherPortFirst: true,
|
|
33
|
+
requiredProbe: async (node, context) => (await import("./ui.js")).probeMobileBridgeTarget(node, context)
|
|
34
|
+
},
|
|
35
|
+
ui: (harness) => ({
|
|
36
|
+
base: harness.createReactNativeBridgeUiTransport({
|
|
37
|
+
bridge: {
|
|
38
|
+
send: async (command, context) => (await import("./ui.js")).sendMobileBridgeCommand(command, context)
|
|
39
|
+
}
|
|
40
|
+
}),
|
|
41
|
+
async execute(action, node, context, standard) {
|
|
42
|
+
if (NATIVE_PROVIDER_UI_ACTIONS.has(action) && (context.env?.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1" || ["ui.swipe", "ui.pan", "ui.drag", "ui.long_press", "ui.capture_surface"].includes(action))) {
|
|
43
|
+
const { executeNativeProviderAction } = await import("./ui.js");
|
|
44
|
+
return executeNativeProviderAction(action, node, context, harness.createNativeUiTransport);
|
|
45
|
+
}
|
|
46
|
+
return standard();
|
|
47
|
+
}
|
|
48
|
+
}),
|
|
49
|
+
wrap: async (adapters) => (await import("./runtime-identity.js")).withMobileNativeRuntimeIdentity(adapters),
|
|
50
|
+
lifecycle: async (adapters) => (await import("./lifecycle.js")).mobileSourceAwareLifecycleAdapters(adapters)
|
|
51
|
+
};
|
|
52
|
+
export {
|
|
53
|
+
mobileActions
|
|
54
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mobileDeviceLiveView,
|
|
3
|
+
mobileDeviceView,
|
|
4
|
+
nextForLive,
|
|
5
|
+
renderAdditionalReachableDevices,
|
|
6
|
+
renderAndroidPortHints,
|
|
7
|
+
renderMobileDeviceList,
|
|
8
|
+
renderMobileLiveBlock
|
|
9
|
+
} from "../../commands/mobile-device-view.js";
|
|
10
|
+
const mobileDevices = {
|
|
11
|
+
view: mobileDeviceView,
|
|
12
|
+
live: mobileDeviceLiveView,
|
|
13
|
+
renderList: renderMobileDeviceList,
|
|
14
|
+
renderAdditional: renderAdditionalReachableDevices,
|
|
15
|
+
renderLive: renderMobileLiveBlock,
|
|
16
|
+
renderPortHints: renderAndroidPortHints,
|
|
17
|
+
nextForLive
|
|
18
|
+
};
|
|
19
|
+
export {
|
|
20
|
+
mobileDevices
|
|
21
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
mobileToolRecovery,
|
|
5
|
+
resolveMobileToolPath
|
|
6
|
+
} from "../../../library/actions/mobile/platform/tool-paths.mjs";
|
|
7
|
+
import { runnerDir } from "../../paths.js";
|
|
8
|
+
function mobileToolDoctorChecks(platform) {
|
|
9
|
+
const specs = platform === "android" ? [{ tool: "adb", platform: "Android" }] : platform === "ios" ? [{ tool: "idb", platform: "iOS" }] : [
|
|
10
|
+
{ tool: "adb", platform: "Android" },
|
|
11
|
+
{ tool: "idb", platform: "iOS" }
|
|
12
|
+
];
|
|
13
|
+
return specs.map(({ tool, platform: platformName }) => {
|
|
14
|
+
const resolved = resolveMobileToolPath(tool);
|
|
15
|
+
return {
|
|
16
|
+
id: `device-tool-${tool}`,
|
|
17
|
+
status: resolved ? "pass" : "fail",
|
|
18
|
+
required: platform !== void 0,
|
|
19
|
+
message: resolved ? `${platformName} device tool ${tool} is ready.` : `${platformName} device tool ${tool} is unavailable.`,
|
|
20
|
+
...resolved ? { detail: resolved } : {},
|
|
21
|
+
...!resolved ? { userAction: mobileToolRecovery(tool) } : {}
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function detectOrphanMetros(target, livePort) {
|
|
26
|
+
try {
|
|
27
|
+
const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
|
|
28
|
+
const out = execFileSync("bash", [script, "detect", target, livePort ?? ""], {
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
timeout: 5e3
|
|
31
|
+
});
|
|
32
|
+
return out.split("\n").map((line) => line.trim()).filter((line) => /^\d+$/u.test(line));
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
detectOrphanMetros,
|
|
39
|
+
mobileToolDoctorChecks
|
|
40
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { EXIT, spawnScriptStreaming } from "../../commands/shared.js";
|
|
3
|
+
import { runnerDir } from "../../paths.js";
|
|
4
|
+
import { FIXTURE_STREAM_TIMEOUT_MS } from "../../commands/fixtures.js";
|
|
5
|
+
const FIXTURE_BRIDGE_READY_TIMEOUT_MS = 18e4;
|
|
6
|
+
const MOBILE_SETUP_WALLET_TIMEOUT_MS = 36e4;
|
|
7
|
+
async function applyMobileFixture({
|
|
8
|
+
target,
|
|
9
|
+
sub,
|
|
10
|
+
fixturePath,
|
|
11
|
+
canonicalFixture,
|
|
12
|
+
fixtureFlags,
|
|
13
|
+
json,
|
|
14
|
+
retryHint
|
|
15
|
+
}) {
|
|
16
|
+
const adapter = "mobile";
|
|
17
|
+
const fixtureFlagsJson = JSON.stringify(fixtureFlags);
|
|
18
|
+
const reset = sub === "reset";
|
|
19
|
+
process.stderr.write(`\u2192 fixtures ${sub} ${adapter} \u2014 ${reset ? "resetting selected app data, relaunching, then applying" : "connecting bridge + applying"} wallet fixture (can take ~30s)\u2026
|
|
20
|
+
`);
|
|
21
|
+
let status;
|
|
22
|
+
let mobileSetupResult;
|
|
23
|
+
let resetError;
|
|
24
|
+
const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
|
|
25
|
+
const previousAppRoot = process.env.APP_ROOT;
|
|
26
|
+
process.env.APP_ROOT = target;
|
|
27
|
+
try {
|
|
28
|
+
if (reset) {
|
|
29
|
+
const platform = process.env.PLATFORM === "android" || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
|
|
30
|
+
const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "8081";
|
|
31
|
+
const resetAppData = path.join(runnerDir, "adapters/mobile/reset-app-data.sh");
|
|
32
|
+
const resetArgs = ["--platform", platform, "--target", target];
|
|
33
|
+
if (platform === "ios") {
|
|
34
|
+
resetArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
|
|
35
|
+
} else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
|
|
36
|
+
resetArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
|
|
37
|
+
}
|
|
38
|
+
const resetResult = await spawnScriptStreaming(resetAppData, resetArgs, target, {
|
|
39
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
40
|
+
});
|
|
41
|
+
if (resetResult.status !== 0) {
|
|
42
|
+
resetError = new Error("Mobile app data reset failed.");
|
|
43
|
+
} else {
|
|
44
|
+
const prewarmBundle = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
|
|
45
|
+
const prewarmResult = await spawnScriptStreaming(
|
|
46
|
+
prewarmBundle,
|
|
47
|
+
["--platform", platform, "--target", target, "--port", port],
|
|
48
|
+
target,
|
|
49
|
+
{
|
|
50
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
if (prewarmResult.status !== 0) {
|
|
54
|
+
resetError = new Error("Mobile bundle prewarm failed after app data reset.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!resetError) {
|
|
58
|
+
const openDevice = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
59
|
+
const openArgs = ["--platform", platform, "--target", target, "--port", port, "--preflight-mode", "fast", "--restart"];
|
|
60
|
+
if (platform === "ios") {
|
|
61
|
+
openArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
|
|
62
|
+
} else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
|
|
63
|
+
openArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
|
|
64
|
+
}
|
|
65
|
+
const openResult = await spawnScriptStreaming(openDevice, openArgs, target, {
|
|
66
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
67
|
+
});
|
|
68
|
+
if (openResult.status !== 0) {
|
|
69
|
+
resetError = new Error("Mobile app relaunch after data reset failed.");
|
|
70
|
+
} else {
|
|
71
|
+
const waitForBridge = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
|
|
72
|
+
const waitResult = await spawnScriptStreaming(
|
|
73
|
+
waitForBridge,
|
|
74
|
+
["--target", target, "--port", port, "--platform", platform, "--timeout-ms", String(FIXTURE_BRIDGE_READY_TIMEOUT_MS)],
|
|
75
|
+
target,
|
|
76
|
+
{ timeoutMs: FIXTURE_BRIDGE_READY_TIMEOUT_MS + 1e4 }
|
|
77
|
+
);
|
|
78
|
+
if (waitResult.status !== 0) {
|
|
79
|
+
resetError = new Error("Mobile bridge did not return after app data reset.");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? MOBILE_SETUP_WALLET_TIMEOUT_MS);
|
|
85
|
+
const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : MOBILE_SETUP_WALLET_TIMEOUT_MS;
|
|
86
|
+
if (!resetError) {
|
|
87
|
+
mobileSetupResult = await spawnScriptStreaming(
|
|
88
|
+
setupWalletSh,
|
|
89
|
+
["--fixture", fixturePath, "--remote-flags-json", fixtureFlagsJson],
|
|
90
|
+
target,
|
|
91
|
+
{
|
|
92
|
+
env: {
|
|
93
|
+
APP_ROOT: target,
|
|
94
|
+
WATCHER_PORT: process.env.WATCHER_PORT ?? "8081",
|
|
95
|
+
METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? "8081"
|
|
96
|
+
},
|
|
97
|
+
timeoutMs
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
status = !resetError && mobileSetupResult?.status === 0 ? "pass" : "fail";
|
|
102
|
+
} finally {
|
|
103
|
+
if (previousAppRoot === void 0) delete process.env.APP_ROOT;
|
|
104
|
+
else process.env.APP_ROOT = previousAppRoot;
|
|
105
|
+
}
|
|
106
|
+
if (!json && status === "fail") {
|
|
107
|
+
console.error(` Next: ${retryHint}`);
|
|
108
|
+
}
|
|
109
|
+
const teaching = status === "pass" ? `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}` : "Wallet fixture was not applied.";
|
|
110
|
+
if (json) {
|
|
111
|
+
console.log(
|
|
112
|
+
JSON.stringify(
|
|
113
|
+
{
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
command: "fixtures",
|
|
116
|
+
action: sub,
|
|
117
|
+
adapter,
|
|
118
|
+
fixture: fixturePath,
|
|
119
|
+
canonicalFixture,
|
|
120
|
+
status,
|
|
121
|
+
exitCode: status === "pass" ? EXIT.ok : EXIT.runtime,
|
|
122
|
+
walletReset: reset && status === "pass",
|
|
123
|
+
message: teaching,
|
|
124
|
+
error: status === "fail" ? {
|
|
125
|
+
code: resetError ? "MOBILE_WALLET_RESET_FAILED" : mobileSetupResult?.timedOut ? "SETUP_WALLET_TIMEOUT" : "SETUP_WALLET_FAILED",
|
|
126
|
+
message: resetError ? `wallet reset failed: ${resetError instanceof Error ? resetError.message : String(resetError)}` : mobileSetupResult?.timedOut ? `wallet fixture setup exceeded its ${String(mobileSetupResult.timeoutMs)}ms bound on Metro port ${process.env.WATCHER_PORT ?? "8081"}` : `wallet fixture setup failed on ${process.env.WATCHER_PORT ?? "8081"}`,
|
|
127
|
+
userAction: retryHint
|
|
128
|
+
} : null
|
|
129
|
+
},
|
|
130
|
+
null,
|
|
131
|
+
2
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
} else if (status === "pass") {
|
|
135
|
+
console.error(teaching);
|
|
136
|
+
}
|
|
137
|
+
return status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
138
|
+
}
|
|
139
|
+
export {
|
|
140
|
+
applyMobileFixture
|
|
141
|
+
};
|