@deeeed/metamask-harness 0.31.0 → 0.33.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/adapters/extension/verify.sh +16 -4
  3. package/dist/adapters.js +73 -1
  4. package/dist/commands/self-test.js +1 -1
  5. package/dist/paths.js +7 -0
  6. package/dist/runner.js +13 -2
  7. package/docs/RECIPES.md +5 -2
  8. package/library/manifests/extension.action-manifest.json +210 -2
  9. package/library/manifests/mobile.action-manifest.json +214 -0
  10. package/package.json +6 -4
  11. /package/library/recipes/{perps/clean-market-testnet.core.recipe.json → core/perps/clean-market-testnet.recipe.json} +0 -0
  12. /package/library/recipes/{perps/order-lifecycle.core.recipe.json → core/perps/order-lifecycle.recipe.json} +0 -0
  13. /package/library/recipes/{perps/read-markets.core.recipe.json → core/perps/read-markets.recipe.json} +0 -0
  14. /package/library/recipes/{perps/smoke.core.recipe.json → core/perps/smoke.recipe.json} +0 -0
  15. /package/library/recipes/{perps/trading-lifecycle.core.recipe.json → core/perps/trading-lifecycle.recipe.json} +0 -0
  16. /package/library/recipes/{runner/smoke.core.recipe.json → core/runner/smoke.recipe.json} +0 -0
  17. /package/library/recipes/{perps/smoke.extension.recipe.json → extension/perps/smoke.recipe.json} +0 -0
  18. /package/library/recipes/{runner/action-validation.extension.recipe.json → extension/runner/action-validation.recipe.json} +0 -0
  19. /package/library/recipes/{runner/smoke.extension.recipe.json → extension/runner/smoke.recipe.json} +0 -0
  20. /package/library/recipes/{wallet/smoke.extension.recipe.json → extension/wallet/smoke.recipe.json} +0 -0
  21. /package/library/recipes/{app/lifecycle.android-smoke.mobile.recipe.json → mobile/app/lifecycle.android-smoke.recipe.json} +0 -0
  22. /package/library/recipes/{perps/performance.background-resume.mobile.recipe.json → mobile/perps/performance.background-resume.recipe.json} +0 -0
  23. /package/library/recipes/{perps/performance.cold-start.mobile.recipe.json → mobile/perps/performance.cold-start.recipe.json} +0 -0
  24. /package/library/recipes/{perps/performance.mobile.recipe.json → mobile/perps/performance.recipe.json} +0 -0
  25. /package/library/recipes/{perps/performance.warm-start.mobile.recipe.json → mobile/perps/performance.warm-start.recipe.json} +0 -0
  26. /package/library/recipes/{perps/smoke.mobile.recipe.json → mobile/perps/smoke.recipe.json} +0 -0
  27. /package/library/recipes/{runner/action-validation.mobile.recipe.json → mobile/runner/action-validation.recipe.json} +0 -0
  28. /package/library/recipes/{runner/smoke.mobile.recipe.json → mobile/runner/smoke.recipe.json} +0 -0
  29. /package/library/recipes/{wallet/smoke.mobile.recipe.json → mobile/wallet/smoke.recipe.json} +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.33.0 - 2026-08-03
6
+
7
+ ### Added
8
+
9
+ - Expose standard full-surface capture and continuous UI gestures for Mobile and Extension by delegating to the shared native and browser transports.
10
+
11
+ ### Changed
12
+
13
+ - Upgrade `@farmslot/agent-runtime` to 0.7.0, `@farmslot/protocol` to 0.18.0, and `@farmslot/recipe-harness` to 0.14.0, and add `@farmslot/expo-recipe` 0.7.1 with `agent-device` 0.19.3 for the native UI provider.
14
+
15
+ ## 0.32.0 - 2026-08-02
16
+
17
+ ### Changed
18
+
19
+ - Organize bundled platform recipe variants under `library/recipes/<adapter>/` while retaining temporary compatibility with legacy filename suffixes.
20
+
5
21
  ## 0.31.0 - 2026-08-02
6
22
 
7
23
  ### Added
@@ -88,7 +88,7 @@ RUNNER_BIN="$HARNESS_DIR/runner/bin/mm-harness"
88
88
  # --out (optional): a task-local recipes dir. Resolve it safely within the target
89
89
  # (resolve_harness_out rejects absolute/.. escapes) and prefer its smoke recipe so
