@deeeed/metamask-harness 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/adapters/extension/artifact-runtime-state.cjs +138 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/launch-browser.cjs +24 -0
- package/adapters/extension/live.sh +82 -12
- package/adapters/extension/readiness.mjs +77 -36
- package/adapters/extension/snapshot-dist.sh +88 -3
- package/adapters/extension/verify.sh +6 -2
- package/adapters/manifest.json +17 -1
- package/adapters/shared/log-tui.mjs +1 -1
- package/dist/adapters/extension/artifact-integrity.js +38 -0
- package/dist/adapters/extension/extension-id.js +23 -4
- package/dist/adapters/extension/release-artifact.js +386 -0
- package/dist/adapters/extension/runtime-decision.js +164 -20
- package/dist/adapters/extension/runtime.js +169 -3
- package/dist/adapters/mobile/provision.js +38 -3
- package/dist/adapters/mobile/release-artifact-state.js +124 -0
- package/dist/adapters/mobile/release-artifact.js +295 -0
- package/dist/adapters.js +78 -6
- package/dist/app-lifecycle.js +4 -3
- package/dist/command-contract.js +16 -0
- package/dist/commands/call.js +6 -2
- package/dist/commands/launch/extension.js +74 -9
- package/dist/commands/launch/index.js +3 -0
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/provision.js +2 -0
- package/dist/commands/run-engine.js +109 -14
- package/dist/commands/run.js +4 -2
- package/dist/commands/runtime-launch.js +178 -10
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +47 -1
- package/dist/mm-harness-cli.js +31 -1
- package/dist/recipe-security.js +40 -0
- package/dist/run-diagnostics.js +1 -1
- package/dist/runner.js +3 -0
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +125 -0
- package/library/actions/extension/assets/finish_send.mjs +128 -0
- package/library/actions/extension/assets/import_custom_token.mjs +227 -0
- package/library/actions/extension/assets/prepare_send.mjs +53 -0
- package/library/actions/extension/assets/read_details.mjs +57 -0
- package/library/actions/extension/assets/read_visible_state.mjs +50 -0
- package/library/actions/extension/assets/review_send.mjs +23 -0
- package/library/actions/extension/assets/select_network_scope.mjs +30 -0
- package/library/actions/extension/assets/send.mjs +104 -0
- package/library/actions/extension/assets/set_custom_gas.mjs +56 -0
- package/library/actions/extension/assets/set_token_visibility.mjs +129 -0
- package/library/actions/extension/assets/verify_sorting.mjs +89 -0
- package/library/actions/extension/networks/add_chainlist.mjs +94 -0
- package/library/actions/extension/networks/add_custom.mjs +123 -0
- package/library/actions/extension/networks/custom_network.mjs +103 -0
- package/library/actions/extension/networks/read_visible_state.mjs +50 -0
- package/library/actions/extension/networks/remove_custom.mjs +68 -0
- package/library/actions/extension/perps/close_orders.mjs +1 -0
- package/library/actions/extension/perps/close_visible_position.mjs +133 -0
- package/library/actions/extension/perps/compare_provider_market.mjs +109 -0
- package/library/actions/extension/perps/edit_margin.mjs +82 -0
- package/library/actions/extension/perps/open_balance_action.mjs +58 -0
- package/library/actions/extension/perps/open_position_action.mjs +80 -0
- package/library/actions/extension/perps/perps.mjs +196 -2
- package/library/actions/extension/perps/place_order.mjs +5 -2
- package/library/actions/extension/perps/read_eligibility.mjs +34 -0
- package/library/actions/extension/perps/read_funds_confirmation.mjs +69 -0
- package/library/actions/extension/perps/read_snapshot.mjs +470 -0
- package/library/actions/extension/perps/read_visible_state.mjs +98 -0
- package/library/actions/extension/perps/select_activity_filter.mjs +44 -0
- package/library/actions/extension/perps/select_market_filter.mjs +71 -0
- package/library/actions/extension/perps/set_market_favorite.mjs +47 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +106 -0
- package/library/actions/extension/platform/cdp.mjs +109 -3
- package/library/actions/extension/settings/set_basic_functionality.mjs +58 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +71 -0
- package/library/actions/extension/swap_bridge/select_assets.mjs +103 -0
- package/library/actions/extension/swap_bridge/set_amount.mjs +36 -0
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +58 -0
- package/library/actions/extension/swap_bridge/set_slippage.mjs +121 -0
- package/library/actions/extension/swap_bridge/submit_transaction.mjs +363 -0
- package/library/actions/extension/ui/activate_and_observe.mjs +96 -0
- package/library/actions/extension/ui/navigate.mjs +264 -8
- package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -6
- package/library/actions/extension/wallet/import.mjs +13 -46
- package/library/actions/extension/wallet/lock.mjs +35 -0
- package/library/actions/extension/wallet/secret-input.mjs +98 -0
- package/library/actions/extension/wallet/select_account.mjs +71 -7
- package/library/actions/mobile/assets/read_visible_state.mjs +37 -0
- package/library/actions/mobile/perps/read_visible_state.mjs +31 -0
- package/library/actions/mobile/platform/native-session-name.mjs +5 -0
- package/library/actions/mobile/platform/observe-ui.mjs +86 -2
- package/library/actions/mobile/swap_bridge/native-session.mjs +45 -0
- package/library/actions/mobile/swap_bridge/read_visible_state.mjs +31 -0
- package/library/actions/mobile/swap_bridge/select_assets.mjs +94 -0
- package/library/actions/mobile/swap_bridge/set_amount.mjs +73 -0
- package/library/actions/mobile/ui/native-navigation.mjs +265 -0
- package/library/actions/mobile/ui/navigate.mjs +10 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +17 -0
- package/library/actions/mobile/wallet/import.mjs +71 -2
- package/library/actions/mobile/wallet/native-ui.mjs +512 -0
- package/library/actions/mobile/wallet/read_state.mjs +16 -0
- package/library/actions/mobile/wallet/reset.mjs +17 -4
- package/library/actions/shared/assets/visible-state.mjs +176 -0
- package/library/actions/shared/networks/visible-state.mjs +70 -0
- package/library/actions/shared/perps/visible-state.mjs +354 -0
- package/library/actions/shared/swap-bridge/transaction.mjs +54 -0
- package/library/actions/shared/swap-bridge/visible-state.mjs +97 -0
- package/library/actions/shared/ui/locators.mjs +7 -0
- package/library/manifests/extension.action-manifest.json +1366 -184
- package/library/manifests/mobile.action-manifest.json +235 -1
- package/library/recipes/assets/release-custom-gas-send.recipe.json +88 -0
- package/library/recipes/assets/release-custom-network-token-persistence.recipe.json +106 -0
- package/library/recipes/assets/release-native-max-cancel.recipe.json +70 -0
- package/library/recipes/assets/release-native-max-send.recipe.json +78 -0
- package/library/recipes/assets/release-own-account-send.recipe.json +80 -0
- package/library/recipes/assets/release-token-address-import.recipe.json +47 -0
- package/library/recipes/assets/release-token-details.recipe.json +48 -0
- package/library/recipes/assets/release-token-hide-readd.recipe.json +55 -0
- package/library/recipes/assets/release-token-list.recipe.json +75 -0
- package/library/recipes/assets/release-token-persistence.recipe.json +73 -0
- package/library/recipes/assets/release-token-search-import.recipe.json +40 -0
- package/library/recipes/assets/release-token-sorting.recipe.json +29 -0
- package/library/recipes/extension/networks/release-balance-stability.recipe.json +51 -0
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +104 -0
- package/library/recipes/extension/perps/release-order-entry.recipe.json +155 -0
- package/library/recipes/extension/perps/release-order-validation.recipe.json +220 -0
- package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
- package/library/recipes/perps/release-activity-filters.recipe.json +51 -0
- package/library/recipes/perps/release-funds-flows.recipe.json +76 -0
- package/library/recipes/perps/release-live-market-order.recipe.json +122 -0
- package/library/recipes/perps/release-market-details.recipe.json +83 -0
- package/library/recipes/perps/release-market-search.recipe.json +79 -0
- package/library/recipes/perps/release-watchlist.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +90 -0
- package/library/recipes/swap-bridge/release-live-max-swap.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-live-swap.recipe.json +101 -0
- package/library/recipes/swap-bridge/release-max-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-surface.recipe.json +43 -0
- package/library/recipes/wallet/import.recipe.json +20 -1
- package/library/recipes/wallet/reset-import.recipe.json +20 -1
- package/package.json +6 -5
- package/scripts/validate-release-report.mjs +319 -0
|
@@ -12,8 +12,15 @@ const BACK_CONTROLS = [
|
|
|
12
12
|
'perps-market-detail-back-button',
|
|
13
13
|
'perps-activity-back-button',
|
|
14
14
|
'perps-withdraw-back-button',
|
|
15
|
+
'wallet-initiated-header-back-button',
|
|
16
|
+
'transaction-details-back-button',
|
|
17
|
+
'asset__back',
|
|
18
|
+
'token-management-header-back-button',
|
|
19
|
+
'custom-token-import-back-button',
|
|
20
|
+
'networks-page-form-back-button',
|
|
21
|
+
'page-header-back-button',
|
|
15
22
|
'back-button',
|
|
16
|
-
].map(dataTestId);
|
|
23
|
+
].map(dataTestId).concat('button[aria-label="Back"]');
|
|
17
24
|
|
|
18
25
|
function text(value) {
|
|
19
26
|
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
@@ -22,13 +29,37 @@ function text(value) {
|
|
|
22
29
|
function pageIntent(node) {
|
|
23
30
|
const page = text(node?.page);
|
|
24
31
|
if (!page) return undefined;
|
|
25
|
-
if (page === 'home' || page === 'perps') return { page };
|
|
32
|
+
if (page === 'back' || page === 'home' || page === 'perps' || page === 'swap') return { page };
|
|
33
|
+
if (page === 'perps-market-list' || page === 'perps-activity') return { page };
|
|
34
|
+
if (page === 'network') {
|
|
35
|
+
const network = text(node.network);
|
|
36
|
+
const chainId = Number(node.chain_id);
|
|
37
|
+
if (!network || !Number.isInteger(chainId) || chainId <= 0) {
|
|
38
|
+
throw new Error('extension ui.navigate page=network requires network and a positive integer chain_id.');
|
|
39
|
+
}
|
|
40
|
+
return { page, network, chainId, addIfMissing: node.add_if_missing !== false };
|
|
41
|
+
}
|
|
26
42
|
if (page === 'perps-market') {
|
|
27
43
|
const market = text(node.market) ?? text(node.symbol);
|
|
28
44
|
if (!market) throw new Error('extension ui.navigate page=perps-market requires market or symbol.');
|
|
29
45
|
return { page, market };
|
|
30
46
|
}
|
|
31
|
-
throw new Error('extension ui.navigate supported page intents: home, perps, perps-market.');
|
|
47
|
+
throw new Error('extension ui.navigate supported page intents: back, home, network, perps, perps-market-list, perps-market, perps-activity, swap.');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function goBack(page, timeoutMs) {
|
|
51
|
+
const before = await currentHref(page);
|
|
52
|
+
const history = await page.session.call('Page.getNavigationHistory');
|
|
53
|
+
const entry = history.entries?.[history.currentIndex - 1];
|
|
54
|
+
if (!entry) throw new Error('extension ui.navigate page=back has no previous browser history entry.');
|
|
55
|
+
await page.session.call('Page.navigateToHistoryEntry', { entryId: entry.id });
|
|
56
|
+
const deadline = Date.now() + timeoutMs;
|
|
57
|
+
while (Date.now() < deadline) {
|
|
58
|
+
const href = await currentHref(page);
|
|
59
|
+
if (href !== before) return { method: 'browser-history', href, previousHref: before };
|
|
60
|
+
await pauseForUi(page);
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`extension ui.navigate page=back did not leave ${JSON.stringify(before)}.`);
|
|
32
63
|
}
|
|
33
64
|
|
|
34
65
|
async function hasVisibleSelector(page, selector) {
|
|
@@ -59,6 +90,12 @@ async function currentHref(page) {
|
|
|
59
90
|
return page.evaluate('location.href');
|
|
60
91
|
}
|
|
61
92
|
|
|
93
|
+
function isHomeHref(href) {
|
|
94
|
+
const hash = href.includes('#') ? href.slice(href.indexOf('#')) : '';
|
|
95
|
+
const route = hash.split('?')[0];
|
|
96
|
+
return route === '' || route === '#' || route === '#/';
|
|
97
|
+
}
|
|
98
|
+
|
|
62
99
|
async function activateSemanticControl(page, selector) {
|
|
63
100
|
return {
|
|
64
101
|
activation: 'trusted-pointer',
|
|
@@ -68,6 +105,7 @@ async function activateSemanticControl(page, selector) {
|
|
|
68
105
|
|
|
69
106
|
export async function openHome(page, timeoutMs) {
|
|
70
107
|
const home = dataTestId('bottom-nav-home');
|
|
108
|
+
const drawerClose = dataTestId('drawer-close-button');
|
|
71
109
|
const selectedHome = `${home}[aria-current="page"]`;
|
|
72
110
|
const legacyHome = dataTestId('account-overview__asset-tab');
|
|
73
111
|
const selectedLegacyHome = `${legacyHome}[aria-selected="true"]`;
|
|
@@ -76,13 +114,20 @@ export async function openHome(page, timeoutMs) {
|
|
|
76
114
|
let pendingActivation;
|
|
77
115
|
|
|
78
116
|
while (Date.now() < deadline) {
|
|
117
|
+
if (await hasVisibleSelector(page, drawerClose)) {
|
|
118
|
+
await activateSemanticControl(page, drawerClose);
|
|
119
|
+
steps.push(drawerClose);
|
|
120
|
+
await pauseForUi(page);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const href = await currentHref(page);
|
|
79
124
|
if (
|
|
80
|
-
(
|
|
81
|
-
(await hasVisibleSelector(page,
|
|
125
|
+
isHomeHref(href) &&
|
|
126
|
+
((await hasVisibleSelector(page, selectedHome)) ||
|
|
127
|
+
(await hasVisibleSelector(page, selectedLegacyHome)))
|
|
82
128
|
) {
|
|
83
|
-
return { method: 'visible-ui', href
|
|
129
|
+
return { method: 'visible-ui', href, steps };
|
|
84
130
|
}
|
|
85
|
-
const href = await currentHref(page);
|
|
86
131
|
if (pendingActivation && pendingActivation.href !== href) {
|
|
87
132
|
pendingActivation = undefined;
|
|
88
133
|
await pauseForUi(page);
|
|
@@ -130,12 +175,19 @@ export async function openPerpsHome(page, timeoutMs) {
|
|
|
130
175
|
const bottomNav = dataTestId('bottom-nav-perps');
|
|
131
176
|
const legacyTab = dataTestId('account-overview__perps-tab');
|
|
132
177
|
const perpsHome = dataTestId('perps-home-page');
|
|
178
|
+
const tutorialSkip = dataTestId('perps-tutorial-skip-button');
|
|
133
179
|
const steps = [];
|
|
134
180
|
const deadline = Date.now() + timeoutMs;
|
|
135
181
|
let pendingActivation;
|
|
136
182
|
let perpsActivation;
|
|
137
183
|
|
|
138
184
|
while (Date.now() < deadline) {
|
|
185
|
+
if (await hasVisibleSelector(page, tutorialSkip)) {
|
|
186
|
+
await activateSemanticControl(page, tutorialSkip);
|
|
187
|
+
steps.push(tutorialSkip);
|
|
188
|
+
await pauseForUi(page);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
139
191
|
if (await hasVisibleSelector(page, perpsHome)) {
|
|
140
192
|
return {
|
|
141
193
|
...(perpsActivation ? { navigation: perpsActivation, selector: bottomNav } : {}),
|
|
@@ -207,8 +259,23 @@ export async function openPerpsHome(page, timeoutMs) {
|
|
|
207
259
|
|
|
208
260
|
export async function openPerpsMarket(page, market, timeoutMs) {
|
|
209
261
|
const deadline = Date.now() + timeoutMs;
|
|
210
|
-
const home = await openPerpsHome(page, Math.max(1, deadline - Date.now()));
|
|
211
262
|
const normalizedMarket = normalizeMarketSymbol(market.trim());
|
|
263
|
+
if (await hasVisibleSelector(page, dataTestId('market-list-view'))) {
|
|
264
|
+
const marketRow = dataTestId(`market-row-${normalizedMarket.replace(':', '-')}`);
|
|
265
|
+
await page.waitForSelector(marketRow, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
266
|
+
const navigation = await activateSemanticControl(page, marketRow);
|
|
267
|
+
await page.waitForSelector(dataTestId('perps-market-detail-page'), {
|
|
268
|
+
timeoutMs: Math.max(1, deadline - Date.now()),
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
navigation,
|
|
272
|
+
method: 'visible-ui',
|
|
273
|
+
href: await currentHref(page),
|
|
274
|
+
selector: marketRow,
|
|
275
|
+
steps: [marketRow],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const home = await openPerpsHome(page, Math.max(1, deadline - Date.now()));
|
|
212
279
|
const selectors = [
|
|
213
280
|
dataTestId(`perps-watchlist-${normalizedMarket}`),
|
|
214
281
|
dataTestId(`explore-markets-${normalizedMarket.replaceAll(':', '-')}`),
|
|
@@ -239,11 +306,178 @@ export async function openPerpsMarket(page, market, timeoutMs) {
|
|
|
239
306
|
};
|
|
240
307
|
}
|
|
241
308
|
|
|
309
|
+
export async function openPerpsActivity(page, timeoutMs) {
|
|
310
|
+
if (await hasVisibleSelector(page, dataTestId('perps-activity-page'))) {
|
|
311
|
+
return { method: 'visible-ui', href: await currentHref(page), steps: [] };
|
|
312
|
+
}
|
|
313
|
+
const deadline = Date.now() + timeoutMs;
|
|
314
|
+
const home = await openPerpsHome(page, Math.max(1, deadline - Date.now()));
|
|
315
|
+
const activity = dataTestId('perps-recent-activity-see-all');
|
|
316
|
+
await page.waitForSelector(activity, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
317
|
+
const navigation = await activateSemanticControl(page, activity);
|
|
318
|
+
await page.waitForSelector(dataTestId('perps-activity-page'), {
|
|
319
|
+
timeoutMs: Math.max(1, deadline - Date.now()),
|
|
320
|
+
});
|
|
321
|
+
return {
|
|
322
|
+
navigation,
|
|
323
|
+
method: 'visible-ui',
|
|
324
|
+
href: await currentHref(page),
|
|
325
|
+
selector: activity,
|
|
326
|
+
steps: [...(home.steps ?? []), activity],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export async function openPerpsMarketList(page, timeoutMs) {
|
|
331
|
+
const marketList = dataTestId('market-list-view');
|
|
332
|
+
if (await hasVisibleSelector(page, marketList)) {
|
|
333
|
+
return { method: 'visible-ui', href: await currentHref(page), steps: [] };
|
|
334
|
+
}
|
|
335
|
+
const deadline = Date.now() + timeoutMs;
|
|
336
|
+
const detailListButton = dataTestId('perps-market-detail-market-list-button');
|
|
337
|
+
if (await hasVisibleSelector(page, detailListButton)) {
|
|
338
|
+
const navigation = await activateSemanticControl(page, detailListButton);
|
|
339
|
+
await page.waitForSelector(marketList, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
340
|
+
return {
|
|
341
|
+
navigation,
|
|
342
|
+
method: 'visible-ui',
|
|
343
|
+
href: await currentHref(page),
|
|
344
|
+
selector: detailListButton,
|
|
345
|
+
steps: [detailListButton],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const home = await openPerpsHome(page, Math.max(1, deadline - Date.now()));
|
|
349
|
+
const explore = dataTestId('perps-explore-markets-row');
|
|
350
|
+
await page.waitForSelector(explore, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
351
|
+
const navigation = await activateSemanticControl(page, explore);
|
|
352
|
+
await page.waitForSelector(marketList, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
353
|
+
return {
|
|
354
|
+
navigation,
|
|
355
|
+
method: 'visible-ui',
|
|
356
|
+
href: await currentHref(page),
|
|
357
|
+
selector: explore,
|
|
358
|
+
steps: [...home.steps, explore],
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function openNetwork(page, network, chainId, addIfMissing, timeoutMs) {
|
|
363
|
+
const deadline = Date.now() + timeoutMs;
|
|
364
|
+
const hexChainId = `0x${chainId.toString(16)}`;
|
|
365
|
+
const installed = dataTestId(`home-network-filter-network-${hexChainId}`);
|
|
366
|
+
const custom = dataTestId(`home-network-filter-custom-${hexChainId}`);
|
|
367
|
+
const additional = dataTestId(`home-network-filter-additional-${hexChainId}`);
|
|
368
|
+
const manage = dataTestId('home-network-filter-manage-networks');
|
|
369
|
+
const sort = dataTestId('sort-by-networks');
|
|
370
|
+
const assets = dataTestId('account-overview__asset-tab');
|
|
371
|
+
const steps = [];
|
|
372
|
+
await openHome(page, Math.max(1, deadline - Date.now()));
|
|
373
|
+
if (!(await hasVisibleSelector(page, `${assets}[aria-selected="true"]`))) {
|
|
374
|
+
await activateSemanticControl(page, assets);
|
|
375
|
+
steps.push(assets);
|
|
376
|
+
if (!(await waitForFirstVisible(page, [`${assets}[aria-selected="true"]`], deadline))) {
|
|
377
|
+
throw new Error('Token assets did not become selected before network navigation.');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
await activateSemanticControl(page, sort);
|
|
381
|
+
steps.push(sort);
|
|
382
|
+
let choice = await waitForFirstVisible(page, [installed, custom, additional, manage], deadline);
|
|
383
|
+
if (!choice) throw new Error(`Network filter did not show ${network} (${chainId}) before timeout.`);
|
|
384
|
+
if (choice !== manage) {
|
|
385
|
+
await activateSemanticControl(page, choice);
|
|
386
|
+
steps.push(choice);
|
|
387
|
+
} else {
|
|
388
|
+
if (!addIfMissing) {
|
|
389
|
+
throw new Error(`Network ${network} (${chainId}) is not added to the wallet.`);
|
|
390
|
+
}
|
|
391
|
+
await activateSemanticControl(page, manage);
|
|
392
|
+
steps.push(manage);
|
|
393
|
+
const listItem = dataTestId(`network-list-item-eip155:${chainId}`);
|
|
394
|
+
if (!await hasVisibleSelector(page, listItem)) {
|
|
395
|
+
const popular = `${dataTestId(`popular-network-${hexChainId}`)} ${dataTestId('test-add-button')}`;
|
|
396
|
+
await page.waitForSelector(popular, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
397
|
+
await activateSemanticControl(page, popular);
|
|
398
|
+
steps.push(popular);
|
|
399
|
+
await page.waitForSelector(listItem, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
400
|
+
}
|
|
401
|
+
const back = dataTestId('page-header-back-button');
|
|
402
|
+
await activateSemanticControl(page, back);
|
|
403
|
+
steps.push(back);
|
|
404
|
+
await page.waitForSelector(sort, { timeoutMs: Math.max(1, deadline - Date.now()) });
|
|
405
|
+
await activateSemanticControl(page, sort);
|
|
406
|
+
steps.push(sort);
|
|
407
|
+
choice = await waitForFirstVisible(page, [installed, custom, additional], deadline);
|
|
408
|
+
if (!choice) throw new Error(`Added network ${network} (${chainId}) did not appear in the wallet filter.`);
|
|
409
|
+
await activateSemanticControl(page, choice);
|
|
410
|
+
steps.push(choice);
|
|
411
|
+
}
|
|
412
|
+
while (Date.now() < deadline) {
|
|
413
|
+
const label = await page.evaluate(`String(document.querySelector(${JSON.stringify(sort)})?.innerText || '')`);
|
|
414
|
+
if (label.toLowerCase().includes(network.toLowerCase())) {
|
|
415
|
+
return { method: 'visible-ui', href: await currentHref(page), network, chainId, steps };
|
|
416
|
+
}
|
|
417
|
+
await pauseForUi(page);
|
|
418
|
+
}
|
|
419
|
+
throw new Error(`Network ${network} (${chainId}) did not become the selected wallet filter.`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export async function openSwap(page, timeoutMs) {
|
|
423
|
+
const sourceAmount = dataTestId('from-amount');
|
|
424
|
+
const sourceToken = dataTestId('bridge-source-button');
|
|
425
|
+
const destinationToken = dataTestId('bridge-destination-button');
|
|
426
|
+
const bottomNav = dataTestId('bottom-nav-swaps');
|
|
427
|
+
const deadline = Date.now() + timeoutMs;
|
|
428
|
+
if (
|
|
429
|
+
await hasVisibleSelector(page, sourceAmount) &&
|
|
430
|
+
await hasVisibleSelector(page, sourceToken) &&
|
|
431
|
+
await hasVisibleSelector(page, destinationToken)
|
|
432
|
+
) {
|
|
433
|
+
return { method: 'visible-ui', href: await currentHref(page), steps: [] };
|
|
434
|
+
}
|
|
435
|
+
const home = await openHome(page, Math.max(1, deadline - Date.now()));
|
|
436
|
+
let navigation;
|
|
437
|
+
let lastActivationAt = 0;
|
|
438
|
+
let observedHref = await currentHref(page);
|
|
439
|
+
while (Date.now() < deadline) {
|
|
440
|
+
if (
|
|
441
|
+
await hasVisibleSelector(page, sourceAmount) &&
|
|
442
|
+
await hasVisibleSelector(page, sourceToken) &&
|
|
443
|
+
await hasVisibleSelector(page, destinationToken)
|
|
444
|
+
) break;
|
|
445
|
+
const href = await currentHref(page);
|
|
446
|
+
if (href !== observedHref) {
|
|
447
|
+
observedHref = href;
|
|
448
|
+
lastActivationAt = Date.now();
|
|
449
|
+
}
|
|
450
|
+
if (await hasVisibleSelector(page, bottomNav) && (!navigation || Date.now() - lastActivationAt >= 1000)) {
|
|
451
|
+
navigation = await activateSemanticControl(page, bottomNav);
|
|
452
|
+
lastActivationAt = Date.now();
|
|
453
|
+
}
|
|
454
|
+
await pauseForUi(page);
|
|
455
|
+
}
|
|
456
|
+
if (
|
|
457
|
+
!(await hasVisibleSelector(page, sourceAmount)) ||
|
|
458
|
+
!(await hasVisibleSelector(page, sourceToken)) ||
|
|
459
|
+
!(await hasVisibleSelector(page, destinationToken))
|
|
460
|
+
) {
|
|
461
|
+
throw new Error('Swap inputs did not appear after visible Swap navigation.');
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
navigation,
|
|
465
|
+
method: 'visible-ui',
|
|
466
|
+
href: await currentHref(page),
|
|
467
|
+
selector: bottomNav,
|
|
468
|
+
steps: [...home.steps, bottomNav],
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
242
472
|
export async function navigateExtensionUi(input) {
|
|
243
473
|
return withExtensionPage(input, async (page) => {
|
|
244
474
|
const intent = pageIntent(input.node);
|
|
245
475
|
if (intent) {
|
|
246
476
|
const timeoutMs = Number(input.node?.timeout_ms ?? 20000);
|
|
477
|
+
if (intent.page === 'back') {
|
|
478
|
+
const result = await goBack(page, timeoutMs);
|
|
479
|
+
return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
|
|
480
|
+
}
|
|
247
481
|
if (intent.page === 'home') {
|
|
248
482
|
const result = await openHome(page, timeoutMs);
|
|
249
483
|
return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
|
|
@@ -252,6 +486,28 @@ export async function navigateExtensionUi(input) {
|
|
|
252
486
|
const result = await openPerpsHome(page, timeoutMs);
|
|
253
487
|
return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
|
|
254
488
|
}
|
|
489
|
+
if (intent.page === 'perps-market-list') {
|
|
490
|
+
const result = await openPerpsMarketList(page, timeoutMs);
|
|
491
|
+
return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
|
|
492
|
+
}
|
|
493
|
+
if (intent.page === 'perps-activity') {
|
|
494
|
+
const result = await openPerpsActivity(page, timeoutMs);
|
|
495
|
+
return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
|
|
496
|
+
}
|
|
497
|
+
if (intent.page === 'network') {
|
|
498
|
+
const result = await openNetwork(
|
|
499
|
+
page,
|
|
500
|
+
intent.network,
|
|
501
|
+
intent.chainId,
|
|
502
|
+
intent.addIfMissing,
|
|
503
|
+
timeoutMs,
|
|
504
|
+
);
|
|
505
|
+
return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
|
|
506
|
+
}
|
|
507
|
+
if (intent.page === 'swap') {
|
|
508
|
+
const result = await openSwap(page, timeoutMs);
|
|
509
|
+
return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
|
|
510
|
+
}
|
|
255
511
|
const result = await openPerpsMarket(page, intent.market, timeoutMs);
|
|
256
512
|
return { action: input.action, ...intent, ...result, proofPath: 'ui-navigation' };
|
|
257
513
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
3
3
|
import { walletFixturePath } from '../../harness-exports.mjs';
|
|
4
4
|
|
|
5
5
|
async function fixturePassword(projectRoot) {
|
|
@@ -32,11 +32,7 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
|
32
32
|
|
|
33
33
|
const password = input.node?.password ?? await fixturePassword(input.context.projectRoot);
|
|
34
34
|
await page.setInput('input[type="password"]', String(password));
|
|
35
|
-
await page.
|
|
36
|
-
const button = document.querySelector('button[type="submit"]') || Array.from(document.querySelectorAll('button')).find((candidate) => /unlock/i.test(candidate.innerText || candidate.textContent || ''));
|
|
37
|
-
if (!button) throw new Error('Unlock button not found.');
|
|
38
|
-
button.click();
|
|
39
|
-
})()`);
|
|
35
|
+
await page.click(dataTestId('unlock-submit'));
|
|
40
36
|
await page.waitForExpression(`!document.querySelector('input[type="password"]')`, { timeoutMs: Number(input.node?.timeout_ms ?? 15000) });
|
|
41
37
|
return { action: input.action, unlocked: true, redacted: true, proofPath: 'extension-password-unlock' };
|
|
42
38
|
}));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveWalletImportCredentials, validateWalletImportOptions, walletImportMethod } from '../../shared/wallet/import-source.mjs';
|
|
2
2
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
3
|
+
import { pasteSecretRecoveryPhrase, setSecretInput } from './secret-input.mjs';
|
|
3
4
|
import { readWalletStateExpression } from './state.mjs';
|
|
4
5
|
|
|
5
6
|
const SELECTORS = {
|
|
@@ -54,42 +55,6 @@ async function waitForFirstSelector(page, selectors, timeoutMs) {
|
|
|
54
55
|
throw new Error('Wallet import timed out waiting for the next onboarding screen.');
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
async function setSecretInput(page, selector, value) {
|
|
58
|
-
await page.evaluate(`(() => {
|
|
59
|
-
const element = document.querySelector(${JSON.stringify(selector)});
|
|
60
|
-
if (!element) throw new Error('Wallet import secret input was not found.');
|
|
61
|
-
element.focus();
|
|
62
|
-
if (typeof element.select === 'function') element.select();
|
|
63
|
-
})()`);
|
|
64
|
-
await page.session.call('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace' });
|
|
65
|
-
await page.session.call('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace' });
|
|
66
|
-
await page.session.call('Input.insertText', { text: value });
|
|
67
|
-
const matches = await page.evaluate(`(() => {
|
|
68
|
-
const element = document.querySelector(${JSON.stringify(selector)});
|
|
69
|
-
if (!element) return false;
|
|
70
|
-
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
71
|
-
return element.value === ${JSON.stringify(value)};
|
|
72
|
-
})()`);
|
|
73
|
-
if (!matches) throw new Error('Wallet import secret input did not retain the supplied value.');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function pasteSecretInput(page, selector, value) {
|
|
77
|
-
const pasted = await page.evaluate(`(() => {
|
|
78
|
-
const element = document.querySelector(${JSON.stringify(selector)});
|
|
79
|
-
if (!element) return false;
|
|
80
|
-
const clipboardData = new DataTransfer();
|
|
81
|
-
clipboardData.setData('text', ${JSON.stringify(value)});
|
|
82
|
-
element.focus();
|
|
83
|
-
element.dispatchEvent(new ClipboardEvent('paste', {
|
|
84
|
-
bubbles: true,
|
|
85
|
-
cancelable: true,
|
|
86
|
-
clipboardData,
|
|
87
|
-
}));
|
|
88
|
-
return true;
|
|
89
|
-
})()`);
|
|
90
|
-
if (!pasted) throw new Error('Wallet import Secret Recovery Phrase input was not found.');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
58
|
async function waitForEnabled(page, selector, timeoutMs) {
|
|
94
59
|
const deadline = Date.now() + timeoutMs;
|
|
95
60
|
while (Date.now() <= deadline) {
|
|
@@ -120,17 +85,19 @@ async function importThroughUi(page, input, timeoutMs) {
|
|
|
120
85
|
if (interests.length > 0) {
|
|
121
86
|
throw new Error('metamask.wallet.import interests are supported only on Mobile.');
|
|
122
87
|
}
|
|
123
|
-
await page
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
88
|
+
if (!(await hasSelector(page, SELECTORS.srp))) {
|
|
89
|
+
await page.click(SELECTORS.importWallet);
|
|
90
|
+
const importChoice = await waitForFirstSelector(
|
|
91
|
+
page,
|
|
92
|
+
[SELECTORS.importWithSrp, SELECTORS.srp],
|
|
93
|
+
timeoutMs,
|
|
94
|
+
);
|
|
95
|
+
if (importChoice === SELECTORS.importWithSrp) {
|
|
96
|
+
await page.click(SELECTORS.importWithSrp);
|
|
97
|
+
}
|
|
98
|
+
await waitForSelector(page, SELECTORS.srp, timeoutMs);
|
|
131
99
|
}
|
|
132
|
-
await
|
|
133
|
-
await pasteSecretInput(page, SELECTORS.srp, credentials.mnemonic);
|
|
100
|
+
await pasteSecretRecoveryPhrase(page, SELECTORS.srp, credentials.mnemonic, timeoutMs);
|
|
134
101
|
await waitForEnabled(page, SELECTORS.confirmSrp, timeoutMs);
|
|
135
102
|
await page.click(SELECTORS.confirmSrp);
|
|
136
103
|
await waitForSelector(page, SELECTORS.password, timeoutMs);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
const MENU = dataTestId('account-options-menu-button');
|
|
4
|
+
const LOCK = dataTestId('global-menu-lock');
|
|
5
|
+
const UNLOCK_PAGE = dataTestId('unlock-page');
|
|
6
|
+
|
|
7
|
+
async function visible(page, selector) {
|
|
8
|
+
return page.evaluate(`(() => {
|
|
9
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
10
|
+
if (!element) return false;
|
|
11
|
+
const style = getComputedStyle(element);
|
|
12
|
+
const rect = element.getBoundingClientRect();
|
|
13
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
14
|
+
})()`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
18
|
+
if (!(await visible(page, LOCK))) await page.click(MENU);
|
|
19
|
+
const deadline = Date.now() + Number(input.node?.timeout_ms ?? 15_000);
|
|
20
|
+
while (!(await visible(page, LOCK))) {
|
|
21
|
+
if (Date.now() >= deadline) throw new Error('Wallet lock control did not become visible.');
|
|
22
|
+
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 100))');
|
|
23
|
+
}
|
|
24
|
+
await page.click(LOCK);
|
|
25
|
+
while (!(await visible(page, UNLOCK_PAGE))) {
|
|
26
|
+
if (Date.now() >= deadline) throw new Error('Wallet did not reach the unlock page after locking.');
|
|
27
|
+
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 100))');
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
action: input.action,
|
|
31
|
+
locked: true,
|
|
32
|
+
route: await page.evaluate('location.hash'),
|
|
33
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
34
|
+
};
|
|
35
|
+
}));
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export async function setSecretInput(page, selector, value) {
|
|
2
|
+
const found = await page.evaluate(`(() => {
|
|
3
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
4
|
+
if (!element) return false;
|
|
5
|
+
element.focus();
|
|
6
|
+
if (typeof element.select === 'function') element.select();
|
|
7
|
+
return true;
|
|
8
|
+
})()`);
|
|
9
|
+
if (!found) throw new Error('Wallet import secret input was not found.');
|
|
10
|
+
|
|
11
|
+
await page.session.call('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace' });
|
|
12
|
+
await page.session.call('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace' });
|
|
13
|
+
await page.session.call('Input.insertText', { text: value });
|
|
14
|
+
|
|
15
|
+
const retainedLength = await page.evaluate(`(() => {
|
|
16
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
17
|
+
if (!element) return -1;
|
|
18
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
19
|
+
return typeof element.value === 'string' ? element.value.length : -1;
|
|
20
|
+
})()`);
|
|
21
|
+
if (retainedLength !== value.length) {
|
|
22
|
+
throw new Error('Wallet import secret input did not retain the supplied value.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function pasteSecretRecoveryPhrase(page, selector, value, timeoutMs) {
|
|
27
|
+
const bridgeKey = 'mm-harness.wallet.recovery-phrase-paste';
|
|
28
|
+
const preload = await page.session.call('Page.addScriptToEvaluateOnNewDocument', {
|
|
29
|
+
source: `(() => {
|
|
30
|
+
const bridgeKey = Symbol.for(${JSON.stringify(bridgeKey)});
|
|
31
|
+
const NativeDataTransfer = globalThis.DataTransfer;
|
|
32
|
+
const NativeClipboardEvent = globalThis.ClipboardEvent;
|
|
33
|
+
Object.defineProperty(globalThis, bridgeKey, {
|
|
34
|
+
configurable: true,
|
|
35
|
+
value(selector) {
|
|
36
|
+
const element = document.querySelector(selector);
|
|
37
|
+
if (!element || typeof element.value !== 'string') return false;
|
|
38
|
+
const clipboardData = new NativeDataTransfer();
|
|
39
|
+
clipboardData.setData('text', element.value);
|
|
40
|
+
element.focus();
|
|
41
|
+
element.dispatchEvent(new NativeClipboardEvent('paste', {
|
|
42
|
+
bubbles: true,
|
|
43
|
+
cancelable: true,
|
|
44
|
+
clipboardData,
|
|
45
|
+
}));
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
})();`,
|
|
50
|
+
});
|
|
51
|
+
if (typeof preload?.identifier !== 'string' || preload.identifier.length === 0) {
|
|
52
|
+
throw new Error('Wallet import recovery phrase preload was not installed.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let actionError;
|
|
56
|
+
try {
|
|
57
|
+
await page.session.call('Page.reload', { ignoreCache: false });
|
|
58
|
+
const deadline = Date.now() + timeoutMs;
|
|
59
|
+
while (Date.now() <= deadline) {
|
|
60
|
+
const found = await page.evaluate(
|
|
61
|
+
`Boolean(document.querySelector(${JSON.stringify(selector)}))`,
|
|
62
|
+
).catch(() => false);
|
|
63
|
+
if (found) break;
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
65
|
+
}
|
|
66
|
+
await setSecretInput(page, selector, value);
|
|
67
|
+
const pasted = await page.evaluate(`(() => {
|
|
68
|
+
const bridge = globalThis[Symbol.for(${JSON.stringify(bridgeKey)})];
|
|
69
|
+
return typeof bridge === 'function' && bridge(${JSON.stringify(selector)});
|
|
70
|
+
})()`);
|
|
71
|
+
if (!pasted) throw new Error('Wallet import Secret Recovery Phrase input was not found.');
|
|
72
|
+
} catch (error) {
|
|
73
|
+
actionError = error;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const cleanupErrors = [];
|
|
77
|
+
try {
|
|
78
|
+
const removed = await page.evaluate(`(() => {
|
|
79
|
+
const key = Symbol.for(${JSON.stringify(bridgeKey)});
|
|
80
|
+
delete globalThis[key];
|
|
81
|
+
return !(key in globalThis);
|
|
82
|
+
})()`);
|
|
83
|
+
if (removed !== true) cleanupErrors.push('page bridge removal was not confirmed');
|
|
84
|
+
} catch (error) {
|
|
85
|
+
cleanupErrors.push(error instanceof Error ? error.message : String(error));
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
await page.session.call('Page.removeScriptToEvaluateOnNewDocument', {
|
|
89
|
+
identifier: preload.identifier,
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
cleanupErrors.push(error instanceof Error ? error.message : String(error));
|
|
93
|
+
}
|
|
94
|
+
if (cleanupErrors.length > 0) {
|
|
95
|
+
throw new Error(`Wallet import recovery phrase bridge cleanup failed: ${cleanupErrors.join('; ')}.`);
|
|
96
|
+
}
|
|
97
|
+
if (actionError) throw actionError;
|
|
98
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
listWalletAccountsExpression,
|
|
4
|
+
readWalletStateExpression,
|
|
5
|
+
selectWalletAccountExpression,
|
|
6
|
+
} from './state.mjs';
|
|
3
7
|
|
|
4
8
|
function normalize(value) {
|
|
5
9
|
return String(value ?? '').toLowerCase();
|
|
@@ -12,10 +16,70 @@ runAdapter((input) => withExtensionPage(input, async (page) => {
|
|
|
12
16
|
if (!requestedAddress && !requestedId && !requestedName) {
|
|
13
17
|
throw new Error('metamask.wallet.select_account extension live adapter requires node.address, node.id, or node.name.');
|
|
14
18
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
try {
|
|
20
|
+
const selected = await page.evaluate(selectWalletAccountExpression({
|
|
21
|
+
address: requestedAddress,
|
|
22
|
+
id: requestedId,
|
|
23
|
+
name: requestedName,
|
|
24
|
+
}), { awaitPromise: true });
|
|
25
|
+
return { action: input.action, selected, proofPath: 'extension-background-account-selection' };
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (!String(error?.message ?? error).includes('submitRequestToBackground is unavailable')) throw error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let searchAddress = requestedAddress;
|
|
31
|
+
if (!searchAddress) {
|
|
32
|
+
const listed = await page.evaluate(listWalletAccountsExpression('all'), { awaitPromise: true });
|
|
33
|
+
const account = listed.accounts.find((entry) =>
|
|
34
|
+
(requestedId && entry.id === requestedId) ||
|
|
35
|
+
(requestedName && entry.name === requestedName));
|
|
36
|
+
searchAddress = normalize(account?.address);
|
|
37
|
+
}
|
|
38
|
+
if (!searchAddress) throw new Error('Requested account does not have a searchable EVM address.');
|
|
39
|
+
|
|
40
|
+
await page.navigateHash('#/account-list');
|
|
41
|
+
const searchSelector = 'input[data-testid="multichain-account-list-search"], [data-testid="multichain-account-list-search"] input';
|
|
42
|
+
await page.waitForSelector(searchSelector);
|
|
43
|
+
await page.setInput(searchSelector, searchAddress);
|
|
44
|
+
|
|
45
|
+
const rowDeadline = Date.now() + 15_000;
|
|
46
|
+
let row;
|
|
47
|
+
while (Date.now() <= rowDeadline) {
|
|
48
|
+
row = await page.evaluate(`(() => {
|
|
49
|
+
const rows = [...document.querySelectorAll('.multichain-account-cell[data-testid^="multichain-account-cell-"]')]
|
|
50
|
+
.filter((element) => {
|
|
51
|
+
const style = getComputedStyle(element);
|
|
52
|
+
const rect = element.getBoundingClientRect();
|
|
53
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
count: rows.length,
|
|
57
|
+
testId: rows.length === 1 ? rows[0].getAttribute('data-testid') : null,
|
|
58
|
+
label: rows.length === 1 ? String(rows[0].innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500) : null,
|
|
59
|
+
};
|
|
60
|
+
})()`);
|
|
61
|
+
if (row.count === 1 && row.testId) break;
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
63
|
+
}
|
|
64
|
+
if (row?.count !== 1 || !row.testId) {
|
|
65
|
+
throw new Error(`Expected one visible account for the requested address, observed ${row?.count ?? 0}.`);
|
|
66
|
+
}
|
|
67
|
+
await page.click(`[data-testid=${JSON.stringify(row.testId)}]`);
|
|
68
|
+
|
|
69
|
+
const selectedDeadline = Date.now() + 15_000;
|
|
70
|
+
let selected;
|
|
71
|
+
while (Date.now() <= selectedDeadline) {
|
|
72
|
+
const state = await page.evaluate(readWalletStateExpression(), { awaitPromise: true });
|
|
73
|
+
selected = state.selectedAccount;
|
|
74
|
+
if (normalize(selected?.address) === searchAddress) {
|
|
75
|
+
return {
|
|
76
|
+
action: input.action,
|
|
77
|
+
selected,
|
|
78
|
+
visibleAccount: row,
|
|
79
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Visible account selection did not select ${searchAddress}.`);
|
|
21
85
|
}));
|