@deeeed/metamask-harness 0.47.4 → 0.49.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 (261) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -2
  3. package/adapters/extension/lib/macos-focus.cjs +2 -2
  4. package/adapters/extension/live.sh +6 -8
  5. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +1 -0
  6. package/adapters/shared/ensure-runner-deps.sh +6 -0
  7. package/dist/adapters/extension/runtime-decision.js +18 -1
  8. package/dist/adapters/extension/runtime.js +5 -5
  9. package/dist/adapters/extension/surface.js +1 -0
  10. package/dist/app-lifecycle.js +3 -1
  11. package/dist/command-contract.js +2 -0
  12. package/dist/commands/call.js +1 -8
  13. package/dist/commands/device-target.js +12 -3
  14. package/dist/commands/doctor.js +7 -5
  15. package/dist/commands/fixtures.js +1 -1
  16. package/dist/commands/help.js +17 -5
  17. package/dist/commands/launch/index.js +10 -0
  18. package/dist/commands/manifest.js +3 -1
  19. package/dist/commands/mobile-device-view.js +13 -3
  20. package/dist/commands/parse-args.js +2 -0
  21. package/dist/commands/run-engine.js +29 -4
  22. package/dist/commands/run.js +1 -1
  23. package/dist/commands/runtime-launch.js +10 -1
  24. package/dist/commands/status.js +8 -4
  25. package/dist/devices.js +31 -9
  26. package/dist/execution-provenance.js +8 -7
  27. package/dist/funding-execution-context.js +62 -17
  28. package/dist/heal-bounds.js +3 -3
  29. package/dist/live-adapter-contract.js +4 -0
  30. package/dist/manifest.js +2 -13
  31. package/dist/metamask-action-validation.js +3 -2
  32. package/dist/mm-harness-cli.js +4 -2
  33. package/dist/runner.js +14 -3
  34. package/docs/RECIPES.md +135 -24
  35. package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
  36. package/library/actions/core/perps/_controller.mjs +24 -24
  37. package/library/actions/core/perps/assert_orders.mjs +3 -5
  38. package/library/actions/core/perps/assert_positions.mjs +3 -5
  39. package/library/actions/core/perps/close_orders.mjs +39 -19
  40. package/library/actions/core/perps/close_positions.mjs +53 -12
  41. package/library/actions/core/perps/edit_order.mjs +2 -0
  42. package/library/actions/core/perps/ensure_orders.mjs +3 -4
  43. package/library/actions/core/perps/ensure_positions.mjs +4 -5
  44. package/library/actions/core/perps/place_order.mjs +7 -14
  45. package/library/actions/core/perps/read_account.mjs +2 -0
  46. package/library/actions/core/perps/read_orders.mjs +2 -0
  47. package/library/actions/core/perps/read_positions.mjs +3 -1
  48. package/library/actions/core/perps/read_snapshot.mjs +3 -0
  49. package/library/actions/core/perps/start_state.mjs +3 -14
  50. package/library/actions/core/perps/teardown_state.mjs +3 -14
  51. package/library/actions/core/perps/update_position_tpsl.mjs +13 -19
  52. package/library/actions/core/wallet/list_accounts.mjs +3 -0
  53. package/library/actions/extension/analytics/consent.mjs +2 -0
  54. package/library/actions/extension/analytics/set_consent.mjs +2 -0
  55. package/library/actions/extension/assets/home-token-identity.mjs +31 -0
  56. package/library/actions/extension/assets/import_custom_token.mjs +63 -79
  57. package/library/actions/extension/assets/open_details.mjs +19 -7
  58. package/library/actions/extension/assets/prepare_send.mjs +1 -0
  59. package/library/actions/extension/assets/read_details.mjs +62 -29
  60. package/library/actions/extension/assets/read_visible_state.mjs +5 -1
  61. package/library/actions/extension/assets/review_send.mjs +1 -0
  62. package/library/actions/extension/assets/select_network_scope.mjs +17 -9
  63. package/library/actions/extension/assets/send.mjs +2 -0
  64. package/library/actions/extension/assets/set_custom_gas.mjs +1 -0
  65. package/library/actions/extension/assets/set_token_visibility.mjs +86 -17
  66. package/library/actions/extension/assets/verify_sorting.mjs +63 -14
  67. package/library/actions/extension/deeplink/open.mjs +1 -0
  68. package/library/actions/extension/networks/add_chainlist.mjs +2 -0
  69. package/library/actions/extension/networks/add_custom.mjs +21 -17
  70. package/library/actions/extension/networks/custom_network.mjs +10 -3
  71. package/library/actions/extension/networks/read_visible_state.mjs +2 -0
  72. package/library/actions/extension/networks/remove_custom.mjs +1 -0
  73. package/library/actions/extension/performance/_navigation-memory.mjs +37 -6
  74. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +5 -2
  75. package/library/actions/extension/performance/measure_detached_dom.mjs +5 -2
  76. package/library/actions/extension/performance/measure_navigation_memory.mjs +5 -2
  77. package/library/actions/extension/perps/assert_orders.mjs +1 -0
  78. package/library/actions/extension/perps/assert_positions.mjs +1 -0
  79. package/library/actions/extension/perps/assert_visible_consistency.mjs +11 -4
  80. package/library/actions/extension/perps/close_orders.mjs +4 -1
  81. package/library/actions/extension/perps/close_positions.mjs +1 -0
  82. package/library/actions/extension/perps/close_visible_position.mjs +160 -11
  83. package/library/actions/extension/perps/compare_provider_market.mjs +2 -0
  84. package/library/actions/extension/perps/edit_margin.mjs +3 -1
  85. package/library/actions/extension/perps/ensure_orders.mjs +1 -0
  86. package/library/actions/extension/perps/ensure_positions.mjs +1 -0
  87. package/library/actions/extension/perps/mutation-receipt.mjs +54 -3
  88. package/library/actions/extension/perps/open_balance_action.mjs +17 -7
  89. package/library/actions/extension/perps/open_position_action.mjs +1 -0
  90. package/library/actions/extension/perps/perps.mjs +550 -69
  91. package/library/actions/extension/perps/place_order.mjs +1 -0
  92. package/library/actions/extension/perps/read_eligibility.mjs +1 -0
  93. package/library/actions/extension/perps/read_funds_confirmation.mjs +10 -0
  94. package/library/actions/extension/perps/read_orders.mjs +1 -0
  95. package/library/actions/extension/perps/read_positions.mjs +1 -0
  96. package/library/actions/extension/perps/read_snapshot.mjs +4 -1
  97. package/library/actions/extension/perps/read_visible_state.mjs +12 -6
  98. package/library/actions/extension/perps/search_markets.mjs +8 -2
  99. package/library/actions/extension/perps/select_activity_filter.mjs +2 -1
  100. package/library/actions/extension/perps/select_market_filter.mjs +75 -45
  101. package/library/actions/extension/perps/set_market_favorite.mjs +7 -3
  102. package/library/actions/extension/perps/state.mjs +2 -0
  103. package/library/actions/extension/perps/update_position_tpsl.mjs +3 -1
  104. package/library/actions/extension/perps/visible-mutation-identity.mjs +6 -2
  105. package/library/actions/extension/platform/cdp.mjs +50 -142
  106. package/library/actions/extension/settings/set_basic_functionality.mjs +1 -0
  107. package/library/actions/extension/swap_bridge/quote-state.mjs +56 -0
  108. package/library/actions/extension/swap_bridge/read_visible_state.mjs +5 -1
  109. package/library/actions/extension/swap_bridge/select_assets.mjs +12 -1
  110. package/library/actions/extension/swap_bridge/set_amount.mjs +11 -8
  111. package/library/actions/extension/swap_bridge/set_max_amount.mjs +18 -18
  112. package/library/actions/extension/swap_bridge/set_slippage.mjs +1 -9
  113. package/library/actions/extension/ui/locators.mjs +1 -0
  114. package/library/actions/extension/ui/navigate.mjs +60 -14
  115. package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -0
  116. package/library/actions/extension/wallet/import.mjs +2 -0
  117. package/library/actions/extension/wallet/list_accounts.mjs +1 -0
  118. package/library/actions/extension/wallet/lock.mjs +4 -1
  119. package/library/actions/extension/wallet/read_state.mjs +1 -0
  120. package/library/actions/extension/wallet/reset.mjs +2 -0
  121. package/library/actions/extension/wallet/secret-input.mjs +2 -0
  122. package/library/actions/extension/wallet/select_account.mjs +2 -0
  123. package/library/actions/extension/wallet/setup.mjs +1 -0
  124. package/library/actions/extension/wallet/state.mjs +2 -0
  125. package/library/actions/extension/wallet/visible-session.mjs +2 -0
  126. package/library/actions/mobile/analytics/consent-settings.mjs +1 -0
  127. package/library/actions/mobile/analytics/set_consent.mjs +1 -0
  128. package/library/actions/mobile/app/network-control.mjs +2 -0
  129. package/library/actions/mobile/app/network.mjs +1 -0
  130. package/library/actions/mobile/app/network_assert.mjs +1 -0
  131. package/library/actions/mobile/app/network_capture.mjs +1 -0
  132. package/library/actions/mobile/assets/import_custom_token.mjs +3 -0
  133. package/library/actions/mobile/assets/open_details.mjs +1 -0
  134. package/library/actions/mobile/assets/read_details.mjs +1 -0
  135. package/library/actions/mobile/assets/read_visible_state.mjs +4 -1
  136. package/library/actions/mobile/assets/set_token_visibility.mjs +15 -9
  137. package/library/actions/mobile/assets/verify_sorting.mjs +90 -7
  138. package/library/actions/mobile/deeplink/open.mjs +1 -0
  139. package/library/actions/mobile/networks/add_custom.mjs +1 -0
  140. package/library/actions/mobile/networks/network-management.mjs +23 -15
  141. package/library/actions/mobile/networks/read_visible_state.mjs +1 -0
  142. package/library/actions/mobile/networks/remove_custom.mjs +7 -2
  143. package/library/actions/mobile/perps/assert_orders.mjs +1 -0
  144. package/library/actions/mobile/perps/assert_positions.mjs +1 -0
  145. package/library/actions/mobile/perps/capture_performance.mjs +1 -0
  146. package/library/actions/mobile/perps/close_orders.mjs +1 -0
  147. package/library/actions/mobile/perps/close_positions.mjs +1 -0
  148. package/library/actions/mobile/perps/ensure_orders.mjs +1 -0
  149. package/library/actions/mobile/perps/ensure_positions.mjs +1 -0
  150. package/library/actions/mobile/perps/measure_homepage_visible.mjs +1 -0
  151. package/library/actions/mobile/perps/performance-capture.mjs +2 -0
  152. package/library/actions/mobile/perps/perps.mjs +8 -1
  153. package/library/actions/mobile/perps/place_order.mjs +1 -0
  154. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +2 -0
  155. package/library/actions/mobile/perps/read-visible-state-loop.mjs +3 -0
  156. package/library/actions/mobile/perps/read_orders.mjs +1 -0
  157. package/library/actions/mobile/perps/read_positions.mjs +1 -0
  158. package/library/actions/mobile/perps/read_visible_state.mjs +1 -0
  159. package/library/actions/mobile/perps/search_markets.mjs +1 -0
  160. package/library/actions/mobile/perps/set_market_favorite.mjs +17 -19
  161. package/library/actions/mobile/platform/bridge.mjs +16 -4
  162. package/library/actions/mobile/platform/native-session-name.mjs +1 -0
  163. package/library/actions/mobile/platform/native-session.mjs +28 -17
  164. package/library/actions/mobile/platform/observe-ui.mjs +4 -2
  165. package/library/actions/mobile/platform/tool-paths.mjs +1 -0
  166. package/library/actions/mobile/swap_bridge/native-session.mjs +1 -0
  167. package/library/actions/mobile/swap_bridge/read_visible_state.mjs +1 -0
  168. package/library/actions/mobile/swap_bridge/select_assets.mjs +2 -0
  169. package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -0
  170. package/library/actions/mobile/swap_bridge/set_max_amount.mjs +1 -0
  171. package/library/actions/mobile/swap_bridge/set_slippage.mjs +1 -0
  172. package/library/actions/mobile/swap_bridge/submit_transaction.mjs +2 -0
  173. package/library/actions/mobile/ui/locators.mjs +1 -0
  174. package/library/actions/mobile/ui/native-navigation.mjs +2 -0
  175. package/library/actions/mobile/ui/navigate.mjs +3 -1
  176. package/library/actions/mobile/wallet/ensure_unlocked.mjs +2 -0
  177. package/library/actions/mobile/wallet/home.mjs +1 -0
  178. package/library/actions/mobile/wallet/import.mjs +2 -0
  179. package/library/actions/mobile/wallet/list_accounts.mjs +1 -0
  180. package/library/actions/mobile/wallet/lock.mjs +1 -0
  181. package/library/actions/mobile/wallet/native-ui.mjs +2 -0
  182. package/library/actions/mobile/wallet/read_state.mjs +2 -0
  183. package/library/actions/mobile/wallet/reset-helper.mjs +1 -0
  184. package/library/actions/mobile/wallet/reset.mjs +1 -0
  185. package/library/actions/mobile/wallet/select_account.mjs +2 -0
  186. package/library/actions/mobile/wallet/setup.mjs +2 -0
  187. package/library/actions/shared/analytics/assert_events.mjs +3 -0
  188. package/library/actions/shared/analytics/consent.mjs +3 -0
  189. package/library/actions/shared/analytics/read_events.mjs +2 -0
  190. package/library/actions/shared/analytics/start_capture.mjs +3 -0
  191. package/library/actions/shared/app/network-artifact.mjs +2 -0
  192. package/library/actions/shared/app/network-assert.mjs +2 -0
  193. package/library/actions/shared/assets/visible-state.mjs +2 -0
  194. package/library/actions/shared/deeplink/url.mjs +3 -0
  195. package/library/actions/shared/networks/visible-state.mjs +2 -0
  196. package/library/actions/shared/perps/visible-state.mjs +16 -3
  197. package/library/actions/shared/swap-bridge/transaction.mjs +3 -0
  198. package/library/actions/shared/swap-bridge/visible-state.mjs +2 -0
  199. package/library/actions/shared/ui/locators.mjs +2 -0
  200. package/library/actions/shared/wallet/import-source.mjs +3 -0
  201. package/library/manifests/core.action-manifest.json +34 -2
  202. package/library/manifests/extension.action-manifest.json +50 -17
  203. package/library/manifests/mobile.action-manifest.json +11 -10
  204. package/library/recipes/core/perps/smoke.recipe.json +23 -0
  205. package/library/recipes/core/perps/snapshot.recipe.json +26 -1
  206. package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
  207. package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
  208. package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
  209. package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
  210. package/library/recipes/mobile/app/lifecycle-smoke.recipe.json +89 -0
  211. package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
  212. package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
  213. package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
  214. package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
  215. package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
  216. package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
  217. package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
  218. package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
  219. package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
  220. package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
  221. package/package.json +3 -3
  222. package/site/assets/help-recipes.json +220 -27
  223. package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
  224. package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
  225. package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
  226. package/library/recipes/core/perps/read-markets.recipe.json +0 -36
  227. package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
  228. package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
  229. package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
  230. package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
  231. package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
  232. package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
  233. package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
  234. package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
  235. package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
  236. package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +0 -69
  237. package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
  238. package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
  239. package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
  240. package/library/recipes/mobile/perps/performance.recipe.json +0 -722
  241. package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
  242. package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
  243. package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
  244. package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
  245. package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
  246. package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
  247. package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
  248. package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
  249. package/library/recipes/perps/lifecycle.recipe.json +0 -144
  250. package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
  251. package/library/recipes/perps/release-watchlist.recipe.json +0 -111
  252. package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
  253. /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
  254. /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
  255. /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
  256. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
  257. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
  258. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
  259. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
  260. /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
  261. /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
