@deeeed/metamask-harness 0.47.4 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +6 -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/launch/index.js +10 -0
  17. package/dist/commands/manifest.js +3 -1
  18. package/dist/commands/mobile-device-view.js +13 -3
  19. package/dist/commands/parse-args.js +2 -0
  20. package/dist/commands/run-engine.js +29 -4
  21. package/dist/commands/run.js +1 -1
  22. package/dist/commands/runtime-launch.js +10 -1
  23. package/dist/commands/status.js +8 -4
  24. package/dist/devices.js +31 -9
  25. package/dist/execution-provenance.js +8 -7
  26. package/dist/funding-execution-context.js +43 -13
  27. package/dist/heal-bounds.js +3 -3
  28. package/dist/live-adapter-contract.js +4 -0
  29. package/dist/manifest.js +2 -13
  30. package/dist/mm-harness-cli.js +2 -1
  31. package/dist/runner.js +14 -3
  32. package/docs/RECIPES.md +133 -22
  33. package/docs/RELEASE-QA-CAPABILITY-MAP.md +3 -2
  34. package/library/actions/core/perps/_controller.mjs +10 -0
  35. package/library/actions/core/perps/close_orders.mjs +13 -17
  36. package/library/actions/extension/assets/home-token-identity.mjs +30 -0
  37. package/library/actions/extension/assets/import_custom_token.mjs +61 -79
  38. package/library/actions/extension/assets/open_details.mjs +18 -7
  39. package/library/actions/extension/assets/read_details.mjs +61 -29
  40. package/library/actions/extension/assets/read_visible_state.mjs +4 -1
  41. package/library/actions/extension/assets/select_network_scope.mjs +16 -9
  42. package/library/actions/extension/assets/set_token_visibility.mjs +84 -17
  43. package/library/actions/extension/assets/verify_sorting.mjs +62 -14
  44. package/library/actions/extension/networks/add_custom.mjs +19 -17
  45. package/library/actions/extension/networks/custom_network.mjs +8 -3
  46. package/library/actions/extension/networks/read_visible_state.mjs +1 -0
  47. package/library/actions/extension/performance/_navigation-memory.mjs +35 -6
  48. package/library/actions/extension/performance/compare_idle_navigation_memory.mjs +3 -2
  49. package/library/actions/extension/performance/measure_detached_dom.mjs +3 -2
  50. package/library/actions/extension/performance/measure_navigation_memory.mjs +3 -2
  51. package/library/actions/extension/perps/assert_visible_consistency.mjs +9 -4
  52. package/library/actions/extension/perps/close_visible_position.mjs +3 -3
  53. package/library/actions/extension/perps/edit_margin.mjs +1 -1
  54. package/library/actions/extension/perps/open_balance_action.mjs +16 -7
  55. package/library/actions/extension/perps/perps.mjs +150 -45
  56. package/library/actions/extension/perps/read_funds_confirmation.mjs +8 -0
  57. package/library/actions/extension/perps/read_snapshot.mjs +2 -1
  58. package/library/actions/extension/perps/read_visible_state.mjs +10 -6
  59. package/library/actions/extension/perps/search_markets.mjs +7 -2
  60. package/library/actions/extension/perps/select_activity_filter.mjs +1 -1
  61. package/library/actions/extension/perps/select_market_filter.mjs +74 -45
  62. package/library/actions/extension/perps/set_market_favorite.mjs +6 -3
  63. package/library/actions/extension/perps/state.mjs +1 -0
  64. package/library/actions/extension/perps/update_position_tpsl.mjs +1 -1
  65. package/library/actions/extension/perps/visible-mutation-identity.mjs +4 -2
  66. package/library/actions/extension/platform/cdp.mjs +23 -142
  67. package/library/actions/extension/swap_bridge/quote-state.mjs +54 -0
  68. package/library/actions/extension/swap_bridge/read_visible_state.mjs +4 -1
  69. package/library/actions/extension/swap_bridge/select_assets.mjs +11 -1
  70. package/library/actions/extension/swap_bridge/set_amount.mjs +10 -8
  71. package/library/actions/extension/swap_bridge/set_max_amount.mjs +17 -18
  72. package/library/actions/extension/swap_bridge/set_slippage.mjs +0 -9
  73. package/library/actions/extension/ui/navigate.mjs +58 -14
  74. package/library/actions/extension/wallet/lock.mjs +2 -1
  75. package/library/actions/mobile/assets/import_custom_token.mjs +1 -0
  76. package/library/actions/mobile/assets/read_visible_state.mjs +3 -1
  77. package/library/actions/mobile/assets/set_token_visibility.mjs +13 -9
  78. package/library/actions/mobile/networks/network-management.mjs +22 -15
  79. package/library/actions/mobile/networks/remove_custom.mjs +6 -2
  80. package/library/actions/mobile/perps/perps.mjs +3 -0
  81. package/library/actions/mobile/perps/read-visible-state-loop.mjs +1 -0
  82. package/library/actions/mobile/perps/set_market_favorite.mjs +15 -19
  83. package/library/actions/mobile/platform/native-session.mjs +26 -17
  84. package/library/actions/mobile/platform/observe-ui.mjs +2 -2
  85. package/library/actions/mobile/ui/navigate.mjs +1 -1
  86. package/library/actions/shared/perps/visible-state.mjs +14 -3
  87. package/library/manifests/core.action-manifest.json +14 -2
  88. package/library/manifests/extension.action-manifest.json +49 -16
  89. package/library/manifests/mobile.action-manifest.json +8 -7
  90. package/library/recipes/core/perps/smoke.recipe.json +23 -0
  91. package/library/recipes/core/perps/snapshot.recipe.json +26 -1
  92. package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +25 -8
  93. package/library/recipes/extension/performance/navigation-memory-suite.recipe.json +219 -0
  94. package/library/recipes/extension/performance/navigation-memory.recipe.json +61 -156
  95. package/library/recipes/extension/wallet/smoke.recipe.json +17 -8
  96. package/library/recipes/mobile/app/lifecycle.android-smoke.recipe.json +23 -4
  97. package/library/recipes/mobile/networks/release-custom-network-persistence.recipe.json +31 -6
  98. package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +5 -4
  99. package/library/recipes/mobile/wallet/smoke.recipe.json +17 -8
  100. package/library/recipes/{assets → shared/assets}/release-token-address-import.recipe.json +7 -0
  101. package/library/recipes/{assets → shared/assets}/release-token-details.recipe.json +2 -2
  102. package/library/recipes/shared/assets/release-token-hide-readd.recipe.json +91 -0
  103. package/library/recipes/{assets → shared/assets}/release-token-persistence.recipe.json +3 -0
  104. package/library/recipes/{assets → shared/assets}/release-token-search-import.recipe.json +4 -1
  105. package/library/recipes/{assets → shared/assets}/release-token-sorting.recipe.json +13 -2
  106. package/library/recipes/{swap-bridge → shared/swap-bridge}/release-quote.recipe.json +29 -3
  107. package/package.json +3 -3
  108. package/site/assets/help-recipes.json +63 -23
  109. package/library/recipes/assets/release-token-hide-readd.recipe.json +0 -57
  110. package/library/recipes/core/perps/clean-market-testnet.recipe.json +0 -44
  111. package/library/recipes/core/perps/order-lifecycle.recipe.json +0 -82
  112. package/library/recipes/core/perps/read-markets.recipe.json +0 -36
  113. package/library/recipes/core/perps/trading-lifecycle.recipe.json +0 -80
  114. package/library/recipes/extension/perps/release-activity-filters.recipe.json +0 -51
  115. package/library/recipes/extension/perps/release-funds-flows.recipe.json +0 -76
  116. package/library/recipes/extension/perps/release-live-limit-order.recipe.json +0 -109
  117. package/library/recipes/extension/perps/release-market-filters.recipe.json +0 -97
  118. package/library/recipes/extension/perps/release-order-entry.recipe.json +0 -158
  119. package/library/recipes/extension/perps/release-order-validation.recipe.json +0 -259
  120. package/library/recipes/extension/perps/release-visible-consistency.recipe.json +0 -47
  121. package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +0 -76
  122. package/library/recipes/mobile/perps/chase-assert-running.recipe.json +0 -110
  123. package/library/recipes/mobile/perps/chase-place.recipe.json +0 -145
  124. package/library/recipes/mobile/perps/chase-terminate.recipe.json +0 -98
  125. package/library/recipes/mobile/perps/performance.recipe.json +0 -722
  126. package/library/recipes/mobile/perps/pro-order-setup.recipe.json +0 -111
  127. package/library/recipes/mobile/perps/pro-order-start-state.recipe.json +0 -53
  128. package/library/recipes/mobile/perps/scale-assert-orders.recipe.json +0 -126
  129. package/library/recipes/mobile/perps/scale-place.recipe.json +0 -186
  130. package/library/recipes/mobile/perps/twap-assert-active.recipe.json +0 -97
  131. package/library/recipes/mobile/perps/twap-place.recipe.json +0 -146
  132. package/library/recipes/mobile/runner/action-validation.recipe.json +0 -336
  133. package/library/recipes/perps/clean-market-testnet.recipe.json +0 -49
  134. package/library/recipes/perps/lifecycle.recipe.json +0 -144
  135. package/library/recipes/perps/release-live-market-order.recipe.json +0 -139
  136. package/library/recipes/perps/release-watchlist.recipe.json +0 -111
  137. package/library/recipes/swap-bridge/release-max-quote.recipe.json +0 -72
  138. /package/library/recipes/{assets → shared/assets}/release-token-list.recipe.json +0 -0
  139. /package/library/recipes/{perps → shared/perps}/release-market-details.recipe.json +0 -0
  140. /package/library/recipes/{perps → shared/perps}/release-market-search.recipe.json +0 -0
  141. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-custom-slippage.recipe.json +0 -0
  142. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-max-swap.recipe.json +0 -0
  143. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-live-swap.recipe.json +0 -0
  144. /package/library/recipes/{swap-bridge → shared/swap-bridge}/release-surface.recipe.json +0 -0
  145. /package/library/recipes/{wallet → shared/wallet}/import.recipe.json +0 -0
  146. /package/library/recipes/{wallet → shared/wallet}/reset-import.recipe.json +0 -0
