@deeeed/metamask-harness 0.38.1 → 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 +58 -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 +27 -17
- 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/assets/open_details.mjs +30 -0
- package/library/actions/extension/assets/set_token_visibility.mjs +5 -0
- 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/search_markets.mjs +32 -0
- 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/set_market_favorite.mjs +6 -1
- 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/swap_bridge/read_visible_state.mjs +2 -6
- package/library/actions/extension/swap_bridge/set_amount.mjs +11 -2
- 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/assets/import_custom_token.mjs +181 -0
- package/library/actions/mobile/assets/open_details.mjs +43 -0
- package/library/actions/mobile/assets/read_details.mjs +96 -0
- package/library/actions/mobile/assets/set_token_visibility.mjs +302 -0
- package/library/actions/mobile/assets/verify_sorting.mjs +104 -0
- package/library/actions/mobile/networks/add_custom.mjs +125 -0
- package/library/actions/mobile/networks/network-management.mjs +132 -0
- package/library/actions/mobile/networks/read_visible_state.mjs +43 -0
- package/library/actions/mobile/networks/remove_custom.mjs +100 -0
- package/library/actions/mobile/perps/search_markets.mjs +46 -0
- package/library/actions/mobile/perps/set_market_favorite.mjs +117 -0
- package/library/actions/mobile/platform/native-session-name.mjs +14 -0
- package/library/actions/mobile/platform/native-session.mjs +59 -0
- package/library/actions/mobile/platform/observe-ui.mjs +9 -1
- package/library/actions/mobile/swap_bridge/native-session.mjs +10 -45
- package/library/actions/mobile/swap_bridge/select_assets.mjs +15 -1
- package/library/actions/mobile/swap_bridge/set_amount.mjs +1 -1
- package/library/actions/mobile/swap_bridge/set_max_amount.mjs +55 -0
- package/library/actions/mobile/swap_bridge/set_slippage.mjs +96 -0
- package/library/actions/mobile/swap_bridge/submit_transaction.mjs +141 -0
- package/library/actions/mobile/ui/native-navigation.mjs +120 -23
- package/library/actions/mobile/ui/navigate.mjs +13 -2
- package/library/actions/mobile/wallet/import.mjs +17 -10
- package/library/actions/mobile/wallet/lock.mjs +14 -0
- package/library/actions/mobile/wallet/native-ui.mjs +88 -15
- package/library/actions/shared/perps/visible-state.mjs +33 -11
- package/library/actions/shared/swap-bridge/visible-state.mjs +15 -0
- package/library/manifests/extension.action-manifest.json +1707 -1241
- package/library/manifests/mobile.action-manifest.json +308 -1
- package/library/recipes/assets/release-token-details.recipe.json +5 -4
- package/library/recipes/assets/release-token-hide-readd.recipe.json +2 -0
- package/library/recipes/{assets → 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/mobile/networks/release-custom-network-persistence.recipe.json +90 -0
- package/library/recipes/mobile/networks/release-multichain-visibility.recipe.json +58 -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/perps/release-market-search.recipe.json +8 -4
- package/library/recipes/perps/release-watchlist.recipe.json +31 -19
- package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +5 -13
- 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
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createAgentDeviceUiTransport } from '@farmslot/expo-recipe';
|
|
2
2
|
import { createAgentDeviceClient } from 'agent-device';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
nativeAgentDeviceStateDir,
|
|
5
|
+
nativeSessionName,
|
|
6
|
+
} from '../platform/native-session-name.mjs';
|
|
4
7
|
|
|
5
8
|
const TEST_IDS = {
|
|
6
9
|
existingWallet: 'wallet-setup-screen-have-an-existing-wallet-button-id',
|
|
@@ -30,6 +33,8 @@ const TEST_IDS = {
|
|
|
30
33
|
perpsHomeBack: 'perps-home-back-button',
|
|
31
34
|
perpsMarket: 'perps-market-details-view',
|
|
32
35
|
perpsMarketBack: 'perps-market-header-back-button',
|
|
36
|
+
perpsProMarket: 'perps-pro-market-scroll-view',
|
|
37
|
+
perpsProMarketBack: 'perps-pro-market-header-back-button',
|
|
33
38
|
hamburger: 'navbar-hamburger-menu-button',
|
|
34
39
|
lock: 'lock-menu-item',
|
|
35
40
|
confirmLock: 'android:id/button1',
|
|
@@ -85,6 +90,10 @@ function isWalletScreen(nodes) {
|
|
|
85
90
|
return nodes.some((node) => WALLET_SCREEN_IDS.includes(String(node.identifier ?? '')));
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
function visibleSnapshotNodes(nodes) {
|
|
94
|
+
return nodes.filter((node) => node.visibleToUser !== false);
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
function hasBlockingSystemUi(nodes) {
|
|
89
98
|
return nodes.some((node) =>
|
|
90
99
|
(node.bundleId === 'com.android.systemui' || PERMISSION_CONTROLLER_PACKAGES.has(node.bundleId)) &&
|
|
@@ -101,7 +110,7 @@ export async function importWalletThroughNativeUi(input, credentials, options) {
|
|
|
101
110
|
const native = nativeSession(input);
|
|
102
111
|
try {
|
|
103
112
|
const initial = await native.normalizeWalletSurface(options.timeoutMs);
|
|
104
|
-
if (initial.wallet) {
|
|
113
|
+
if (initial.wallet && !initial.visible.has(TEST_IDS.walletTourSkip)) {
|
|
105
114
|
if (options.requireFresh) {
|
|
106
115
|
throw new Error('metamask.wallet.import method=ui requires a fresh Mobile onboarding state.');
|
|
107
116
|
}
|
|
@@ -183,6 +192,27 @@ export async function unlockWalletThroughNativeUi(input, password, timeoutMs) {
|
|
|
183
192
|
}
|
|
184
193
|
}
|
|
185
194
|
|
|
195
|
+
export async function lockWalletThroughNativeUi(input) {
|
|
196
|
+
const native = nativeSession(input);
|
|
197
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 30_000);
|
|
198
|
+
try {
|
|
199
|
+
const state = await native.normalizeWalletSurface(timeoutMs);
|
|
200
|
+
if (state.visible.has(TEST_IDS.login)) {
|
|
201
|
+
return { alreadyLocked: true, provider: 'android-native-accessibility' };
|
|
202
|
+
}
|
|
203
|
+
if (!state.wallet) throw new Error('Opaque Mobile wallet lock could not reach wallet home.');
|
|
204
|
+
await native.press(TEST_IDS.hamburger, timeoutMs);
|
|
205
|
+
await native.press(TEST_IDS.lock, timeoutMs);
|
|
206
|
+
if (await native.exists(TEST_IDS.confirmLock, 3000)) {
|
|
207
|
+
await native.press(TEST_IDS.confirmLock, timeoutMs);
|
|
208
|
+
}
|
|
209
|
+
await native.wait(TEST_IDS.login, timeoutMs);
|
|
210
|
+
return { alreadyLocked: false, provider: 'android-native-accessibility' };
|
|
211
|
+
} finally {
|
|
212
|
+
await native.close();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
186
216
|
export async function resetWalletThroughNativeUi(input) {
|
|
187
217
|
const native = nativeSession(input);
|
|
188
218
|
const timeoutMs = Number(input.node?.timeout_ms ?? 60_000);
|
|
@@ -219,8 +249,10 @@ function nativeSession(input) {
|
|
|
219
249
|
if (!device) throw new Error('Opaque Mobile UI execution requires one explicit Android device serial.');
|
|
220
250
|
const app = String(env.ANDROID_PACKAGE_ID ?? 'io.metamask');
|
|
221
251
|
const session = nativeSessionName(env, device, app);
|
|
252
|
+
const stateDir = nativeAgentDeviceStateDir(env, input.context.projectRoot, device);
|
|
222
253
|
const client = createAgentDeviceClient({
|
|
223
254
|
session,
|
|
255
|
+
stateDir,
|
|
224
256
|
lockPolicy: 'reject',
|
|
225
257
|
lockPlatform: 'android',
|
|
226
258
|
});
|
|
@@ -230,6 +262,7 @@ function nativeSession(input) {
|
|
|
230
262
|
device,
|
|
231
263
|
app,
|
|
232
264
|
session,
|
|
265
|
+
stateDir,
|
|
233
266
|
client,
|
|
234
267
|
});
|
|
235
268
|
const context = {
|
|
@@ -240,7 +273,11 @@ function nativeSession(input) {
|
|
|
240
273
|
};
|
|
241
274
|
const execute = (action, node) => transport.execute(action, node, context);
|
|
242
275
|
return {
|
|
243
|
-
press: (testId, timeoutMs) => execute('ui.press', {
|
|
276
|
+
press: (testId, timeoutMs) => execute('ui.press', {
|
|
277
|
+
test_id: testId,
|
|
278
|
+
timeout_ms: timeoutMs,
|
|
279
|
+
settle: false,
|
|
280
|
+
}),
|
|
244
281
|
fill: (testId, value, timeoutMs) => execute('ui.set_input', {
|
|
245
282
|
test_id: testId,
|
|
246
283
|
value,
|
|
@@ -259,7 +296,7 @@ function nativeSession(input) {
|
|
|
259
296
|
interactiveOnly: false,
|
|
260
297
|
forceFull: true,
|
|
261
298
|
});
|
|
262
|
-
const actual = new Map(snapshot.nodes
|
|
299
|
+
const actual = new Map(visibleSnapshotNodes(snapshot.nodes)
|
|
263
300
|
.filter((node) => /^phrase-input-id_\d+$/u.test(String(node.identifier ?? '')))
|
|
264
301
|
.map((node) => [
|
|
265
302
|
Number(String(node.identifier).split('_').at(-1)),
|
|
@@ -288,11 +325,12 @@ function nativeSession(input) {
|
|
|
288
325
|
interactiveOnly: false,
|
|
289
326
|
forceFull: true,
|
|
290
327
|
});
|
|
328
|
+
const nodes = visibleSnapshotNodes(snapshot.nodes);
|
|
291
329
|
return {
|
|
292
|
-
nodes
|
|
293
|
-
visible: new Set(
|
|
294
|
-
wallet: isWalletScreen(
|
|
295
|
-
postPassword: isPostPasswordScreen(
|
|
330
|
+
nodes,
|
|
331
|
+
visible: new Set(nodes.map((node) => String(node.identifier ?? ''))),
|
|
332
|
+
wallet: isWalletScreen(nodes),
|
|
333
|
+
postPassword: isPostPasswordScreen(nodes) || isMetaMaskBiometricPrompt(nodes),
|
|
296
334
|
};
|
|
297
335
|
},
|
|
298
336
|
async normalizeWalletSurface(timeoutMs) {
|
|
@@ -307,7 +345,12 @@ function nativeSession(input) {
|
|
|
307
345
|
) {
|
|
308
346
|
return state;
|
|
309
347
|
}
|
|
310
|
-
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)) {
|
|
311
354
|
await this.press(TEST_IDS.perpsMarketBack, timeoutMs);
|
|
312
355
|
} else if (state.visible.has(TEST_IDS.perpsHomeBack)) {
|
|
313
356
|
await this.press(TEST_IDS.perpsHomeBack, timeoutMs);
|
|
@@ -333,13 +376,15 @@ function nativeSession(input) {
|
|
|
333
376
|
let walletTourSkipped = false;
|
|
334
377
|
let interestsHandled = false;
|
|
335
378
|
let awaitingNotificationPermission = false;
|
|
379
|
+
let stableWalletCaptures = 0;
|
|
336
380
|
while (Date.now() <= deadline) {
|
|
337
381
|
const snapshot = await client.capture.snapshot({
|
|
338
382
|
...selection,
|
|
339
383
|
interactiveOnly: false,
|
|
340
384
|
forceFull: true,
|
|
341
385
|
});
|
|
342
|
-
|
|
386
|
+
const nodes = visibleSnapshotNodes(snapshot.nodes);
|
|
387
|
+
if (isMetaMaskBiometricPrompt(nodes)) {
|
|
343
388
|
if (options.biometricsEnabled) {
|
|
344
389
|
throw new Error('Mobile biometrics=true requires manual device authentication and is not supported by an unattended recipe.');
|
|
345
390
|
}
|
|
@@ -351,7 +396,7 @@ function nativeSession(input) {
|
|
|
351
396
|
biometricPromptDismissed = true;
|
|
352
397
|
continue;
|
|
353
398
|
}
|
|
354
|
-
if (isMetaMaskNotificationPermissionPrompt(
|
|
399
|
+
if (isMetaMaskNotificationPermissionPrompt(nodes) && awaitingNotificationPermission) {
|
|
355
400
|
await client.command.alert({
|
|
356
401
|
...selection,
|
|
357
402
|
action: 'accept',
|
|
@@ -362,12 +407,21 @@ function nativeSession(input) {
|
|
|
362
407
|
notificationsEnabled = true;
|
|
363
408
|
continue;
|
|
364
409
|
}
|
|
365
|
-
if (hasBlockingSystemUi(
|
|
410
|
+
if (hasBlockingSystemUi(nodes)) {
|
|
366
411
|
throw new Error('Unexpected Android system UI blocked Mobile wallet creation; refusing to dismiss it as biometric authentication.');
|
|
367
412
|
}
|
|
368
|
-
const visible = new Set(
|
|
413
|
+
const visible = new Set(nodes.map((node) => String(node.identifier ?? '')));
|
|
414
|
+
const walletVisible = WALLET_SCREEN_IDS.some((testId) => visible.has(testId));
|
|
415
|
+
if (!walletVisible) stableWalletCaptures = 0;
|
|
369
416
|
if (visible.has(TEST_IDS.metricsContinue)) {
|
|
370
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
|
+
}
|
|
371
425
|
await this.press(TEST_IDS.metricsCheckbox, options.timeoutMs);
|
|
372
426
|
}
|
|
373
427
|
await this.press(TEST_IDS.metricsContinue, options.timeoutMs);
|
|
@@ -402,6 +456,7 @@ function nativeSession(input) {
|
|
|
402
456
|
continue;
|
|
403
457
|
}
|
|
404
458
|
if (visible.has(TEST_IDS.walletTourSkip)) {
|
|
459
|
+
stableWalletCaptures = 0;
|
|
405
460
|
if (options.skipWalletTour) {
|
|
406
461
|
await this.press(TEST_IDS.walletTourSkip, options.timeoutMs);
|
|
407
462
|
walletTourSkipped = true;
|
|
@@ -409,10 +464,15 @@ function nativeSession(input) {
|
|
|
409
464
|
}
|
|
410
465
|
throw new Error('Mobile wallet-home tour remains visible because skip_wallet_tour=false; wallet readiness cannot be proven.');
|
|
411
466
|
}
|
|
412
|
-
if (
|
|
467
|
+
if (walletVisible) {
|
|
413
468
|
if (options.interests.length > 0 && !interestsHandled) {
|
|
414
469
|
throw new Error('Mobile onboarding interests were requested, but the questionnaire is not enabled for this build.');
|
|
415
470
|
}
|
|
471
|
+
stableWalletCaptures += 1;
|
|
472
|
+
if (stableWalletCaptures < 2) {
|
|
473
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
416
476
|
return {
|
|
417
477
|
biometricsEnabled: options.biometricsEnabled,
|
|
418
478
|
biometricPromptDismissed,
|
|
@@ -431,10 +491,23 @@ function nativeSession(input) {
|
|
|
431
491
|
timeout_ms: timeoutMs,
|
|
432
492
|
settle: false,
|
|
433
493
|
}),
|
|
494
|
+
scroll: (direction, timeoutMs) => execute('ui.scroll', {
|
|
495
|
+
direction,
|
|
496
|
+
amount: 0.75,
|
|
497
|
+
timeout_ms: timeoutMs,
|
|
498
|
+
settle: false,
|
|
499
|
+
}),
|
|
434
500
|
async waitForWallet(timeoutMs) {
|
|
435
501
|
const deadline = Date.now() + timeoutMs;
|
|
502
|
+
let stableCaptures = 0;
|
|
436
503
|
while (Date.now() <= deadline) {
|
|
437
|
-
|
|
504
|
+
const state = await this.screenState();
|
|
505
|
+
if (state.wallet && state.visible.has(TEST_IDS.walletAddress)) {
|
|
506
|
+
stableCaptures += 1;
|
|
507
|
+
if (stableCaptures >= 2) return;
|
|
508
|
+
} else {
|
|
509
|
+
stableCaptures = 0;
|
|
510
|
+
}
|
|
438
511
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
439
512
|
}
|
|
440
513
|
throw new Error('Mobile wallet home did not become visible before the readiness timeout.');
|
|
@@ -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(
|
|
@@ -132,6 +133,7 @@ function marketFromTestId(testId) {
|
|
|
132
133
|
for (const prefix of ['perps-watchlist-', 'explore-markets-', 'perps-market-row-item-', 'market-row-']) {
|
|
133
134
|
if (!testId.startsWith(prefix)) continue;
|
|
134
135
|
const raw = testId.slice(prefix.length);
|
|
136
|
+
if (/(?:-token-logo|-asset-label)$/u.test(raw)) return undefined;
|
|
135
137
|
if ((prefix === 'explore-markets-' || prefix === 'market-row-') && raw.startsWith('xyz-')) {
|
|
136
138
|
return `XYZ:${raw.slice(4).toUpperCase()}`;
|
|
137
139
|
}
|
|
@@ -142,6 +144,9 @@ function marketFromTestId(testId) {
|
|
|
142
144
|
|
|
143
145
|
function classifySurface(route, testIds) {
|
|
144
146
|
const joined = [...testIds].join('\n');
|
|
147
|
+
if (/confirm-transaction\/.*[?&]goBackTo=%2Fperps-home(?:&|$)/u.test(route)) {
|
|
148
|
+
return 'funds';
|
|
149
|
+
}
|
|
145
150
|
if (/\/perps\/trade\//u.test(route)) {
|
|
146
151
|
return 'order-entry';
|
|
147
152
|
}
|
|
@@ -173,18 +178,19 @@ function compactEntries(items, maximum) {
|
|
|
173
178
|
function visibleFeatures(items) {
|
|
174
179
|
const ids = new Set(items.map((item) => item.testId));
|
|
175
180
|
const has = (pattern) => [...ids].some((testId) => pattern.test(testId));
|
|
176
|
-
const long = has(/(?:long-cta-button|direction-long)$/u);
|
|
177
|
-
const short = has(/(?:short-cta-button|direction-short)$/u);
|
|
181
|
+
const long = has(/(?:long-cta-button|direction-long|market-details-long-button)$/u);
|
|
182
|
+
const short = has(/(?:short-cta-button|direction-short|market-details-short-button)$/u);
|
|
178
183
|
return {
|
|
179
184
|
activity: has(/^(?:transaction-card-|perps-(?:recent-activity|activity))/u),
|
|
180
|
-
balance: has(/^perps-balance-dropdown/u),
|
|
181
|
-
chart: has(
|
|
185
|
+
balance: has(/^(?:perps-balance-dropdown|perps-market-(?:balance-value|available-balance-text))/u),
|
|
186
|
+
chart: has(/^(?:chart_surface|tv_chart_container)$/u) ||
|
|
187
|
+
has(/perps-(?:candlestick-chart|market-detail-chart|market-details-fullscreen-chart|pro-market-chart)/u),
|
|
182
188
|
'long-short': long && short,
|
|
183
189
|
'market-list': has(/^(?:market-list-view|perps-market-list|perps-watchlist-|explore-markets-|market-row-)/u),
|
|
184
190
|
'order-entry': has(/perps-(?:order-entry|pro-order-form)/u),
|
|
185
|
-
price: has(/perps-(?:market-detail-price|pro-market-price)$/u),
|
|
186
|
-
'price-change': has(/perps-(?:market-detail-change|pro-market-price-change)$/u),
|
|
187
|
-
stats: has(/perps-(?:stats-section|market-detail-oracle-price|pro-market-stats)/u),
|
|
191
|
+
price: has(/perps-(?:market-detail-price|market-header-price|pro-market-price)$/u),
|
|
192
|
+
'price-change': has(/perps-(?:market-detail-change|market-header-price-change|pro-market-price-change)$/u),
|
|
193
|
+
stats: has(/perps-(?:stats-section|market-details-market-summary|market-detail-oracle-price|pro-market-stats)/u),
|
|
188
194
|
};
|
|
189
195
|
}
|
|
190
196
|
|
|
@@ -213,20 +219,20 @@ export function normalizeVisiblePerpsState(raw, options, platform) {
|
|
|
213
219
|
...(route ? { route } : {}),
|
|
214
220
|
...(text(raw.title) ? { title: text(raw.title) } : {}),
|
|
215
221
|
balance: compactEntries(
|
|
216
|
-
selected(unique, (item) =>
|
|
222
|
+
selected(unique, (item) => /^(?:perps-balance-dropdown|perps-market-balance-value)$/u.test(item.testId), 1),
|
|
217
223
|
1,
|
|
218
224
|
)[0] ?? null,
|
|
219
225
|
markets: marketItems,
|
|
220
226
|
positions: compactEntries(
|
|
221
|
-
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),
|
|
222
228
|
options.maxItems,
|
|
223
229
|
),
|
|
224
230
|
orders: compactEntries(
|
|
225
|
-
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),
|
|
226
232
|
options.maxItems,
|
|
227
233
|
),
|
|
228
234
|
activity: compactEntries(
|
|
229
|
-
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),
|
|
230
236
|
options.maxItems,
|
|
231
237
|
),
|
|
232
238
|
orderEntry: raw.orderEntry && typeof raw.orderEntry === 'object' ? {
|
|
@@ -267,6 +273,16 @@ export function normalizeVisiblePerpsState(raw, options, platform) {
|
|
|
267
273
|
clippedTestIds: Array.isArray(raw.layout.clippedTestIds)
|
|
268
274
|
? raw.layout.clippedTestIds.map(text).filter(Boolean).slice(0, 100)
|
|
269
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
|
+
: [],
|
|
270
286
|
} : null,
|
|
271
287
|
};
|
|
272
288
|
assertVisibleState(state, options);
|
|
@@ -279,6 +295,12 @@ function assertVisibleState(state, options) {
|
|
|
279
295
|
)) {
|
|
280
296
|
throw new Error(`Visible Perps layout overflows horizontally: ${JSON.stringify(state.layout)}.`);
|
|
281
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
|
+
}
|
|
282
304
|
if (options.surface !== 'auto' && state.surface !== options.surface) {
|
|
283
305
|
throw new Error(
|
|
284
306
|
`Expected visible Perps surface ${options.surface}, but observed ${state.surface}.`,
|
|
@@ -17,6 +17,8 @@ export function resolveVisibleSwapBridgeOptions(node = {}) {
|
|
|
17
17
|
destinationSymbol: symbol(node.destination_symbol),
|
|
18
18
|
requireAmountInputs: node.require_amount_inputs !== false,
|
|
19
19
|
requireSettings: node.require_settings === true,
|
|
20
|
+
requireQuote: node.require_quote === true,
|
|
21
|
+
expectedSlippage: node.expected_slippage == null ? null : Number(node.expected_slippage),
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -42,6 +44,11 @@ function matchesSymbol(value, expected) {
|
|
|
42
44
|
.test(String(value ?? '').toUpperCase());
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
function slippagePercent(value) {
|
|
48
|
+
const match = String(value ?? '').match(/(\d+(?:\.\d+)?)\s*%/u);
|
|
49
|
+
return match ? Number(match[1]) : Number.NaN;
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
export function normalizeVisibleSwapBridgeState(raw, options, platform) {
|
|
46
53
|
const items = [...(raw.items ?? []), ...(raw.offscreenItems ?? [])]
|
|
47
54
|
.map(normalizedItem)
|
|
@@ -55,6 +62,7 @@ export function normalizeVisibleSwapBridgeState(raw, options, platform) {
|
|
|
55
62
|
const settings = byId.get(platform === 'extension' ? 'bridge__header-settings-button' : 'bridge-slippage-settings-button');
|
|
56
63
|
const noQuotes = byId.get(platform === 'extension' ? 'prepare-swap-page-error' : 'bridge-no-quotes');
|
|
57
64
|
const missingPrice = byId.get(platform === 'extension' ? 'bridge-missing-price-banner' : 'bridge-missing-price-banner');
|
|
65
|
+
const slippage = byId.get(platform === 'extension' ? 'slippage-edit-button' : 'edit-slippage-button');
|
|
58
66
|
const state = {
|
|
59
67
|
source: 'visible-ui',
|
|
60
68
|
platform,
|
|
@@ -65,6 +73,7 @@ export function normalizeVisibleSwapBridgeState(raw, options, platform) {
|
|
|
65
73
|
destinationAsset,
|
|
66
74
|
sourceAmount: text(sourceInput?.value ?? sourceInput?.label) ?? null,
|
|
67
75
|
destinationAmount: text(destinationInput?.value ?? destinationInput?.label) ?? null,
|
|
76
|
+
slippage: text(raw.slippage ?? raw.quote?.slippage ?? slippage?.value ?? slippage?.label) ?? null,
|
|
68
77
|
controls: unique.slice(0, 100),
|
|
69
78
|
features: {
|
|
70
79
|
'amount-inputs': Boolean(sourceInput && destinationInput),
|
|
@@ -88,6 +97,12 @@ function assertVisibleSwapBridgeState(state, options) {
|
|
|
88
97
|
if (options.requireSettings && !state.features.settings) {
|
|
89
98
|
throw new Error('Visible Swap settings control was not observed.');
|
|
90
99
|
}
|
|
100
|
+
if (options.requireQuote && !(Number(state.destinationAmount) > 0)) {
|
|
101
|
+
throw new Error('Visible Swap quote did not contain a positive destination amount.');
|
|
102
|
+
}
|
|
103
|
+
if (options.expectedSlippage !== null && slippagePercent(state.slippage) !== options.expectedSlippage) {
|
|
104
|
+
throw new Error(`Expected visible Swap slippage ${options.expectedSlippage}%, but observed ${JSON.stringify(state.slippage)}.`);
|
|
105
|
+
}
|
|
91
106
|
if (!matchesSymbol(state.sourceAsset, options.sourceSymbol)) {
|
|
92
107
|
throw new Error(`Expected visible Swap source ${options.sourceSymbol}, but observed ${JSON.stringify(state.sourceAsset)}.`);
|
|
93
108
|
}
|