@deeeed/metamask-harness 0.37.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 +27 -0
- package/adapters/extension/artifact-runtime-state.cjs +13 -3
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +2 -0
- package/adapters/extension/live.sh +1 -1
- package/adapters/extension/readiness.mjs +1 -1
- package/adapters/manifest.json +8 -0
- package/dist/adapters/extension/runtime-decision.js +4 -1
- package/dist/adapters/extension/runtime.js +42 -3
- package/dist/adapters/mobile/provision.js +38 -3
- package/dist/adapters.js +68 -3
- package/dist/app-lifecycle.js +4 -3
- package/dist/commands/call.js +4 -1
- package/dist/commands/launch/extension.js +21 -5
- package/dist/commands/launch/index.js +3 -0
- package/dist/commands/run-engine.js +40 -29
- package/dist/commands/run.js +1 -1
- package/dist/recipe-security.js +40 -0
- package/dist/runner.js +3 -0
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +116 -141
- 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 +194 -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_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 +104 -1
- 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/lock.mjs +35 -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 +5 -3
- 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 +45 -5
- package/library/actions/mobile/ui/navigate.mjs +3 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +17 -0
- package/library/actions/mobile/wallet/native-ui.mjs +21 -2
- 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/manifests/extension.action-manifest.json +1255 -189
- package/library/manifests/mobile.action-manifest.json +219 -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/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/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
|
}));
|
|
@@ -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
|
+
}));
|
|
@@ -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
|
}));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { runAdapter } from '../platform/bridge.mjs';
|
|
2
|
+
import { observeNativeUi } from '../platform/observe-ui.mjs';
|
|
3
|
+
import {
|
|
4
|
+
normalizeVisibleAssetsState,
|
|
5
|
+
resolveVisibleAssetsOptions,
|
|
6
|
+
} from '../../shared/assets/visible-state.mjs';
|
|
7
|
+
|
|
8
|
+
runAdapter(async (input) => {
|
|
9
|
+
const options = resolveVisibleAssetsOptions(input.node);
|
|
10
|
+
const observed = await observeNativeUi(
|
|
11
|
+
{ refs: ['ui.screen', 'ui.visible'], node: input.node },
|
|
12
|
+
input.context,
|
|
13
|
+
);
|
|
14
|
+
const screen = observed.observations?.['ui.screen'];
|
|
15
|
+
const visible = observed.observations?.['ui.visible'];
|
|
16
|
+
if (!screen || !visible) {
|
|
17
|
+
const warning = observed.warnings?.map((entry) => entry.message).filter(Boolean).join('; ');
|
|
18
|
+
throw new Error(`Mobile visible Assets state is unavailable${warning ? `: ${warning}` : '.'}`);
|
|
19
|
+
}
|
|
20
|
+
const items = [...(visible.items ?? []), ...(visible.hidden_or_offscreen ?? [])]
|
|
21
|
+
.filter((item) => String(item.test_id ?? '').startsWith('asset-'))
|
|
22
|
+
.map((item) => ({
|
|
23
|
+
testId: item.test_id,
|
|
24
|
+
label: item.label,
|
|
25
|
+
symbol: String(item.test_id).slice('asset-'.length),
|
|
26
|
+
}));
|
|
27
|
+
return {
|
|
28
|
+
action: input.action,
|
|
29
|
+
...normalizeVisibleAssetsState({
|
|
30
|
+
route: screen.route,
|
|
31
|
+
title: screen.title,
|
|
32
|
+
items,
|
|
33
|
+
truncated: visible.truncated,
|
|
34
|
+
}, options, 'mobile'),
|
|
35
|
+
proofPath: 'native-accessibility',
|
|
36
|
+
};
|
|
37
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { runAdapter } from '../platform/bridge.mjs';
|
|
2
|
+
import { observeNativeUi } from '../platform/observe-ui.mjs';
|
|
3
|
+
import {
|
|
4
|
+
normalizeVisiblePerpsState,
|
|
5
|
+
resolveVisibleStateOptions,
|
|
6
|
+
} from '../../shared/perps/visible-state.mjs';
|
|
7
|
+
|
|
8
|
+
runAdapter(async (input) => {
|
|
9
|
+
const options = resolveVisibleStateOptions(input.node);
|
|
10
|
+
const observed = await observeNativeUi(
|
|
11
|
+
{ refs: ['ui.screen', 'ui.visible'], node: input.node },
|
|
12
|
+
input.context,
|
|
13
|
+
);
|
|
14
|
+
const screen = observed.observations?.['ui.screen'];
|
|
15
|
+
const visible = observed.observations?.['ui.visible'];
|
|
16
|
+
if (!screen || !visible) {
|
|
17
|
+
const warning = observed.warnings?.map((entry) => entry.message).filter(Boolean).join('; ');
|
|
18
|
+
throw new Error(`Mobile visible Perps state is unavailable${warning ? `: ${warning}` : '.'}`);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
action: input.action,
|
|
22
|
+
...normalizeVisiblePerpsState({
|
|
23
|
+
route: screen.route,
|
|
24
|
+
title: screen.title,
|
|
25
|
+
items: visible.items,
|
|
26
|
+
offscreenItems: visible.hidden_or_offscreen,
|
|
27
|
+
truncated: visible.truncated,
|
|
28
|
+
}, options, 'mobile'),
|
|
29
|
+
proofPath: 'native-accessibility',
|
|
30
|
+
};
|
|
31
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export function nativeSessionName(env, device, app = 'io.metamask') {
|
|
2
|
+
const configured = String(env.METAMASK_RECIPE_MOBILE_SESSION ?? '').trim();
|
|
3
|
+
const source = configured || `mm-harness-${app}-${device}`;
|
|
4
|
+
return source.replace(/[^A-Za-z0-9._-]/gu, '-').slice(0, 120);
|
|
5
|
+
}
|
|
@@ -3,6 +3,7 @@ import { promisify } from 'node:util';
|
|
|
3
3
|
import { createAgentDeviceClient } from 'agent-device';
|
|
4
4
|
|
|
5
5
|
import { bridgeCommand } from './bridge.mjs';
|
|
6
|
+
import { nativeSessionName } from './native-session-name.mjs';
|
|
6
7
|
import { mobileToolRecovery, resolveMobileToolPath } from './tool-paths.mjs';
|
|
7
8
|
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
@@ -140,7 +141,8 @@ async function readAndroidHierarchy(node, env) {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
async function readAndroidAgentDeviceHierarchy(serial, env) {
|
|
143
|
-
const
|
|
144
|
+
const app = text(env.ANDROID_PACKAGE_ID) ?? 'io.metamask';
|
|
145
|
+
const session = nativeSessionName(env, serial, app);
|
|
144
146
|
const client = createAgentDeviceClient({
|
|
145
147
|
session,
|
|
146
148
|
lockPolicy: 'reject',
|
|
@@ -152,13 +154,13 @@ async function readAndroidAgentDeviceHierarchy(serial, env) {
|
|
|
152
154
|
target: 'mobile',
|
|
153
155
|
serial,
|
|
154
156
|
session,
|
|
155
|
-
app
|
|
157
|
+
app,
|
|
156
158
|
interactiveOnly: false,
|
|
157
159
|
forceFull: true,
|
|
158
160
|
});
|
|
159
161
|
const hierarchy = flattenAgentDeviceHierarchy(
|
|
160
162
|
snapshot.nodes,
|
|
161
|
-
|
|
163
|
+
app,
|
|
162
164
|
);
|
|
163
165
|
if (hierarchy.nodeCount === 0) {
|
|
164
166
|
throw new Error('Agent Device returned an empty Android accessibility hierarchy.');
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createAgentDeviceUiTransport } from '@farmslot/expo-recipe';
|
|
2
|
+
import { createAgentDeviceClient } from 'agent-device';
|
|
3
|
+
import { nativeSessionName } from '../platform/native-session-name.mjs';
|
|
4
|
+
|
|
5
|
+
export function createSwapNativeSession(input) {
|
|
6
|
+
const env = { ...process.env, ...(input.context?.env ?? {}) };
|
|
7
|
+
const device = String(env.ADB_SERIAL ?? env.ANDROID_SERIAL ?? '').trim();
|
|
8
|
+
if (!device) throw new Error('Opaque Mobile Swap actions require one explicit Android device serial.');
|
|
9
|
+
const app = String(env.ANDROID_PACKAGE_ID ?? 'io.metamask');
|
|
10
|
+
const session = nativeSessionName(env, device, app);
|
|
11
|
+
const client = createAgentDeviceClient({ session, lockPolicy: 'reject', lockPlatform: 'android' });
|
|
12
|
+
const selection = { platform: 'android', target: 'mobile', serial: device, session };
|
|
13
|
+
const transport = createAgentDeviceUiTransport({ platform: 'android', device, app, session, client });
|
|
14
|
+
const context = {
|
|
15
|
+
nodeId: String(input.context?.nodeId ?? 'action'),
|
|
16
|
+
projectRoot: input.context.projectRoot,
|
|
17
|
+
artifactsDir: input.context.artifactsDir,
|
|
18
|
+
env,
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
client,
|
|
22
|
+
transport,
|
|
23
|
+
selection,
|
|
24
|
+
context,
|
|
25
|
+
app,
|
|
26
|
+
execute: (action, node) => transport.execute(action, node, context),
|
|
27
|
+
snapshot: () => client.capture.snapshot({ ...selection, app, interactiveOnly: false, forceFull: true }),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function closeSwapNativeSession(session) {
|
|
32
|
+
try {
|
|
33
|
+
await session.transport.close();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (!/No active session|SESSION_NOT_FOUND/iu.test(String(error?.message ?? error))) throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function nativeNode(snapshot, identifier) {
|
|
40
|
+
return snapshot.nodes.find((node) => String(node.identifier ?? '') === identifier);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function nativeLabel(node) {
|
|
44
|
+
return String(node?.label ?? node?.value ?? '').replace(/\s+/gu, ' ').trim();
|
|
45
|
+
}
|