@deeeed/metamask-harness 0.48.0 → 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 (177) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/commands/help.js +17 -5
  3. package/dist/funding-execution-context.js +19 -4
  4. package/dist/metamask-action-validation.js +3 -2
  5. package/dist/mm-harness-cli.js +2 -1
  6. package/docs/RECIPES.md +2 -2
  7. package/library/actions/core/perps/_controller.mjs +14 -24
  8. package/library/actions/core/perps/assert_orders.mjs +3 -5
  9. package/library/actions/core/perps/assert_positions.mjs +3 -5
  10. package/library/actions/core/perps/close_orders.mjs +28 -4
  11. package/library/actions/core/perps/close_positions.mjs +53 -12
  12. package/library/actions/core/perps/edit_order.mjs +2 -0
  13. package/library/actions/core/perps/ensure_orders.mjs +3 -4
  14. package/library/actions/core/perps/ensure_positions.mjs +4 -5
  15. package/library/actions/core/perps/place_order.mjs +7 -14
  16. package/library/actions/core/perps/read_account.mjs +2 -0
  17. package/library/actions/core/perps/read_orders.mjs +2 -0
  18. package/library/actions/core/perps/read_positions.mjs +3 -1
  19. package/library/actions/core/perps/read_snapshot.mjs +3 -0
  20. package/library/actions/core/perps/start_state.mjs +3 -14
  21. package/library/actions/core/perps/teardown_state.mjs +3 -14
  22. package/library/actions/core/perps/update_position_tpsl.mjs +13 -19
  23. package/library/actions/core/wallet/list_accounts.mjs +3 -0
  24. package/library/actions/extension/analytics/consent.mjs +2 -0
  25. package/library/actions/extension/analytics/set_consent.mjs +2 -0
  26. package/library/actions/extension/assets/home-token-identity.mjs +1 -0
  27. package/library/actions/extension/assets/import_custom_token.mjs +2 -0
  28. package/library/actions/extension/assets/open_details.mjs +1 -0
  29. package/library/actions/extension/assets/prepare_send.mjs +1 -0
  30. package/library/actions/extension/assets/read_details.mjs +1 -0
  31. package/library/actions/extension/assets/read_visible_state.mjs +1 -0
  32. package/library/actions/extension/assets/review_send.mjs +1 -0
  33. package/library/actions/extension/assets/select_network_scope.mjs +1 -0
  34. package/library/actions/extension/assets/send.mjs +2 -0
  35. package/library/actions/extension/assets/set_custom_gas.mjs +1 -0
  36. package/library/actions/extension/assets/set_token_visibility.mjs +2 -0
  37. package/library/actions/extension/assets/verify_sorting.mjs +1 -0
  38. package/library/actions/extension/deeplink/open.mjs +1 -0
  39. package/library/actions/extension/networks/add_chainlist.mjs +2 -0
  40. package/library/actions/extension/networks/add_custom.mjs +2 -0
  41. package/library/actions/extension/networks/custom_network.mjs +2 -0
  42. package/library/actions/extension/networks/read_visible_state.mjs +1 -0
  43. package/library/actions/extension/networks/remove_custom.mjs +1 -0
  44. package/library/actions/extension/performance/_navigation-memory.mjs +2 -0
  45. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +2 -0
  46. package/library/actions/extension/performance/measure_detached_dom.mjs +2 -0
  47. package/library/actions/extension/performance/measure_navigation_memory.mjs +2 -0
  48. package/library/actions/extension/perps/assert_orders.mjs +1 -0
  49. package/library/actions/extension/perps/assert_positions.mjs +1 -0
  50. package/library/actions/extension/perps/assert_visible_consistency.mjs +2 -0
  51. package/library/actions/extension/perps/close_orders.mjs +4 -1
  52. package/library/actions/extension/perps/close_positions.mjs +1 -0
  53. package/library/actions/extension/perps/close_visible_position.mjs +159 -10
  54. package/library/actions/extension/perps/compare_provider_market.mjs +2 -0
  55. package/library/actions/extension/perps/edit_margin.mjs +2 -0
  56. package/library/actions/extension/perps/ensure_orders.mjs +1 -0
  57. package/library/actions/extension/perps/ensure_positions.mjs +1 -0
  58. package/library/actions/extension/perps/mutation-receipt.mjs +54 -3
  59. package/library/actions/extension/perps/open_balance_action.mjs +1 -0
  60. package/library/actions/extension/perps/open_position_action.mjs +1 -0
  61. package/library/actions/extension/perps/perps.mjs +401 -25
  62. package/library/actions/extension/perps/place_order.mjs +1 -0
  63. package/library/actions/extension/perps/read_eligibility.mjs +1 -0
  64. package/library/actions/extension/perps/read_funds_confirmation.mjs +2 -0
  65. package/library/actions/extension/perps/read_orders.mjs +1 -0
  66. package/library/actions/extension/perps/read_positions.mjs +1 -0
  67. package/library/actions/extension/perps/read_snapshot.mjs +2 -0
  68. package/library/actions/extension/perps/read_visible_state.mjs +2 -0
  69. package/library/actions/extension/perps/search_markets.mjs +1 -0
  70. package/library/actions/extension/perps/select_activity_filter.mjs +1 -0
  71. package/library/actions/extension/perps/select_market_filter.mjs +1 -0
  72. package/library/actions/extension/perps/set_market_favorite.mjs +1 -0
  73. package/library/actions/extension/perps/state.mjs +1 -0
  74. package/library/actions/extension/perps/update_position_tpsl.mjs +2 -0
  75. package/library/actions/extension/perps/visible-mutation-identity.mjs +2 -0
  76. package/library/actions/extension/platform/cdp.mjs +29 -2
  77. package/library/actions/extension/settings/set_basic_functionality.mjs +1 -0
  78. package/library/actions/extension/swap_bridge/quote-state.mjs +2 -0
  79. package/library/actions/extension/swap_bridge/read_visible_state.mjs +1 -0
  80. package/library/actions/extension/swap_bridge/select_assets.mjs +1 -0
  81. package/library/actions/extension/swap_bridge/set_amount.mjs +1 -0
  82. package/library/actions/extension/swap_bridge/set_max_amount.mjs +1 -0
  83. package/library/actions/extension/swap_bridge/set_slippage.mjs +1 -0
  84. package/library/actions/extension/ui/locators.mjs +1 -0
  85. package/library/actions/extension/ui/navigate.mjs +2 -0
  86. package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -0
  87. package/library/actions/extension/wallet/import.mjs +2 -0
  88. package/library/actions/extension/wallet/list_accounts.mjs +1 -0
  89. package/library/actions/extension/wallet/lock.mjs +2 -0
  90. package/library/actions/extension/wallet/read_state.mjs +1 -0
  91. package/library/actions/extension/wallet/reset.mjs +2 -0
  92. package/library/actions/extension/wallet/secret-input.mjs +2 -0
  93. package/library/actions/extension/wallet/select_account.mjs +2 -0
  94. package/library/actions/extension/wallet/setup.mjs +1 -0
  95. package/library/actions/extension/wallet/state.mjs +2 -0
  96. package/library/actions/extension/wallet/visible-session.mjs +2 -0
  97. package/library/actions/mobile/analytics/consent-settings.mjs +1 -0
  98. package/library/actions/mobile/analytics/set_consent.mjs +1 -0
  99. package/library/actions/mobile/app/network-control.mjs +2 -0
  100. package/library/actions/mobile/app/network.mjs +1 -0
  101. package/library/actions/mobile/app/network_assert.mjs +1 -0
  102. package/library/actions/mobile/app/network_capture.mjs +1 -0
  103. package/library/actions/mobile/assets/import_custom_token.mjs +2 -0
  104. package/library/actions/mobile/assets/open_details.mjs +1 -0
  105. package/library/actions/mobile/assets/read_details.mjs +1 -0
  106. package/library/actions/mobile/assets/read_visible_state.mjs +1 -0
  107. package/library/actions/mobile/assets/set_token_visibility.mjs +2 -0
  108. package/library/actions/mobile/assets/verify_sorting.mjs +90 -7
  109. package/library/actions/mobile/deeplink/open.mjs +1 -0
  110. package/library/actions/mobile/networks/add_custom.mjs +1 -0
  111. package/library/actions/mobile/networks/network-management.mjs +1 -0
  112. package/library/actions/mobile/networks/read_visible_state.mjs +1 -0
  113. package/library/actions/mobile/networks/remove_custom.mjs +1 -0
  114. package/library/actions/mobile/perps/assert_orders.mjs +1 -0
  115. package/library/actions/mobile/perps/assert_positions.mjs +1 -0
  116. package/library/actions/mobile/perps/capture_performance.mjs +1 -0
  117. package/library/actions/mobile/perps/close_orders.mjs +1 -0
  118. package/library/actions/mobile/perps/close_positions.mjs +1 -0
  119. package/library/actions/mobile/perps/ensure_orders.mjs +1 -0
  120. package/library/actions/mobile/perps/ensure_positions.mjs +1 -0
  121. package/library/actions/mobile/perps/measure_homepage_visible.mjs +1 -0
  122. package/library/actions/mobile/perps/performance-capture.mjs +2 -0
  123. package/library/actions/mobile/perps/perps.mjs +6 -2
  124. package/library/actions/mobile/perps/place_order.mjs +1 -0
  125. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +2 -0
  126. package/library/actions/mobile/perps/read-visible-state-loop.mjs +2 -0
  127. package/library/actions/mobile/perps/read_orders.mjs +1 -0
  128. package/library/actions/mobile/perps/read_positions.mjs +1 -0
  129. package/library/actions/mobile/perps/read_visible_state.mjs +1 -0
  130. package/library/actions/mobile/perps/search_markets.mjs +1 -0
  131. package/library/actions/mobile/perps/set_market_favorite.mjs +2 -0
  132. package/library/actions/mobile/platform/bridge.mjs +16 -4
  133. package/library/actions/mobile/platform/native-session-name.mjs +1 -0
  134. package/library/actions/mobile/platform/native-session.mjs +2 -0
  135. package/library/actions/mobile/platform/observe-ui.mjs +2 -0
  136. package/library/actions/mobile/platform/tool-paths.mjs +1 -0
  137. package/library/actions/mobile/swap_bridge/native-session.mjs +1 -0
  138. package/library/actions/mobile/swap_bridge/read_visible_state.mjs +1 -0
  139. package/library/actions/mobile/swap_bridge/select_assets.mjs +2 -0
  140. package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -0
  141. package/library/actions/mobile/swap_bridge/set_max_amount.mjs +1 -0
  142. package/library/actions/mobile/swap_bridge/set_slippage.mjs +1 -0
  143. package/library/actions/mobile/swap_bridge/submit_transaction.mjs +2 -0
  144. package/library/actions/mobile/ui/locators.mjs +1 -0
  145. package/library/actions/mobile/ui/native-navigation.mjs +2 -0
  146. package/library/actions/mobile/ui/navigate.mjs +2 -0
  147. package/library/actions/mobile/wallet/ensure_unlocked.mjs +2 -0
  148. package/library/actions/mobile/wallet/home.mjs +1 -0
  149. package/library/actions/mobile/wallet/import.mjs +2 -0
  150. package/library/actions/mobile/wallet/list_accounts.mjs +1 -0
  151. package/library/actions/mobile/wallet/lock.mjs +1 -0
  152. package/library/actions/mobile/wallet/native-ui.mjs +2 -0
  153. package/library/actions/mobile/wallet/read_state.mjs +2 -0
  154. package/library/actions/mobile/wallet/reset-helper.mjs +1 -0
  155. package/library/actions/mobile/wallet/reset.mjs +1 -0
  156. package/library/actions/mobile/wallet/select_account.mjs +2 -0
  157. package/library/actions/mobile/wallet/setup.mjs +2 -0
  158. package/library/actions/shared/analytics/assert_events.mjs +3 -0
  159. package/library/actions/shared/analytics/consent.mjs +3 -0
  160. package/library/actions/shared/analytics/read_events.mjs +2 -0
  161. package/library/actions/shared/analytics/start_capture.mjs +3 -0
  162. package/library/actions/shared/app/network-artifact.mjs +2 -0
  163. package/library/actions/shared/app/network-assert.mjs +2 -0
  164. package/library/actions/shared/assets/visible-state.mjs +2 -0
  165. package/library/actions/shared/deeplink/url.mjs +3 -0
  166. package/library/actions/shared/networks/visible-state.mjs +2 -0
  167. package/library/actions/shared/perps/visible-state.mjs +2 -0
  168. package/library/actions/shared/swap-bridge/transaction.mjs +3 -0
  169. package/library/actions/shared/swap-bridge/visible-state.mjs +2 -0
  170. package/library/actions/shared/ui/locators.mjs +2 -0
  171. package/library/actions/shared/wallet/import-source.mjs +3 -0
  172. package/library/manifests/core.action-manifest.json +20 -0
  173. package/library/manifests/extension.action-manifest.json +2 -2
  174. package/library/manifests/mobile.action-manifest.json +3 -3
  175. package/library/recipes/mobile/app/{lifecycle.android-smoke.recipe.json → lifecycle-smoke.recipe.json} +12 -11
  176. package/package.json +1 -1
  177. package/site/assets/help-recipes.json +188 -35
