@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,5 @@
1
+ // Opens named wallet screens through UI controls or history, and accepts direct URL/hash navigation.
2
+ // Named navigation may dismiss tutorials or drawers and add a missing network before selecting its filter.
1
3
  import { pathToFileURL } from 'node:url';
2
4
 
3
5
  import {
@@ -8,6 +10,7 @@ import {
8
10
  } from '../platform/cdp.mjs';
9
11
 
10
12
  const BACK_CONTROLS = [
13
+ 'custom-token-import-close-button',
11
14
  'perps-order-entry-back-button',
12
15
  'perps-market-detail-back-button',
13
16
  'perps-activity-back-button',
@@ -23,6 +26,18 @@ const BACK_CONTROLS = [
23
26
  ].map(dataTestId).concat('button[aria-label="Back"]');
24
27
  const UI_POLL_MS = 200;
25
28
  const PERPS_HOME_SETTLE_MS = 1000;
29
+ const PERPS_MARKET_LIST = [
30
+ 'market-list-view',
31
+ 'parent-selector-perps-market-list',
32
+ ].map(dataTestId).join(',');
33
+ const PERPS_MARKET_DETAIL = [
34
+ 'perps-market-detail-page',
35
+ 'parent-selector-perps-market-detail',
36
+ ].map(dataTestId).join(',');
37
+ const PERPS_ACTIVITY = [
38
+ 'perps-activity-page',
39
+ 'parent-selector-perps-activity',
40
+ ].map(dataTestId).join(',');
26
41
 
27
42
  function text(value) {
28
43
  return typeof value === 'string' && value.length > 0 ? value : undefined;
@@ -68,6 +83,7 @@ async function hasVisibleSelector(page, selector) {
68
83
  return page.evaluate(`(() => {
69
84
  const element = document.querySelector(${JSON.stringify(selector)});
70
85
  if (!element) return false;
86
+ if (element.closest('dialog')?.open === false) return false;
71
87
  const style = getComputedStyle(element);
72
88
  const rect = element.getBoundingClientRect();
73
89
  return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
@@ -116,6 +132,13 @@ export async function openHome(page, timeoutMs) {
116
132
  let pendingActivation;
117
133
 
118
134
  while (Date.now() < deadline) {
135
+ const importNetworkClose = '[data-testid="custom-token-import-network-selector"] button[aria-label="Close"]';
136
+ if (await hasVisibleSelector(page, importNetworkClose)) {
137
+ await activateSemanticControl(page, importNetworkClose);
138
+ steps.push(importNetworkClose);
139
+ await pauseForUi(page);
140
+ continue;
141
+ }
119
142
  if (await hasVisibleSelector(page, drawerClose)) {
120
143
  await activateSemanticControl(page, drawerClose);
121
144
  steps.push(drawerClose);
@@ -274,11 +297,11 @@ export async function openPerpsHome(page, timeoutMs) {
274
297
  export async function openPerpsMarket(page, market, timeoutMs) {
275
298
  const deadline = Date.now() + timeoutMs;
276
299
  const normalizedMarket = normalizeMarketSymbol(market.trim());
277
- if (await hasVisibleSelector(page, dataTestId('market-list-view'))) {
300
+ if (await hasVisibleSelector(page, PERPS_MARKET_LIST)) {
278
301
  const marketRow = dataTestId(`market-row-${normalizedMarket.replace(':', '-')}`);
279
302
  await page.waitForSelector(marketRow, { timeoutMs: Math.max(1, deadline - Date.now()) });
280
303
  const navigation = await activateSemanticControl(page, marketRow);
281
- await page.waitForSelector(dataTestId('perps-market-detail-page'), {
304
+ await page.waitForSelector(PERPS_MARKET_DETAIL, {
282
305
  timeoutMs: Math.max(1, deadline - Date.now()),
283
306
  });
284
307
  return {
@@ -308,7 +331,7 @@ export async function openPerpsMarket(page, market, timeoutMs) {
308
331
  );
309
332
  }
310
333
  const navigation = await activateSemanticControl(page, marketControl);
311
- await page.waitForSelector(dataTestId('perps-market-detail-page'), {
334
+ await page.waitForSelector(PERPS_MARKET_DETAIL, {
312
335
  timeoutMs: Math.max(1, deadline - Date.now()),
313
336
  });
314
337
  return {
@@ -321,7 +344,7 @@ export async function openPerpsMarket(page, market, timeoutMs) {
321
344
  }
322
345
 
323
346
  export async function openPerpsActivity(page, timeoutMs) {
324
- if (await hasVisibleSelector(page, dataTestId('perps-activity-page'))) {
347
+ if (await hasVisibleSelector(page, PERPS_ACTIVITY)) {
325
348
  return { method: 'visible-ui', href: await currentHref(page), steps: [] };
326
349
  }
327
350
  const deadline = Date.now() + timeoutMs;
@@ -329,7 +352,7 @@ export async function openPerpsActivity(page, timeoutMs) {
329
352
  const activity = dataTestId('perps-recent-activity-see-all');
330
353
  await page.waitForSelector(activity, { timeoutMs: Math.max(1, deadline - Date.now()) });
331
354
  const navigation = await activateSemanticControl(page, activity);
332
- await page.waitForSelector(dataTestId('perps-activity-page'), {
355
+ await page.waitForSelector(PERPS_ACTIVITY, {
333
356
  timeoutMs: Math.max(1, deadline - Date.now()),
334
357
  });
335
358
  return {
@@ -342,7 +365,7 @@ export async function openPerpsActivity(page, timeoutMs) {
342
365
  }
343
366
 
344
367
  export async function openPerpsMarketList(page, timeoutMs) {
345
- const marketList = dataTestId('market-list-view');
368
+ const marketList = PERPS_MARKET_LIST;
346
369
  if (await hasVisibleSelector(page, marketList)) {
347
370
  return { method: 'visible-ui', href: await currentHref(page), steps: [] };
348
371
  }
@@ -417,17 +440,37 @@ export async function openNetwork(page, network, chainId, addIfMissing, timeoutM
417
440
  throw new Error('Token assets did not become selected before network navigation.');
418
441
  }
419
442
  }
420
- await activateSemanticControl(page, sort);
421
- steps.push(sort);
443
+ if (!(await hasVisibleSelector(page, dataTestId('home-network-filter-all-default')))) {
444
+ await activateSemanticControl(page, sort);
445
+ steps.push(sort);
446
+ }
422
447
  let choice = await waitForFirstVisible(page, [installed, custom, additional, manage], deadline);
423
448
  if (!choice) throw new Error(`Network filter did not show ${network} (${chainId}) before timeout.`);
424
- if (choice !== manage) {
449
+ if (!addIfMissing && (choice === additional || choice === manage)) {
450
+ throw new Error(`Network ${network} (${chainId}) is not added to the wallet.`);
451
+ }
452
+ let added = false;
453
+ if (choice === additional) {
454
+ await page.scroll({ selector: additional, intoView: true });
455
+ await page.waitFor({ selector: additional, expected: 'visible', timeoutMs: Math.max(1, deadline - Date.now()) });
456
+ await activateSemanticControl(page, additional);
457
+ steps.push(additional);
458
+ await page.waitFor({ selector: additional, expected: 'hidden', timeoutMs: Math.max(1, deadline - Date.now()) });
459
+ await activateSemanticControl(page, sort);
460
+ steps.push(sort);
461
+ choice = await waitForFirstVisible(page, [installed, custom], deadline);
462
+ if (!choice) throw new Error(`Added network ${network} (${chainId}) did not appear in the wallet filter.`);
463
+ await page.scroll({ selector: choice, intoView: true });
464
+ await page.waitFor({ selector: choice, expected: 'visible', timeoutMs: Math.max(1, deadline - Date.now()) });
465
+ await activateSemanticControl(page, choice);
466
+ steps.push(choice);
467
+ added = true;
468
+ } else if (choice !== manage) {
469
+ await page.scroll({ selector: choice, intoView: true });
470
+ await page.waitFor({ selector: choice, expected: 'visible', timeoutMs: Math.max(1, deadline - Date.now()) });
425
471
  await activateSemanticControl(page, choice);
426
472
  steps.push(choice);
427
473
  } else {
428
- if (!addIfMissing) {
429
- throw new Error(`Network ${network} (${chainId}) is not added to the wallet.`);
430
- }
431
474
  await activateSemanticControl(page, manage);
432
475
  steps.push(manage);
433
476
  const listItem = dataTestId(`network-list-item-eip155:${chainId}`);
@@ -437,6 +480,7 @@ export async function openNetwork(page, network, chainId, addIfMissing, timeoutM
437
480
  await activateSemanticControl(page, popular);
438
481
  steps.push(popular);
439
482
  await page.waitForSelector(listItem, { timeoutMs: Math.max(1, deadline - Date.now()) });
483
+ added = true;
440
484
  }
441
485
  const back = dataTestId('page-header-back-button');
442
486
  await activateSemanticControl(page, back);
@@ -444,15 +488,17 @@ export async function openNetwork(page, network, chainId, addIfMissing, timeoutM
444
488
  await page.waitForSelector(sort, { timeoutMs: Math.max(1, deadline - Date.now()) });
445
489
  await activateSemanticControl(page, sort);
446
490
  steps.push(sort);
447
- choice = await waitForFirstVisible(page, [installed, custom, additional], deadline);
491
+ choice = await waitForFirstVisible(page, [installed, custom], deadline);
448
492
  if (!choice) throw new Error(`Added network ${network} (${chainId}) did not appear in the wallet filter.`);
493
+ await page.scroll({ selector: choice, intoView: true });
494
+ await page.waitFor({ selector: choice, expected: 'visible', timeoutMs: Math.max(1, deadline - Date.now()) });
449
495
  await activateSemanticControl(page, choice);
450
496
  steps.push(choice);
451
497
  }
452
498
  while (Date.now() < deadline) {
453
499
  const label = await page.evaluate(`String(document.querySelector(${JSON.stringify(sort)})?.innerText || '')`);
454
500
  if (label.toLowerCase().includes(network.toLowerCase())) {
455
- return { method: 'visible-ui', href: await currentHref(page), network, chainId, steps };
501
+ return { method: 'visible-ui', href: await currentHref(page), network, chainId, added, steps };
456
502
  }
457
503
  await pauseForUi(page);
458
504
  }
@@ -1,3 +1,5 @@
1
+ // Unlocks an existing wallet with the supplied or fixture password when a password input is present.
2
+ // Rejects onboarding profiles and waits for the password input to disappear after submission.
1
3
  import { readFile } from 'node:fs/promises';
2
4
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
3
5
  import { walletFixturePath } from '../../harness-exports.mjs';
@@ -1,3 +1,5 @@
1
+ // Imports a wallet through onboarding or verifies an existing profile against the expected account.
2
+ // May unlock the wallet, finish onboarding, and select the expected account through visible UI.
1
3
  import { resolveWalletImportCredentials, validateWalletImportOptions, walletImportMethod } from '../../shared/wallet/import-source.mjs';
2
4
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
3
5
  import { pasteSecretRecoveryPhrase, setSecretInput } from './secret-input.mjs';
@@ -1,3 +1,4 @@
1
+ // Lists account identities from Extension state hooks, filtered by selected, named, EVM, or all scope.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { listWalletAccountsExpression } from './state.mjs';
3
4
 
@@ -1,3 +1,5 @@
1
+ // Locks the wallet through visible menu controls and waits for the unlock page.
2
+ // Opens Home first when the account menu is unavailable.
1
3
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
 
3
5
  const MENU = dataTestId('account-options-menu-button');
@@ -21,6 +23,7 @@ async function clickUncovered(page, selector, deadline) {
21
23
  point = await page.evaluate(`(() => {
22
24
  const element = document.querySelector(${JSON.stringify(selector)});
23
25
  if (!element) return null;
26
+ element.scrollIntoView({ block: 'center', inline: 'center' });
24
27
  const style = getComputedStyle(element);
25
28
  const rect = element.getBoundingClientRect();
26
29
  if (style.display === 'none' || style.visibility === 'hidden' || rect.width <= 0 || rect.height <= 0) return null;
@@ -74,7 +77,7 @@ export async function lockWallet(page, timeoutMs = 15_000) {
74
77
  if (Date.now() >= deadline) throw new Error('Wallet lock control did not become visible.');
75
78
  await page.evaluate('new Promise((resolve) => setTimeout(resolve, 100))');
76
79
  }
77
- await page.click(LOCK);
80
+ await clickUncovered(page, LOCK, deadline);
78
81
  while (!(await visible(page, UNLOCK_PAGE))) {
79
82
  if (Date.now() >= deadline) throw new Error('Wallet did not reach the unlock page after locking.');
80
83
  await page.evaluate('new Promise((resolve) => setTimeout(resolve, 100))');
@@ -1,3 +1,4 @@
1
+ // Reads wallet identity, onboarding, and network state from state hooks alongside the password-input marker.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { readWalletStateExpression } from './state.mjs';
3
4
 
@@ -1,3 +1,5 @@
1
+ // Clears the wallet through the visible forgot-password reset flow unless it is already fresh.
2
+ // Checks that onboarding is incomplete, the route is onboarding, and no account is selected.
1
3
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import { readWalletStateExpression } from './state.mjs';
3
5
 
@@ -1,3 +1,5 @@
1
+ // Enters wallet secrets through CDP input events and checks their retained length.
2
+ // Recovery-phrase paste reloads the page to install a temporary clipboard bridge, then removes it.
1
3
  export async function setSecretInput(page, selector, value) {
2
4
  const found = await page.evaluate(`(() => {
3
5
  const element = document.querySelector(${JSON.stringify(selector)});
@@ -1,3 +1,5 @@
1
+ // Selects an account by address, ID, or name through the background request hook.
2
+ // Falls back to visible account search when that hook is unavailable and checks the selected address.
1
3
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import {
3
5
  listWalletAccountsExpression,
@@ -1,3 +1,4 @@
1
+ // Checks for a seeded wallet profile via a password input or completed onboarding with a selected account.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { readWalletStateExpression } from './state.mjs';
3
4
 
@@ -1,3 +1,5 @@
1
+ // Builds page expressions for reading wallet/account state and changing the selected account through state hooks.
2
+ // Account selection sends a background request and polls until the selected ID matches.
1
3
  const walletStatePreamble = `
2
4
  const readWalletState = async () => {
3
5
  const hooks = globalThis.stateHooks || {};
@@ -1,3 +1,5 @@
1
+ // Dismisses onboarding completion and navigates the account list to select and check an expected EVM account.
2
+ // Validates address search with an alternate-address control and returns a hash of the matched identity.
1
3
  import { createHash } from 'node:crypto';
2
4
 
3
5
  const ACCOUNT_SEARCH = 'input[data-testid="multichain-account-list-search"], [data-testid="multichain-account-list-search"] input';
@@ -1,3 +1,4 @@
1
+ // Set analytics and marketing consent through Settings switch handlers, then verify stored consent.
1
2
  import { evalAsync, navigate } from '../platform/bridge.mjs';
2
3
 
3
4
  const SWITCH_IDS = {
@@ -1,3 +1,4 @@
1
+ // Apply requested Mobile analytics consent and return the verified Settings result.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { consentParams } from '../../shared/analytics/consent.mjs';
3
4
  import { setMobileConsent } from './consent-settings.mjs';
@@ -1,3 +1,5 @@
1
+ // Toggle airplane mode on the selected Android device and verify the requested state.
2
+ // Foreground MetaMask if needed and wait for the configured settling period.
1
3
  import { execFile } from 'node:child_process';
2
4
  import { promisify } from 'node:util';
3
5
 
@@ -1,3 +1,4 @@
1
+ // Run the Android network-state action, which toggles airplane mode and ensures app focus.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { setMobileNetworkState } from './network-control.mjs';
3
4
 
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // Expose shared network-capture assertions through the Mobile action runner.
2
3
  import { pathToFileURL } from 'node:url';
3
4
 
4
5
  import { assertNetwork } from '../../shared/app/network-assert.mjs';
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // Start or end filtered Mobile network capture through the bridge and save the summary artifact.
2
3
  import { mkdir, writeFile } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
5
  import { pathToFileURL } from 'node:url';
@@ -1,3 +1,5 @@
1
+ // Import an ERC-20 token through the native form after checking its resolved metadata.
2
+ // Confirm the imported symbol appears in the token list.
1
3
  import { runAdapter } from '../platform/bridge.mjs';
2
4
  import {
3
5
  closeNativeSession,
@@ -168,6 +170,7 @@ runAdapter(async (input) => {
168
170
  const token = await findTokenRow(session, expectedSymbol, timeoutMs);
169
171
  return {
170
172
  action: input.action,
173
+ changed: true,
171
174
  network,
172
175
  chainId,
173
176
  address,
@@ -1,3 +1,4 @@
1
+ // Open a token's native details view and wait for the requested symbol to appear.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import {
3
4
  closeNativeSession,
@@ -1,3 +1,4 @@
1
+ // Check visible token details for a formatted price and optional chart or market cap requirements.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import {
3
4
  closeNativeSession,
@@ -1,3 +1,4 @@
1
+ // Normalize token rows from Mobile accessibility observations into visible asset state.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { observeNativeUi } from '../platform/observe-ui.mjs';
3
4
  import {
@@ -17,7 +18,7 @@ runAdapter(async (input) => {
17
18
  const warning = observed.warnings?.map((entry) => entry.message).filter(Boolean).join('; ');
18
19
  throw new Error(`Mobile visible Assets state is unavailable${warning ? `: ${warning}` : '.'}`);
19
20
  }
20
- const items = [...(visible.items ?? []), ...(visible.hidden_or_offscreen ?? [])]
21
+ const items = (visible.items ?? [])
21
22
  .filter((item) => String(item.test_id ?? '').startsWith('asset-'))
22
23
  .map((item) => ({
23
24
  testId: item.test_id,
@@ -32,6 +33,8 @@ runAdapter(async (input) => {
32
33
  items,
33
34
  truncated: visible.truncated,
34
35
  }, options, 'mobile'),
36
+ observationProvider: visible.provider ?? screen.provider,
37
+ deviceId: screen.deviceId,
35
38
  proofPath: 'native-accessibility',
36
39
  };
37
40
  });
@@ -1,3 +1,5 @@
1
+ // Import or remove a token through native controls and check its resulting visibility state.
2
+ // Reject ambiguous symbol matches and unsupported exact asset IDs.
1
3
  import { runAdapter } from '../platform/bridge.mjs';
2
4
  import {
3
5
  closeNativeSession,
@@ -25,25 +27,24 @@ function descendants(snapshot, root) {
25
27
 
26
28
  function searchedToken(snapshot, query) {
27
29
  const normalized = query.toUpperCase();
28
- const rows = snapshot.nodes.filter((node) => node.identifier === 'searched-token-result');
30
+ const rows = snapshot.nodes.filter((node) => node.identifier === 'searched-token-result' && node.visibleToUser !== false);
31
+ const matches = [];
29
32
  for (const row of rows) {
30
33
  const children = descendants(snapshot, row);
31
- const label = children.map(nativeLabel).find((value) => {
34
+ const label = [nativeLabel(row), ...children.map(nativeLabel)].find((value) => {
32
35
  const parts = value.split(',').map((part) => part.trim().toUpperCase());
33
36
  return parts.includes(normalized);
34
37
  });
35
38
  if (label) {
36
- return {
39
+ matches.push({
37
40
  row,
38
41
  label,
39
42
  imported: row.enabled === false && children.some((node) => /CheckBox$/u.test(String(node.type))),
40
- };
43
+ });
41
44
  }
42
45
  }
43
- const imported = rows.find((row) =>
44
- row.enabled === false && descendants(snapshot, row).some((node) => /CheckBox$/u.test(String(node.type)))
45
- );
46
- return imported ? { row: imported, label: query, imported: true } : null;
46
+ if (matches.length > 1) throw new Error(`Mobile token search matched ${matches.length} rows for ${query}; token identity is ambiguous.\nNext: mm-harness actions --action metamask.assets.set_token_visibility --json`);
47
+ return matches[0] ?? null;
47
48
  }
48
49
 
49
50
  function visibleNode(snapshot, identifier) {
@@ -201,7 +202,9 @@ async function findVisibleAsset(session, query, timeoutMs) {
201
202
  )).snapshot;
202
203
  }
203
204
  for (let attempt = 0; attempt < 8; attempt += 1) {
204
- if (nativeNode(snapshot, `asset-${query.toUpperCase()}`)) return true;
205
+ const matches = snapshot.nodes.filter(node => node.identifier === `asset-${query.toUpperCase()}` && node.visibleToUser !== false);
206
+ if (matches.length > 1) throw new Error(`Mobile token list matched ${matches.length} rows for ${query}; removal is ambiguous.\nNext: mm-harness actions --action metamask.assets.set_token_visibility --json`);
207
+ if (matches.length === 1) return true;
205
208
  await session.execute('ui.scroll', { direction: 'down', timeout_ms: timeoutMs, settle: false });
206
209
  snapshot = await session.snapshot();
207
210
  }
@@ -271,6 +274,9 @@ async function disableToken(session, query, requireUnchanged, requireChange, tim
271
274
  }
272
275
 
273
276
  runAdapter(async (input) => {
277
+ if (String(input.node?.asset_id ?? '').trim()) {
278
+ throw new Error('Exact asset-ID corroboration is not supported on Mobile.\nNext: mm-harness actions --adapter mobile --action metamask.assets.set_token_visibility --json');
279
+ }
274
280
  const query = String(input.node?.query ?? '').trim();
275
281
  const timeoutMs = Number(input.node?.timeout_ms ?? 45_000);
276
282
  const requested = input.node?.visible;
@@ -1,3 +1,5 @@
1
+ // Switch token sorting to check alphabetical and declining fiat order in visible rows.
2
+ // Restore the original sort preference and report restoration failures.
1
3
  import { runAdapter } from '../platform/bridge.mjs';
2
4
  import {
3
5
  closeNativeSession,
@@ -31,7 +33,8 @@ function assetRows(snapshot) {
31
33
 
32
34
  function isAlphabetical(rows) {
33
35
  const names = rows.map((row) => row.name);
34
- const expected = [...names].sort((left, right) => left.localeCompare(right, 'en', { sensitivity: 'base' }));
36
+ // MetaMask's SES lockdown compares names by UTF-16 code units.
37
+ const expected = [...names].sort();
35
38
  return names.length >= 2 && names.every((name, index) => name === expected[index]);
36
39
  }
37
40
 
@@ -40,18 +43,78 @@ function isDeclining(rows) {
40
43
  return values.length >= 2 && values.every((value, index) => index === 0 || values[index - 1] >= value);
41
44
  }
42
45
 
43
- async function choose(session, option, timeoutMs) {
46
+ async function openSortMenu(session, timeoutMs) {
47
+ if (sessionNode(await session.snapshot(), 'sort-alphabetical')) return;
44
48
  await session.execute('ui.press', { test_id: 'token-sort-button', timeout_ms: timeoutMs, settle: false });
49
+ }
50
+
51
+ async function selectedSort(session, timeoutMs) {
52
+ await openSortMenu(session, timeoutMs);
53
+ const deadline = Date.now() + timeoutMs;
54
+ do {
55
+ const snapshot = await session.snapshot();
56
+ const byIndex = new Map(snapshot.nodes.map((node) => [node.index, node]));
57
+ const selected = ['sort-alphabetical', 'sort-declining-balance'].filter((option) => {
58
+ const row = sessionNode(snapshot, option);
59
+ if (!row) return false;
60
+ // ListItemSelect renders a checkbox child only for the selected option.
61
+ return snapshot.nodes.some((node) => {
62
+ if (node.visibleToUser === false || !/checkbox$/iu.test(String(node.type))) return false;
63
+ let parent = byIndex.get(node.parentIndex);
64
+ for (let depth = 0; depth < 40 && parent; depth += 1) {
65
+ if (parent.index === row.index) return true;
66
+ parent = byIndex.get(parent.parentIndex);
67
+ }
68
+ return false;
69
+ });
70
+ });
71
+ if (selected.length === 1) return selected[0];
72
+ if (Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 200));
73
+ } while (Date.now() < deadline);
74
+ throw new Error('Asset sort preference is unavailable or ambiguous.\nNext: mm-harness call ui.screenshot --adapter mobile');
75
+ }
76
+
77
+ async function choose(session, option, timeoutMs) {
78
+ await openSortMenu(session, timeoutMs);
45
79
  await session.execute('ui.press', { test_id: option, timeout_ms: timeoutMs, settle: false });
46
80
  const deadline = Date.now() + timeoutMs;
47
81
  let rows = [];
48
82
  while (Date.now() <= deadline) {
49
83
  const snapshot = await session.snapshot();
50
84
  rows = assetRows(snapshot);
51
- if (rows.length >= 2 && !sessionNode(snapshot, option)) return rows;
85
+ const ordered = option === 'sort-alphabetical' ? isAlphabetical(rows) : isDeclining(rows);
86
+ if (ordered && !sessionNode(snapshot, option)) return rows;
52
87
  await new Promise((resolve) => setTimeout(resolve, 200));
53
88
  }
54
- throw new Error(`Asset sort ${option} did not return to a visible token list: ${JSON.stringify(rows)}.`);
89
+ throw new Error(`Asset sort ${option} did not produce the expected visible row order: ${JSON.stringify(rows)}.`);
90
+ }
91
+
92
+ async function visibleTokenList(session, deadline) {
93
+ while (Date.now() <= deadline) {
94
+ const snapshot = await session.snapshot();
95
+ if (!sessionNode(snapshot, 'sort-alphabetical')
96
+ && sessionNode(snapshot, 'token-sort-button') && assetRows(snapshot).length >= 2) return;
97
+ await new Promise((resolve) => setTimeout(resolve, 200));
98
+ }
99
+ throw new Error('Asset token list was not visible after restoring the sort preference.\nNext: mm-harness call ui.screenshot --adapter mobile');
100
+ }
101
+
102
+ async function restoreSort(session, option, timeoutMs) {
103
+ const deadline = Date.now() + timeoutMs;
104
+ const remaining = () => {
105
+ const ms = deadline - Date.now();
106
+ if (ms <= 0) throw new Error('Asset sort restoration timed out.\nNext: mm-harness call ui.screenshot --adapter mobile');
107
+ return ms;
108
+ };
109
+ if (await selectedSort(session, remaining()) !== option) {
110
+ await session.execute('ui.press', { test_id: option, timeout_ms: remaining(), settle: false });
111
+ await visibleTokenList(session, deadline);
112
+ if (await selectedSort(session, remaining()) !== option) {
113
+ throw new Error('Asset sort preference was not restored.\nNext: mm-harness call ui.screenshot --adapter mobile');
114
+ }
115
+ }
116
+ await session.execute('ui.press', { test_id: option, timeout_ms: remaining(), settle: false });
117
+ await visibleTokenList(session, deadline);
55
118
  }
56
119
 
57
120
  async function revealTokenList(session, timeoutMs) {
@@ -76,9 +139,13 @@ function sessionNode(snapshot, identifier) {
76
139
  runAdapter(async (input) => {
77
140
  const timeoutMs = Number(input.node?.timeout_ms ?? 30_000);
78
141
  const session = createNativeSession(input, 'Assets', `sort-${process.pid}`);
142
+ const errors = [];
143
+ let initialSort;
144
+ let result;
79
145
  try {
80
146
  await session.open();
81
147
  await revealTokenList(session, timeoutMs);
148
+ initialSort = await selectedSort(session, timeoutMs);
82
149
  const alphabetical = await choose(session, 'sort-alphabetical', timeoutMs);
83
150
  const declining = await choose(session, 'sort-declining-balance', timeoutMs);
84
151
  const failures = [];
@@ -91,14 +158,30 @@ runAdapter(async (input) => {
91
158
  if (failures.length > 0) {
92
159
  throw new Error(`Asset token sorting failed: ${failures.join('; ')}.`);
93
160
  }
94
- return {
161
+ result = {
95
162
  action: input.action,
96
163
  alphabetical: alphabetical.map((row) => row.name),
97
164
  decliningFiat: declining.map((row) => row.fiat),
98
- restoredSort: 'declining-balance',
165
+ initialSort: initialSort.slice('sort-'.length),
166
+ restoredSort: initialSort.slice('sort-'.length),
99
167
  proofPath: 'native-accessibility-token-sorting',
100
168
  };
101
- } finally {
169
+ } catch (error) {
170
+ errors.push(error);
171
+ }
172
+ if (initialSort) {
173
+ try {
174
+ await restoreSort(session, initialSort, timeoutMs);
175
+ } catch (error) {
176
+ errors.push(error);
177
+ }
178
+ }
179
+ try {
102
180
  await closeNativeSession(session);
181
+ } catch (error) {
182
+ errors.push(error);
103
183
  }
184
+ if (errors.length === 1) throw errors[0];
185
+ if (errors.length > 1) throw new AggregateError(errors, `Asset sorting failed: ${errors.map((error) => error.message).join('; ')}`);
186
+ return result;
104
187
  });
@@ -1,3 +1,4 @@
1
+ // Open a validated external deeplink through the selected device's OS URL handler.
1
2
  import {
2
3
  externalDeeplinkTimeout,
3
4
  externalDeeplinkUrl,
@@ -1,3 +1,4 @@
1
+ // Add a missing custom network through native Settings and verify its saved row.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import {
3
4
  closeNativeSession,
@@ -1,3 +1,4 @@
1
+ // Navigate native Network Settings and search for network rows by chain ID and name.
1
2
  import { nativeLabel, nativeNode } from '../platform/native-session.mjs';
2
3
 
3
4
  const IDS = {
@@ -26,6 +27,10 @@ export async function waitForSnapshot(session, timeoutMs, predicate, message) {
26
27
  const deadline = Date.now() + timeoutMs;
27
28
  while (Date.now() <= deadline) {
28
29
  const snapshot = await session.snapshot();
30
+ const labels = snapshot.nodes.map(nativeLabel);
31
+ if (labels.includes('Reload') && labels.includes('Go home') && labels.includes('Tools button')) {
32
+ throw new Error('Mobile network navigation opened the Expo developer menu instead of the requested control.\nNext: mm-harness call ui.key_press key=BACK --adapter mobile');
33
+ }
29
34
  const value = predicate(snapshot);
30
35
  if (value) return { snapshot, value };
31
36
  await new Promise((resolve) => setTimeout(resolve, 200));
@@ -42,23 +47,26 @@ async function systemBack(session) {
42
47
  }
43
48
 
44
49
  export async function openNetworkManager(session, timeoutMs) {
45
- for (let attempt = 0; attempt < 8; attempt += 1) {
46
- const snapshot = await session.snapshot();
47
- if (visibleNode(snapshot, IDS.manager)) return snapshot;
48
- if (visibleNode(snapshot, IDS.details)) {
49
- await systemBack(session);
50
- continue;
51
- }
52
- if (visibleNode(snapshot, IDS.menu)) {
53
- await session.execute('ui.press', { test_id: IDS.menu, timeout_ms: timeoutMs, settle: false });
54
- continue;
55
- }
56
- if (IDS.wallet.some((identifier) => nativeNode(snapshot, identifier))) {
57
- await session.execute('ui.press', { test_id: IDS.hamburger, timeout_ms: timeoutMs, settle: false });
58
- continue;
59
- }
50
+ const snapshot = await session.snapshot();
51
+ if (visibleNode(snapshot, IDS.manager)) return snapshot;
52
+ if (visibleNode(snapshot, IDS.details)) {
60
53
  await systemBack(session);
54
+ return (await waitForSnapshot(
55
+ session, timeoutMs, (current) => visibleNode(current, IDS.manager),
56
+ 'Mobile Networks settings did not return from network details.',
57
+ )).snapshot;
58
+ }
59
+ if (!visibleNode(snapshot, IDS.menu)) {
60
+ if (!IDS.wallet.some((identifier) => nativeNode(snapshot, identifier))) {
61
+ throw new Error('Mobile network navigation requires Home, the account menu, or network settings.');
62
+ }
63
+ await session.execute('ui.press', { test_id: IDS.hamburger, timeout_ms: timeoutMs, settle: false });
64
+ await waitForSnapshot(
65
+ session, timeoutMs, (current) => visibleNode(current, IDS.menu),
66
+ 'The Mobile account menu did not expose the Networks control after opening.',
67
+ );
61
68
  }
69
+ await session.execute('ui.press', { test_id: IDS.menu, timeout_ms: timeoutMs, settle: false });
62
70
  return (await waitForSnapshot(
63
71
  session,
64
72
  timeoutMs,
@@ -1,3 +1,4 @@
1
+ // Open Network Settings and locate a network, optionally rechecking it after a settling period.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import {
3
4
  closeNativeSession,