@@ -1,9 +1,38 @@
1
+ import { mkdtemp, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
1
4
  const DEFAULT_SCREEN_TIMEOUT_MS = 15_000;
2
5
  const DEFAULT_SETTLE_MS = 1_500;
3
6
  const DEFAULT_GC_SETTLE_MS = 300;
4
7
  const DEFAULT_ACTION_TIMEOUT_MS = 400_000;
5
8
  const DEFAULT_ROUND_DURATION_MS = 10_000;
6
9
 
10
+ export async function withMemoryFailureReport(input, measure) {
11
+ try {
12
+ return await measure();
13
+ } catch (error) {
14
+ if (!error?.memoryMeasurement) throw error;
15
+ const directory = await mkdtemp(path.join(input.context.artifactsDir, 'navigation-memory-'));
16
+ const report = path.join(directory, 'measurement.json');
17
+ await writeFile(report, `${JSON.stringify({
18
+ nodeId: input.context.nodeId,
19
+ action: input.action,
20
+ ...error.memoryMeasurement,
21
+ }, null, 2)}\n`, { mode: 0o600 });
22
+ await writeFile(input.outputPath, `${JSON.stringify({ artifacts: [{
23
+ path: path.relative(input.context.artifactsDir, report),
24
+ type: 'report',
25
+ nodeId: input.context.nodeId,
26
+ label: 'Failed memory measurement',
27
+ category: 'diagnostic',
28
+ mimeType: 'application/json',
29
+ }] })}\n`, { mode: 0o600 });
30
+ delete error.memoryMeasurement;
31
+ error.message += `\nNext: jq . ${JSON.stringify(report)}`;
32
+ throw error;
33
+ }
34
+ }
35
+
7
36
  function finiteNumber(value, fallback, label) {
8
37
  const parsed = value ?? fallback;
9
38
  if (!Number.isFinite(parsed)) throw new Error(`${label} must be a finite number.`);
@@ -217,9 +246,9 @@ export async function measureNavigationSlope(page, options, bounds) {
217
246
  passed,
218
247
  };
219
248
  if (!passed.nodes || !passed.listeners) {
220
- throw new Error(
249
+ throw Object.assign(new Error(
221
250
  `Navigation memory bounds exceeded: nodes ${slopes.nodes.toFixed(2)}/${bounds.maxNodesPerCycle} per cycle; listeners ${slopes.listeners.toFixed(2)}/${bounds.maxListenersPerCycle} per cycle.`,
222
- );
251
+ ), { memoryMeasurement: result });
223
252
  }
224
253
  return result;
225
254
  }
@@ -282,9 +311,9 @@ export async function measureDetachedDom(page, options, maxDetachedGrowth) {
282
311
  passed: after - before <= maxDetachedGrowth,
283
312
  };
284
313
  if (!result.passed) {
285
- throw new Error(
314
+ throw Object.assign(new Error(
286
315
  `Detached DOM bound exceeded: growth ${result.growth}/${maxDetachedGrowth} nodes.`,
287
- );
316
+ ), { memoryMeasurement: result });
288
317
  }
289
318
  return result;
290
319
  }