@@ -1,3 +1,5 @@
1
+ // Read bounded, normalized Perps snapshots through background APIs, optionally sampling market changes.
2
+ // History checks can require a matching visible activity card identity.
1
3
  import { pathToFileURL } from 'node:url';
2
4
 
3
5
  import { normalizeMarketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
@@ -1,3 +1,5 @@
1
+ // Capture Perps DOM controls, order-entry values, and layout within configured item and visibility bounds.
2
+ // Normalize the observation and check requested visible-state expectations.
1
3
  import { pathToFileURL } from 'node:url';
2
4
 
3
5
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
@@ -1,3 +1,4 @@
1
+ // Enter a Perps market search query and wait for at least one displayed result row.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  runAdapter(async (input) => withExtensionPage(input, async (page) => {
@@ -1,3 +1,4 @@
1
+ // Select a Perps activity filter and wait for its route, label, and loaded cards or empty state.
1
2
  import { pathToFileURL } from 'node:url';
2
3
 
3
4
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
@@ -1,3 +1,4 @@
1
+ // Select a Perps market category through the dropdown or category rail and inspect the resulting list.
1
2
  import { pathToFileURL } from 'node:url';
2
3
 
3
4
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
@@ -1,3 +1,4 @@
1
+ // Set the current market's favorite state through its visible control, or require it to be unchanged.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  runAdapter(async (input) => withExtensionPage(input, async (page) => {
@@ -1,3 +1,4 @@
1
+ // Build a page expression that reads Perps provider, network, initialization, and eligibility from state hooks.
1
2
  export function readPerpsRuntimeStateExpression() {
2
3
  return `(async () => {
3
4
  const hooks = globalThis.stateHooks || {};
@@ -1,3 +1,5 @@
1
+ // Submit live take-profit and stop-loss changes through visible controls with mutation receipts.
2
+ // Check the funded position identity and observe the exact requested prices.
1
3
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
4
  import {
3
5
  assertTrustedExternalMutationTarget,
@@ -1,3 +1,5 @@
1
+ // Parse and match visible Perps position and order identities using USD base-unit tolerances.
2
+ // Also wait for a stable, loaded Perps home view, optionally requiring zero positions and orders.
1
3
  const INTEGER = /^(0|[1-9][0-9]{0,29})$/u;
2
4
 
3
5
  export function parseUsdBaseUnits(value) {
@@ -1,3 +1,6 @@
1
+ // Provide checkout-bound Extension CDP sessions, page actions and screenshot helpers.
2
+ // Bind browser and service-worker observation to the selected runtime.
3
+
1
4
  import {
2
5
  constants,
3
6
  access,
@@ -736,6 +739,24 @@ export class ExtensionPage extends CdpWebPage {
736
739
  });
737
740
  }
738
741
 
742
+ async connectServiceWorker() {
743
+ const browserVersion = await jsonGet(`http://127.0.0.1:${this.port}/json/version`);
744
+ const current = extensionRuntimeIdentity({ port: this.port, target: this.target, browserVersion });
745
+ if (current.runtimeSessionId !== this.runtimeIdentity.runtimeSessionId) {
746
+ throw new Error('Extension browser changed before service-worker observation.');
747
+ }
748
+ const targets = await jsonGet(`http://127.0.0.1:${this.port}/json/list`);
749
+ const workers = targets.filter((target) => target.type === 'service_worker'
750
+ && String(target.url).startsWith(`${this.origin}/`));
751
+ if (workers.length !== 1) throw new Error('Extension requires one matching service worker for order observation.');
752
+ const worker = workers[0];
753
+ const socket = new URL(worker.webSocketDebuggerUrl);
754
+ if (socket.protocol !== 'ws:' || socket.hostname !== '127.0.0.1' || Number(socket.port) !== this.port) {
755
+ throw new Error('Extension service-worker observation requires this browser localhost endpoint.');
756
+ }
757
+ return { session: await CdpSession.connect(socket.href), targetId: worker.id };
758
+ }
759
+
739
760
  async setInput(selector, value) {
740
761
  const input = String(value ?? '');
741
762
  const target = await this.evaluate(`(() => {
@@ -747,8 +768,14 @@ export class ExtensionPage extends CdpWebPage {
747
768
  if (typeof element.select === 'function') element.select();
748
769
  return { tagName: element.tagName, previousLength: String(element.value ?? '').length };
749
770
  })()`);
750
- await this.session.call('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace' });
751
- await this.session.call('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace' });
771
+ await this.session.call('Input.dispatchKeyEvent', {
772
+ type: 'keyDown', key: 'Backspace', code: 'Backspace',
773
+ windowsVirtualKeyCode: 8, nativeVirtualKeyCode: 8,
774
+ });
775
+ await this.session.call('Input.dispatchKeyEvent', {
776
+ type: 'keyUp', key: 'Backspace', code: 'Backspace',
777
+ windowsVirtualKeyCode: 8, nativeVirtualKeyCode: 8,
778
+ });
752
779
  for (const character of input) {
753
780
  const upper = character.toUpperCase();
754
781
  const isLetter = /^[A-Z]$/u.test(upper);
@@ -1,3 +1,4 @@
1
+ // Sets Basic Functionality in Privacy settings, accepting the disable confirmation when needed.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  const TOGGLE_SELECTOR = dataTestId('basic-functionality-toggle');
@@ -1,3 +1,5 @@
1
+ // Compares exposed bridge quote state with the selected assets and displayed amounts.
2
+ // Requires debug store access to distinguish matching, stale, loading, and unavailable quotes.
1
3
  export async function readQuoteInputIdentity(page) {
2
4
  return page.evaluate(`(() => {
3
5
  const state = globalThis.stateHooks?.store?.getState?.();
@@ -1,3 +1,4 @@
1
+ // Reads displayed swap/bridge inputs and quote details, optionally checking quote identity in the store.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { readQuoteInputIdentity } from './quote-state.mjs';
3
4
  import {
@@ -1,3 +1,4 @@
1
+ // Opens Swap and selects source and destination assets by exact ID or unambiguous chain and symbol.
1
2
  import { runAdapter, withExtensionPage, dataTestId } from '../platform/cdp.mjs';
2
3
  import { openSwap } from '../ui/navigate.mjs';
3
4
  import { resolveAssetSelectionOptions } from '../../shared/swap-bridge/transaction.mjs';
@@ -1,3 +1,4 @@
1
+ // Enters a swap/bridge amount and optionally waits for a matching usable quote or an unavailable route.
1
2
  import { runAdapter, withExtensionPage, dataTestId } from '../platform/cdp.mjs';
2
3
  import { resolveAmountOptions } from '../../shared/swap-bridge/transaction.mjs';
3
4
  import { readQuoteInputIdentity } from './quote-state.mjs';
@@ -1,3 +1,4 @@
1
+ // Uses the visible Max control and optionally waits for a matching usable quote or an unavailable route.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import { resolveAmountOptions } from '../../shared/swap-bridge/transaction.mjs';
3
4
  import { requireVisibleObservation } from '../../shared/ui/locators.mjs';
@@ -1,3 +1,4 @@
1
+ // Saves custom swap/bridge slippage and waits for the displayed quote and submit control to refresh.
1
2
  import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
 
3
4
  async function pause(page) {
@@ -1,3 +1,4 @@
1
+ // Derives locator candidates from the Extension page's current visible-UI observation.
1
2
  import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
2
3
  import {
3
4
  locatorsFromObservation,
@@ -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 {
@@ -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');
@@ -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,
@@ -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 {
@@ -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,
@@ -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 = {
@@ -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,
@@ -1,3 +1,4 @@
1
+ // Delete a custom network through native Settings and wait for its row to disappear.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import {
3
4
  closeNativeSession,
@@ -1,3 +1,4 @@
1
+ // Assert that selected Perps orders are open or absent using the Mobile controller.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { assertOrders } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Assert that selected Perps positions are open or absent using the Mobile controller.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { assertPositions } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Start or end a Perps performance log capture, writing artifacts and checking required events at end.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { capturePerpsPerformance } from './performance-capture.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Cancel selected Perps orders through the Mobile controller and assert their absence.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { assertOrders, closeOrders } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Close selected Perps positions through the Mobile controller and assert their absence.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { assertPositions, closePositions } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Create a limit order when none matches, or cancel selected orders, then assert the requested state.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { ensureOrders } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Submit an order when no position matches, or close selected positions, then assert the requested state.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { ensurePositions } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Unlock the wallet and measure the scroll transition until a requested homepage target becomes visible.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { measureHomepageVisible } from './perps.mjs';
3
4
 
@@ -1,3 +1,5 @@
1
+ // Capture a bounded section of Perps console logs and write performance measurements and validation artifacts.
2
+ // Persist the start offset and file identity to reject replaced or rewritten logs when the capture ends.
1
3
  import { createHash } from "node:crypto";
2
4
  import { mkdir, open, readFile, rm, stat, writeFile } from "node:fs/promises";
3
5
  import path from "node:path";
@@ -1,3 +1,5 @@
1
+ // Implement Mobile Perps navigation, trading mutations, assertions, and setup or cleanup through the bridge.
2
+ // Strategy getters used during cleanup can settle tracked collateral.
1
3
  import { pathToFileURL } from 'node:url';
2
4
  import {
3
5
  MOBILE_BRIDGE_RESULT_ERROR_CODES,
@@ -1771,7 +1773,7 @@ async function applyTutorialState(input, config) {
1771
1773
 
1772
1774
  export async function startState(input) {
1773
1775
  if (input.node?.account !== undefined && (typeof input.node.account !== 'string' || !input.node.account.trim())) {
1774
- throw new Error('Perps start state requires a non-empty named account when account is supplied.\nNext: mm-harness actions metamask.perps.start_state --adapter mobile --json');
1776
+ throw new Error('Perps start state requires a non-empty account name or address when account is supplied.\nNext: mm-harness actions metamask.perps.start_state --adapter mobile --json');
1775
1777
  }
1776
1778
  const params = paramsForState(input);
1777
1779
  const config = applyTradingStatePolicy(
@@ -1831,7 +1833,9 @@ export async function startState(input) {
1831
1833
  node: {
1832
1834
  ...accountInput.node,
1833
1835
  action: 'metamask.wallet.select_account',
1834
- name: config.account.trim(),
1836
+ ...(/^0x[0-9a-f]{40}$/iu.test(config.account.trim())
1837
+ ? { address: config.account.trim() }
1838
+ : { name: config.account.trim() }),
1835
1839
  },
1836
1840
  })
1837
1841
  : { skipped: true };
@@ -1,3 +1,4 @@
1
+ // Submit a Perps order through the Mobile controller and assert a matching open position.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { assertPositions, placeOrder } from './perps.mjs';
3
4
  runAdapter(async (input) => {
@@ -1,3 +1,5 @@
1
+ // Apply and verify an adb reverse mapping for a local snapshot port on the selected Android device.
2
+ // Skip the mapping when no Android serial is configured.
1
3
  import { execFile } from 'node:child_process';
2
4
  import { promisify } from 'node:util';
3
5
 
@@ -1,3 +1,5 @@
1
+ // Poll native screen and accessibility observations until Perps state satisfies the requested checks.
2
+ // Return normalized state or report the last assertion or observation failure at the deadline.
1
3
  import { observeNativeUi } from '../platform/observe-ui.mjs';
2
4
  import {
3
5
  normalizeVisiblePerpsState,
@@ -1,3 +1,4 @@
1
+ // Return selected open Perps orders with redacted fields and counts from the Mobile controller.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { readOrders } from './perps.mjs';
3
4
 
@@ -1,3 +1,4 @@
1
+ // Return selected Perps positions with redacted fields and counts from the Mobile controller.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { readPerpsPositions } from './perps.mjs';
3
4
  runAdapter(readPerpsPositions);
@@ -1,3 +1,4 @@
1
+ // Read Perps state from native accessibility observations, waiting for requested assertions to pass.
1
2
  import { runAdapter } from '../platform/bridge.mjs';
2
3
  import { readVisiblePerpsState } from './read-visible-state-loop.mjs';
3
4