90
90
  # `live --out <dir>` does not silently fall back to the installed default.
91
- SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/runner/smoke.extension.recipe.json"
91
+ SMOKE_RECIPE="$HARNESS_DIR/runner/recipes/extension/runner/smoke.recipe.json"
92
92
  if [ -n "$OUT" ]; then
93
93
  # Fail fast on a task-local --out that does not contain the requested recipe.
94
94
  # Silently falling back to the installed default would validate a different
@@ -98,11 +98,23 @@ if [ -n "$OUT" ]; then
98
98
  echo "extension verify: --out '$OUT' did not resolve to a safe path under the target." >&2
99
99
  exit 2
100
100
  fi
101
- if [ ! -f "$OUT_ABS/runner/smoke.extension.recipe.json" ]; then
102
- echo "extension verify: --out '$OUT' (resolved: $OUT_ABS) has no runner/smoke.extension.recipe.json. Refusing to fall back to the installed default recipe; place the recipe under --out or omit --out." >&2
101
+ CANONICAL_SMOKE_RECIPE="$OUT_ABS/extension/runner/smoke.recipe.json"
102
+ LEGACY_SMOKE_RECIPE="$OUT_ABS/runner/smoke.extension.recipe.json"
103
+ if [ -f "$CANONICAL_SMOKE_RECIPE" ] && [ -f "$LEGACY_SMOKE_RECIPE" ]; then
104
+ echo "extension verify: --out '$OUT' declares runner.smoke for extension in both canonical and legacy paths." >&2
105
+ echo "Next: Remove $LEGACY_SMOKE_RECIPE and rerun verification." >&2
103
106
  exit 2
104
107
  fi
105
- SMOKE_RECIPE="$OUT_ABS/runner/smoke.extension.recipe.json"
108
+ if [ -f "$CANONICAL_SMOKE_RECIPE" ]; then
109
+ SMOKE_RECIPE="$CANONICAL_SMOKE_RECIPE"
110
+ elif [ -f "$LEGACY_SMOKE_RECIPE" ]; then
111
+ SMOKE_RECIPE="$LEGACY_SMOKE_RECIPE"
112
+ else
113
+ echo "extension verify: --out '$OUT' has no extension runner.smoke recipe." >&2
114
+ echo "Next: Add extension/runner/smoke.recipe.json under $OUT_ABS or omit --out." >&2
115
+ exit 2
116
+ fi
117
+ unset CANONICAL_SMOKE_RECIPE LEGACY_SMOKE_RECIPE
106
118
  fi
107
119
  ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/verify/$(date -u +%Y%m%dT%H%M%SZ)}"
108
120
  mkdir -p "$ARTIFACTS/logs"
package/dist/adapters.js CHANGED
@@ -6,6 +6,13 @@ import {
6
6
  import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs";
7
7
  import { bridgeCommand, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
8
8
  import { observeNativeUi } from "../library/actions/mobile/platform/observe-ui.mjs";
9
+ const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
10
+ "ui.swipe",
11
+ "ui.pan",
12
+ "ui.drag",
13
+ "ui.long_press",
14
+ "ui.capture_surface"
15
+ ]);
9
16
  function sleep(ms) {
10
17
  return new Promise((resolve) => setTimeout(resolve, ms));
11
18
  }
@@ -480,7 +487,7 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
480
487
  return {
481
488
  async execute(action) {
482
489
  throw new Error(
483
- `core adapter is headless and cannot execute UI action ${action}. Use a perps read action or run the recipe against the mobile/extension adapter.`
490
+ `core adapter is headless and cannot execute UI action ${action}. Next: mm-harness actions --matrix`
484
491
  );
485
492
  }
486
493
  };
@@ -516,10 +523,75 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
516
523
  if (live) return live.output;
517
524
  throw new Error(`ui.navigate requires library/actions/${platform}/ui/navigate.mjs.`);
518
525
  }
526
+ if (platform === "mobile" && NATIVE_PROVIDER_UI_ACTIONS.has(action)) {
527
+ return executeNativeProviderAction(action, node, context, harness.createNativeUiTransport);
528
+ }
519
529
  return base.execute(action, action === "ui.wait_for" ? normalizeUiWaitNode(node) : node, context);
520
530
  }
521
531
  };
522
532
  }
