@deeeed/metamask-harness 0.19.1 → 0.21.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 (42) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/adapters/core/inject.sh +1 -3
  3. package/adapters/extension/ensure-browser.sh +6 -4
  4. package/adapters/extension/inject.mjs +1 -1
  5. package/adapters/extension/launch-browser.cjs +6 -1
  6. package/adapters/extension/lib/chrome-args.cjs +11 -1
  7. package/adapters/extension/start-watch.sh +1 -1
  8. package/adapters/extension/verify.sh +17 -0
  9. package/adapters/mobile/inject.sh +1 -1
  10. package/adapters/mobile/open-device.sh +12 -1
  11. package/adapters/mobile/wait-for-bridge.sh +1 -1
  12. package/dist/adapters/extension/ensure-ready.js +7 -2
  13. package/dist/adapters/extension/runtime-decision.js +4 -6
  14. package/dist/adapters/extension/runtime.js +114 -17
  15. package/dist/adapters/mobile/prepare.js +40 -6
  16. package/dist/command-contract.js +58 -5
  17. package/dist/commands/call.js +7 -4
  18. package/dist/commands/check.js +9 -2
  19. package/dist/commands/checklist.js +117 -14
  20. package/dist/commands/launch/extension.js +8 -3
  21. package/dist/commands/launch/index.js +38 -25
  22. package/dist/commands/launch/mobile.js +2 -2
  23. package/dist/commands/manifest.js +86 -25
  24. package/dist/commands/run-engine.js +7 -2
  25. package/dist/heal-bounds.js +1 -1
  26. package/dist/manifest.js +85 -101
  27. package/dist/metamask-action-validation.js +45 -0
  28. package/dist/mm-harness-cli.js +7 -4
  29. package/dist/runner.js +9 -4
  30. package/docs/CONTRIBUTING.md +8 -0
  31. package/docs/RECIPES.md +2 -11
  32. package/library/actions/extension/platform/cdp.mjs +8 -3
  33. package/library/actions/extension/ui/navigate.mjs +239 -16
  34. package/library/actions/mobile/ui/navigate.mjs +1 -1
  35. package/library/manifests/core.action-manifest.json +357 -489
  36. package/library/manifests/extension.action-manifest.json +616 -885
  37. package/library/manifests/mobile.action-manifest.json +647 -931
  38. package/library/recipes/perps/lifecycle.recipe.json +3 -9
  39. package/library/recipes/runner/action-validation.extension.recipe.json +5 -4
  40. package/package.json +5 -4
  41. package/scripts/completions.sh +2 -2
  42. package/library/library.json +0 -7
@@ -1,31 +1,249 @@
1
- import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
1
+ import { pathToFileURL } from 'node:url';
2
2
 
3
- const PAGE_HASHES = {
4
- home: '#/',
5
- perps: '#/perps-home',
6
- };
3
+ import {
4
+ dataTestId,
5
+ normalizeMarketSymbol,
6
+ runAdapter,
7
+ withExtensionPage,
8
+ } from '../platform/cdp.mjs';
9
+
10
+ const BACK_CONTROLS = [
11
+ 'perps-order-entry-back-button',
12
+ 'perps-market-detail-back-button',
13
+ 'perps-activity-back-button',
14
+ 'perps-withdraw-back-button',
15
+ 'back-button',
16
+ ].map(dataTestId);
7
17
 
8
18
  function text(value) {
9
19
  return typeof value === 'string' && value.length > 0 ? value : undefined;
10
20
  }
11
21
 