@@ -333,9 +362,9 @@ export async function compareIdleNavigation(page, options, maxAttributableListen
333
362
  passed: attributableListenersPerCycle <= maxAttributableListenersPerCycle,
334
363
  };
335
364
  if (!result.passed) {
336
- throw new Error(
365
+ throw Object.assign(new Error(
337
366
  `Idle-control bound exceeded: attributable listeners ${attributableListenersPerCycle.toFixed(2)}/${maxAttributableListenersPerCycle} per cycle (navigation ${navigation.slopes.listeners.toFixed(2)}, idle ${idleListenersPerCycle.toFixed(2)}).`,
338
- );
367
+ ), { memoryMeasurement: result });
339
368
  }
340
369
  return result;
341
370
  }
@@ -3,10 +3,11 @@ import {
3
3
  compareIdleNavigation,
4
4
  idleControlBound,
5
5
  navigationMemoryOptions,
6
+ withMemoryFailureReport,
6
7
  } from './_navigation-memory.mjs';
7
8
 
8
9
  runAdapter((input) =>
9
- withExtensionPage(input, async (page) => ({
10
+ withMemoryFailureReport(input, () => withExtensionPage(input, async (page) => ({
10
11
  action: input.action,
11
12
  status: 'pass',
12
13
  ...(await compareIdleNavigation(
@@ -14,5 +15,5 @@ runAdapter((input) =>
14
15
  navigationMemoryOptions(input.node, 'rounds', 8),
15
16
  idleControlBound(input.node),
16
17
  )),
17
- })),
18
+ }))),
18
19
  );
@@ -3,10 +3,11 @@ import {
3
3
  detachedDomBound,
4
4
  measureDetachedDom,
5
5
  navigationMemoryOptions,
6
+ withMemoryFailureReport,
6
7
  } from './_navigation-memory.mjs';
7
8
 
8
9
  runAdapter((input) =>
9
- withExtensionPage(input, async (page) => ({
10
+ withMemoryFailureReport(input, () => withExtensionPage(input, async (page) => ({
10
11
  action: input.action,
11
12
  status: 'pass',
12
13
  ...(await measureDetachedDom(
@@ -14,5 +15,5 @@ runAdapter((input) =>
14
15
  navigationMemoryOptions(input.node, 'cycles', 5),
15
16
  detachedDomBound(input.node),
16
17
  )),
17
- })),
18
+ }))),
18
19
  );
@@ -3,10 +3,11 @@ import {
3
3
  measureNavigationSlope,
4
4
  navigationMemoryOptions,
5
5
  navigationSlopeBounds,
6
+ withMemoryFailureReport,
6
7
  } from './_navigation-memory.mjs';
7
8
 
8
9
  runAdapter((input) =>
9
- withExtensionPage(input, async (page) => ({
10
+ withMemoryFailureReport(input, () => withExtensionPage(input, async (page) => ({
10
11
  action: input.action,
11
12
  status: 'pass',
12
13
  ...(await measureNavigationSlope(
@@ -14,5 +15,5 @@ runAdapter((input) =>
14
15
  navigationMemoryOptions(input.node),
15
16
  navigationSlopeBounds(input.node),
16
17
  )),
17
- })),
18
+ }))),
18
19
  );
@@ -86,7 +86,8 @@ async function visiblePrice(page, surface, market) {
86
86
  if (!element) return false;
87
87
  const style = getComputedStyle(element);
88
88
  const rect = element.getBoundingClientRect();
89
- return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
89
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
90
+ && rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
90
91
  };
91
92
  const root = document.querySelector('[data-testid=${JSON.stringify(rootTestId)}]');
92
93
  const price = ${priceTestId
@@ -111,7 +112,8 @@ async function marketRows(page, maxItems) {
111
112
  if (testId.startsWith('market-row-ticker-') || /-(?:token-logo|asset-label)$/u.test(testId)) return false;
112
113
  const style = getComputedStyle(element);
113
114
  const rect = element.getBoundingClientRect();
114
- return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
115
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
116
+ && rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
115
117
  }).slice(0, ${JSON.stringify(maxItems)}).map((element) => ({
116
118
  testId: String(element.getAttribute('data-testid') || '').slice(0, 500),
117
119
  label: String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500),
@@ -124,7 +126,8 @@ async function watchlistRows(page, maxItems) {
124
126
  ].filter((element) => {
125
127
  const style = getComputedStyle(element);
126
128
  const rect = element.getBoundingClientRect();
127
- return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
129
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
130
+ && rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
128
131
  }).slice(0, ${JSON.stringify(maxItems)}).map((element) => ({
129
132
  testId: String(element.getAttribute('data-testid') || '').slice(0, 500),
130
133
  label: String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500),
@@ -138,6 +141,7 @@ function changedRows(first, last) {
138
141
  .map((entry) => entry.testId);
139
142
  }
140
143
 
144
+ // Containment checks loaded full-list rows, including rows below the viewport.
141
145
  async function activityIds(page, maxItems) {
142
146
  const selector = '[data-testid^="transaction-card-"]';
143
147
  return page.evaluate(`[
@@ -155,7 +159,8 @@ async function recentActivityState(page, maxItems) {
155
159
  if (!element) return false;
156
160
  const style = getComputedStyle(element);
157
161
  const rect = element.getBoundingClientRect();
158
- return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
162
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0
163
+ && rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;
159
164
  };
160
165
  return {
161
166
  ids: [...document.querySelectorAll('[data-testid^="transaction-card-"]')]
@@ -10,7 +10,7 @@ import {
10
10
  trustedExternalMutationPlanIsExact,
11
11
  withExternalMutationRecovery,
12
12
  } from './mutation-receipt.mjs';
13
- import { requireCurrentVisibleFundingBalance, requirePerpsTradingContext } from './perps.mjs';
13
+ import { placementReceiptSelection, requireCurrentVisibleFundingBalance, requirePerpsTradingContext } from './perps.mjs';
14
14
  import {
15
15
  absoluteDelta,
16
16
  parsePositionDetail,
@@ -281,7 +281,7 @@ export async function closeVisiblePosition(input) {
281
281
  throw new Error('Visible Perps position notional is unavailable or exceeds the authorized close cap.');
282
282
  }
283
283
  if (!cleanupOnly) {
284
- await readPriorMutationReceipt(input, identity, 'metamask.perps.place_order');
284
+ await readPriorMutationReceipt(input, identity, 'metamask.perps.place_order', placementReceiptSelection(input));
285
285
  }
286
286
  const balanceBeforeBaseUnits = fundingBalance.observedBaseUnits;
287
287
  if (!/^(?:0|[1-9][0-9]{0,29})$/u.test(balanceBeforeBaseUnits)) {
@@ -303,7 +303,6 @@ export async function closeVisiblePosition(input) {
303
303
  let dispatchTarget;
304
304
  try {
305
305
  await requireCurrentVisibleFundingBalance(input, page, timeoutMs, fundingBalance.observedBaseUnits);
306
- await requirePerpsTradingContext(input, page);
307
306
  assertTrustedExternalMutationTarget(input, page, identity);
308
307
  const current = await readVisibleClosePosition(page, market);
309
308
  const currentPosition = parsePositionDetail({ pair: `${market}-USD`, ...current });
@@ -322,6 +321,7 @@ export async function closeVisiblePosition(input) {
322
321
  const rect = element.getBoundingClientRect();
323
322
  return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
324
323
  })()`);
324
+ await requirePerpsTradingContext(input, page);
325
325
  assertTrustedExternalMutationTarget(input, page, identity);
326
326
  } catch (error) {
327
327
  throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
@@ -266,7 +266,6 @@ export async function editMargin(input) {
266
266
  let dispatchTarget;
267
267
  try {
268
268
  await requireCurrentVisibleFundingBalance(input, page, timeoutMs, fundingBalance.observedBaseUnits);
269
- await requirePerpsTradingContext(input, page);
270
269
  assertTrustedExternalMutationTarget(input, page, identity);
271
270
  const current = await readPosition(page, market);
272
271
  if (current.marginBaseUnits !== before.marginBaseUnits || current.size !== before.size
@@ -284,6 +283,7 @@ export async function editMargin(input) {
284
283
  const rect = element.getBoundingClientRect();
285
284
  return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
286
285
  })()`);
286
+ await requirePerpsTradingContext(input, page);
287
287
  assertTrustedExternalMutationTarget(input, page, identity);
288
288
  } catch (error) {
289
289
  throw externalMutationNotDispatched(error instanceof Error ? error.message : String(error));
@@ -11,13 +11,22 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
11
11
  if (!actionTestId) {
12
12
  throw new Error(`metamask.perps.open_balance_action received unsupported balance_action ${JSON.stringify(balanceAction)}.`);
13
13
  }
14
- await page.click(dataTestId('perps-balance-dropdown-balance'));
15
- const actionSelector = dataTestId(actionTestId);
16
- await page.waitForSelector(actionSelector, { timeoutMs: 10_000 });
14
+ const directSelector = dataTestId(`perps-balance-actions-${balanceAction}`);
15
+ const directVisible = await page.evaluate(`(() => {
16
+ const element = document.querySelector(${JSON.stringify(directSelector)});
17
+ if (!element) return false;
18
+ const style = getComputedStyle(element), rect = element.getBoundingClientRect();
19
+ return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
20
+ })()`);
21
+ const actionSelector = directVisible ? directSelector : dataTestId(actionTestId);
22
+ if (!directVisible) {
23
+ await page.click(dataTestId('perps-balance-dropdown-balance'));
24
+ await page.waitForSelector(actionSelector, { timeoutMs: 10_000 });
25
+ }
17
26
  await page.click(actionSelector);
18
27
  const expected = balanceAction === 'withdraw'
19
- ? ['perps-withdraw-page', 'perps-withdraw-container', 'perps-withdraw-view']
20
- : ['confirm-page-container-content', 'confirm-page-container', 'transaction-confirmation'];
28
+ ? ['parent-selector-perps-withdraw', 'parent-selector-confirmation-page', 'perps-withdraw-page', 'perps-withdraw-container', 'perps-withdraw-view']
29
+ : ['parent-selector-confirmation-page', 'confirm-page-container-content', 'confirm-page-container', 'transaction-confirmation'];
21
30
  const resultDeadline = Date.now() + 15_000;
22
31
  let observed;
23
32
  while (Date.now() <= resultDeadline) {
@@ -38,10 +47,10 @@ runAdapter(async (input) => withExtensionPage(input, async (page) => {
38
47
  text: String(document.body?.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 10_000),
39
48
  };
40
49
  })()`);
41
- if (observed.matched.length > 0 || observed.route !== '#/perps-home') break;
50
+ if (observed.matched.length > 0) break;
42
51
  await new Promise((resolve) => setTimeout(resolve, 100));
43
52
  }
44
- if (!observed || (observed.matched.length === 0 && observed.route === '#/perps-home')) {
53
+ if (!observed || observed.matched.length === 0) {
45
54
  throw new Error(`Perps ${balanceAction} did not open: ${JSON.stringify(observed)}.`);
46
55
  }
47
56
  return {