533
+ async function executeNativeProviderAction(action, node, context, createTransport) {
534
+ if (!createTransport) {
535
+ throw new Error(
536
+ `${action} requires the MetaMask native UI provider. Next: npm install -g @deeeed/metamask-harness@latest`
537
+ );
538
+ }
539
+ const platform = mobileNativePlatform(context.env);
540
+ const device = platform === "android" ? context.env.ADB_SERIAL ?? context.env.ANDROID_SERIAL ?? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL : context.env.SIM_UDID ?? context.env.IOS_SIMULATOR ?? process.env.SIM_UDID ?? process.env.IOS_SIMULATOR;
541
+ if (!device) {
542
+ throw new Error(
543
+ `${action} requires one selected ${platform} device. Next: mm-harness status --adapter mobile --all-devices --json`
544
+ );
545
+ }
546
+ const app = platform === "android" ? context.env.ANDROID_PACKAGE_ID ?? process.env.ANDROID_PACKAGE_ID : context.env.IOS_BUNDLE_ID ?? process.env.IOS_BUNDLE_ID;
547
+ if (!app) {
548
+ const variable = platform === "android" ? "ANDROID_PACKAGE_ID" : "IOS_BUNDLE_ID";
549
+ throw new Error(
550
+ `${action} requires the selected MetaMask app id. Next: export ${variable}=<installed-app-id>`
551
+ );
552
+ }
553
+ const session = `mm-harness-${process.pid}-${context.nodeId}`.replace(/[^a-zA-Z0-9._-]/gu, "-");
554
+ let transport;
555
+ try {
556
+ transport = await createTransport({ platform, device, app, session });
557
+ } catch (error) {
558
+ throw new Error(
559
+ `${action} could not start the MetaMask native UI provider. Next: npm install -g @deeeed/metamask-harness@latest`,
560
+ { cause: error }
561
+ );
562
+ }
563
+ let executionError;
564
+ let output;
565
+ try {
566
+ output = await transport.execute(action, node, context);
567
+ } catch (error) {
568
+ const message = error instanceof Error ? error.message : String(error);
569
+ executionError = /^Native recipe actions require the optional agent-device provider:/u.test(message) ? new Error(
570
+ `${action} could not start the MetaMask native UI provider. Next: npm install -g @deeeed/metamask-harness@latest`,
571
+ { cause: error }
572
+ ) : error;
573
+ }
574
+ let closeError;
575
+ try {
576
+ await transport.close?.();
577
+ } catch (error) {
578
+ closeError = error;
579
+ }
580
+ if (executionError && closeError) {
581
+ const message = executionError instanceof Error ? executionError.message : String(executionError);
582
+ throw new AggregateError([executionError, closeError], message);
583
+ }
584
+ if (executionError) throw executionError;
585
+ if (closeError) throw closeError;
586
+ return output;
587
+ }
588
+ function mobileNativePlatform(env) {
589
+ if (env.PLATFORM === "android" || env.PLATFORM === "ios") return env.PLATFORM;
590
+ if (env.ADB_SERIAL || env.ANDROID_SERIAL) return "android";
591
+ if (env.SIM_UDID || env.IOS_SIMULATOR) return "ios";
592
+ if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") return process.env.PLATFORM;
593
+ return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
594
+ }
523
595
  function normalizeUiWaitNode(node) {
524
596
  if (node.expected !== void 0 || typeof node.visible !== "boolean") return node;
525
597
  return { ...node, expected: node.visible ? "visible" : "absent" };
@@ -26,7 +26,7 @@ async function runSelfTest(options) {
26
26
  const manifest = loadActionManifest(adapter);
27
27
  const manifestValidation = await validateManifest(manifest);
28
28
  const smokeRecipe = recipePath(
29
- adapter === "mobile" ? "runner/smoke.mobile.recipe.json" : "runner/smoke.extension.recipe.json"
29
+ adapter === "mobile" ? "mobile/runner/smoke.recipe.json" : "extension/runner/smoke.recipe.json"
30
30
  );
31
31
  const artifactsDir = path.join(root, adapter);
32
32
  const result = await runRecipe(adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
package/dist/paths.js CHANGED
@@ -128,6 +128,12 @@ async function importRecipeHarnessRuntimeReactNativeBridge() {
128
128
  "packages/recipe-harness/src/runtime/react-native-bridge.ts"
129
129
  );
130
130
  }
131
+ async function importMetaMaskNativeUiProvider() {
132
+ return importProtocolPackage(
133
+ "@farmslot/expo-recipe",
134
+ "packages/expo-recipe/src/index.ts"
135
+ );
136
+ }
131
137
  async function importRecipeHarnessCli() {
132
138
  return importProtocolPackage(
133
139
  "@farmslot/recipe-harness/cli",
@@ -164,6 +170,7 @@ export {
164
170
  DEFAULT_RECIPE_RUNTIME_DIR,
165
171
  assertAdapter,
166
172
  extensionIdPath,
173
+ importMetaMaskNativeUiProvider,
167
174
  importRecipeHarness,
168
175
  importRecipeHarnessAppLifecycle,
169
176
  importRecipeHarnessCli,
package/dist/runner.js CHANGED
@@ -12,7 +12,14 @@ import {
12
12
  prepareLiveAdapterScript
13
13
  } from "./live-adapter-contract.js";
14
14
  import { createMetaMaskRecordingTargetProvider } from "./recording-target.js";
15
- import { importRecipeHarness, importRecipeHarnessAppLifecycle, importRecipeHarnessRuntimeCdp, importRecipeHarnessRuntimeReactNativeBridge, runnerDir } from "./paths.js";
15
+ import {
16
+ importMetaMaskNativeUiProvider,
17
+ importRecipeHarness,
18
+ importRecipeHarnessAppLifecycle,
19
+ importRecipeHarnessRuntimeCdp,
20
+ importRecipeHarnessRuntimeReactNativeBridge,
21
+ runnerDir
22
+ } from "./paths.js";
16
23
  const OFFICIAL_ACTIONS = new Set(OFFICIAL_RECIPE_ACTIONS);
17
24
  async function createMetaMaskMobileRunner(options = {}) {
18
25
  return createMetaMaskRunner(
@@ -51,7 +58,11 @@ async function createMetaMaskRunner(adapter, actionManifest, options = {}) {
51
58
  actions: actions.filter((action) => !projectOwnedOfficialActions.has(action)),
52
59
  transport: createMetaMaskUiTransport(adapter, {
53
60
  createCdpWebUiTransport,
54
- createReactNativeBridgeUiTransport
61
+ createReactNativeBridgeUiTransport,
62
+ async createNativeUiTransport(options2) {
63
+ const { createAgentDeviceUiTransport } = await importMetaMaskNativeUiProvider();
64
+ return createAgentDeviceUiTransport(options2);
65
+ }
55
66
  }, preparedLiveAdapters)
56
67
  });
57
68
  const existing = new Set([...core, ...ui].map((entry) => entry.action));
package/docs/RECIPES.md CHANGED
@@ -116,7 +116,7 @@ The protocol is authoritative:
116
116
  team-recipes/
117
117
  manifests/extension.action-manifest.json
118
118
  actions/extension/wallet/ensure_ready.mjs
119
- recipes/onboarding/smoke.extension.recipe.json
119
+ recipes/extension/onboarding/smoke.recipe.json
120
120
  ```
121
121
 
122
122
  ```bash
@@ -129,7 +129,10 @@ mm-harness run onboarding.smoke --plan
129
129
  Use `--library wallet=/path/to/library` for one command. The alias is the
130
130
  source name; without one, the directory name is used. Resolution follows the
131
131
  explicit library order, then bundled MetaMask. Adapter-specific variants are
132
- selected deterministically. Every run records the root recipe, exact resolved
132
+ selected deterministically from `recipes/<adapter>/<domain>/`. Legacy
133
+ `*.<adapter>.recipe.json` paths remain readable during migration, but new and
134
+ migrated libraries use the adapter-first layout. A file cannot declare its
135
+ adapter through both forms. Every run records the root recipe, exact resolved
133
136
  dependency documents, their digests, call edges, selected sources, and shadows.
134
137
 
135
138
  ## Direct product access
@@ -664,6 +664,183 @@
664
664
  "additionalProperties": false
665
665
  }
666
666
  },
667
+ "ui.swipe": {
668
+ "description": "Swipe from a test-id target or screen coordinates in one cardinal direction through the shared browser UI transport.",
669
+ "schema": {
670
+ "type": "object",
671
+ "properties": {
672
+ "target": {
673
+ "type": ["string", "object"],
674
+ "properties": {
675
+ "x": { "type": "number" },
676
+ "y": { "type": "number" }
677
+ },
678
+ "required": ["x", "y"],
679
+ "additionalProperties": false
680
+ },
681
+ "direction": {
682
+ "type": "string",
683
+ "enum": ["up", "down", "left", "right"]
684
+ },
685
+ "distance": { "type": "number", "minimum": 1 },
686
+ "duration_ms": { "type": "integer", "minimum": 1 },
687
+ "timeout_ms": { "type": "integer", "minimum": 1 },
688
+ "settle": { "type": "boolean" }
689
+ },
690
+ "required": ["target", "direction", "distance", "duration_ms"],
691
+ "additionalProperties": false
692
+ },
693
+ "examples": [
694
+ {
695
+ "action": "ui.swipe",
696
+ "target": "example-scrollable",
697
+ "direction": "up",
698
+ "distance": 240,
699
+ "duration_ms": 300,
700
+ "intent": "Swipe the requested visible surface.",
701
+ "next": "done"
702
+ }
703
+ ]
704
+ },
705
+ "ui.pan": {
706
+ "description": "Pan through a continuous pointer path from a test-id target or screen coordinates through the shared browser UI transport.",
707
+ "schema": {
708
+ "type": "object",
709
+ "properties": {
710
+ "target": {
711
+ "type": ["string", "object"],
712
+ "properties": {
713
+ "x": { "type": "number" },
714
+ "y": { "type": "number" }
715
+ },
716
+ "required": ["x", "y"],
717
+ "additionalProperties": false
718
+ },
719
+ "path": {
720
+ "type": "array",
721
+ "minItems": 1,
722
+ "items": {
723
+ "type": "object",
724
+ "properties": {
725
+ "x": { "type": "number" },
726
+ "y": { "type": "number" }
727
+ },
728
+ "required": ["x", "y"],
729
+ "additionalProperties": false
730
+ }
731
+ },
732
+ "delta": {
733
+ "type": "object",
734
+ "properties": {
735
+ "x": { "type": "number" },
736
+ "y": { "type": "number" }
737
+ },
738
+ "required": ["x", "y"],
739
+ "additionalProperties": false
740
+ },
741
+ "duration_ms": { "type": "integer", "minimum": 1 },
742
+ "timeout_ms": { "type": "integer", "minimum": 1 },
743
+ "settle": { "type": "boolean" }
744
+ },
745
+ "required": ["target", "duration_ms"],
746
+ "additionalProperties": false
747
+ },
748
+ "examples": [
749
+ {
750
+ "action": "ui.pan",
751
+ "target": "example-surface",
752
+ "path": [{ "x": 40, "y": 10 }, { "x": 80, "y": 20 }],
753
+ "duration_ms": 400,
754
+ "intent": "Pan through the requested pointer path.",
755
+ "next": "done"
756
+ }
757
+ ]
758
+ },
759
+ "ui.drag": {
760
+ "description": "Drag a test-id target or screen coordinate through the shared browser UI transport.",
761
+ "schema": {
762
+ "type": "object",
763
+ "properties": {
764
+ "target": {
765
+ "type": ["string", "object"],
766
+ "properties": {
767
+ "x": { "type": "number" },
768
+ "y": { "type": "number" }
769
+ },
770
+ "required": ["x", "y"],
771
+ "additionalProperties": false
772
+ },
773
+ "path": {
774
+ "type": "array",
775
+ "minItems": 1,
776
+ "items": {
777
+ "type": "object",
778
+ "properties": {
779
+ "x": { "type": "number" },
780
+ "y": { "type": "number" }
781
+ },
782
+ "required": ["x", "y"],
783
+ "additionalProperties": false
784
+ }
785
+ },
786
+ "delta": {
787
+ "type": "object",
788
+ "properties": {
789
+ "x": { "type": "number" },
790
+ "y": { "type": "number" }
791
+ },
792
+ "required": ["x", "y"],
793
+ "additionalProperties": false
794
+ },
795
+ "duration_ms": { "type": "integer", "minimum": 1 },
796
+ "timeout_ms": { "type": "integer", "minimum": 1 },
797
+ "settle": { "type": "boolean" }
798
+ },
799
+ "required": ["target", "duration_ms"],
800
+ "additionalProperties": false
801
+ },
802
+ "examples": [
803
+ {
804
+ "action": "ui.drag",
805
+ "target": "example-slider",
806
+ "delta": { "x": 120, "y": 0 },
807
+ "duration_ms": 500,
808
+ "intent": "Drag the requested control to a new value.",
809
+ "next": "done"
810
+ }
811
+ ]
812
+ },
813
+ "ui.long_press": {
814
+ "description": "Hold a test-id target or screen coordinate through the shared browser UI transport.",
815
+ "schema": {
816
+ "type": "object",
817
+ "properties": {
818
+ "target": {
819
+ "type": ["string", "object"],
820
+ "properties": {
821
+ "x": { "type": "number" },
822
+ "y": { "type": "number" }
823
+ },
824
+ "required": ["x", "y"],
825
+ "additionalProperties": false
826
+ },
827
+ "hold_ms": { "type": "integer", "minimum": 1 },
828
+ "timeout_ms": { "type": "integer", "minimum": 1 },
829
+ "settle": { "type": "boolean" }
830
+ },
831
+ "required": ["target", "hold_ms"],
832
+ "additionalProperties": false
833
+ },
834
+ "examples": [
835
+ {
836
+ "action": "ui.long_press",
837
+ "target": "example-control",
838
+ "hold_ms": 700,
839
+ "intent": "Hold the requested visible control.",
840
+ "next": "done"
841
+ }
842
+ ]
843
+ },
667
844
  "ui.wait_for": {
668
845
  "description": "Wait for a UI condition through the Extension CDP adapter.",
669
846
  "schema": {
@@ -763,6 +940,27 @@
763
940
  "additionalProperties": false
764
941
  }
765
942
  },
943
+ "ui.capture_surface": {
944
+ "description": "Capture the complete current browser page as one registered PNG while leaving ui.screenshot viewport-only.",
945
+ "schema": {
946
+ "type": "object",
947
+ "properties": {
948
+ "path": { "type": "string" },
949
+ "label": { "type": "string" },
950
+ "category": { "type": "string" },
951
+ "timeout_ms": { "type": "number" }
952
+ },
953
+ "additionalProperties": false
954
+ },
955
+ "examples": [
956
+ {
957
+ "action": "ui.capture_surface",
958
+ "path": "screenshots/full-page.png",
959
+ "intent": "Preserve the complete page context for human review.",
960
+ "next": "done"
961
+ }
962
+ ]
963
+ },
766
964
  "app.status": {
767
965
  "description": "Report the adapter's static status — platform, project root, resolved checkout shape, and headless compatibility mode (no live route or account).",
768
966
  "examples": [
@@ -2982,8 +3180,13 @@
2982
3180
  "ui.key_press",
2983
3181
  "ui.set_input",
2984
3182
  "ui.scroll",
3183
+ "ui.swipe",
3184
+ "ui.pan",
3185
+ "ui.drag",
3186
+ "ui.long_press",
2985
3187
  "ui.wait_for",
2986
- "ui.screenshot"
3188
+ "ui.screenshot",
3189
+ "ui.capture_surface"
2987
3190
  ]
2988
3191
  },
2989
3192
  {
@@ -2995,8 +3198,13 @@
2995
3198
  "ui.key_press",
2996
3199
  "ui.set_input",
2997
3200
  "ui.scroll",
3201
+ "ui.swipe",
3202
+ "ui.pan",
3203
+ "ui.drag",
3204
+ "ui.long_press",
2998
3205
  "ui.wait_for",
2999
- "ui.screenshot"
3206
+ "ui.screenshot",
3207
+ "ui.capture_surface"
3000
3208
  ]
3001
3209
  }
3002
3210
  ]
@@ -675,6 +675,185 @@
675
675
  "additionalProperties": false
676
676
  }
677
677
  },
