@deeeed/metamask-harness 0.65.0 → 0.66.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 CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.66.0 - 2026-09-25
6
+
7
+ ### Added
8
+
9
+ - Observe Top Movers pills and visible counts separately from ordinary market rows.
10
+ - Expose market-filter selection matches from observed controls for composable proof assertions.
11
+
12
+ ### Fixed
13
+
14
+ - Give Extension Perps smoke its own runtime assertions and refuse a visible tutorial gate.
15
+ - Refuse absence claims on truncated Perps observations and exclude pills clipped by the viewport or scroll containers from Top Movers visible counts.
16
+ - Capture the driven market-search page through the existing surface-capture action.
17
+
5
18
  ## 0.65.0 - 2026-09-25
6
19
 
7
20
  ### Added
@@ -12,6 +12,12 @@ function pointInside(point, left, top, right, bottom) {
12
12
  return point && point.x >= left && point.x <= right && point.y >= top && point.y <= bottom;
13
13
  }
14
14
 
15
+ export function topMoverFitsVisibleBounds(rect, bounds) {
16
+ return rect.width > 0 && rect.height > 0 && bounds.every((clip) =>
17
+ rect.left >= clip.left - 1 && rect.right <= clip.right + 1 &&
18
+ rect.top >= clip.top - 1 && rect.bottom <= clip.bottom + 1);
19
+ }
20
+
15
21
  export function classifyActionableControlLayout(controls) {
16
22
  const bounded = Array.isArray(controls) ? controls.slice(0, 200) : [];
17
23
  return {
@@ -42,6 +48,26 @@ export async function readVisiblePerpsState(page, node = {}) {
42
48
  const raw = await page.evaluate(`(() => {
43
49
  const includeOffscreen = ${JSON.stringify(options.includeOffscreen)};
44
50
  const maximum = ${JSON.stringify(options.maxItems)};
51
+ const topMoverFitsVisibleBounds = ${topMoverFitsVisibleBounds.toString()};
52
+ const topMoverVisible = (element, rect) => {
53
+ const bounds = [{ left: 0, top: 0, right: innerWidth, bottom: innerHeight }];
54
+ for (let ancestor = element.parentElement; ancestor; ancestor = ancestor.parentElement) {
55
+ const style = getComputedStyle(ancestor);
56
+ const clipsX = /^(hidden|clip|scroll|auto|overlay)$/.test(style.overflowX);
57
+ const clipsY = /^(hidden|clip|scroll|auto|overlay)$/.test(style.overflowY);
58
+ if (!clipsX && !clipsY) continue;
59
+ const box = ancestor.getBoundingClientRect();
60
+ const left = box.left + ancestor.clientLeft;
61
+ const top = box.top + ancestor.clientTop;
62
+ bounds.push({
63
+ left: clipsX ? left : -Infinity,
64
+ right: clipsX ? left + ancestor.clientWidth : Infinity,
65
+ top: clipsY ? top : -Infinity,
66
+ bottom: clipsY ? top + ancestor.clientHeight : Infinity,
67
+ });
68
+ }
69
+ return topMoverFitsVisibleBounds(rect, bounds);
70
+ };
45
71
  const relevant = /^(?:perps-|parent-selector-perps-(?:market-list|order-entry)|explore-markets-|market-list-view|market-row-|filter-select-button|sort-dropdown-button|search-input|position-card-|order-card-|transaction-card-|bottom-nav-|back-button|direction-|order-type-|amount-|balance-percent-|leverage-|auto-close|submit-order-button|limit-price-|tp-|sl-|custom-amount-|pay-with-|confirm-footer-button|wallet-initiated-header-back-button|transaction-fee-|bridge-(?:fee|time)-|receive-(?:row|value))/u;
46
72
  const items = [];
47
73
  const offscreenItems = [];
@@ -70,7 +96,8 @@ export async function readVisiblePerpsState(page, node = {}) {
70
96
  const rendered = style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
71
97
  if (!rendered) continue;
72
98
  matched += 1;
73
- const visible = rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
99
+ const visible = rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth &&
100
+ (!testId.startsWith('perps-top-movers-pill-') || topMoverVisible(element, rect));
74
101
  if (rect.bottom > 0 && rect.top < innerHeight && (rect.left < -1 || rect.right > innerWidth + 1)) {
75
102
  clippedTestIds.push(testId);
76
103
  }
@@ -60,7 +60,7 @@ async function selectDropdownFilter(page, filter, option, timeoutMs, pollInterva
60
60
  }
61
61
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
62
62
  }
63
- return { selectedLabel: selectedState.label, controlKind: 'dropdown', alreadySelected: false };
63
+ return { selectedLabel: selectedState.label, selectionMatches: selectedState.visible && selectedState.label === optionState.label, controlKind: 'dropdown', alreadySelected: false };
64
64
  }
65
65
 
66
66
  async function categoryRailState(page) {
@@ -109,7 +109,7 @@ async function selectCategoryRail(page, option, timeoutMs, pollIntervalMs) {
109
109
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
110
110
  selected = await categoryRailState(page);
111
111
  }
112
- return { selectedLabel: selected.active[0]?.label ?? null, controlKind: 'category-rail', alreadySelected };
112
+ return { selectedLabel: selected.active[0]?.label ?? null, selectionMatches: matches(selected), controlKind: 'category-rail', alreadySelected };
113
113
  }
114
114
 
115
115
  export async function selectMarketFilter(page, input, pollOptions = {}) {
@@ -239,6 +239,15 @@ export function normalizeVisiblePerpsState(raw, options, platform) {
239
239
  };
240
240
  const detailPair = detailField(/^(?:perps-market-detail-pair|perps-(?:pro-)?market-header-subtitle)$/u);
241
241
  const detailSymbol = detailPair?.match(/^(.+?)-(?:USDC|USD|PERP)(?:\s+perp)?$/iu)?.[1]?.toUpperCase() ?? null;
242
+ const moverList = items.find((item) => item.testId === 'perps-top-movers-list');
243
+ const moverPills = items.filter((item) => item.testId.startsWith('perps-top-movers-pill-'))
244
+ .map((item) => ({
245
+ market: marketFromTestId(item.testId.replace('perps-top-movers-pill-', 'explore-markets-')),
246
+ testId: item.testId,
247
+ label: item.label ?? null,
248
+ visible: item.visible,
249
+ }))
250
+ .filter((item) => item.market);
242
251
  const state = {
243
252
  source: 'visible-ui',
244
253
  platform,
@@ -259,6 +268,12 @@ export function normalizeVisiblePerpsState(raw, options, platform) {
259
268
  )[0] ?? null,
260
269
  markets: marketItems,
261
270
  marketCount: marketItems.length,
271
+ topMovers: moverList ? {
272
+ visible: moverList.visible,
273
+ pills: moverPills,
274
+ count: moverPills.length,
275
+ visibleCount: moverPills.filter((item) => item.visible).length,
276
+ } : null,
262
277
  positions: compactEntries(
263
278
  selected(unique, (item) => /^(?:position-card-(?!roe-)|perps-position-row-|perps-position-size-value$|perps-pro-market-position-row-|perps-home-position-card-)/u.test(item.testId), options.maxItems),
264
279
  options.maxItems,
@@ -321,11 +336,14 @@ export function normalizeVisiblePerpsState(raw, options, platform) {
321
336
  : [],
322
337
  } : null,
323
338
  };
324
- assertVisibleState(state, options);
339
+ assertVisibleState(state, options, testIds);
325
340
  return state;
326
341
  }
327
342
 
328
- function assertVisibleState(state, options) {
343
+ function assertVisibleState(state, options, observed) {
344
+ if (options.forbidTestIds.length > 0 && state.truncated) {
345
+ throw new Error('Cannot prove forbidden Perps controls are absent from a truncated observation.');
346
+ }
329
347
  if (options.requireNoHorizontalOverflow && (
330
348
  !state.layout || state.layout.horizontalOverflow || state.layout.clippedTestIds.length > 0
331
349
  )) {
@@ -361,7 +379,6 @@ function assertVisibleState(state, options) {
361
379
  if (state.activity.length < options.minimumActivityCount) {
362
380
  throw new Error(`Expected at least ${options.minimumActivityCount} visible Perps activity entries, but observed ${state.activity.length}.`);
363
381
  }
364
- const observed = new Set(state.observedTestIds);
365
382
  const missing = options.requireTestIds.filter((testId) => !observed.has(testId));
366
383
  if (missing.length > 0) {
367
384
  throw new Error(`Missing required visible Perps test IDs: ${missing.join(', ')}.`);
@@ -2809,7 +2809,7 @@
2809
2809
  ]
2810
2810
  },
2811
2811
  "metamask.perps.read_visible_state": {
2812
- "description": "Read and assert bounded visible Perps UI state without product background hooks. marketDetails exposes visible symbol, pair, name, price and priceChange as text, or null when absent/ambiguous. marketCount counts the bounded observed markets array, not an unbounded total. favorite is boolean only when the requested market pair and one favorite control are both visible; otherwise null, never an assumed false.",
2812
+ "description": "Read and assert bounded visible Perps UI state without product background hooks. marketDetails exposes visible symbol, pair, name, price and priceChange as text, or null when absent/ambiguous. marketCount counts the bounded observed markets array, not an unbounded total. topMovers separately reports its bounded pills, count and visibleCount when that list is observed; otherwise null. favorite is boolean only when the requested market pair and one favorite control are both visible; otherwise null, never an assumed false.",
2813
2813
  "schema": {
2814
2814
  "type": "object",
2815
2815
  "properties": {
@@ -2919,6 +2919,7 @@
2919
2919
  "default": false
2920
2920
  },
2921
2921
  "forbid_test_ids": {
2922
+ "description": "Requires a complete observation; truncated results fail rather than assuming these controls are absent.",
2922
2923
  "type": "array",
2923
2924
  "items": {
2924
2925
  "type": "string"
@@ -2178,7 +2178,7 @@
2178
2178
  "execution_capabilities": ["app-mutation"]
2179
2179
  },
2180
2180
  "metamask.perps.read_visible_state": {
2181
- "description": "Read and assert bounded visible Perps UI state through native accessibility. favorite is boolean only when the requested market pair and one favorite control are both observed as visible; otherwise null, never an assumed false. marketDetails exposes visible symbol, pair, name, price and priceChange as text, or null when absent/ambiguous. marketCount counts the bounded observed markets array, not an unbounded total.",
2181
+ "description": "Read and assert bounded visible Perps UI state through native accessibility. favorite is boolean only when the requested market pair and one favorite control are both observed as visible; otherwise null, never an assumed false. marketDetails exposes visible symbol, pair, name, price and priceChange as text, or null when absent/ambiguous. marketCount counts the bounded observed markets array, not an unbounded total. topMovers separately reports its bounded pills, count and visibleCount when that list is observed; otherwise null.",
2182
2182
  "schema": {
2183
2183
  "type": "object",
2184
2184
  "properties": {
@@ -2227,6 +2227,7 @@
2227
2227
  "default": 0
2228
2228
  },
2229
2229
  "forbid_test_ids": {
2230
+ "description": "Requires a complete observation; truncated results fail rather than assuming these controls are absent.",
2230
2231
  "type": "array",
2231
2232
  "items": { "type": "string" },
2232
2233
  "maxItems": 100
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
3
3
  "title": "MetaMask Extension Perps smoke",
4
- "description": "Non-mutating Perps domain smoke for MetaMask Extension: verifies CDP, unlocks if needed, opens the Perps page, and reads live Perps positions and orders.",
4
+ "description": "Read-only Extension Perps smoke: verifies CDP, unlocks if needed, opens Perps, proves visible home with market data, and reads positions/orders. Requires approved wallet setup and completed onboarding gates; a visible tutorial is setup, not passing smoke.",
5
5
  "workflow": {
6
6
  "entry": "status",
7
7
  "nodes": {
@@ -26,7 +26,7 @@
26
26
  "open-perps": {
27
27
  "action": "ui.navigate",
28
28
  "page": "perps",
29
- "next": "read-positions",
29
+ "next": "read-home",
30
30
  "intent": "Show the Perps home surface"
31
31
  },
32
32
  "read-positions": {
@@ -42,7 +42,56 @@
42
42
  "done": {
43
43
  "action": "end",
44
44
  "status": "pass"
45
+ },
46
+ "read-home": {
47
+ "action": "metamask.perps.read_visible_state",
48
+ "surface": "home",
49
+ "minimum_market_count": 1,
50
+ "require_features": [
51
+ "balance",
52
+ "market-list"
53
+ ],
54
+ "intent": "Observe a populated Perps home after setup",
55
+ "next": "assert-home",
56
+ "forbid_test_ids": [
57
+ "perps-tutorial-skip-button"
58
+ ],
59
+ "max_items": 200
60
+ },
61
+ "assert-home": {
62
+ "action": "assert_output",
63
+ "source": "read-home",
64
+ "assert": {
65
+ "path": "$.surface",
66
+ "operator": "eq",
67
+ "value": "home"
68
+ },
69
+ "proves": [
70
+ "perps-home"
71
+ ],
72
+ "intent": "The observed surface is Perps home",
73
+ "next": "assert-markets"
74
+ },
75
+ "assert-markets": {
76
+ "action": "assert_output",
77
+ "source": "read-home",
78
+ "assert": {
79
+ "path": "$.marketCount",
80
+ "operator": "gte",
81
+ "value": 1
82
+ },
83
+ "proves": [
84
+ "perps-home"
85
+ ],
86
+ "intent": "Perps home contains market data rather than only a loading shell",
87
+ "next": "read-positions"
45
88
  }
46
89
  }
47
- }
90
+ },
91
+ "proofTargets": [
92
+ {
93
+ "id": "perps-home",
94
+ "claim": "The unlocked Perps home has visible market data and no observed tutorial gate."
95
+ }
96
+ ]
48
97
  }
@@ -72,7 +72,7 @@
72
72
  "next": "verify-result"
73
73
  },
74
74
  "capture": {
75
- "action": "ui.screenshot",
75
+ "action": "ui.capture_surface",
76
76
  "path": "screenshots/perps-market-search.png",
77
77
  "label": "Perps release market search",
78
78
  "intent": "Preserve reviewer-visible proof of the filtered market list",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"