12
- function pageHash(node) {
22
+ function pageIntent(node) {
13
23
  const page = text(node?.page);
14
24
  if (!page) return undefined;
15
- if (PAGE_HASHES[page]) return { page, hash: PAGE_HASHES[page] };
25
+ if (page === 'home' || page === 'perps') return { page };
16
26
  if (page === 'perps-market') {
17
27
  const market = text(node.market) ?? text(node.symbol);
18
28
  if (!market) throw new Error('extension ui.navigate page=perps-market requires market or symbol.');
19
- return { page, hash: `#/perps/market/${encodeURIComponent(market)}` };
29
+ return { page, market };
20
30
  }
21
- throw new Error('extension ui.navigate supported page aliases: home, perps, perps-market.');
31
+ throw new Error('extension ui.navigate supported page intents: home, perps, perps-market.');
32
+ }
33
+
34
+ async function hasVisibleSelector(page, selector) {
35
+ return page.evaluate(`(() => {
36
+ const element = document.querySelector(${JSON.stringify(selector)});
37
+ if (!element) return false;
38
+ const style = getComputedStyle(element);
39
+ const rect = element.getBoundingClientRect();
40
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
41
+ })()`);
42
+ }
43
+
44
+ async function pauseForUi(page) {
45
+ await page.evaluate('new Promise((resolve) => setTimeout(resolve, 200))');
22
46
  }
23
47
 
24
- runAdapter((input) => withExtensionPage(input, async (page) => {
25
- const alias = pageHash(input.node);
26
- if (alias) {
27
- const navigation = await page.navigateHash(alias.hash);
28
- return { action: input.action, ...alias, navigation, proofPath: 'ui-navigation' };
48
+ async function waitForFirstVisible(page, selectors, deadline) {
49
+ while (Date.now() < deadline) {
50
+ for (const selector of selectors) {
51
+ if (await hasVisibleSelector(page, selector)) return selector;
52
+ }
53
+ await pauseForUi(page);
54
+ }
55
+ return undefined;
56
+ }
57
+
58
+ async function currentHref(page) {
59
+ return page.evaluate('location.href');
60
+ }
61
+
62
+ async function activateSemanticControl(page, selector) {
63
+ return {
64
+ activation: 'trusted-pointer',
65
+ interaction: await page.click(selector),
66
+ };
67
+ }
68
+
69
+ export async function openHome(page, timeoutMs) {
70
+ const home = dataTestId('bottom-nav-home');
71
+ const selectedHome = `${home}[aria-current="page"]`;
72
+ const deadline = Date.now() + timeoutMs;
73
+ const steps = [];
74
+ let pendingActivation;
75
+
76
+ while (Date.now() < deadline) {
77
+ if (await hasVisibleSelector(page, selectedHome)) {
78
+ return { method: 'visible-ui', href: await currentHref(page), steps };
79
+ }
80
+ const href = await currentHref(page);
81
+ if (pendingActivation && pendingActivation.href !== href) {
82
+ pendingActivation = undefined;
83
+ await pauseForUi(page);
84
+ continue;
85
+ }
86
+ if (await hasVisibleSelector(page, home)) {
87
+ if (pendingActivation?.selector !== home) pendingActivation = undefined;
88
+ if (!pendingActivation) {
89
+ await activateSemanticControl(page, home);
90
+ steps.push(home);
91
+ pendingActivation = { selector: home, href };
92
+ }
93
+ await pauseForUi(page);
94
+ continue;
95
+ }
96
+ let backControl;
97
+ for (const selector of BACK_CONTROLS) {
98
+ if (await hasVisibleSelector(page, selector)) {
99
+ backControl = selector;
100
+ break;
101
+ }
102
+ }
103
+ if (backControl) {
104
+ if (pendingActivation?.selector !== backControl) pendingActivation = undefined;
105
+ if (!pendingActivation || pendingActivation.selector !== backControl) {
106
+ await activateSemanticControl(page, backControl);
107
+ steps.push(backControl);
108
+ pendingActivation = { selector: backControl, href };
109
+ }
110
+ }
111
+ await pauseForUi(page);
112
+ }
113
+
114
+ throw new Error(
115
+ `extension ui.navigate page=home could not resolve Home through visible controls; observed href ${JSON.stringify(await currentHref(page))}.`,
116
+ );
117
+ }
118
+
119
+ export async function openPerpsHome(page, timeoutMs) {
120
+ const bottomNav = dataTestId('bottom-nav-perps');
121
+ const legacyTab = dataTestId('account-overview__perps-tab');
122
+ const perpsHome = dataTestId('perps-home-page');
123
+ const steps = [];
124
+ const deadline = Date.now() + timeoutMs;
125
+ let pendingActivation;
126
+ let perpsActivation;
127
+
128
+ while (Date.now() < deadline) {
129
+ if (await hasVisibleSelector(page, perpsHome)) {
130
+ return {
131
+ ...(perpsActivation ? { navigation: perpsActivation, selector: bottomNav } : {}),
132
+ method: 'visible-ui',
133
+ href: await currentHref(page),
134
+ steps,
135
+ };
136
+ }
137
+ if (await hasVisibleSelector(page, bottomNav)) {
138
+ if (!perpsActivation) {
139
+ perpsActivation = await activateSemanticControl(page, bottomNav);
140
+ steps.push(bottomNav);
141
+ }
142
+ await pauseForUi(page);
143
+ continue;
144
+ }
145
+ if (await hasVisibleSelector(page, legacyTab)) {
146
+ const navigation = await activateSemanticControl(page, legacyTab);
147
+ await page.waitForSelector(dataTestId('perps-view'), {
148
+ timeoutMs: Math.max(1, deadline - Date.now()),
149
+ });
150
+ return {
151
+ navigation,
152
+ method: 'visible-ui',
153
+ href: await currentHref(page),
154
+ selector: legacyTab,
155
+ steps,
156
+ };
157
+ }
158
+
159
+ let backControl;
160
+ for (const selector of BACK_CONTROLS) {
161
+ if (await hasVisibleSelector(page, selector)) {
162
+ backControl = selector;
163
+ break;
164
+ }
165
+ }
166
+ if (backControl) {
167
+ const href = await currentHref(page);
168
+ if (pendingActivation && pendingActivation.href !== href) {
169
+ pendingActivation = undefined;
170
+ }
171
+ if (!pendingActivation || pendingActivation.selector !== backControl) {
172
+ await activateSemanticControl(page, backControl);
173
+ steps.push(backControl);
174
+ pendingActivation = { selector: backControl, href };
175
+ }
176
+ }
177
+ await pauseForUi(page);
178
+ }
179
+
180
+ const state = await page.evaluate(`({
181
+ href: location.href,
182
+ title: document.title,
183
+ visibleTestIds: [...document.querySelectorAll('[data-testid]')]
184
+ .filter((element) => {
185
+ const style = getComputedStyle(element);
186
+ const rect = element.getBoundingClientRect();
187
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
188
+ })
189
+ .map((element) => element.getAttribute('data-testid'))
190
+ .filter(Boolean)
191
+ .slice(0, 30),
192
+ })`);
193
+ throw new Error(
194
+ `extension ui.navigate page=perps could not resolve the Perps home through visible UI; observed ${JSON.stringify(state)}. Capture the current UI with mm-harness call ui.screenshot, then use a verified visible control or raw hash/path.`,
195
+ );
196
+ }
197
+
198
+ export async function openPerpsMarket(page, market, timeoutMs) {
199
+ const deadline = Date.now() + timeoutMs;
200
+ const home = await openPerpsHome(page, Math.max(1, deadline - Date.now()));
201
+ const normalizedMarket = normalizeMarketSymbol(market.trim());
202
+ const selectors = [
203
+ dataTestId(`perps-watchlist-${normalizedMarket}`),
204
+ dataTestId(`explore-markets-${normalizedMarket.replaceAll(':', '-')}`),
205
+ ];
206
+ const marketControl = await waitForFirstVisible(page, selectors, deadline);
207
+ if (!marketControl) {
208
+ const visibleMarketControls = await page.evaluate(`[
209
+ ...document.querySelectorAll('[data-testid^="perps-watchlist-"], [data-testid^="explore-markets-"]')
210
+ ].filter((element) => {
211
+ const style = getComputedStyle(element);
212
+ const rect = element.getBoundingClientRect();
213
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
214
+ }).map((element) => element.getAttribute('data-testid')).filter(Boolean)`);
215
+ throw new Error(
216
+ `extension ui.navigate page=perps-market could not find ${JSON.stringify(normalizedMarket)} in the current visible market controls ${JSON.stringify(visibleMarketControls)}.`,
217
+ );
218
+ }
219
+ const navigation = await activateSemanticControl(page, marketControl);
220
+ await page.waitForSelector(dataTestId('perps-market-detail-page'), {
221
+ timeoutMs: Math.max(1, deadline - Date.now()),
222
+ });
223
+ return {
224
+ navigation,
225
+ method: 'visible-ui',
226
+ href: await currentHref(page),
227
+ selector: marketControl,
228
+ steps: [...home.steps, marketControl],
229
+ };
230
+ }
231
+
232
+ export async function navigateExtensionUi(input) {
233
+ return withExtensionPage(input, async (page) => {
234
+ const intent = pageIntent(input.node);
235
+ if (intent) {
236
+ const timeoutMs = Number(input.node?.timeout_ms ?? 20000);
237
+ if (intent.page === 'home') {
238
+ const result = await openHome(page, timeoutMs);
239
+ return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
240
+ }
241
+ if (intent.page === 'perps') {
242
+ const result = await openPerpsHome(page, timeoutMs);
243
+ return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
244
+ }
245
+ const result = await openPerpsMarket(page, intent.market, timeoutMs);
246
+ return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
29
247
  }
30
248
 
31
249
  const url = input.node?.url;
@@ -40,5 +258,10 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
40
258
  return { action: input.action, hash, navigation, proofPath: 'ui-navigation' };
41
259
  }
42
260
 
43
- throw new Error('extension ui.navigate requires page, raw extension url, hash, or path.');
44
- }));
261
+ throw new Error('extension ui.navigate requires page, raw extension url, hash, or path.');
262
+ });
263
+ }
264
+
265
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
266
+ runAdapter(navigateExtensionUi);
267
+ }
@@ -18,7 +18,7 @@ function pageRoute(node) {
18
18
  if (!market) throw new Error('mobile ui.navigate page=perps-market requires market or symbol.');
19
19
  return { page, route: 'PerpsMarketDetails', params: { market: { symbol: market } } };
20
20
  }
21
- throw new Error('mobile ui.navigate supported page aliases: home, perps, perps-market.');
21
+ throw new Error('mobile ui.navigate supported page intents: home, perps, perps-market.');
22
22
  }
23
23
 
24
24
  runAdapter(async (input) => {