678
+ "ui.swipe": {
679
+ "description": "Swipe from a test-id target or screen coordinates in one cardinal direction through the shared native UI provider.",
680
+ "schema": {
681
+ "type": "object",
682
+ "properties": {
683
+ "target": {
684
+ "type": ["string", "object"],
685
+ "properties": {
686
+ "x": { "type": "number" },
687
+ "y": { "type": "number" }
688
+ },
689
+ "required": ["x", "y"],
690
+ "additionalProperties": false
691
+ },
692
+ "direction": {
693
+ "type": "string",
694
+ "enum": ["up", "down", "left", "right"]
695
+ },
696
+ "distance": { "type": "number", "minimum": 1 },
697
+ "duration_ms": { "type": "integer", "minimum": 1 },
698
+ "timeout_ms": { "type": "integer", "minimum": 1 },
699
+ "settle": { "type": "boolean" }
700
+ },
701
+ "required": ["target", "direction", "distance", "duration_ms"],
702
+ "additionalProperties": false
703
+ },
704
+ "examples": [
705
+ {
706
+ "action": "ui.swipe",
707
+ "target": "example-scrollable",
708
+ "direction": "up",
709
+ "distance": 240,
710
+ "duration_ms": 300,
711
+ "intent": "Swipe the requested visible surface.",
712
+ "next": "done"
713
+ }
714
+ ]
715
+ },
716
+ "ui.pan": {
717
+ "description": "Pan from a test-id target or screen coordinates through the shared native UI provider.",
718
+ "schema": {
719
+ "type": "object",
720
+ "properties": {
721
+ "target": {
722
+ "type": ["string", "object"],
723
+ "properties": {
724
+ "x": { "type": "number" },
725
+ "y": { "type": "number" }
726
+ },
727
+ "required": ["x", "y"],
728
+ "additionalProperties": false
729
+ },
730
+ "path": {
731
+ "type": "array",
732
+ "minItems": 1,
733
+ "maxItems": 1,
734
+ "items": {
735
+ "type": "object",
736
+ "properties": {
737
+ "x": { "type": "number" },
738
+ "y": { "type": "number" }
739
+ },
740
+ "required": ["x", "y"],
741
+ "additionalProperties": false
742
+ }
743
+ },
744
+ "delta": {
745
+ "type": "object",
746
+ "properties": {
747
+ "x": { "type": "number" },
748
+ "y": { "type": "number" }
749
+ },
750
+ "required": ["x", "y"],
751
+ "additionalProperties": false
752
+ },
753
+ "duration_ms": { "type": "integer", "minimum": 1 },
754
+ "timeout_ms": { "type": "integer", "minimum": 1 },
755
+ "settle": { "type": "boolean" }
756
+ },
757
+ "required": ["target", "duration_ms"],
758
+ "additionalProperties": false
759
+ },
760
+ "examples": [
761
+ {
762
+ "action": "ui.pan",
763
+ "target": "example-surface",
764
+ "path": [{ "x": 80, "y": 20 }],
765
+ "duration_ms": 400,
766
+ "intent": "Pan through the requested pointer path.",
767
+ "next": "done"
768
+ }
769
+ ]
770
+ },
771
+ "ui.drag": {
772
+ "description": "Drag a test-id target or screen coordinate through the shared native UI provider.",
773
+ "schema": {
774
+ "type": "object",
775
+ "properties": {
776
+ "target": {
777
+ "type": ["string", "object"],
778
+ "properties": {
779
+ "x": { "type": "number" },
780
+ "y": { "type": "number" }
781
+ },
782
+ "required": ["x", "y"],
783
+ "additionalProperties": false
784
+ },
785
+ "path": {
786
+ "type": "array",
787
+ "minItems": 1,
788
+ "maxItems": 1,
789
+ "items": {
790
+ "type": "object",
791
+ "properties": {
792
+ "x": { "type": "number" },
793
+ "y": { "type": "number" }
794
+ },
795
+ "required": ["x", "y"],
796
+ "additionalProperties": false
797
+ }
798
+ },
799
+ "delta": {
800
+ "type": "object",
801
+ "properties": {
802
+ "x": { "type": "number" },
803
+ "y": { "type": "number" }
804
+ },
805
+ "required": ["x", "y"],
806
+ "additionalProperties": false
807
+ },
808
+ "duration_ms": { "type": "integer", "minimum": 1 },
809
+ "timeout_ms": { "type": "integer", "minimum": 1 },
810
+ "settle": { "type": "boolean" }
811
+ },
812
+ "required": ["target", "duration_ms"],
813
+ "additionalProperties": false
814
+ },
815
+ "examples": [
816
+ {
817
+ "action": "ui.drag",
818
+ "target": "example-slider",
819
+ "delta": { "x": 120, "y": 0 },
820
+ "duration_ms": 500,
821
+ "intent": "Drag the requested control to a new value.",
822
+ "next": "done"
823
+ }
824
+ ]
825
+ },
826
+ "ui.long_press": {
827
+ "description": "Hold a test-id target or screen coordinate through the shared native UI provider.",
828
+ "schema": {
829
+ "type": "object",
830
+ "properties": {
831
+ "target": {
832
+ "type": ["string", "object"],
833
+ "properties": {
834
+ "x": { "type": "number" },
835
+ "y": { "type": "number" }
836
+ },
837
+ "required": ["x", "y"],
838
+ "additionalProperties": false
839
+ },
840
+ "hold_ms": { "type": "integer", "minimum": 1 },
841
+ "timeout_ms": { "type": "integer", "minimum": 1 },
842
+ "settle": { "type": "boolean" }
843
+ },
844
+ "required": ["target", "hold_ms"],
845
+ "additionalProperties": false
846
+ },
847
+ "examples": [
848
+ {
849
+ "action": "ui.long_press",
850
+ "target": "example-control",
851
+ "hold_ms": 700,
852
+ "intent": "Hold the requested visible control.",
853
+ "next": "done"
854
+ }
855
+ ]
856
+ },
678
857
  "ui.wait_for": {
679
858
  "description": "Wait for a mounted React Native testID/test_id. Optional text validates text returned by that testID; text alone is not supported on Mobile.",
680
859
  "schema": {
@@ -781,6 +960,31 @@
781
960
  "additionalProperties": false
782
961
  }
783
962
  },
