@deeeed/metamask-harness 0.47.4 → 0.48.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 +6 -0
- package/README.md +3 -2
- package/adapters/extension/lib/macos-focus.cjs +2 -2
- package/adapters/extension/live.sh +6 -8
- package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
- package/adapters/shared/ensure-runner-deps.sh +6 -0
- package/dist/adapters/extension/runtime-decision.js +18 -1
- package/dist/adapters/extension/runtime.js +5 -5
- package/dist/adapters/extension/surface.js +1 -0
- package/dist/app-lifecycle.js +3 -1
- package/dist/command-contract.js +2 -0
- package/dist/commands/call.js +1 -8
- package/dist/commands/device-target.js +12 -3
- package/dist/commands/doctor.js +7 -5
- package/dist/commands/fixtures.js +1 -1
- package/dist/commands/launch/index.js +10 -0
- package/dist/commands/manifest.js +3 -1
- package/dist/commands/mobile-device-view.js +13 -3
- package/dist/commands/parse-args.js +2 -0
- package/dist/commands/run-engine.js +29 -4
- package/dist/commands/run.js +1 -1
- package/dist/commands/runtime-launch.js +10 -1
- package/dist/commands/status.js +8 -4
- package/dist/devices.js +31 -9
- package/dist/execution-provenance.js +8 -7
- package/dist/funding-execution-context.js +43 -13
- package/dist/heal-bounds.js +3 -3
- package/dist/live-adapter-contract.js +4 -0
- package/dist/manifest.js +2 -13
- package/dist/mm-harness-cli.js +2 -1
- package/dist/runner.js +14 -3
- package/docs/RECIPES.md +133 -22
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
- package/library/actions/core/perps/_controller.mjs +10 -0
- package/library/actions/core/perps/close_orders.mjs +13 -17
- package/library/actions/extension/assets/home-token-identity.mjs +30 -0
- package/library/actions/extension/assets/import_custom_token.mjs +61 -79
- package/library/actions/extension/assets/open_details.mjs +18 -7
- package/library/actions/extension/assets/read_details.mjs +61 -29
- package/library/actions/extension/assets/read_visible_state.mjs +4 -1
- package/library/actions/extension/assets/select_network_scope.mjs +16 -9
- package/library/actions/extension/assets/set_token_visibility.mjs +84 -17
- package/library/actions/extension/assets/verify_sorting.mjs +62 -14
- package/library/actions/extension/networks/add_custom.mjs +19 -17
- package/library/actions/extension/networks/custom_network.mjs +8 -3
- package/library/actions/extension/networks/read_visible_state.mjs +1 -0
- package/library/actions/extension/performance/_navigation-memory.mjs +35 -6
- package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +3 -2
- package/library/actions/extension/performance/measure_detached_dom.mjs +3 -2
- package/library/actions/extension/performance/measure_navigation_memory.mjs +3 -2
- package/library/actions/extension/perps/assert_visible_consistency.mjs +9 -4
- package/library/actions/extension/perps/close_visible_position.mjs +3 -3
- package/library/actions/extension/perps/edit_margin.mjs +1 -1
- package/library/actions/extension/perps/open_balance_action.mjs +16 -7
- package/library/actions/extension/perps/perps.mjs +150 -45
- package/library/actions/extension/perps/read_funds_confirmation.mjs +8 -0
- package/library/actions/extension/perps/read_snapshot.mjs +2 -1
- package/library/actions/extension/perps/read_visible_state.mjs +10 -6
- package/library/actions/extension/perps/search_markets.mjs +7 -2
- package/library/actions/extension/perps/select_activity_filter.mjs +1 -1
- package/library/actions/extension/perps/select_market_filter.mjs +74 -45
- package/library/actions/extension/perps/set_market_favorite.mjs +6 -3
- package/library/actions/extension/perps/state.mjs +1 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +1 -1
- package/library/actions/extension/perps/visible-mutation-identity.mjs +4 -2
- package/library/actions/extension/platform/cdp.mjs +23 -142
- package/library/actions/extension/swap_bridge/quote-state.mjs +54 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +4 -1
- package/library/actions/extension/swap_bridge/select_assets.mjs +11 -1
- package/library/actions/extension/swap_bridge/set_amount.mjs +10 -8
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +17 -18
- package/library/actions/extension/swap_bridge/set_slippage.mjs +0 -9
- package/library/actions/extension/ui/navigate.mjs +58 -14
- package/library/actions/extension/wallet/lock.mjs +2 -1
- package/library/actions/mobile/assets/import_custom_token.mjs +1 -0
- package/library/actions/mobile/assets/read_visible_state.mjs +3 -1
- package/library/actions/mobile/assets/set_token_visibility.mjs +13 -9
- package/library/actions/mobile/networks/network-management.mjs +22 -15
- package/library/actions/mobile/networks/remove_custom.mjs +6 -2
- package/library/actions/mobile/perps/perps.mjs +3 -0
- package/library/actions/mobile/perps/read-visible-state-loop.mjs +1 -0
- package/library/actions/mobile/perps/set_market_favorite.mjs +15 -19
- package/library/actions/mobile/platform/native-session.mjs +26 -17
- package/library/actions/mobile/platform/observe-ui.mjs +2 -2
- package/library/actions/mobile/ui/navigate.mjs +1 -1
- package/library/actions/shared/perps/visible-state.mjs +14 -3
- package/library/manifests/core.action-manifest.json +14 -2
- package/library/manifests/extension.action-manifest.json +49 -16
- package/library/manifests/mobile.action-manifest.json +8 -7
- package/library/recipes/core/perps/smoke.recipe.json +23 -0
- package/library/recipes/core/perps/snapshot.recipe.json +26 -1
- package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
- package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
- package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
- package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
- package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +23 -4
- package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
- package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
- package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
- package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
- package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
- package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
- package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
- package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
- package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
- package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
- package/package.json +3 -3
- package/site/assets/help-recipes.json +63 -23
- package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
- package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
- package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
- package/library/recipes/core/perps/read-markets.recipe.json +0 -36
- package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
- package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
- package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
- package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
- package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
- package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
- package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
- package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
- package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
- package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
- package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
- package/library/recipes/mobile/perps/performance.recipe.json +0 -722
- package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
- package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
- package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
- package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
- package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
- package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
- package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
- package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
- package/library/recipes/perps/lifecycle.recipe.json +0 -144
- package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
- package/library/recipes/perps/release-watchlist.recipe.json +0 -111
- package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
- /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
- /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
- /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
- /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
- /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
- /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
|
@@ -6,19 +6,30 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
6
6
|
if (!/^[A-Z0-9._-]{2,20}$/u.test(symbol)) {
|
|
7
7
|
throw new Error('metamask.assets.open_details requires a valid token symbol.');
|
|
8
8
|
}
|
|
9
|
-
const
|
|
9
|
+
const match = await page.evaluate(`(() => {
|
|
10
10
|
const symbol = ${JSON.stringify(symbol)};
|
|
11
|
-
|
|
11
|
+
const selectors = [];
|
|
12
|
+
for (const row of document.querySelectorAll('[data-testid="multichain-token-list-button"]')) {
|
|
13
|
+
const style = getComputedStyle(row);
|
|
14
|
+
const rect = row.getBoundingClientRect();
|
|
15
|
+
if (style.display === 'none' || style.visibility === 'hidden' || rect.width <= 0 || rect.height <= 0
|
|
16
|
+
|| rect.bottom <= 0 || rect.right <= 0 || rect.top >= innerHeight || rect.left >= innerWidth) continue;
|
|
12
17
|
const amount = String(row.querySelector('[data-testid="multichain-token-list-item-value"]')?.innerText || '').trim();
|
|
13
18
|
const observed = amount.match(/\\b([A-Za-z][A-Za-z0-9._-]{1,19})$/u)?.[1]?.toUpperCase();
|
|
14
19
|
if (observed !== symbol) continue;
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
const parts = [];
|
|
21
|
+
for (let element = row; element; element = element.parentElement) {
|
|
22
|
+
const index = element.parentElement
|
|
23
|
+
? [...element.parentElement.children].indexOf(element) + 1 : 1;
|
|
24
|
+
parts.unshift(element.localName + ':nth-child(' + index + ')');
|
|
25
|
+
}
|
|
26
|
+
selectors.push(parts.join(' > '));
|
|
17
27
|
}
|
|
18
|
-
return null;
|
|
28
|
+
return { count: selectors.length, selector: selectors.length === 1 ? selectors[0] : null };
|
|
19
29
|
})()`);
|
|
20
|
-
if (!
|
|
21
|
-
|
|
30
|
+
if (!match.count) throw new Error(`Visible asset ${symbol} was not found.`);
|
|
31
|
+
if (match.count !== 1) throw new Error(`Visible asset ${symbol} matches ${match.count} rows; selection is ambiguous.\nNext: use a uniquely visible token symbol or narrow the product network filter before retrying.`);
|
|
32
|
+
await page.click(match.selector);
|
|
22
33
|
const deadline = Date.now() + timeoutMs;
|
|
23
34
|
while (Date.now() <= deadline) {
|
|
24
35
|
if (String(await page.evaluate('location.hash')).startsWith('#/asset/')) {
|
|
@@ -6,17 +6,29 @@ function clean(value) {
|
|
|
6
6
|
return normalized ? normalized.slice(0, 500) : null;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
runAdapter(async (input) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
runAdapter(async (input) =>
|
|
10
|
+
withExtensionPage(input, async (page) => {
|
|
11
|
+
const expectedSymbol = clean(input.node?.expected_symbol)?.toUpperCase() ?? null;
|
|
12
|
+
const requireChart = input.node?.require_chart === true;
|
|
13
|
+
const requireMarketCap = input.node?.require_market_cap === true;
|
|
14
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
|
|
15
|
+
const deadline = Date.now() + timeoutMs;
|
|
16
|
+
let samples = 0;
|
|
17
|
+
while (true) {
|
|
18
|
+
const raw = await page.evaluate(`(() => {
|
|
14
19
|
const text = (selector) => {
|
|
15
20
|
const value = document.querySelector(selector)?.innerText;
|
|
16
21
|
return typeof value === 'string' ? value.replace(/\\s+/gu, ' ').trim().slice(0, 500) : null;
|
|
17
22
|
};
|
|
18
23
|
const body = String(document.body?.innerText || '').replace(/\\s+/gu, ' ').trim();
|
|
19
24
|
const name = text('[data-testid="asset-name"]');
|
|
25
|
+
const chart = document.querySelector('[data-testid="asset-price-chart"]');
|
|
26
|
+
const price = document.querySelector('[data-testid="asset-hovered-price"]');
|
|
27
|
+
const priceText = String(price?.innerText || '').trim();
|
|
28
|
+
const chartLabels = [...(chart?.querySelectorAll('p') || [])]
|
|
29
|
+
.filter(element => !element.closest('button, [role="button"]'))
|
|
30
|
+
.map(element => String(element.innerText || '').trim())
|
|
31
|
+
.filter(value => /[0-9]/u.test(value));
|
|
20
32
|
return {
|
|
21
33
|
route: String(location.hash),
|
|
22
34
|
name,
|
|
@@ -25,33 +37,53 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
25
37
|
marketCap: text('[data-testid="asset-market-cap"]'),
|
|
26
38
|
chartRendered: Boolean(document.querySelector('[data-testid="asset-price-chart"] canvas')),
|
|
27
39
|
chartEmpty: Boolean(document.querySelector('[data-testid="asset-chart-empty-state"]')),
|
|
40
|
+
chartHasPriceLabels: chartLabels.length >= 2,
|
|
41
|
+
priceSettled: Boolean(price && /[0-9]/u.test(priceText) && Number(getComputedStyle(price).opacity) === 1),
|
|
28
42
|
malformed: /(?:\\bNaN\\b|\\bundefined\\b|\\bnull\\b)/u.test(body),
|
|
29
43
|
hasActivity: /\\bYour activity\\b/u.test(body),
|
|
30
44
|
};
|
|
31
45
|
})()`);
|
|
46
|
+
samples += 1;
|
|
32
47
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
if (raw.malformed) {
|
|
46
|
-
throw new Error('Asset details contain a malformed visible value.');
|
|
47
|
-
}
|
|
48
|
+
if (!raw.route.startsWith('#/asset/')) {
|
|
49
|
+
throw new Error(`Expected an asset details route, but observed ${raw.route || 'no route'}.`);
|
|
50
|
+
}
|
|
51
|
+
if (expectedSymbol && raw.symbol && raw.symbol !== expectedSymbol) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Expected ${expectedSymbol} asset details, but observed ${raw.symbol || 'no symbol'}.`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (raw.malformed) {
|
|
57
|
+
throw new Error('Asset details contain a malformed visible value.');
|
|
58
|
+
}
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
const pending = [];
|
|
61
|
+
if (expectedSymbol && !raw.symbol) pending.push('asset symbol');
|
|
62
|
+
if (
|
|
63
|
+
requireChart &&
|
|
64
|
+
(!raw.chartRendered || raw.chartEmpty || !raw.chartHasPriceLabels || !raw.priceSettled)
|
|
65
|
+
)
|
|
66
|
+
pending.push('settled chart data and price');
|
|
67
|
+
if (requireMarketCap && !raw.marketCap) pending.push('formatted market cap');
|
|
68
|
+
if (pending.length) {
|
|
69
|
+
const remaining = deadline - Date.now();
|
|
70
|
+
if (remaining <= 0)
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Asset details did not show ${pending.join(', ')} within ${timeoutMs}ms. Last state: ${JSON.stringify(raw)}\nNext: inspect the asset page and captured failure; do not refresh to manufacture readiness.`,
|
|
73
|
+
);
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(100, remaining)));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
action: input.action,
|
|
80
|
+
source: 'visible-ui',
|
|
81
|
+
platform: 'extension',
|
|
82
|
+
surface: 'asset-details',
|
|
83
|
+
...raw,
|
|
84
|
+
samples,
|
|
85
|
+
proofPath: 'visible-dom-accessibility',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
@@ -12,7 +12,8 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
12
12
|
.filter((element) => {
|
|
13
13
|
const style = getComputedStyle(element);
|
|
14
14
|
const rect = element.getBoundingClientRect();
|
|
15
|
-
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
|
|
15
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
|
|
16
|
+
&& rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
|
|
16
17
|
})
|
|
17
18
|
.slice(0, maximum)
|
|
18
19
|
.map((element) => {
|
|
@@ -38,6 +39,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
38
39
|
return {
|
|
39
40
|
route: location.hash,
|
|
40
41
|
title: document.title,
|
|
42
|
+
lowValueExpanded: document.querySelector('[data-testid="low-value-assets-toggle"]')?.getAttribute('aria-expanded') ?? null,
|
|
41
43
|
items: rows,
|
|
42
44
|
truncated: document.querySelectorAll('[data-testid="multichain-token-list-button"]').length > rows.length,
|
|
43
45
|
};
|
|
@@ -45,6 +47,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
45
47
|
return {
|
|
46
48
|
action: input.action,
|
|
47
49
|
...normalizeVisibleAssetsState(raw, options, 'extension'),
|
|
50
|
+
lowValueExpanded: raw.lowValueExpanded,
|
|
48
51
|
proofPath: 'visible-dom-accessibility',
|
|
49
52
|
};
|
|
50
53
|
}));
|
|
@@ -11,16 +11,23 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
11
11
|
await openHome(page, timeoutMs);
|
|
12
12
|
const filter = dataTestId('sort-by-networks');
|
|
13
13
|
const all = dataTestId('home-network-filter-all-default');
|
|
14
|
+
const deadline = Date.now() + timeoutMs;
|
|
15
|
+
const remaining = () => Math.max(1, deadline - Date.now());
|
|
16
|
+
const readLabel = () => page.evaluate(`String(document.querySelector(${JSON.stringify(filter)})?.innerText || '')`);
|
|
17
|
+
const alreadySelected = /All (?:default )?networks/iu.test(await readLabel());
|
|
18
|
+
if (await visible(page, all)) {
|
|
19
|
+
await page.waitFor({ selector: all, expected: 'visible', timeoutMs: remaining() });
|
|
20
|
+
await page.click(all);
|
|
21
|
+
} else if (!alreadySelected) {
|
|
22
|
+
await waitFor(page, () => visible(page, filter), 'Network filter did not become visible.', remaining());
|
|
23
|
+
await page.click(filter);
|
|
24
|
+
await page.waitFor({ selector: all, expected: 'visible', timeoutMs: remaining() });
|
|
25
|
+
await page.click(all);
|
|
26
|
+
}
|
|
14
27
|
const selected = await waitFor(page, async () => {
|
|
15
|
-
const label = await
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
await page.click(all);
|
|
19
|
-
} else if (await visible(page, filter)) {
|
|
20
|
-
await page.click(filter);
|
|
21
|
-
}
|
|
22
|
-
return null;
|
|
23
|
-
}, 'All networks did not become selected.', timeoutMs);
|
|
28
|
+
const label = await readLabel();
|
|
29
|
+
return /All (?:default )?networks/iu.test(label) && !(await visible(page, all)) ? label : null;
|
|
30
|
+
}, 'All networks did not become selected with its menu closed.', remaining());
|
|
24
31
|
return {
|
|
25
32
|
action: input.action,
|
|
26
33
|
scope,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import { verifyHomeIdentity } from './home-token-identity.mjs';
|
|
2
3
|
|
|
3
4
|
const OPTIONS = dataTestId('asset-list-control-bar-action-button');
|
|
4
5
|
const MANAGE = dataTestId('manageTokens__button');
|
|
5
|
-
const PAGE = dataTestId('token-management-page')
|
|
6
|
+
const PAGE = `${dataTestId('parent-selector-token-management-page')}, ${dataTestId('token-management-page')}`;
|
|
6
7
|
const SEARCH = dataTestId('token-management-search-input');
|
|
7
8
|
const BACK = dataTestId('token-management-header-back-button');
|
|
8
9
|
const LOW_VALUE = dataTestId('low-value-assets-toggle');
|
|
@@ -27,12 +28,15 @@ async function waitFor(page, predicate, message, timeoutMs) {
|
|
|
27
28
|
throw new Error(message);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
async function matchingToggle(page, query) {
|
|
31
|
+
async function matchingToggle(page, query, assetId) {
|
|
31
32
|
return page.evaluate(`(() => {
|
|
32
33
|
const query = ${JSON.stringify(query.toLowerCase())};
|
|
34
|
+
const assetId = ${JSON.stringify(assetId)};
|
|
33
35
|
const candidates = [...document.querySelectorAll('[data-testid^="token-management-cell-search-"][data-testid$="-toggle-control"]')];
|
|
36
|
+
const matches = [];
|
|
34
37
|
for (const element of candidates) {
|
|
35
38
|
const testId = element.getAttribute('data-testid');
|
|
39
|
+
if (assetId && testId !== 'token-management-cell-search-' + assetId + '-toggle-control') continue;
|
|
36
40
|
const row = document.querySelector('[data-testid="' + testId.slice(0, -'-toggle-control'.length) + '"]');
|
|
37
41
|
const label = String(row?.innerText || element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500);
|
|
38
42
|
const words = label.toLowerCase().split(/[^a-z0-9]+/u).filter(Boolean);
|
|
@@ -40,28 +44,48 @@ async function matchingToggle(page, query) {
|
|
|
40
44
|
const style = getComputedStyle(element);
|
|
41
45
|
const rect = element.getBoundingClientRect();
|
|
42
46
|
if (style.display === 'none' || style.visibility === 'hidden' || rect.width <= 0 || rect.height <= 0) continue;
|
|
43
|
-
|
|
47
|
+
matches.push({
|
|
44
48
|
testId,
|
|
45
49
|
label,
|
|
46
50
|
enabled: Boolean(element.querySelector('.toggle-button--on')),
|
|
47
|
-
|
|
51
|
+
busy: element.getAttribute('aria-busy') === 'true',
|
|
52
|
+
});
|
|
48
53
|
}
|
|
49
|
-
|
|
54
|
+
if (matches.length > 1) throw new Error('Token management matched ' + matches.length + ' rows; query is ambiguous.\\nNext: mm-harness actions --action metamask.assets.set_token_visibility --json');
|
|
55
|
+
return matches[0] || null;
|
|
50
56
|
})()`);
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
async function homeToken(page, query) {
|
|
59
|
+
async function homeToken(page, query, verifyIdentity = false) {
|
|
54
60
|
return page.evaluate(`(() => {
|
|
55
61
|
const query = ${JSON.stringify(query.toLowerCase())};
|
|
62
|
+
const verifyIdentity = ${JSON.stringify(verifyIdentity)};
|
|
63
|
+
const matches = [];
|
|
56
64
|
for (const row of document.querySelectorAll('[data-testid="multichain-token-list-button"]')) {
|
|
57
65
|
const name = String(row.querySelector('[data-testid="multichain-token-list-item-token-name"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
|
|
58
66
|
const amount = String(row.querySelector('[data-testid="multichain-token-list-item-value"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
|
|
59
67
|
const symbol = amount.match(/\\b([A-Za-z][A-Za-z0-9]{1,11})$/u)?.[1] || '';
|
|
60
68
|
if (symbol.toLowerCase() === query || name.toLowerCase().includes(query)) {
|
|
61
|
-
|
|
69
|
+
const rect = row.getBoundingClientRect();
|
|
70
|
+
const style = getComputedStyle(row);
|
|
71
|
+
if (rect.width <= 0 || rect.height <= 0 || style.display === 'none' || style.visibility === 'hidden') continue;
|
|
72
|
+
matches.push({ row, name, symbol: symbol.toUpperCase(), amount });
|
|
62
73
|
}
|
|
63
74
|
}
|
|
64
|
-
|
|
75
|
+
if (verifyIdentity && matches.length > 1) throw new Error('Home token query is ambiguous; cannot verify asset identity.');
|
|
76
|
+
const match = matches[0];
|
|
77
|
+
if (!match) return null;
|
|
78
|
+
const { row, ...token } = match;
|
|
79
|
+
if (verifyIdentity) {
|
|
80
|
+
row.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
81
|
+
const rect = row.getBoundingClientRect();
|
|
82
|
+
const x = rect.left + rect.width / 2;
|
|
83
|
+
const y = rect.top + rect.height / 2;
|
|
84
|
+
const hit = document.elementFromPoint(x, y);
|
|
85
|
+
if (!hit || !row.contains(hit)) return null;
|
|
86
|
+
token.point = { x, y };
|
|
87
|
+
}
|
|
88
|
+
return token;
|
|
65
89
|
})()`);
|
|
66
90
|
}
|
|
67
91
|
|
|
@@ -77,6 +101,31 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
77
101
|
const requireUnchanged = input.node?.require_unchanged === true;
|
|
78
102
|
const requireChange = input.node?.require_change === true;
|
|
79
103
|
const timeoutMs = Number(input.node?.timeout_ms ?? 45_000);
|
|
104
|
+
const assetId = String(input.node?.asset_id ?? '').trim().toLowerCase();
|
|
105
|
+
if (assetId && !/^eip155:[1-9][0-9]{0,19}\/erc20:0x[0-9a-f]{40}$/u.test(assetId)) {
|
|
106
|
+
throw new Error('asset_id requires an ERC-20 CAIP asset ID.\nNext: mm-harness actions --action metamask.assets.set_token_visibility --json');
|
|
107
|
+
}
|
|
108
|
+
if (assetId && !requested && (!requireChange || requireUnchanged)) {
|
|
109
|
+
throw new Error('Exact token hiding requires require_change=true and require_unchanged=false.\nNext: mm-harness actions --action metamask.assets.set_token_visibility --json');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let identity;
|
|
113
|
+
if (assetId && !requested) {
|
|
114
|
+
if (await visible(page, LOW_VALUE)) {
|
|
115
|
+
const expanded = await page.evaluate(`document.querySelector(${JSON.stringify(LOW_VALUE)})?.getAttribute('aria-expanded') === 'true'`);
|
|
116
|
+
if (!expanded) await page.click(LOW_VALUE);
|
|
117
|
+
}
|
|
118
|
+
const token = await waitFor(
|
|
119
|
+
page,
|
|
120
|
+
() => homeToken(page, query, true),
|
|
121
|
+
`${query} must be visible on Home before its exact identity can be hidden.`,
|
|
122
|
+
timeoutMs,
|
|
123
|
+
);
|
|
124
|
+
identity = {
|
|
125
|
+
...await verifyHomeIdentity(page, token, assetId, timeoutMs),
|
|
126
|
+
verifiedAt: 'before-hide',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
80
129
|
|
|
81
130
|
if (!(await visible(page, MANAGE))) await page.click(OPTIONS);
|
|
82
131
|
await waitFor(page, () => visible(page, MANAGE), 'Manage tokens did not become visible.', timeoutMs);
|
|
@@ -85,8 +134,11 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
85
134
|
await page.setInput(SEARCH, query);
|
|
86
135
|
const before = await waitFor(
|
|
87
136
|
page,
|
|
88
|
-
() =>
|
|
89
|
-
|
|
137
|
+
async () => {
|
|
138
|
+
const state = await matchingToggle(page, query, assetId);
|
|
139
|
+
return state && !state.busy ? state : null;
|
|
140
|
+
},
|
|
141
|
+
`Token management returned no settled visible result for ${query}.`,
|
|
90
142
|
timeoutMs,
|
|
91
143
|
);
|
|
92
144
|
if (requireUnchanged && before.enabled !== requested) {
|
|
@@ -101,8 +153,8 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
101
153
|
const after = await waitFor(
|
|
102
154
|
page,
|
|
103
155
|
async () => {
|
|
104
|
-
const state = await matchingToggle(page, query);
|
|
105
|
-
return state
|
|
156
|
+
const state = await matchingToggle(page, query, assetId);
|
|
157
|
+
return state && !state.busy && state.enabled === requested ? state : null;
|
|
106
158
|
},
|
|
107
159
|
`${query} token visibility did not become ${requested ? 'enabled' : 'disabled'}.`,
|
|
108
160
|
timeoutMs,
|
|
@@ -110,14 +162,26 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
110
162
|
await page.click(BACK);
|
|
111
163
|
await waitFor(page, () => visible(page, OPTIONS), 'Token list did not reopen.', timeoutMs);
|
|
112
164
|
|
|
113
|
-
if (requested && (await visible(page, LOW_VALUE))) {
|
|
165
|
+
if ((requested || assetId) && (await visible(page, LOW_VALUE))) {
|
|
114
166
|
const expanded = await page.evaluate(`document.querySelector(${JSON.stringify(LOW_VALUE)})?.getAttribute('aria-expanded') === 'true'`);
|
|
115
167
|
if (!expanded) await page.click(LOW_VALUE);
|
|
116
168
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
169
|
+
let token = null;
|
|
170
|
+
if (requested) {
|
|
171
|
+
token = await waitFor(page, () => homeToken(page, query, Boolean(assetId)), `${query} did not appear in the token list.`, timeoutMs);
|
|
172
|
+
if (assetId) identity = await verifyHomeIdentity(page, token, assetId, timeoutMs);
|
|
173
|
+
} else if (assetId) {
|
|
174
|
+
await waitFor(
|
|
175
|
+
page,
|
|
176
|
+
async () => !(await homeToken(page, query)),
|
|
177
|
+
`${query} remained visible after disabling it.`,
|
|
178
|
+
timeoutMs,
|
|
179
|
+
);
|
|
180
|
+
identity.homeAbsent = true;
|
|
181
|
+
} else {
|
|
182
|
+
token = await homeToken(page, query);
|
|
183
|
+
if (token) throw new Error(`${query} remained visible after disabling it.`);
|
|
184
|
+
}
|
|
121
185
|
|
|
122
186
|
return {
|
|
123
187
|
action: input.action,
|
|
@@ -126,8 +190,11 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
126
190
|
requireUnchanged,
|
|
127
191
|
requireChange,
|
|
128
192
|
changed: before.enabled !== after.enabled,
|
|
193
|
+
beforeEnabled: before.enabled,
|
|
194
|
+
afterEnabled: after.enabled,
|
|
129
195
|
searchResult: after.label,
|
|
130
196
|
toggleTestId: after.testId,
|
|
197
|
+
...(identity ? { identity } : {}),
|
|
131
198
|
token,
|
|
132
199
|
proofPath: 'trusted-pointer-visible-ui',
|
|
133
200
|
};
|
|
@@ -45,9 +45,9 @@ function assertDeclining(rows) {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
async function
|
|
48
|
+
async function openMenu(page) {
|
|
49
49
|
const isVisible = async () => page.evaluate(`(() => {
|
|
50
|
-
const element = document.querySelector(${JSON.stringify(
|
|
50
|
+
const element = document.querySelector(${JSON.stringify(ALPHABETICAL)});
|
|
51
51
|
if (!element) return false;
|
|
52
52
|
const style = getComputedStyle(element);
|
|
53
53
|
const rect = element.getBoundingClientRect();
|
|
@@ -57,24 +57,71 @@ async function choose(page, selector) {
|
|
|
57
57
|
const deadline = Date.now() + 5_000;
|
|
58
58
|
while (!(await isVisible())) {
|
|
59
59
|
if (Date.now() >= deadline) throw new Error('Asset sort menu did not become visible.');
|
|
60
|
-
await
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function selectedSort(page) {
|
|
65
|
+
await openMenu(page);
|
|
66
|
+
const selected = await page.evaluate(`(() => ${JSON.stringify([ALPHABETICAL, DECLINING])}
|
|
67
|
+
.filter(selector => document.querySelector(selector)?.classList.contains('selectable-list-item--selected')))()`);
|
|
68
|
+
if (selected.length !== 1) throw new Error('Asset sort preference is unavailable or ambiguous; no sort will be changed.');
|
|
69
|
+
return selected[0];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function choose(page, selector) {
|
|
73
|
+
const current = await selectedSort(page);
|
|
74
|
+
await page.click(current === selector ? TOGGLE : selector);
|
|
75
|
+
await page.waitForExpression(`(() => {
|
|
76
|
+
const element = document.querySelector(${JSON.stringify(ALPHABETICAL)});
|
|
77
|
+
if (!element) return true;
|
|
78
|
+
const rect = element.getBoundingClientRect();
|
|
79
|
+
const style = getComputedStyle(element);
|
|
80
|
+
return rect.width === 0 || rect.height === 0 || style.display === 'none' || style.visibility === 'hidden';
|
|
81
|
+
})()`, { timeoutMs: 5000 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function waitForRows(page, assertion) {
|
|
85
|
+
const deadline = Date.now() + 5000;
|
|
86
|
+
while (true) {
|
|
87
|
+
const rows = await readRows(page);
|
|
88
|
+
try {
|
|
89
|
+
assertion(rows);
|
|
90
|
+
return rows;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (Date.now() >= deadline) throw error;
|
|
93
|
+
}
|
|
94
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
61
95
|
}
|
|
62
|
-
await page.click(selector);
|
|
63
|
-
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 500))');
|
|
64
96
|
}
|
|
65
97
|
|
|
66
98
|
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
67
|
-
if (!String(await page.evaluate('location.hash'))
|
|
99
|
+
if (!['#/', '#', ''].includes(String(await page.evaluate('location.hash')))) {
|
|
68
100
|
throw new Error('metamask.assets.verify_sorting requires the wallet home token list.');
|
|
69
101
|
}
|
|
70
102
|
|
|
71
|
-
await
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
103
|
+
const initialSort = await selectedSort(page);
|
|
104
|
+
let alphabetical;
|
|
105
|
+
let declining;
|
|
106
|
+
let proofError;
|
|
107
|
+
try {
|
|
108
|
+
await choose(page, ALPHABETICAL);
|
|
109
|
+
alphabetical = await waitForRows(page, assertAlphabetical);
|
|
110
|
+
await choose(page, DECLINING);
|
|
111
|
+
declining = await waitForRows(page, assertDeclining);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
proofError = error;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
await choose(page, initialSort);
|
|
117
|
+
await openMenu(page);
|
|
118
|
+
await page.waitForExpression(`document.querySelector(${JSON.stringify(initialSort)})?.classList.contains('selectable-list-item--selected') === true`, { timeoutMs: 5000 });
|
|
119
|
+
await page.click(TOGGLE);
|
|
120
|
+
} catch (restoreError) {
|
|
121
|
+
if (proofError) throw new AggregateError([proofError, restoreError], 'Asset sorting proof and preference restoration both failed.');
|
|
122
|
+
throw restoreError;
|
|
123
|
+
}
|
|
124
|
+
if (proofError) throw proofError;
|
|
78
125
|
|
|
79
126
|
return {
|
|
80
127
|
action: input.action,
|
|
@@ -83,7 +130,8 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
83
130
|
surface: 'token-list',
|
|
84
131
|
alphabetical: alphabetical.map((row) => row.name),
|
|
85
132
|
decliningFiat: declining.map((row) => row.fiat),
|
|
86
|
-
|
|
133
|
+
initialSort: initialSort === ALPHABETICAL ? 'alphabetical' : 'declining-balance',
|
|
134
|
+
restoredSort: initialSort === ALPHABETICAL ? 'alphabetical' : 'declining-balance',
|
|
87
135
|
proofPath: 'trusted-pointer-visible-ui',
|
|
88
136
|
};
|
|
89
137
|
}));
|
|
@@ -37,17 +37,19 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
37
37
|
await page.setInput(dataTestId('network-form-chain-id'), chainId);
|
|
38
38
|
await page.setInput(dataTestId('network-form-ticker-input'), symbol);
|
|
39
39
|
|
|
40
|
-
await page.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
await page.click(dataTestId('test-add-rpc-drop-down'));
|
|
41
|
+
const addRpc = await waitFor(
|
|
42
|
+
page,
|
|
43
|
+
() => semanticButtonOrNull(page, 'Add RPC URL'),
|
|
44
|
+
'Add RPC URL did not become visible after opening the menu.',
|
|
45
|
+
timeoutMs,
|
|
46
|
+
);
|
|
47
|
+
await page.waitFor({ selector: addRpc, expected: 'visible', timeoutMs });
|
|
48
48
|
await page.click(addRpc);
|
|
49
49
|
await page.setInput(dataTestId('rpc-url-input-test'), rpcUrl);
|
|
50
|
-
|
|
50
|
+
const rpcSubmit = `${dataTestId('page-container-footer-next')}:not(:disabled):not([aria-disabled="true"])`;
|
|
51
|
+
await page.waitFor({ selector: rpcSubmit, expected: 'visible', timeoutMs });
|
|
52
|
+
await page.click(rpcSubmit);
|
|
51
53
|
await waitFor(
|
|
52
54
|
page,
|
|
53
55
|
() => page.evaluate(`!document.querySelector(${JSON.stringify(dataTestId('rpc-url-input-test'))})`),
|
|
@@ -56,14 +58,14 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
|
56
58
|
);
|
|
57
59
|
|
|
58
60
|
if (explorerUrl) {
|
|
59
|
-
await page.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
await page.click(dataTestId('test-explorer-drop-down'));
|
|
62
|
+
const addExplorer = await waitFor(
|
|
63
|
+
page,
|
|
64
|
+
() => semanticButtonOrNull(page, 'Add a block explorer URL'),
|
|
65
|
+
'Add a block explorer URL did not become visible after opening the menu.',
|
|
66
|
+
timeoutMs,
|
|
67
|
+
);
|
|
68
|
+
await page.waitFor({ selector: addExplorer, expected: 'visible', timeoutMs });
|
|
67
69
|
await page.click(addExplorer);
|
|
68
70
|
await page.setInput(dataTestId('explorer-url-input'), explorerUrl);
|
|
69
71
|
await page.click(await semanticButton(page, 'Add URL'));
|
|
@@ -90,7 +90,7 @@ export async function openNetworkManager(page, timeoutMs) {
|
|
|
90
90
|
const networks = dataTestId('global-menu-networks');
|
|
91
91
|
const deadline = Date.now() + timeoutMs;
|
|
92
92
|
await clickUncovered(page, menu, deadline);
|
|
93
|
-
await waitFor(
|
|
93
|
+
await page.waitFor({ selector: networks, expected: 'visible', timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
94
94
|
await page.click(networks);
|
|
95
95
|
await waitFor(
|
|
96
96
|
page,
|
|
@@ -115,7 +115,9 @@ export async function findNetworkItem(page, network, chainId) {
|
|
|
115
115
|
const label = String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500);
|
|
116
116
|
const nameMatches = [...element.querySelectorAll('[data-testid]')]
|
|
117
117
|
.some((child) => String(child.getAttribute('data-testid') || '').toLowerCase() === network);
|
|
118
|
-
if (chainIds.has(suffix)
|
|
118
|
+
if (!chainIds.has(suffix)) continue;
|
|
119
|
+
if (!nameMatches) throw new Error('Chain ' + ${JSON.stringify(chainId)} + ' is already configured under a different name.\\nNext: mm-harness call ui.locators --adapter extension --json');
|
|
120
|
+
return { testId, label, suffix };
|
|
119
121
|
}
|
|
120
122
|
return null;
|
|
121
123
|
})()`);
|
|
@@ -128,6 +130,7 @@ export function assertNetworkAdditionAllowed(item, requireAbsent, network, chain
|
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
export async function selectHomeNetwork(page, network, chainId, timeoutMs) {
|
|
133
|
+
const deadline = Date.now() + timeoutMs;
|
|
131
134
|
const sort = dataTestId('sort-by-networks');
|
|
132
135
|
const chainHex = `0x${chainId.toString(16)}`;
|
|
133
136
|
const choices = [
|
|
@@ -135,13 +138,15 @@ export async function selectHomeNetwork(page, network, chainId, timeoutMs) {
|
|
|
135
138
|
dataTestId(`home-network-filter-custom-${chainHex}`),
|
|
136
139
|
dataTestId(`home-network-filter-additional-${chainHex}`),
|
|
137
140
|
];
|
|
138
|
-
await page.click(sort);
|
|
141
|
+
if (!(await visible(page, dataTestId('home-network-filter-all-default')))) await page.click(sort);
|
|
139
142
|
const choice = await waitFor(page, async () => {
|
|
140
143
|
for (const candidate of choices) {
|
|
141
144
|
if (await visible(page, candidate)) return candidate;
|
|
142
145
|
}
|
|
143
146
|
return null;
|
|
144
147
|
}, `${network} did not appear in the home network filter.`, timeoutMs);
|
|
148
|
+
await page.scroll({ selector: choice, intoView: true });
|
|
149
|
+
await page.waitFor({ selector: choice, expected: 'visible', timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
145
150
|
await page.click(choice);
|
|
146
151
|
await waitFor(page, async () => {
|
|
147
152
|
const label = await page.evaluate(`String(document.querySelector(${JSON.stringify(sort)})?.innerText || '')`);
|
|
@@ -40,6 +40,7 @@ async function capture(page, maximum) {
|
|
|
40
40
|
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
41
41
|
const options = resolveVisibleNetworkOptions(input.node);
|
|
42
42
|
const before = await capture(page, options.assets.maxItems);
|
|
43
|
+
normalizeVisibleNetworkState(before, before, options, 'extension');
|
|
43
44
|
if (options.settleMs > 0) await sleep(options.settleMs);
|
|
44
45
|
const after = options.settleMs > 0 ? await capture(page, options.assets.maxItems) : before;
|
|
45
46
|
return {
|