@deeeed/metamask-harness 0.65.0 → 0.66.1
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 +21 -0
- package/docs/RECIPES.md +1 -1
- package/library/actions/extension/perps/assert_visible_consistency.mjs +4 -2
- package/library/actions/extension/perps/read_visible_state.mjs +28 -1
- package/library/actions/extension/perps/select_market_filter.mjs +2 -2
- package/library/actions/extension/ui/navigate.mjs +31 -41
- package/library/actions/shared/perps/visible-state.mjs +20 -3
- package/library/manifests/extension.action-manifest.json +4 -3
- package/library/manifests/mobile.action-manifest.json +2 -1
- package/library/recipes/extension/perps/smoke.recipe.json +52 -3
- package/library/recipes/shared/perps/release-market-details.recipe.json +11 -4
- package/library/recipes/shared/perps/release-market-search.recipe.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.66.1 - 2026-09-25
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Capture market-detail report evidence from the driven document, excluding adjacent Extension views. Require an explicit market and assert no position before proving Long/Short controls. Clarify window versus document captures in discovery.
|
|
10
|
+
|
|
11
|
+
- Recognize current and legacy Perps Home markers and wait for loading to finish. Navigation reports tutorial gates without dismissing them; dependent navigation and consistency checks require explicit setup.
|
|
12
|
+
|
|
13
|
+
## 0.66.0 - 2026-09-25
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Observe Top Movers pills and visible counts separately from ordinary market rows.
|
|
18
|
+
- Expose market-filter selection matches from observed controls for composable proof assertions.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Give Extension Perps smoke its own runtime assertions and refuse a visible tutorial gate.
|
|
23
|
+
- Refuse absence claims on truncated Perps observations and exclude pills clipped by the viewport or scroll containers from Top Movers visible counts.
|
|
24
|
+
- Capture the driven market-search page through the existing surface-capture action.
|
|
25
|
+
|
|
5
26
|
## 0.65.0 - 2026-09-25
|
|
6
27
|
|
|
7
28
|
### Added
|
package/docs/RECIPES.md
CHANGED
|
@@ -213,7 +213,7 @@ mm-harness run perps.release-market-filters --describe --adapter extension --jso
|
|
|
213
213
|
| Deposit and withdraw previews without submission | `perps.release-funds-flows` |
|
|
214
214
|
| Resting limit placement and exact cancellation | `perps.release-live-order order_type=limit limit_price='"<price>"'` |
|
|
215
215
|
| Filled market placement and exact position close | `perps.release-live-order order_type=market` |
|
|
216
|
-
| Market header and statistics | `perps.release-market-details` |
|
|
216
|
+
| Market header and statistics on an unheld market | `perps.release-market-details market='<unheld symbol>'` |
|
|
217
217
|
| Market categories | `perps.release-market-filters` |
|
|
218
218
|
| Market text search | `perps.release-market-search` |
|
|
219
219
|
| Long/Short and Market/Limit entry controls | `perps.release-order-entry case=configuration` |
|
|
@@ -284,7 +284,8 @@ export async function assertVisibleConsistency(page, input, testOptions = {}) {
|
|
|
284
284
|
|
|
285
285
|
if (options.mode === 'watchlist-stream') {
|
|
286
286
|
const openHome = testOptions.openHome ?? openPerpsHome;
|
|
287
|
-
await openHome(page, options.timeoutMs);
|
|
287
|
+
const home = await openHome(page, options.timeoutMs);
|
|
288
|
+
if (home?.gate) throw new Error('Perps tutorial blocks navigation; run perps.dismiss-tutorial as explicit setup first.');
|
|
288
289
|
const first = await pollLoaded(
|
|
289
290
|
() => watchlistRows(page, options.maxItems),
|
|
290
291
|
(rows) => rows.length > 0,
|
|
@@ -343,7 +344,8 @@ export async function assertVisibleConsistency(page, input, testOptions = {}) {
|
|
|
343
344
|
{ action: 'metamask.perps.select_activity_filter', node: { filter } },
|
|
344
345
|
{ timeoutMs },
|
|
345
346
|
));
|
|
346
|
-
await openHome(page, options.timeoutMs);
|
|
347
|
+
const home = await openHome(page, options.timeoutMs);
|
|
348
|
+
if (home?.gate) throw new Error('Perps tutorial blocks navigation; run perps.dismiss-tutorial as explicit setup first.');
|
|
347
349
|
const recent = await pollLoaded(
|
|
348
350
|
() => recentActivityState(page, options.maxItems),
|
|
349
351
|
(state) => state.ids.length >= options.minimumRecentActivityCount &&
|
|
@@ -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 = {}) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Opens named wallet screens through UI controls or history, and accepts direct URL/hash navigation.
|
|
2
|
-
// Named navigation may dismiss
|
|
2
|
+
// Named navigation reports tutorial gates, may dismiss drawers, and may add a missing network before selecting its filter.
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -81,12 +81,12 @@ async function goBack(page, timeoutMs) {
|
|
|
81
81
|
|
|
82
82
|
async function hasVisibleSelector(page, selector) {
|
|
83
83
|
return page.evaluate(`(() => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
return [...document.querySelectorAll(${JSON.stringify(selector)})].some((element) => {
|
|
85
|
+
if (element.closest('dialog')?.open === false) return false;
|
|
86
|
+
const style = getComputedStyle(element);
|
|
87
|
+
const rect = element.getBoundingClientRect();
|
|
88
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
89
|
+
});
|
|
90
90
|
})()`);
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -199,18 +199,25 @@ export async function openHome(page, timeoutMs) {
|
|
|
199
199
|
export async function openPerpsHome(page, timeoutMs) {
|
|
200
200
|
const bottomNav = dataTestId('bottom-nav-perps');
|
|
201
201
|
const legacyTab = dataTestId('account-overview__perps-tab');
|
|
202
|
-
const perpsHome =
|
|
202
|
+
const perpsHome = ['perps-home-page', 'parent-selector-perps-tab', 'perps-view'].map(dataTestId).join(',');
|
|
203
|
+
const perpsLoading = dataTestId('perps-view-loading');
|
|
203
204
|
const tutorialSkip = dataTestId('perps-tutorial-skip-button');
|
|
204
205
|
const steps = [];
|
|
205
206
|
const deadline = Date.now() + timeoutMs;
|
|
206
207
|
let pendingActivation;
|
|
207
208
|
let perpsActivation;
|
|
209
|
+
let perpsControl;
|
|
208
210
|
let perpsHomeVisibleAt;
|
|
209
211
|
|
|
210
212
|
while (Date.now() < deadline) {
|
|
211
213
|
if (await hasVisibleSelector(page, tutorialSkip)) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
return {
|
|
215
|
+
gate: 'perps-tutorial', gateSelector: tutorialSkip, method: 'visible-ui',
|
|
216
|
+
...(perpsActivation ? { navigation: perpsActivation, selector: perpsControl } : {}),
|
|
217
|
+
href: await currentHref(page), steps,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (await hasVisibleSelector(page, perpsLoading)) {
|
|
214
221
|
perpsHomeVisibleAt = undefined;
|
|
215
222
|
await pauseForUi(page);
|
|
216
223
|
continue;
|
|
@@ -226,7 +233,7 @@ export async function openPerpsHome(page, timeoutMs) {
|
|
|
226
233
|
continue;
|
|
227
234
|
}
|
|
228
235
|
return {
|
|
229
|
-
...(perpsActivation ? { navigation: perpsActivation, selector:
|
|
236
|
+
...(perpsActivation ? { navigation: perpsActivation, selector: perpsControl } : {}),
|
|
230
237
|
method: 'visible-ui',
|
|
231
238
|
href: await currentHref(page),
|
|
232
239
|
steps,
|
|
@@ -236,23 +243,20 @@ export async function openPerpsHome(page, timeoutMs) {
|
|
|
236
243
|
if (await hasVisibleSelector(page, bottomNav)) {
|
|
237
244
|
if (!perpsActivation) {
|
|
238
245
|
perpsActivation = await activateSemanticControl(page, bottomNav);
|
|
246
|
+
perpsControl = bottomNav;
|
|
239
247
|
steps.push(bottomNav);
|
|
240
248
|
}
|
|
241
249
|
await pauseForUi(page);
|
|
242
250
|
continue;
|
|
243
251
|
}
|
|
244
252
|
if (await hasVisibleSelector(page, legacyTab)) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
href: await currentHref(page),
|
|
253
|
-
selector: legacyTab,
|
|
254
|
-
steps,
|
|
255
|
-
};
|
|
253
|
+
if (!perpsActivation) {
|
|
254
|
+
perpsActivation = await activateSemanticControl(page, legacyTab);
|
|
255
|
+
perpsControl = legacyTab;
|
|
256
|
+
steps.push(legacyTab);
|
|
257
|
+
}
|
|
258
|
+
await pauseForUi(page);
|
|
259
|
+
continue;
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
let backControl;
|
|
@@ -313,6 +317,7 @@ export async function openPerpsMarket(page, market, timeoutMs) {
|
|
|
313
317
|
};
|
|
314
318
|
}
|
|
315
319
|
const home = await openPerpsHome(page, Math.max(1, deadline - Date.now()));
|
|
320
|
+
if (home.gate) throw new Error('Perps tutorial blocks navigation; run perps.dismiss-tutorial as explicit setup first.');
|
|
316
321
|
const selectors = [
|
|
317
322
|
dataTestId(`perps-watchlist-${normalizedMarket}`),
|
|
318
323
|
dataTestId(`explore-markets-${normalizedMarket.replaceAll(':', '-')}`),
|
|
@@ -349,6 +354,7 @@ export async function openPerpsActivity(page, timeoutMs) {
|
|
|
349
354
|
}
|
|
350
355
|
const deadline = Date.now() + timeoutMs;
|
|
351
356
|
const home = await openPerpsHome(page, Math.max(1, deadline - Date.now()));
|
|
357
|
+
if (home.gate) throw new Error('Perps tutorial blocks navigation; run perps.dismiss-tutorial as explicit setup first.');
|
|
352
358
|
const activity = dataTestId('perps-recent-activity-see-all');
|
|
353
359
|
await page.waitForSelector(activity, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
354
360
|
const navigation = await activateSemanticControl(page, activity);
|
|
@@ -383,33 +389,17 @@ export async function openPerpsMarketList(page, timeoutMs) {
|
|
|
383
389
|
};
|
|
384
390
|
}
|
|
385
391
|
const home = await openPerpsHome(page, Math.max(1, deadline - Date.now()));
|
|
392
|
+
if (home.gate) throw new Error('Perps tutorial blocks navigation; run perps.dismiss-tutorial as explicit setup first.');
|
|
386
393
|
const explore = dataTestId('perps-explore-markets-row');
|
|
387
394
|
const tutorialSkip = dataTestId('perps-tutorial-skip-button');
|
|
388
395
|
if (!(await waitForFirstVisible(page, [explore], deadline))) {
|
|
389
396
|
throw new Error('Perps Explore Markets did not become visible before timeout.');
|
|
390
397
|
}
|
|
391
|
-
|
|
398
|
+
const navigation = await activateSemanticControl(page, explore);
|
|
392
399
|
const destination = await waitForFirstVisible(page, [marketList, tutorialSkip], deadline);
|
|
393
400
|
const steps = [...home.steps, explore];
|
|
394
401
|
if (destination === tutorialSkip) {
|
|
395
|
-
|
|
396
|
-
steps.push(tutorialSkip);
|
|
397
|
-
let exploreReady = false;
|
|
398
|
-
while (Date.now() < deadline) {
|
|
399
|
-
if (!(await hasVisibleSelector(page, tutorialSkip)) && await hasVisibleSelector(page, explore)) {
|
|
400
|
-
exploreReady = true;
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
await pauseForUi(page);
|
|
404
|
-
}
|
|
405
|
-
if (!exploreReady) {
|
|
406
|
-
throw new Error('Perps Explore Markets did not become visible after dismissing the tutorial.');
|
|
407
|
-
}
|
|
408
|
-
navigation = await activateSemanticControl(page, explore);
|
|
409
|
-
steps.push(explore);
|
|
410
|
-
if (!(await waitForFirstVisible(page, [marketList], deadline))) {
|
|
411
|
-
throw new Error('Perps market list did not become visible after dismissing the tutorial.');
|
|
412
|
-
}
|
|
402
|
+
throw new Error('Perps tutorial blocks navigation; run perps.dismiss-tutorial as explicit setup first.');
|
|
413
403
|
} else if (destination !== marketList) {
|
|
414
404
|
throw new Error('Perps market list did not become visible before timeout.');
|
|
415
405
|
}
|
|
@@ -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(', ')}.`);
|
|
@@ -495,7 +495,7 @@
|
|
|
495
495
|
]
|
|
496
496
|
},
|
|
497
497
|
"ui.navigate": {
|
|
498
|
-
"description": "Navigate by semantic page intent when available; the adapter uses the current visible UI or a verified route. Raw url/hash/path values must be verified against the current app first.",
|
|
498
|
+
"description": "Navigate by semantic page intent when available; the adapter uses the current visible UI or a verified route. Raw url/hash/path values must be verified against the current app first. Perps Home returns gate=perps-tutorial and gateSelector without dismissing a visible tutorial. Complete explicit tutorial setup before dependent market or activity navigation.",
|
|
499
499
|
"schema": {
|
|
500
500
|
"type": "object",
|
|
501
501
|
"properties": {
|
|
@@ -1132,7 +1132,7 @@
|
|
|
1132
1132
|
]
|
|
1133
1133
|
},
|
|
1134
1134
|
"ui.screenshot": {
|
|
1135
|
-
"description": "Capture the current native browser window on macOS, with bounded Chrome viewport capture as fallback. Always inspect the resulting PNG
|
|
1135
|
+
"description": "Capture the current native browser window on macOS, with bounded Chrome viewport capture as fallback. A native window can include a separate side panel or a tab other than the driven page. For report evidence of the driven document, use ui.capture_surface. Always inspect the resulting PNG and artifact metadata; failed real capture does not produce a reconstructed DOM image.",
|
|
1136
1136
|
"examples": [
|
|
1137
1137
|
{
|
|
1138
1138
|
"action": "ui.screenshot",
|
|
@@ -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": "
|
|
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-
|
|
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
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
|
|
3
3
|
"title": "Perps release market details",
|
|
4
|
-
"description": "Opens one live Perps market through visible UI and proves its header price, change, statistics, and Long/Short controls; it does not assert chart data.",
|
|
4
|
+
"description": "Opens one live Perps market through visible UI and proves its header price, change, statistics, and Long/Short controls; it does not assert chart data. Requires no existing position in the chosen market: held markets show Modify/Close instead. Verify the fixture with a supported read before selecting market; never close a position to satisfy this recipe.",
|
|
5
5
|
"paramsSchema": {
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
|
+
"required": ["market"],
|
|
8
9
|
"properties": {
|
|
9
10
|
"market": {
|
|
10
11
|
"type": "string",
|
|
11
|
-
"
|
|
12
|
-
"description": "Perps market symbol to open and verify."
|
|
12
|
+
"description": "Perps market symbol with no existing position, so Long/Short controls are expected."
|
|
13
13
|
},
|
|
14
14
|
"timeout_ms": {
|
|
15
15
|
"type": "integer",
|
|
@@ -45,6 +45,13 @@
|
|
|
45
45
|
],
|
|
46
46
|
"max_items": 100,
|
|
47
47
|
"intent": "Prove the requested market appears in a populated live market list",
|
|
48
|
+
"next": "require-flat-market"
|
|
49
|
+
},
|
|
50
|
+
"require-flat-market": {
|
|
51
|
+
"action": "metamask.perps.assert_positions",
|
|
52
|
+
"market": "{{params.market}}",
|
|
53
|
+
"state": "none",
|
|
54
|
+
"intent": "Require the flat-market fixture before proving Long and Short controls; never change existing positions",
|
|
48
55
|
"next": "open-market"
|
|
49
56
|
},
|
|
50
57
|
"open-market": {
|
|
@@ -70,7 +77,7 @@
|
|
|
70
77
|
"next": "verify-directions"
|
|
71
78
|
},
|
|
72
79
|
"capture-market": {
|
|
73
|
-
"action": "ui.
|
|
80
|
+
"action": "ui.capture_surface",
|
|
74
81
|
"path": "screenshots/perps-market-details.png",
|
|
75
82
|
"label": "Perps release market details",
|
|
76
83
|
"intent": "Preserve reviewer-visible proof of the selected live market",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"next": "verify-result"
|
|
73
73
|
},
|
|
74
74
|
"capture": {
|
|
75
|
-
"action": "ui.
|
|
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",
|