963
+ "ui.capture_surface": {
964
+ "description": "Capture the complete current native scroll surface as one registered PNG while leaving ui.screenshot viewport-only.",
965
+ "schema": {
966
+ "type": "object",
967
+ "properties": {
968
+ "path": { "type": "string" },
969
+ "label": { "type": "string" },
970
+ "surface_test_id": { "type": "string" },
971
+ "until_test_id": { "type": "string" },
972
+ "max_scrolls": { "type": "integer", "minimum": 1 },
973
+ "restore_position": { "type": "boolean" }
974
+ },
975
+ "additionalProperties": false
976
+ },
977
+ "examples": [
978
+ {
979
+ "action": "ui.capture_surface",
980
+ "path": "screenshots/full-screen.png",
981
+ "surface_test_id": "screen-scroll-surface",
982
+ "until_test_id": "screen-bottom",
983
+ "intent": "Preserve the complete scrollable context for human review.",
984
+ "next": "done"
985
+ }
986
+ ]
987
+ },
784
988
  "app.lifecycle": {
785
989
  "description": "Drive outer mobile app lifecycle for deterministic performance start states without rebuilding: foreground/launch, background, terminate, or restart the installed app.",
786
990
  "examples": [
@@ -3101,7 +3305,12 @@
3101
3305
  "metamask.deeplink.open",
3102
3306
  "ui.navigate",
3103
3307
  "ui.press",
3308
+ "ui.swipe",
3309
+ "ui.pan",
3310
+ "ui.drag",
3311
+ "ui.long_press",
3104
3312
  "ui.screenshot",
3313
+ "ui.capture_surface",
3105
3314
  "ui.scroll",
3106
3315
  "ui.set_input",
3107
3316
  "ui.wait_for"
@@ -3113,7 +3322,12 @@
3113
3322
  "metamask.deeplink.open",
3114
3323
  "ui.navigate",
3115
3324
  "ui.press",
3325
+ "ui.swipe",
3326
+ "ui.pan",
3327
+ "ui.drag",
3328
+ "ui.long_press",
3116
3329
  "ui.screenshot",
3330
+ "ui.capture_surface",
3117
3331
  "ui.scroll",
3118
3332
  "ui.set_input",
3119
3333
  "ui.wait_for"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.31.0",
3
+ "version": "0.33.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -20,10 +20,12 @@
20
20
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
21
21
  },
22
22
  "dependencies": {
23
- "@farmslot/agent-runtime": "0.5.1",
23
+ "@farmslot/agent-runtime": "0.7.0",
24
+ "@farmslot/expo-recipe": "0.7.1",
24
25
  "@farmslot/handoff": "^0.3.1",
25
- "@farmslot/protocol": "0.16.0",
26
- "@farmslot/recipe-harness": "0.12.1",
26
+ "@farmslot/protocol": "0.18.0",
27
+ "@farmslot/recipe-harness": "0.14.0",
28
+ "agent-device": "0.19.3",
27
29
  "commander": "^12.0.0",
28
30
  "es-module-lexer": "2.3.1",
29
31
  "esbuild": "0.28.1",