@deeeed/metamask-harness 0.39.0 → 0.40.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 +46 -0
- package/adapters/extension/lib/playwright-cdp.cjs +17 -1
- package/adapters/extension/sidepanel-toggle.sh +135 -13
- package/dist/adapters/mobile/release-artifact.js +65 -0
- package/dist/adapters.js +8 -7
- package/dist/command-contract.js +10 -2
- package/dist/commands/call.js +7 -4
- package/dist/commands/launch/index.js +55 -3
- package/dist/commands/parse-args.js +6 -0
- package/dist/commands/run-engine.js +75 -39
- package/dist/funding-execution-context.js +1386 -0
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +170 -3
- package/dist/mm-harness-cli.js +8 -2
- package/dist/recipe-security.js +5 -12
- package/dist/runner.js +102 -6
- package/docs/RECIPES.md +74 -1
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +6 -5
- package/library/actions/extension/networks/add_chainlist.mjs +4 -0
- package/library/actions/extension/networks/add_custom.mjs +4 -0
- package/library/actions/extension/networks/custom_network.mjs +48 -1
- package/library/actions/extension/perps/assert_visible_consistency.mjs +366 -0
- package/library/actions/extension/perps/close_visible_position.mjs +299 -37
- package/library/actions/extension/perps/compare_provider_market.mjs +77 -0
- package/library/actions/extension/perps/edit_margin.mjs +285 -44
- package/library/actions/extension/perps/mutation-receipt.mjs +781 -0
- package/library/actions/extension/perps/perps.mjs +1029 -363
- package/library/actions/extension/perps/read_funds_confirmation.mjs +126 -13
- package/library/actions/extension/perps/read_snapshot.mjs +124 -3
- package/library/actions/extension/perps/read_visible_state.mjs +63 -5
- package/library/actions/extension/perps/select_activity_filter.mjs +81 -18
- package/library/actions/extension/perps/select_market_filter.mjs +48 -4
- package/library/actions/extension/perps/update_position_tpsl.mjs +251 -69
- package/library/actions/extension/perps/visible-mutation-identity.mjs +164 -0
- package/library/actions/extension/platform/cdp.mjs +88 -9
- package/library/actions/extension/settings/set_basic_functionality.mjs +35 -7
- package/library/actions/extension/ui/navigate.mjs +45 -5
- package/library/actions/extension/wallet/import.mjs +70 -29
- package/library/actions/extension/wallet/lock.mjs +62 -5
- package/library/actions/extension/wallet/visible-session.mjs +218 -0
- package/library/actions/mobile/ui/native-navigation.mjs +45 -0
- package/library/actions/mobile/wallet/import.mjs +17 -10
- package/library/actions/mobile/wallet/native-ui.mjs +21 -1
- package/library/actions/shared/perps/visible-state.mjs +25 -5
- package/library/manifests/extension.action-manifest.json +1696 -1271
- package/library/recipes/extension/assets/release-custom-network-token-persistence.recipe.json +1 -0
- package/library/recipes/{perps → extension/perps}/release-activity-filters.recipe.json +5 -5
- package/library/recipes/{perps → extension/perps}/release-funds-flows.recipe.json +3 -3
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +24 -19
- package/library/recipes/extension/perps/release-market-filters.recipe.json +97 -0
- package/library/recipes/extension/perps/release-order-entry.recipe.json +4 -1
- package/library/recipes/extension/perps/release-order-validation.recipe.json +53 -14
- package/library/recipes/extension/perps/release-visible-consistency.recipe.json +47 -0
- package/library/recipes/extension/perps/source-dev-snapshot-consistency.recipe.json +76 -0
- package/library/recipes/perps/release-live-market-order.recipe.json +30 -13
- package/library/recipes/perps/release-market-details.recipe.json +1 -1
- package/library/recipes/swap-bridge/release-quote.recipe.json +1 -0
- package/library/recipes/wallet/import.recipe.json +4 -14
- package/package.json +1 -1
- package/library/actions/extension/assets/finish_send.mjs +0 -128
- package/library/actions/extension/swap_bridge/submit_transaction.mjs +0 -363
- package/library/actions/extension/ui/activate_and_observe.mjs +0 -96
- package/library/recipes/assets/release-custom-gas-send.recipe.json +0 -88
- package/library/recipes/assets/release-native-max-cancel.recipe.json +0 -70
- package/library/recipes/assets/release-native-max-send.recipe.json +0 -78
- package/library/recipes/assets/release-own-account-send.recipe.json +0 -80
- package/library/recipes/extension/runner/action-validation.recipe.json +0 -343
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const ACCOUNT_SEARCH = 'input[data-testid="multichain-account-list-search"], [data-testid="multichain-account-list-search"] input';
|
|
4
|
+
const ONBOARDING_COMPLETE = '[data-testid="onboarding-complete-done"]';
|
|
5
|
+
const HOME_BOUNDARY = '[data-testid="bottom-nav-home"], [data-testid="account-overview__asset-tab"]';
|
|
6
|
+
|
|
7
|
+
async function hasVisibleSelector(page, selector, deadline) {
|
|
8
|
+
const expression = `(() => {
|
|
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
|
+
if (deadline === undefined) return page.evaluate(expression);
|
|
16
|
+
const result = await page.session.call('Runtime.evaluate', {
|
|
17
|
+
expression,
|
|
18
|
+
awaitPromise: true,
|
|
19
|
+
returnByValue: true,
|
|
20
|
+
}, { timeoutMs: Math.min(1000, Math.max(1, deadline - Date.now())) });
|
|
21
|
+
if (result.exceptionDetails) throw new Error('Visible wallet selector probe failed.');
|
|
22
|
+
return result.result?.value === true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function dismissVisibleOnboardingCompletion(page) {
|
|
26
|
+
if (!(await hasVisibleSelector(page, ONBOARDING_COMPLETE))) return false;
|
|
27
|
+
await page.click(ONBOARDING_COMPLETE);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function accountProbeAddress(expectedAddress) {
|
|
32
|
+
const replacement = expectedAddress.endsWith('0') ? '1' : '0';
|
|
33
|
+
return `${expectedAddress.slice(0, -1)}${replacement}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function currentRoute(page, deadline) {
|
|
37
|
+
const history = await page.session.call('Page.getNavigationHistory', {}, {
|
|
38
|
+
timeoutMs: Math.min(1000, Math.max(1, deadline - Date.now())),
|
|
39
|
+
});
|
|
40
|
+
const url = String(history.entries?.[history.currentIndex]?.url ?? '');
|
|
41
|
+
const route = new URL(url).hash.split('?')[0];
|
|
42
|
+
return route.replace(/0x[0-9a-f]{40}/giu, '<redacted-address>').slice(0, 200);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function routeError(page, message, deadline) {
|
|
46
|
+
let route = '<deadline-expired>';
|
|
47
|
+
if (Date.now() < deadline) {
|
|
48
|
+
try {
|
|
49
|
+
route = await currentRoute(page, deadline);
|
|
50
|
+
} catch {
|
|
51
|
+
route = '<unavailable>';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return new Error(`${message} (route ${JSON.stringify(route)}).`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function visibleAccountMatch(page, deadline) {
|
|
58
|
+
const result = await page.session.call('Runtime.evaluate', {
|
|
59
|
+
expression: `(() => {
|
|
60
|
+
const input = document.querySelector(${JSON.stringify(ACCOUNT_SEARCH)});
|
|
61
|
+
const query = String(input?.value || '').trim().toLowerCase();
|
|
62
|
+
const rows = [...document.querySelectorAll('.multichain-account-cell[data-testid^="multichain-account-cell-"]')]
|
|
63
|
+
.filter((element) => {
|
|
64
|
+
const style = getComputedStyle(element);
|
|
65
|
+
const rect = element.getBoundingClientRect();
|
|
66
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
67
|
+
});
|
|
68
|
+
const row = rows.length === 1 ? rows[0] : null;
|
|
69
|
+
const accountName = String(row?.querySelector('.multichain-account-cell__account-name')?.textContent || '')
|
|
70
|
+
.trim()
|
|
71
|
+
.toLowerCase();
|
|
72
|
+
return {
|
|
73
|
+
query,
|
|
74
|
+
count: rows.length,
|
|
75
|
+
testId: row?.getAttribute('data-testid') || null,
|
|
76
|
+
selected: Boolean(row?.classList.contains('is-selected')),
|
|
77
|
+
addressMatchedBySearch: Boolean(/^0x[0-9a-f]{40}$/u.test(query) && !accountName.includes(query)),
|
|
78
|
+
};
|
|
79
|
+
})()`,
|
|
80
|
+
awaitPromise: true,
|
|
81
|
+
returnByValue: true,
|
|
82
|
+
}, { timeoutMs: Math.min(1000, Math.max(1, deadline - Date.now())) });
|
|
83
|
+
if (result.exceptionDetails || !result.result?.value) {
|
|
84
|
+
throw new Error('Visible wallet account probe failed.');
|
|
85
|
+
}
|
|
86
|
+
return result.result.value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function navigateHashThroughPage(page, hash, deadline) {
|
|
90
|
+
const pathname = new URL(String(page.target?.url ?? `${page.origin}/home.html`)).pathname;
|
|
91
|
+
const walletPath = pathname === '/sidepanel.html' ? pathname : '/home.html';
|
|
92
|
+
await page.session.call('Page.navigate', { url: `${page.origin}${walletPath}${hash}` }, {
|
|
93
|
+
timeoutMs: Math.min(1000, Math.max(1, deadline - Date.now())),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function withinDeadline(promise, deadline) {
|
|
98
|
+
const timeoutMs = Math.max(1, deadline - Date.now());
|
|
99
|
+
let timer;
|
|
100
|
+
try {
|
|
101
|
+
return await Promise.race([
|
|
102
|
+
promise,
|
|
103
|
+
new Promise((_, reject) => {
|
|
104
|
+
timer = setTimeout(() => reject(new Error('Visible wallet stage exceeded its shared deadline.')), timeoutMs);
|
|
105
|
+
}),
|
|
106
|
+
]);
|
|
107
|
+
} finally {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function leavePerpsForWalletHome(page, deadline) {
|
|
113
|
+
if (!(await currentRoute(page, deadline)).includes('/perps')) return;
|
|
114
|
+
while (Date.now() <= deadline) {
|
|
115
|
+
try {
|
|
116
|
+
await navigateHashThroughPage(page, '#/', deadline);
|
|
117
|
+
if (await hasVisibleSelector(page, HOME_BOUNDARY, deadline)) return;
|
|
118
|
+
} catch {
|
|
119
|
+
// Retry within the shared deadline while the authenticated Home surface settles.
|
|
120
|
+
}
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
122
|
+
}
|
|
123
|
+
throw await routeError(page, 'Visible wallet session did not reach Home before account-list navigation', deadline);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function openAccountList(page, deadline) {
|
|
127
|
+
await leavePerpsForWalletHome(page, deadline);
|
|
128
|
+
while (Date.now() <= deadline) {
|
|
129
|
+
try {
|
|
130
|
+
await navigateHashThroughPage(page, '#/account-list', deadline);
|
|
131
|
+
if (await hasVisibleSelector(page, ACCOUNT_SEARCH, deadline)) return;
|
|
132
|
+
} catch {
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw await routeError(page, 'Visible wallet session did not reach the authenticated account list', deadline);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function proveAddressSearchControl(page, expectedAddress, deadline) {
|
|
140
|
+
const probeAddress = accountProbeAddress(expectedAddress);
|
|
141
|
+
try {
|
|
142
|
+
await withinDeadline(page.setInput(ACCOUNT_SEARCH, probeAddress), deadline);
|
|
143
|
+
} catch {
|
|
144
|
+
throw await routeError(page, 'Visible wallet account search could not enter the alternate-address control', deadline);
|
|
145
|
+
}
|
|
146
|
+
let consecutiveEmpty = 0;
|
|
147
|
+
while (Date.now() <= deadline) {
|
|
148
|
+
let observed;
|
|
149
|
+
try {
|
|
150
|
+
observed = await visibleAccountMatch(page, deadline);
|
|
151
|
+
} catch {
|
|
152
|
+
throw await routeError(page, 'Visible wallet alternate-address result did not become readable', deadline);
|
|
153
|
+
}
|
|
154
|
+
if (observed.query === probeAddress && observed.count === 0) {
|
|
155
|
+
consecutiveEmpty += 1;
|
|
156
|
+
if (consecutiveEmpty === 2) return;
|
|
157
|
+
} else {
|
|
158
|
+
consecutiveEmpty = 0;
|
|
159
|
+
}
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
161
|
+
}
|
|
162
|
+
throw await routeError(page, 'Visible wallet account search did not reject the alternate address control', deadline);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function queryExpectedAccount(page, expectedAddress, deadline, requireSelected) {
|
|
166
|
+
await proveAddressSearchControl(page, expectedAddress, deadline);
|
|
167
|
+
try {
|
|
168
|
+
await withinDeadline(page.setInput(ACCOUNT_SEARCH, expectedAddress), deadline);
|
|
169
|
+
} catch {
|
|
170
|
+
throw await routeError(page, 'Visible wallet account search could not enter the derived release identity', deadline);
|
|
171
|
+
}
|
|
172
|
+
let observed;
|
|
173
|
+
while (Date.now() <= deadline) {
|
|
174
|
+
try {
|
|
175
|
+
observed = await visibleAccountMatch(page, deadline);
|
|
176
|
+
} catch {
|
|
177
|
+
throw await routeError(page, 'Visible wallet derived-account result did not become readable', deadline);
|
|
178
|
+
}
|
|
179
|
+
const exactMatch = observed.query === expectedAddress &&
|
|
180
|
+
observed.count === 1 &&
|
|
181
|
+
observed.testId &&
|
|
182
|
+
observed.addressMatchedBySearch;
|
|
183
|
+
if (exactMatch && (!requireSelected || observed.selected)) return observed;
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
185
|
+
}
|
|
186
|
+
if (requireSelected) {
|
|
187
|
+
throw await routeError(page, 'Visible wallet session did not converge to the derived release account', deadline);
|
|
188
|
+
}
|
|
189
|
+
throw await routeError(page, 'Visible wallet session did not resolve the derived release account to one rendered account row', deadline);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function ensureVisibleSelectedAccount(page, expectedAddress, timeoutMs) {
|
|
193
|
+
if (!/^0x[0-9a-f]{40}$/u.test(expectedAddress)) {
|
|
194
|
+
throw new Error('Visible wallet session requires a derived EVM account address.');
|
|
195
|
+
}
|
|
196
|
+
const deadline = Date.now() + timeoutMs;
|
|
197
|
+
await openAccountList(page, deadline);
|
|
198
|
+
const initial = await queryExpectedAccount(page, expectedAddress, deadline, false);
|
|
199
|
+
let selectionPerformed = false;
|
|
200
|
+
if (!initial.selected) {
|
|
201
|
+
selectionPerformed = true;
|
|
202
|
+
try {
|
|
203
|
+
await withinDeadline(page.click(`[data-testid=${JSON.stringify(initial.testId)}]`), deadline);
|
|
204
|
+
} catch {
|
|
205
|
+
throw await routeError(page, 'Visible wallet session could not select the derived release account', deadline);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await openAccountList(page, deadline);
|
|
210
|
+
await queryExpectedAccount(page, expectedAddress, deadline, true);
|
|
211
|
+
return {
|
|
212
|
+
selectedIdentitySha256: createHash('sha256').update(expectedAddress).digest('hex'),
|
|
213
|
+
selectedAddressMatched: true,
|
|
214
|
+
selectionPerformed,
|
|
215
|
+
accountSurface: 'visible-account-list',
|
|
216
|
+
redacted: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -22,6 +22,8 @@ const IDS = {
|
|
|
22
22
|
tutorialSkip: 'perps-tutorial-skip-button',
|
|
23
23
|
modeLite: 'perps-mode-selection-lite-option',
|
|
24
24
|
modePro: 'perps-mode-selection-pro-option',
|
|
25
|
+
modeToggleLite: 'perps-mode-toggle-lite',
|
|
26
|
+
modeTogglePro: 'perps-mode-toggle-pro',
|
|
25
27
|
market: 'perps-market-details-view',
|
|
26
28
|
marketBack: 'perps-market-header-back-button',
|
|
27
29
|
marketSubtitle: 'perps-market-header-subtitle',
|
|
@@ -109,6 +111,8 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
109
111
|
observedScreen = 'wallet-home';
|
|
110
112
|
} else if (destination.page === 'swap') {
|
|
111
113
|
observedScreen = await navigateSwap({ identifiers, press, wait, back });
|
|
114
|
+
} else if (destination.page === 'perps') {
|
|
115
|
+
observedScreen = await navigatePerpsHome({ identifiers, press, wait, back, timeoutMs: Number(input.node?.timeout_ms ?? 30_000), perpsMode });
|
|
112
116
|
} else if (destination.page === 'perps-market-list') {
|
|
113
117
|
observedScreen = await navigatePerpsMarketList({ identifiers, press, wait, back, timeoutMs: Number(input.node?.timeout_ms ?? 30_000), perpsMode });
|
|
114
118
|
} else {
|
|
@@ -265,6 +269,47 @@ async function navigatePerps({ identifiers, press, wait, back, timeoutMs, perpsM
|
|
|
265
269
|
return waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode);
|
|
266
270
|
}
|
|
267
271
|
|
|
272
|
+
async function navigatePerpsHome({ identifiers, press, wait, back, timeoutMs, perpsMode }) {
|
|
273
|
+
const deadline = Date.now() + timeoutMs;
|
|
274
|
+
let observed = await navigatePerps({ identifiers, press, wait, back, timeoutMs, perpsMode });
|
|
275
|
+
while (Date.now() <= deadline) {
|
|
276
|
+
let ids = await identifiers();
|
|
277
|
+
if (ids.has(IDS.perpsHome)) return IDS.perpsHome;
|
|
278
|
+
if (perpsMode === 'lite' && ids.has(IDS.proMarket)) {
|
|
279
|
+
if (!ids.has(IDS.modeTogglePro)) {
|
|
280
|
+
throw new Error('Opaque Mobile navigation reached Pro market details without the visible Pro mode control.');
|
|
281
|
+
}
|
|
282
|
+
await press(IDS.modeTogglePro);
|
|
283
|
+
observed = await waitForPerpsMode(identifiers, press, deadline, 'lite');
|
|
284
|
+
ids = await identifiers();
|
|
285
|
+
}
|
|
286
|
+
if (ids.has(IDS.market)) {
|
|
287
|
+
await press(IDS.marketBack);
|
|
288
|
+
} else if (ids.has(IDS.marketList)) {
|
|
289
|
+
await press(IDS.marketListClose);
|
|
290
|
+
} else {
|
|
291
|
+
observed = await navigatePerps({ identifiers, press, wait, back, timeoutMs: Math.max(1, deadline - Date.now()), perpsMode });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`Opaque Mobile navigation reached ${observed}, but did not converge to Perps Home.`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function waitForPerpsMode(identifiers, press, deadline, perpsMode) {
|
|
298
|
+
let selected = false;
|
|
299
|
+
while (Date.now() <= deadline) {
|
|
300
|
+
const ids = await identifiers();
|
|
301
|
+
const expected = perpsMode === 'pro' ? IDS.proMarket : IDS.market;
|
|
302
|
+
if (ids.has(expected)) return expected;
|
|
303
|
+
const option = perpsMode === 'pro' ? IDS.modePro : IDS.modeLite;
|
|
304
|
+
if (!selected && ids.has(option)) {
|
|
305
|
+
await press(option);
|
|
306
|
+
selected = true;
|
|
307
|
+
}
|
|
308
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`Opaque Mobile navigation did not visibly switch Perps to ${perpsMode} mode.`);
|
|
311
|
+
}
|
|
312
|
+
|
|
268
313
|
async function navigatePerpsMarketList({ identifiers, press, wait, back, timeoutMs, perpsMode }) {
|
|
269
314
|
let ids = await identifiers();
|
|
270
315
|
if (ids.has(IDS.marketList)) return IDS.marketList;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -45,6 +46,18 @@ function selectedAddress(status, input) {
|
|
|
45
46
|
return String(selectedEntry(status, input)?.account?.address ?? '').toLowerCase();
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
export function redactedSelectedIdentity(expectedAddress, actualAddress) {
|
|
50
|
+
const expected = String(expectedAddress ?? '').toLowerCase();
|
|
51
|
+
const actual = String(actualAddress ?? '').toLowerCase();
|
|
52
|
+
if (!expected || actual !== expected) {
|
|
53
|
+
throw new Error('Mobile wallet import did not visibly select the expected account.');
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
selectedIdentitySha256: createHash('sha256').update(expected).digest('hex'),
|
|
57
|
+
selectedAddressMatched: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
function walletReady(observed, input) {
|
|
49
62
|
const route = String(selectedEntry(observed, input)?.route?.name ?? '');
|
|
50
63
|
return Boolean(
|
|
@@ -199,9 +212,7 @@ async function importThroughUi(input, timeoutMs, options) {
|
|
|
199
212
|
observed = await status(input);
|
|
200
213
|
}
|
|
201
214
|
const actualAddress = selectedAddress(observed, input);
|
|
202
|
-
|
|
203
|
-
throw new Error(`Wallet import selected ${actualAddress || 'no address'}, expected ${credentials.expectedAddress}.`);
|
|
204
|
-
}
|
|
215
|
+
redactedSelectedIdentity(credentials.expectedAddress, actualAddress);
|
|
205
216
|
const { consent } = await setMobileConsent(
|
|
206
217
|
input,
|
|
207
218
|
metametricsEnabled,
|
|
@@ -255,7 +266,7 @@ runAdapter(async (input) => {
|
|
|
255
266
|
method: proof.alreadyReady ? 'native-profile' : 'native-ui-import',
|
|
256
267
|
credentialSource: credentials.source,
|
|
257
268
|
credentialSourceName: credentials.sourceName,
|
|
258
|
-
|
|
269
|
+
...redactedSelectedIdentity(credentials.expectedAddress, proof.selectedAddress),
|
|
259
270
|
metametricsRequested: metametricsEnabled,
|
|
260
271
|
biometricsRequested: biometricsEnabled,
|
|
261
272
|
biometricPromptDismissed: proof.biometricPromptDismissed ?? false,
|
|
@@ -294,7 +305,7 @@ runAdapter(async (input) => {
|
|
|
294
305
|
method: 'ui-import',
|
|
295
306
|
credentialSource: imported.credentials.source,
|
|
296
307
|
credentialSourceName: imported.credentials.sourceName,
|
|
297
|
-
|
|
308
|
+
...redactedSelectedIdentity(imported.credentials.expectedAddress, importedEntry.account.address),
|
|
298
309
|
route: importedEntry.route ?? null,
|
|
299
310
|
metametricsEnabled: imported.metametricsEnabled,
|
|
300
311
|
interests: imported.interests,
|
|
@@ -311,16 +322,12 @@ runAdapter(async (input) => {
|
|
|
311
322
|
...(route === 'Login' ? { password: credentials.password } : {}),
|
|
312
323
|
},
|
|
313
324
|
});
|
|
314
|
-
const actualAddress = String(unlocked.account?.address ?? address).toLowerCase();
|
|
315
|
-
if (actualAddress !== credentials.expectedAddress) {
|
|
316
|
-
throw new Error(`Wallet profile selected ${actualAddress || 'no address'}, expected ${credentials.expectedAddress}.`);
|
|
317
|
-
}
|
|
318
325
|
return {
|
|
319
326
|
action: input.action,
|
|
320
327
|
method: 'profile',
|
|
321
328
|
credentialSource: credentials.source,
|
|
322
329
|
credentialSourceName: credentials.sourceName,
|
|
323
|
-
|
|
330
|
+
...redactedSelectedIdentity(credentials.expectedAddress, unlocked.account?.address ?? address),
|
|
324
331
|
route: unlocked.route ?? entry.route ?? null,
|
|
325
332
|
redacted: true,
|
|
326
333
|
proofPath: 'mobile-fixture-profile',
|
|
@@ -33,6 +33,8 @@ const TEST_IDS = {
|
|
|
33
33
|
perpsHomeBack: 'perps-home-back-button',
|
|
34
34
|
perpsMarket: 'perps-market-details-view',
|
|
35
35
|
perpsMarketBack: 'perps-market-header-back-button',
|
|
36
|
+
perpsProMarket: 'perps-pro-market-scroll-view',
|
|
37
|
+
perpsProMarketBack: 'perps-pro-market-header-back-button',
|
|
36
38
|
hamburger: 'navbar-hamburger-menu-button',
|
|
37
39
|
lock: 'lock-menu-item',
|
|
38
40
|
confirmLock: 'android:id/button1',
|
|
@@ -343,7 +345,12 @@ function nativeSession(input) {
|
|
|
343
345
|
) {
|
|
344
346
|
return state;
|
|
345
347
|
}
|
|
346
|
-
if (
|
|
348
|
+
if (
|
|
349
|
+
state.visible.has(TEST_IDS.perpsProMarket) &&
|
|
350
|
+
state.visible.has(TEST_IDS.perpsProMarketBack)
|
|
351
|
+
) {
|
|
352
|
+
await this.press(TEST_IDS.perpsProMarketBack, timeoutMs);
|
|
353
|
+
} else if (state.visible.has(TEST_IDS.perpsMarketBack)) {
|
|
347
354
|
await this.press(TEST_IDS.perpsMarketBack, timeoutMs);
|
|
348
355
|
} else if (state.visible.has(TEST_IDS.perpsHomeBack)) {
|
|
349
356
|
await this.press(TEST_IDS.perpsHomeBack, timeoutMs);
|
|
@@ -408,6 +415,13 @@ function nativeSession(input) {
|
|
|
408
415
|
if (!walletVisible) stableWalletCaptures = 0;
|
|
409
416
|
if (visible.has(TEST_IDS.metricsContinue)) {
|
|
410
417
|
if (!options.metametricsEnabled) {
|
|
418
|
+
if (!visible.has(TEST_IDS.metricsCheckbox)) {
|
|
419
|
+
await this.scroll('down', options.timeoutMs);
|
|
420
|
+
const afterScroll = await this.screenState();
|
|
421
|
+
if (!afterScroll.visible.has(TEST_IDS.metricsCheckbox)) {
|
|
422
|
+
throw new Error('Mobile MetaMetrics opt-out checkbox remained unavailable after one bounded onboarding scroll.');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
411
425
|
await this.press(TEST_IDS.metricsCheckbox, options.timeoutMs);
|
|
412
426
|
}
|
|
413
427
|
await this.press(TEST_IDS.metricsContinue, options.timeoutMs);
|
|
@@ -477,6 +491,12 @@ function nativeSession(input) {
|
|
|
477
491
|
timeout_ms: timeoutMs,
|
|
478
492
|
settle: false,
|
|
479
493
|
}),
|
|
494
|
+
scroll: (direction, timeoutMs) => execute('ui.scroll', {
|
|
495
|
+
direction,
|
|
496
|
+
amount: 0.75,
|
|
497
|
+
timeout_ms: timeoutMs,
|
|
498
|
+
settle: false,
|
|
499
|
+
}),
|
|
480
500
|
async waitForWallet(timeoutMs) {
|
|
481
501
|
const deadline = Date.now() + timeoutMs;
|
|
482
502
|
let stableCaptures = 0;
|
|
@@ -87,6 +87,7 @@ export function resolveVisibleStateOptions(node = {}) {
|
|
|
87
87
|
maximumOrderCount: boundedInteger(node.maximum_order_count, 200, 0, 200, 'maximum_order_count'),
|
|
88
88
|
minimumActivityCount: boundedInteger(node.minimum_activity_count, 0, 0, 200, 'minimum_activity_count'),
|
|
89
89
|
requireNoHorizontalOverflow: node.require_no_horizontal_overflow === true,
|
|
90
|
+
requireNoControlOverlap: node.require_no_control_overlap === true,
|
|
90
91
|
requireFeatures: stringList(node.require_features, 'require_features').map((feature) => {
|
|
91
92
|
if (!FEATURES.has(feature)) {
|
|
92
93
|
throw new Error(
|
|
@@ -143,6 +144,9 @@ function marketFromTestId(testId) {
|
|
|
143
144
|
|
|
144
145
|
function classifySurface(route, testIds) {
|
|
145
146
|
const joined = [...testIds].join('\n');
|
|
147
|
+
if (/confirm-transaction\/.*[?&]goBackTo=%2Fperps-home(?:&|$)/u.test(route)) {
|
|
148
|
+
return 'funds';
|
|
149
|
+
}
|
|
146
150
|
if (/\/perps\/trade\//u.test(route)) {
|
|
147
151
|
return 'order-entry';
|
|
148
152
|
}
|
|
@@ -178,7 +182,7 @@ function visibleFeatures(items) {
|
|
|
178
182
|
const short = has(/(?:short-cta-button|direction-short|market-details-short-button)$/u);
|
|
179
183
|
return {
|
|
180
184
|
activity: has(/^(?:transaction-card-|perps-(?:recent-activity|activity))/u),
|
|
181
|
-
balance: has(/^perps-balance-dropdown/u),
|
|
185
|
+
balance: has(/^(?:perps-balance-dropdown|perps-market-(?:balance-value|available-balance-text))/u),
|
|
182
186
|
chart: has(/^(?:chart_surface|tv_chart_container)$/u) ||
|
|
183
187
|
has(/perps-(?:candlestick-chart|market-detail-chart|market-details-fullscreen-chart|pro-market-chart)/u),
|
|
184
188
|
'long-short': long && short,
|
|
@@ -215,20 +219,20 @@ export function normalizeVisiblePerpsState(raw, options, platform) {
|
|
|
215
219
|
...(route ? { route } : {}),
|
|
216
220
|
...(text(raw.title) ? { title: text(raw.title) } : {}),
|
|
217
221
|
balance: compactEntries(
|
|
218
|
-
selected(unique, (item) =>
|
|
222
|
+
selected(unique, (item) => /^(?:perps-balance-dropdown|perps-market-balance-value)$/u.test(item.testId), 1),
|
|
219
223
|
1,
|
|
220
224
|
)[0] ?? null,
|
|
221
225
|
markets: marketItems,
|
|
222
226
|
positions: compactEntries(
|
|
223
|
-
selected(unique, (item) => /^(?:position-card
|
|
227
|
+
selected(unique, (item) => /^(?:position-card-(?!roe-)|perps-position-row-|perps-position-size-value$|perps-pro-market-position-row-|perps-home-position-card-)/u.test(item.testId), options.maxItems),
|
|
224
228
|
options.maxItems,
|
|
225
229
|
),
|
|
226
230
|
orders: compactEntries(
|
|
227
|
-
selected(unique, (item) => /^(?:order-card-|perps-order-row-)/u.test(item.testId), options.maxItems),
|
|
231
|
+
selected(unique, (item) => /^(?:order-card-|perps-order-row-|perps-home-order-card-)/u.test(item.testId), options.maxItems),
|
|
228
232
|
options.maxItems,
|
|
229
233
|
),
|
|
230
234
|
activity: compactEntries(
|
|
231
|
-
selected(unique, (item) => /^(?:transaction-card-|perps-activity-row-)/u.test(item.testId), options.maxItems),
|
|
235
|
+
selected(unique, (item) => /^(?:transaction-card-|transaction-item(?:-|$)|perps-activity-row-)/u.test(item.testId), options.maxItems),
|
|
232
236
|
options.maxItems,
|
|
233
237
|
),
|
|
234
238
|
orderEntry: raw.orderEntry && typeof raw.orderEntry === 'object' ? {
|
|
@@ -269,6 +273,16 @@ export function normalizeVisiblePerpsState(raw, options, platform) {
|
|
|
269
273
|
clippedTestIds: Array.isArray(raw.layout.clippedTestIds)
|
|
270
274
|
? raw.layout.clippedTestIds.map(text).filter(Boolean).slice(0, 100)
|
|
271
275
|
: [],
|
|
276
|
+
obscuredControlTestIds: Array.isArray(raw.layout.obscuredControlTestIds)
|
|
277
|
+
? raw.layout.obscuredControlTestIds.map(text).filter(Boolean).slice(0, 100)
|
|
278
|
+
: [],
|
|
279
|
+
overlappingControlPairs: Array.isArray(raw.layout.overlappingControlPairs)
|
|
280
|
+
? raw.layout.overlappingControlPairs
|
|
281
|
+
.filter((pair) => Array.isArray(pair) && pair.length === 2)
|
|
282
|
+
.map((pair) => pair.map(text))
|
|
283
|
+
.filter((pair) => pair.every(Boolean))
|
|
284
|
+
.slice(0, 100)
|
|
285
|
+
: [],
|
|
272
286
|
} : null,
|
|
273
287
|
};
|
|
274
288
|
assertVisibleState(state, options);
|
|
@@ -281,6 +295,12 @@ function assertVisibleState(state, options) {
|
|
|
281
295
|
)) {
|
|
282
296
|
throw new Error(`Visible Perps layout overflows horizontally: ${JSON.stringify(state.layout)}.`);
|
|
283
297
|
}
|
|
298
|
+
if (options.requireNoControlOverlap && (
|
|
299
|
+
!state.layout || state.layout.obscuredControlTestIds.length > 0 ||
|
|
300
|
+
state.layout.overlappingControlPairs.length > 0
|
|
301
|
+
)) {
|
|
302
|
+
throw new Error(`Visible Perps controls overlap: ${JSON.stringify(state.layout)}.`);
|
|
303
|
+
}
|
|
284
304
|
if (options.surface !== 'auto' && state.surface !== options.surface) {
|
|
285
305
|
throw new Error(
|
|
286
306
|
`Expected visible Perps surface ${options.surface}, but observed ${state.surface}.`,
|