@@ -1,3 +1,4 @@
1
+ // Reads asset details from the DOM, rejecting malformed values and waiting for requested chart data.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  function clean(value) {
@@ -6,17 +7,29 @@ function clean(value) {
6
7
  return normalized ? normalized.slice(0, 500) : null;
7
8
  }
8
9
 
9
- runAdapter(async (input) => withExtensionPage(input, async (page) => {
10
- const expectedSymbol = clean(input.node?.expected_symbol)?.toUpperCase() ?? null;
11
- const requireChart = input.node?.require_chart === true;
12
- const requireMarketCap = input.node?.require_market_cap === true;
13
- const raw = await page.evaluate(`(() => {
10
+ runAdapter(async (input) =>
11
+ withExtensionPage(input, async (page) => {
12
+ const expectedSymbol = clean(input.node?.expected_symbol)?.toUpperCase() ?? null;
13
+ const requireChart = input.node?.require_chart === true;
14
+ const requireMarketCap = input.node?.require_market_cap === true;
15
+ const timeoutMs = Number(input.node?.timeout_ms ?? 30000);
16
+ const deadline = Date.now() + timeoutMs;
17
+ let samples = 0;
18
+ while (true) {
19
+ const raw = await page.evaluate(`(() => {
14
20
  const text = (selector) => {
15
21
  const value = document.querySelector(selector)?.innerText;
16
22
  return typeof value === 'string' ? value.replace(/\\s+/gu, ' ').trim().slice(0, 500) : null;
17
23
  };
18
24
  const body = String(document.body?.innerText || '').replace(/\\s+/gu, ' ').trim();
19
25
  const name = text('[data-testid="asset-name"]');
26
+ const chart = document.querySelector('[data-testid="asset-price-chart"]');
27
+ const price = document.querySelector('[data-testid="asset-hovered-price"]');
28
+ const priceText = String(price?.innerText || '').trim();
29
+ const chartLabels = [...(chart?.querySelectorAll('p') || [])]
30
+ .filter(element => !element.closest('button, [role="button"]'))
31
+ .map(element => String(element.innerText || '').trim())
32
+ .filter(value => /[0-9]/u.test(value));
20
33
  return {
21
34
  route: String(location.hash),
22
35
  name,
@@ -25,33 +38,53 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
25
38
  marketCap: text('[data-testid="asset-market-cap"]'),
26
39
  chartRendered: Boolean(document.querySelector('[data-testid="asset-price-chart"] canvas')),
27
40
  chartEmpty: Boolean(document.querySelector('[data-testid="asset-chart-empty-state"]')),
41
+ chartHasPriceLabels: chartLabels.length >= 2,
42
+ priceSettled: Boolean(price && /[0-9]/u.test(priceText) && Number(getComputedStyle(price).opacity) === 1),
28
43
  malformed: /(?:\\bNaN\\b|\\bundefined\\b|\\bnull\\b)/u.test(body),
29
44
  hasActivity: /\\bYour activity\\b/u.test(body),
30
45
  };
31
46
  })()`);
47
+ samples += 1;
32
48
 
33
- if (!raw.route.startsWith('#/asset/')) {
34
- throw new Error(`Expected an asset details route, but observed ${raw.route || 'no route'}.`);
35
- }
36
- if (expectedSymbol && raw.symbol !== expectedSymbol) {
37
- throw new Error(`Expected ${expectedSymbol} asset details, but observed ${raw.symbol || 'no symbol'}.`);
38
- }
39
- if (requireChart && (!raw.chartRendered || raw.chartEmpty)) {
40
- throw new Error('Expected a rendered asset price chart without an empty state.');
41
- }
42
- if (requireMarketCap && !raw.marketCap) {
43
- throw new Error('Expected a formatted asset market cap.');
44
- }
45
- if (raw.malformed) {
46
- throw new Error('Asset details contain a malformed visible value.');
47
- }
49
+ if (!raw.route.startsWith('#/asset/')) {
50
+ throw new Error(`Expected an asset details route, but observed ${raw.route || 'no route'}.`);
51
+ }
52
+ if (expectedSymbol && raw.symbol && raw.symbol !== expectedSymbol) {
53
+ throw new Error(
54
+ `Expected ${expectedSymbol} asset details, but observed ${raw.symbol || 'no symbol'}.`,
55
+ );
56
+ }
57
+ if (raw.malformed) {
58
+ throw new Error('Asset details contain a malformed visible value.');
59
+ }
48
60
 
49
- return {
50
- action: input.action,
51
- source: 'visible-ui',
52
- platform: 'extension',
53
- surface: 'asset-details',
54
- ...raw,
55
- proofPath: 'visible-dom-accessibility',
56
- };
57
- }));
61
+ const pending = [];
62
+ if (expectedSymbol && !raw.symbol) pending.push('asset symbol');
63
+ if (
64
+ requireChart &&
65
+ (!raw.chartRendered || raw.chartEmpty || !raw.chartHasPriceLabels || !raw.priceSettled)
66
+ )
67
+ pending.push('settled chart data and price');
68
+ if (requireMarketCap && !raw.marketCap) pending.push('formatted market cap');
69
+ if (pending.length) {
70
+ const remaining = deadline - Date.now();
71
+ if (remaining <= 0)
72
+ throw new Error(
73
+ `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.`,
74
+ );
75
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, remaining)));
76
+ continue;
77
+ }
78
+
79
+ return {
80
+ action: input.action,
81
+ source: 'visible-ui',
82
+ platform: 'extension',
83
+ surface: 'asset-details',
84
+ ...raw,
85
+ samples,
86
+ proofPath: 'visible-dom-accessibility',
87
+ };
88
+ }
89
+ }),
90
+ );
@@ -1,3 +1,4 @@
1
+ // Captures token rows within the viewport, including displayed balances and price-change tones.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import {
3
4
  normalizeVisibleAssetsState,
@@ -12,7 +13,8 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
12
13
  .filter((element) => {
13
14
  const style = getComputedStyle(element);
14
15
  const rect = element.getBoundingClientRect();
15
- return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
16
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
17
+ && rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
16
18
  })
17
19
  .slice(0, maximum)
18
20
  .map((element) => {
@@ -38,6 +40,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
38
40
  return {
39
41
  route: location.hash,
40
42
  title: document.title,
43
+ lowValueExpanded: document.querySelector('[data-testid="low-value-assets-toggle"]')?.getAttribute('aria-expanded') ?? null,
41
44
  items: rows,
42
45
  truncated: document.querySelectorAll('[data-testid="multichain-token-list-button"]').length > rows.length,
43
46
  };
@@ -45,6 +48,7 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
45
48
  return {
46
49
  action: input.action,
47
50
  ...normalizeVisibleAssetsState(raw, options, 'extension'),
51
+ lowValueExpanded: raw.lowValueExpanded,
48
52
  proofPath: 'visible-dom-accessibility',
49
53
  };
50
54
  }));
@@ -1,3 +1,4 @@
1
+ // Advances a prepared send through alerts to confirmation and checks that a network fee is shown.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { bodyState, selectors, visible, waitFor } from './send.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Opens Home and selects the all-networks token filter, then checks the label and closed menu.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { openHome } from '../ui/navigate.mjs';
3
4
  import { visible, waitFor } from './send.mjs';
@@ -11,16 +12,23 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
11
12
  await openHome(page, timeoutMs);
12
13
  const filter = dataTestId('sort-by-networks');
13
14
  const all = dataTestId('home-network-filter-all-default');
15
+ const deadline = Date.now() + timeoutMs;
16
+ const remaining = () => Math.max(1, deadline - Date.now());
17
+ const readLabel = () => page.evaluate(`String(document.querySelector(${JSON.stringify(filter)})?.innerText || '')`);
18
+ const alreadySelected = /All (?:default )?networks/iu.test(await readLabel());
19
+ if (await visible(page, all)) {
20
+ await page.waitFor({ selector: all, expected: 'visible', timeoutMs: remaining() });
21
+ await page.click(all);
22
+ } else if (!alreadySelected) {
23
+ await waitFor(page, () => visible(page, filter), 'Network filter did not become visible.', remaining());
24
+ await page.click(filter);
25
+ await page.waitFor({ selector: all, expected: 'visible', timeoutMs: remaining() });
26
+ await page.click(all);
27
+ }
14
28
  const selected = await waitFor(page, async () => {
15
- const label = await page.evaluate(`String(document.querySelector(${JSON.stringify(filter)})?.innerText || '')`);
16
- if (/All (?:default )?networks/iu.test(label)) return label;
17
- if (await visible(page, all)) {
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);
29
+ const label = await readLabel();
30
+ return /All (?:default )?networks/iu.test(label) && !(await visible(page, all)) ? label : null;
31
+ }, 'All networks did not become selected with its menu closed.', remaining());
24
32
  return {
25
33
  action: input.action,
26
34
  scope,
@@ -1,3 +1,5 @@
1
+ // Shares send controls, UI polling, and transaction-state readers.
2
+ // Token and activity lookup helpers mark matching DOM rows for later clicks.
1
3
  import { dataTestId } from '../platform/cdp.mjs';
2
4
 
3
5
  export async function visible(page, selector) {
@@ -1,3 +1,4 @@
1
+ // Saves a custom gas limit in send confirmation, supplying a positive priority fee if needed.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { bodyState, visible, waitFor } from './send.mjs';
3
4
 
@@ -1,8 +1,11 @@
1
+ // Sets a token-management toggle and checks Home visibility, with optional exact ERC-20 identity.
2
+ // Can require a change or an already-matching preference.
1
3
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
4
+ import { verifyHomeIdentity } from './home-token-identity.mjs';
2
5
 
3
6
  const OPTIONS = dataTestId('asset-list-control-bar-action-button');
4
7
  const MANAGE = dataTestId('manageTokens__button');
5
- const PAGE = dataTestId('token-management-page');
8
+ const PAGE = `${dataTestId('parent-selector-token-management-page')}, ${dataTestId('token-management-page')}`;
6
9
  const SEARCH = dataTestId('token-management-search-input');
7
10
  const BACK = dataTestId('token-management-header-back-button');
8
11
  const LOW_VALUE = dataTestId('low-value-assets-toggle');
@@ -27,12 +30,15 @@ async function waitFor(page, predicate, message, timeoutMs) {
27
30
  throw new Error(message);
28
31
  }
29
32
 
30
- async function matchingToggle(page, query) {
33
+ async function matchingToggle(page, query, assetId) {
31
34
  return page.evaluate(`(() => {
32
35
  const query = ${JSON.stringify(query.toLowerCase())};
36
+ const assetId = ${JSON.stringify(assetId)};
33
37
  const candidates = [...document.querySelectorAll('[data-testid^="token-management-cell-search-"][data-testid$="-toggle-control"]')];
38
+ const matches = [];
34
39
  for (const element of candidates) {
35
40
  const testId = element.getAttribute('data-testid');
41
+ if (assetId && testId !== 'token-management-cell-search-' + assetId + '-toggle-control') continue;
36
42
  const row = document.querySelector('[data-testid="' + testId.slice(0, -'-toggle-control'.length) + '"]');
37
43
  const label = String(row?.innerText || element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500);
38
44
  const words = label.toLowerCase().split(/[^a-z0-9]+/u).filter(Boolean);
@@ -40,28 +46,48 @@ async function matchingToggle(page, query) {
40
46
  const style = getComputedStyle(element);
41
47
  const rect = element.getBoundingClientRect();
42
48
  if (style.display === 'none' || style.visibility === 'hidden' || rect.width <= 0 || rect.height <= 0) continue;
43
- return {
49
+ matches.push({
44
50
  testId,
45
51
  label,
46
52
  enabled: Boolean(element.querySelector('.toggle-button--on')),
47
- };
53
+ busy: element.getAttribute('aria-busy') === 'true',
54
+ });
48
55
  }
49
- return null;
56
+ 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');
57
+ return matches[0] || null;
50
58
  })()`);
51
59
  }
52
60
 
53
- async function homeToken(page, query) {
61
+ async function homeToken(page, query, verifyIdentity = false) {
54
62
  return page.evaluate(`(() => {
55
63
  const query = ${JSON.stringify(query.toLowerCase())};
64
+ const verifyIdentity = ${JSON.stringify(verifyIdentity)};
65
+ const matches = [];
56
66
  for (const row of document.querySelectorAll('[data-testid="multichain-token-list-button"]')) {
57
67
  const name = String(row.querySelector('[data-testid="multichain-token-list-item-token-name"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
58
68
  const amount = String(row.querySelector('[data-testid="multichain-token-list-item-value"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
59
69
  const symbol = amount.match(/\\b([A-Za-z][A-Za-z0-9]{1,11})$/u)?.[1] || '';
60
70
  if (symbol.toLowerCase() === query || name.toLowerCase().includes(query)) {
61
- return { name, symbol: symbol.toUpperCase(), amount };
71
+ const rect = row.getBoundingClientRect();
72
+ const style = getComputedStyle(row);
73
+ if (rect.width <= 0 || rect.height <= 0 || style.display === 'none' || style.visibility === 'hidden') continue;
74
+ matches.push({ row, name, symbol: symbol.toUpperCase(), amount });
62
75
  }
63
76
  }
64
- return null;
77
+ if (verifyIdentity && matches.length > 1) throw new Error('Home token query is ambiguous; cannot verify asset identity.');
78
+ const match = matches[0];
79
+ if (!match) return null;
80
+ const { row, ...token } = match;
81
+ if (verifyIdentity) {
82
+ row.scrollIntoView({ block: 'center', inline: 'nearest' });
83
+ const rect = row.getBoundingClientRect();
84
+ const x = rect.left + rect.width / 2;
85
+ const y = rect.top + rect.height / 2;
86
+ const hit = document.elementFromPoint(x, y);
87
+ if (!hit || !row.contains(hit)) return null;
88
+ token.point = { x, y };
89
+ }
90
+ return token;
65
91
  })()`);
66
92
  }
67
93
 
@@ -77,6 +103,31 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
77
103
  const requireUnchanged = input.node?.require_unchanged === true;
78
104
  const requireChange = input.node?.require_change === true;
79
105
  const timeoutMs = Number(input.node?.timeout_ms ?? 45_000);
106
+ const assetId = String(input.node?.asset_id ?? '').trim().toLowerCase();
107
+ if (assetId && !/^eip155:[1-9][0-9]{0,19}\/erc20:0x[0-9a-f]{40}$/u.test(assetId)) {
108
+ throw new Error('asset_id requires an ERC-20 CAIP asset ID.\nNext: mm-harness actions --action metamask.assets.set_token_visibility --json');
109
+ }
110
+ if (assetId && !requested && (!requireChange || requireUnchanged)) {
111
+ 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');
112
+ }
113
+
114
+ let identity;
115
+ if (assetId && !requested) {
116
+ if (await visible(page, LOW_VALUE)) {
117
+ const expanded = await page.evaluate(`document.querySelector(${JSON.stringify(LOW_VALUE)})?.getAttribute('aria-expanded') === 'true'`);
118
+ if (!expanded) await page.click(LOW_VALUE);
119
+ }
120
+ const token = await waitFor(
121
+ page,
122
+ () => homeToken(page, query, true),
123
+ `${query} must be visible on Home before its exact identity can be hidden.`,
124
+ timeoutMs,
125
+ );
126
+ identity = {
127
+ ...await verifyHomeIdentity(page, token, assetId, timeoutMs),
128
+ verifiedAt: 'before-hide',
129
+ };
130
+ }
80
131
 
81
132
  if (!(await visible(page, MANAGE))) await page.click(OPTIONS);
82
133
  await waitFor(page, () => visible(page, MANAGE), 'Manage tokens did not become visible.', timeoutMs);
@@ -85,8 +136,11 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
85
136
  await page.setInput(SEARCH, query);
86
137
  const before = await waitFor(
87
138
  page,
88
- () => matchingToggle(page, query),
89
- `Token management returned no visible result for ${query}.`,
139
+ async () => {
140
+ const state = await matchingToggle(page, query, assetId);
141
+ return state && !state.busy ? state : null;
142
+ },
143
+ `Token management returned no settled visible result for ${query}.`,
90
144
  timeoutMs,
91
145
  );
92
146
  if (requireUnchanged && before.enabled !== requested) {
@@ -101,8 +155,8 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
101
155
  const after = await waitFor(
102
156
  page,
103
157
  async () => {
104
- const state = await matchingToggle(page, query);
105
- return state?.enabled === requested ? state : null;
158
+ const state = await matchingToggle(page, query, assetId);
159
+ return state && !state.busy && state.enabled === requested ? state : null;
106
160
  },
107
161
  `${query} token visibility did not become ${requested ? 'enabled' : 'disabled'}.`,
108
162
  timeoutMs,
@@ -110,14 +164,26 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
110
164
  await page.click(BACK);
111
165
  await waitFor(page, () => visible(page, OPTIONS), 'Token list did not reopen.', timeoutMs);
112
166
 
113
- if (requested && (await visible(page, LOW_VALUE))) {
167
+ if ((requested || assetId) && (await visible(page, LOW_VALUE))) {
114
168
  const expanded = await page.evaluate(`document.querySelector(${JSON.stringify(LOW_VALUE)})?.getAttribute('aria-expanded') === 'true'`);
115
169
  if (!expanded) await page.click(LOW_VALUE);
116
170
  }
117
- const token = requested
118
- ? await waitFor(page, () => homeToken(page, query), `${query} did not appear in the token list.`, timeoutMs)
119
- : await homeToken(page, query);
120
- if (!requested && token) throw new Error(`${query} remained visible after disabling it.`);
171
+ let token = null;
172
+ if (requested) {
173
+ token = await waitFor(page, () => homeToken(page, query, Boolean(assetId)), `${query} did not appear in the token list.`, timeoutMs);
174
+ if (assetId) identity = await verifyHomeIdentity(page, token, assetId, timeoutMs);
175
+ } else if (assetId) {
176
+ await waitFor(
177
+ page,
178
+ async () => !(await homeToken(page, query)),
179
+ `${query} remained visible after disabling it.`,
180
+ timeoutMs,
181
+ );
182
+ identity.homeAbsent = true;
183
+ } else {
184
+ token = await homeToken(page, query);
185
+ if (token) throw new Error(`${query} remained visible after disabling it.`);
186
+ }
121
187
 
122
188
  return {
123
189
  action: input.action,
@@ -126,8 +192,11 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
126
192
  requireUnchanged,
127
193
  requireChange,
128
194
  changed: before.enabled !== after.enabled,
195
+ beforeEnabled: before.enabled,
196
+ afterEnabled: after.enabled,
129
197
  searchResult: after.label,
130
198
  toggleTestId: after.testId,
199
+ ...(identity ? { identity } : {}),
131
200
  token,
132
201
  proofPath: 'trusted-pointer-visible-ui',
133
202
  };
@@ -1,3 +1,4 @@
1
+ // Checks Home token order under alphabetical and declining-balance sorts, then restores the preference.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  const TOGGLE = dataTestId('sort-by-popover-toggle');
@@ -45,9 +46,9 @@ function assertDeclining(rows) {
45
46
  }
46
47
  }
47
48
 
48
- async function choose(page, selector) {
49
+ async function openMenu(page) {
49
50
  const isVisible = async () => page.evaluate(`(() => {
50
- const element = document.querySelector(${JSON.stringify(selector)});
51
+ const element = document.querySelector(${JSON.stringify(ALPHABETICAL)});
51
52
  if (!element) return false;
52
53
  const style = getComputedStyle(element);
53
54
  const rect = element.getBoundingClientRect();
@@ -57,24 +58,71 @@ async function choose(page, selector) {
57
58
  const deadline = Date.now() + 5_000;
58
59
  while (!(await isVisible())) {
59
60
  if (Date.now() >= deadline) throw new Error('Asset sort menu did not become visible.');
60
- await page.evaluate('new Promise((resolve) => setTimeout(resolve, 100))');
61
+ await new Promise(resolve => setTimeout(resolve, 100));
62
+ }
63
+ }
64
+
65
+ async function selectedSort(page) {
66
+ await openMenu(page);
67
+ const selected = await page.evaluate(`(() => ${JSON.stringify([ALPHABETICAL, DECLINING])}
68
+ .filter(selector => document.querySelector(selector)?.classList.contains('selectable-list-item--selected')))()`);
69
+ if (selected.length !== 1) throw new Error('Asset sort preference is unavailable or ambiguous; no sort will be changed.');
70
+ return selected[0];
71
+ }
72
+
73
+ async function choose(page, selector) {
74
+ const current = await selectedSort(page);
75
+ await page.click(current === selector ? TOGGLE : selector);
76
+ await page.waitForExpression(`(() => {
77
+ const element = document.querySelector(${JSON.stringify(ALPHABETICAL)});
78
+ if (!element) return true;
79
+ const rect = element.getBoundingClientRect();
80
+ const style = getComputedStyle(element);
81
+ return rect.width === 0 || rect.height === 0 || style.display === 'none' || style.visibility === 'hidden';
82
+ })()`, { timeoutMs: 5000 });
83
+ }
84
+
85
+ async function waitForRows(page, assertion) {
86
+ const deadline = Date.now() + 5000;
87
+ while (true) {
88
+ const rows = await readRows(page);
89
+ try {
90
+ assertion(rows);
91
+ return rows;
92
+ } catch (error) {
93
+ if (Date.now() >= deadline) throw error;
94
+ }
95
+ await new Promise(resolve => setTimeout(resolve, 100));
61
96
  }
62
- await page.click(selector);
63
- await page.evaluate('new Promise((resolve) => setTimeout(resolve, 500))');
64
97
  }
65
98
 
66
99
  runAdapter(async (input) => withExtensionPage(input, async (page) => {
67
- if (!String(await page.evaluate('location.hash')).startsWith('#/')) {
100
+ if (!['#/', '#', ''].includes(String(await page.evaluate('location.hash')))) {
68
101
  throw new Error('metamask.assets.verify_sorting requires the wallet home token list.');
69
102
  }
70
103
 
71
- await choose(page, ALPHABETICAL);
72
- const alphabetical = await readRows(page);
73
- assertAlphabetical(alphabetical);
74
-
75
- await choose(page, DECLINING);
76
- const declining = await readRows(page);
77
- assertDeclining(declining);
104
+ const initialSort = await selectedSort(page);
105
+ let alphabetical;
106
+ let declining;
107
+ let proofError;
108
+ try {
109
+ await choose(page, ALPHABETICAL);
110
+ alphabetical = await waitForRows(page, assertAlphabetical);
111
+ await choose(page, DECLINING);
112
+ declining = await waitForRows(page, assertDeclining);
113
+ } catch (error) {
114
+ proofError = error;
115
+ }
116
+ try {
117
+ await choose(page, initialSort);
118
+ await openMenu(page);
119
+ await page.waitForExpression(`document.querySelector(${JSON.stringify(initialSort)})?.classList.contains('selectable-list-item--selected') === true`, { timeoutMs: 5000 });
120
+ await page.click(TOGGLE);
121
+ } catch (restoreError) {
122
+ if (proofError) throw new AggregateError([proofError, restoreError], 'Asset sorting proof and preference restoration both failed.');
123
+ throw restoreError;
124
+ }
125
+ if (proofError) throw proofError;
78
126
 
79
127
  return {
80
128
  action: input.action,
@@ -83,7 +131,8 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
83
131
  surface: 'token-list',
84
132
  alphabetical: alphabetical.map((row) => row.name),
85
133
  decliningFiat: declining.map((row) => row.fiat),
86
- restoredSort: 'declining-balance',
134
+ initialSort: initialSort === ALPHABETICAL ? 'alphabetical' : 'declining-balance',
135
+ restoredSort: initialSort === ALPHABETICAL ? 'alphabetical' : 'declining-balance',
87
136
  proofPath: 'trusted-pointer-visible-ui',
88
137
  };
89
138
  }));
@@ -1,3 +1,4 @@
1
+ // Navigates the Extension page to an external deeplink URL and returns the navigation result.
1
2
  import { pathToFileURL } from 'node:url';
2
3
 
3
4
  import {
@@ -1,3 +1,5 @@
1
+ // Adds a missing network through ChainList and selects it in the Home network filter.
2
+ // Can reject existing networks so later cleanup does not delete prior configuration.
1
3
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import {
3
5
  assertNetworkAdditionAllowed,
@@ -1,3 +1,5 @@
1
+ // Adds a missing custom RPC network through the UI and selects it on Home.
2
+ // Can reject existing networks so later cleanup does not delete prior configuration.
1
3
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import {
3
5
  assertNetworkAdditionAllowed,
@@ -37,17 +39,19 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
37
39
  await page.setInput(dataTestId('network-form-chain-id'), chainId);
38
40
  await page.setInput(dataTestId('network-form-ticker-input'), symbol);
39
41
 
40
- await page.evaluate('new Promise((resolve) => setTimeout(resolve, 1000))');
41
- let addRpc;
42
- for (let attempt = 0; attempt < 3 && !addRpc; attempt += 1) {
43
- await page.click(dataTestId('test-add-rpc-drop-down'));
44
- await page.evaluate('new Promise((resolve) => setTimeout(resolve, 250))');
45
- addRpc = await semanticButtonOrNull(page, 'Add RPC URL');
46
- }
47
- if (!addRpc) throw new Error('Visible button "Add RPC URL" was not found after opening the RPC menu.');
42
+ await page.click(dataTestId('test-add-rpc-drop-down'));
43
+ const addRpc = await waitFor(
44
+ page,
45
+ () => semanticButtonOrNull(page, 'Add RPC URL'),
46
+ 'Add RPC URL did not become visible after opening the menu.',
47
+ timeoutMs,
48
+ );
49
+ await page.waitFor({ selector: addRpc, expected: 'visible', timeoutMs });
48
50
  await page.click(addRpc);
49
51
  await page.setInput(dataTestId('rpc-url-input-test'), rpcUrl);
50
- await page.click(dataTestId('page-container-footer-next'));
52
+ const rpcSubmit = `${dataTestId('page-container-footer-next')}:not(:disabled):not([aria-disabled="true"])`;
53
+ await page.waitFor({ selector: rpcSubmit, expected: 'visible', timeoutMs });
54
+ await page.click(rpcSubmit);
51
55
  await waitFor(
52
56
  page,
53
57
  () => page.evaluate(`!document.querySelector(${JSON.stringify(dataTestId('rpc-url-input-test'))})`),
@@ -56,14 +60,14 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
56
60
  );
57
61
 
58
62
  if (explorerUrl) {
59
- await page.evaluate('new Promise((resolve) => setTimeout(resolve, 1000))');
60
- let addExplorer;
61
- for (let attempt = 0; attempt < 3 && !addExplorer; attempt += 1) {
62
- await page.click(dataTestId('test-explorer-drop-down'));
63
- await page.evaluate('new Promise((resolve) => setTimeout(resolve, 250))');
64
- addExplorer = await semanticButtonOrNull(page, 'Add a block explorer URL');
65
- }
66
- if (!addExplorer) throw new Error('Visible button "Add a block explorer URL" was not found after opening the explorer menu.');
63
+ await page.click(dataTestId('test-explorer-drop-down'));
64
+ const addExplorer = await waitFor(
65
+ page,
66
+ () => semanticButtonOrNull(page, 'Add a block explorer URL'),
67
+ 'Add a block explorer URL did not become visible after opening the menu.',
68
+ timeoutMs,
69
+ );
70
+ await page.waitFor({ selector: addExplorer, expected: 'visible', timeoutMs });
67
71
  await page.click(addExplorer);
68
72
  await page.setInput(dataTestId('explorer-url-input'), explorerUrl);
69
73
  await page.click(await semanticButton(page, 'Add URL'));
@@ -1,3 +1,5 @@
1
+ // Shares network-manager navigation, chain/name matching, and Home network selection.
2
+ // Button lookup marks DOM targets; network additions can reject existing configuration.
1
3
  import { dataTestId } from '../platform/cdp.mjs';
2
4
 
3
5
  async function visible(page, selector) {
@@ -90,7 +92,7 @@ export async function openNetworkManager(page, timeoutMs) {
90
92
  const networks = dataTestId('global-menu-networks');
91
93
  const deadline = Date.now() + timeoutMs;
92
94
  await clickUncovered(page, menu, deadline);
93
- await waitFor(page, () => visible(page, networks), 'Networks did not become visible in the account menu.', timeoutMs);
95
+ await page.waitFor({ selector: networks, expected: 'visible', timeoutMs: Math.max(1, deadline - Date.now()) });
94
96
  await page.click(networks);
95
97
  await waitFor(
96
98
  page,
@@ -115,7 +117,9 @@ export async function findNetworkItem(page, network, chainId) {
115
117
  const label = String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500);
116
118
  const nameMatches = [...element.querySelectorAll('[data-testid]')]
117
119
  .some((child) => String(child.getAttribute('data-testid') || '').toLowerCase() === network);
118
- if (chainIds.has(suffix) && nameMatches) return { testId, label, suffix };
120
+ if (!chainIds.has(suffix)) continue;
121
+ 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');
122
+ return { testId, label, suffix };
119
123
  }
120
124
  return null;
121
125
  })()`);
@@ -128,6 +132,7 @@ export function assertNetworkAdditionAllowed(item, requireAbsent, network, chain
128
132
  }
129
133
 
130
134
  export async function selectHomeNetwork(page, network, chainId, timeoutMs) {
135
+ const deadline = Date.now() + timeoutMs;
131
136
  const sort = dataTestId('sort-by-networks');
132
137
  const chainHex = `0x${chainId.toString(16)}`;
133
138
  const choices = [
@@ -135,13 +140,15 @@ export async function selectHomeNetwork(page, network, chainId, timeoutMs) {
135
140
  dataTestId(`home-network-filter-custom-${chainHex}`),
136
141
  dataTestId(`home-network-filter-additional-${chainHex}`),
137
142
  ];
138
- await page.click(sort);
143
+ if (!(await visible(page, dataTestId('home-network-filter-all-default')))) await page.click(sort);
139
144
  const choice = await waitFor(page, async () => {
140
145
  for (const candidate of choices) {
141
146
  if (await visible(page, candidate)) return candidate;
142
147
  }
143
148
  return null;
144
149
  }, `${network} did not appear in the home network filter.`, timeoutMs);
150
+ await page.scroll({ selector: choice, intoView: true });
151
+ await page.waitFor({ selector: choice, expected: 'visible', timeoutMs: Math.max(1, deadline - Date.now()) });
145
152
  await page.click(choice);
146
153
  await waitFor(page, async () => {
147
154
  const label = await page.evaluate(`String(document.querySelector(${JSON.stringify(sort)})?.innerText || '')`);
@@ -1,3 +1,4 @@
1
+ // Samples the displayed network, token rows, and errors, with an optional settling interval.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import {
3
4
  normalizeVisibleNetworkState,
@@ -40,6 +41,7 @@ async function capture(page, maximum) {
40
41
  runAdapter(async (input) => withExtensionPage(input, async (page) => {
41
42
  const options = resolveVisibleNetworkOptions(input.node);
42
43
  const before = await capture(page, options.assets.maxItems);
44
+ normalizeVisibleNetworkState(before, before, options, 'extension');
43
45
  if (options.settleMs > 0) await sleep(options.settleMs);
44
46
  const after = options.settleMs > 0 ? await capture(page, options.assets.maxItems) : before;
45
47
  return {
@@ -1,3 +1,4 @@
1
+ // Switches Home to Ethereum, deletes the named custom network, and checks its absence in the manager.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import {
3
4
  findNetworkItem,