@deeeed/metamask-harness 0.38.0 → 0.39.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 +18 -0
- package/dist/adapters.js +20 -11
- 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/perps/open_balance_action.mjs +1 -6
- package/library/actions/extension/perps/search_markets.mjs +32 -0
- package/library/actions/extension/perps/select_market_filter.mjs +3 -2
- package/library/actions/extension/perps/set_market_favorite.mjs +6 -1
- 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/swap_bridge/submit_transaction.mjs +11 -11
- 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 +75 -23
- package/library/actions/mobile/ui/navigate.mjs +13 -2
- package/library/actions/mobile/wallet/lock.mjs +14 -0
- package/library/actions/mobile/wallet/native-ui.mjs +67 -14
- package/library/actions/shared/perps/visible-state.mjs +8 -6
- package/library/actions/shared/swap-bridge/visible-state.mjs +15 -0
- package/library/manifests/extension.action-manifest.json +43 -2
- 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/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-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/package.json +1 -1
- package/scripts/validate-release-report.mjs +28 -14
- /package/library/recipes/{assets → extension/assets}/release-custom-network-token-persistence.recipe.json +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { runAdapter } from '../platform/bridge.mjs';
|
|
2
|
+
import { resolveAmountOptions } from '../../shared/swap-bridge/transaction.mjs';
|
|
3
|
+
import {
|
|
4
|
+
closeNativeSession,
|
|
5
|
+
createNativeSession,
|
|
6
|
+
nativeLabel,
|
|
7
|
+
nativeNode,
|
|
8
|
+
} from '../platform/native-session.mjs';
|
|
9
|
+
|
|
10
|
+
runAdapter(async (input) => {
|
|
11
|
+
const options = resolveAmountOptions({
|
|
12
|
+
amount: '1',
|
|
13
|
+
wait_for_quote: input.node?.wait_for_quote,
|
|
14
|
+
timeout_ms: input.node?.timeout_ms,
|
|
15
|
+
});
|
|
16
|
+
const session = createNativeSession(input, 'Swap', `max-${process.pid}`);
|
|
17
|
+
try {
|
|
18
|
+
await session.open();
|
|
19
|
+
await session.execute('ui.press', {
|
|
20
|
+
test_id: 'token-input-area-max-button',
|
|
21
|
+
timeout_ms: options.timeoutMs,
|
|
22
|
+
settle: false,
|
|
23
|
+
});
|
|
24
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
25
|
+
let observed;
|
|
26
|
+
do {
|
|
27
|
+
const snapshot = await session.snapshot();
|
|
28
|
+
const cta = nativeNode(snapshot, 'bridge-confirm-button')
|
|
29
|
+
?? nativeNode(snapshot, 'bridge-confirm-button-keypad');
|
|
30
|
+
observed = {
|
|
31
|
+
sourceAmount: nativeLabel(nativeNode(snapshot, 'source-token-area-input')),
|
|
32
|
+
destinationAmount: nativeLabel(nativeNode(snapshot, 'dest-token-area-input')),
|
|
33
|
+
cta: nativeLabel(cta) || null,
|
|
34
|
+
ctaEnabled: cta?.enabled !== false,
|
|
35
|
+
noQuote: Boolean(nativeNode(snapshot, 'bridge-no-quotes')),
|
|
36
|
+
};
|
|
37
|
+
if (Number(observed.sourceAmount) > 0 && (
|
|
38
|
+
!options.waitForQuote
|
|
39
|
+
|| observed.noQuote
|
|
40
|
+
|| (Number(observed.destinationAmount) > 0 && cta)
|
|
41
|
+
)) break;
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
43
|
+
} while (Date.now() < deadline);
|
|
44
|
+
if (!(Number(observed?.sourceAmount) > 0)) {
|
|
45
|
+
throw new Error('Swap Max did not populate a positive source amount.');
|
|
46
|
+
}
|
|
47
|
+
if (options.waitForQuote && !observed.noQuote
|
|
48
|
+
&& (!(Number(observed.destinationAmount) > 0) || !observed.cta)) {
|
|
49
|
+
throw new Error(`Swap Max quote did not resolve within ${options.timeoutMs}ms.`);
|
|
50
|
+
}
|
|
51
|
+
return { action: input.action, ...observed, proofPath: 'native-accessibility-max-quote' };
|
|
52
|
+
} finally {
|
|
53
|
+
await closeNativeSession(session);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { runAdapter } from '../platform/bridge.mjs';
|
|
2
|
+
import {
|
|
3
|
+
closeNativeSession,
|
|
4
|
+
createNativeSession,
|
|
5
|
+
nativeLabel,
|
|
6
|
+
nativeNode,
|
|
7
|
+
} from '../platform/native-session.mjs';
|
|
8
|
+
|
|
9
|
+
async function waitForNode(session, testId, timeoutMs) {
|
|
10
|
+
const deadline = Date.now() + timeoutMs;
|
|
11
|
+
let snapshot;
|
|
12
|
+
while (Date.now() <= deadline) {
|
|
13
|
+
snapshot = await session.snapshot();
|
|
14
|
+
const node = nativeNode(snapshot, testId);
|
|
15
|
+
if (node) return { node, snapshot };
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Swap control ${testId} did not become visible.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function replaceSlippage(session, percent, timeoutMs) {
|
|
22
|
+
const inputId = 'input-stepper-input';
|
|
23
|
+
await waitForNode(session, inputId, timeoutMs);
|
|
24
|
+
await session.execute('ui.press', { test_id: inputId, timeout_ms: timeoutMs, settle: false });
|
|
25
|
+
let current = nativeLabel(nativeNode(await session.snapshot(), inputId));
|
|
26
|
+
for (let index = 0; index < Math.min(20, current.length + 2); index += 1) {
|
|
27
|
+
await session.execute('ui.press', {
|
|
28
|
+
test_id: 'keypad-delete-button',
|
|
29
|
+
timeout_ms: timeoutMs,
|
|
30
|
+
settle: false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
current = nativeLabel(nativeNode(await session.snapshot(), inputId));
|
|
34
|
+
const value = String(percent);
|
|
35
|
+
const keys = value.startsWith('0.') && current === '0' ? value.slice(1) : value;
|
|
36
|
+
for (const key of keys) {
|
|
37
|
+
if (!/[0-9.]/u.test(key)) {
|
|
38
|
+
throw new Error(`Swap slippage contains unsupported keypad character ${JSON.stringify(key)}.`);
|
|
39
|
+
}
|
|
40
|
+
await session.execute('ui.press', {
|
|
41
|
+
test_id: key === '.' ? 'keypad-key-dot' : `keypad-key-${key}`,
|
|
42
|
+
timeout_ms: timeoutMs,
|
|
43
|
+
settle: false,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const observed = nativeLabel(nativeNode(await session.snapshot(), inputId));
|
|
47
|
+
if (Number(observed) !== percent) {
|
|
48
|
+
throw new Error(`Swap slippage input mismatch: expected ${percent}, observed ${JSON.stringify(observed)}.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
runAdapter(async (input) => {
|
|
53
|
+
const percent = Number(input.node?.percent);
|
|
54
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 30_000);
|
|
55
|
+
if (!Number.isFinite(percent) || percent <= 0 || percent > 50) {
|
|
56
|
+
throw new Error('metamask.swap_bridge.set_slippage requires percent greater than 0 and at most 50.');
|
|
57
|
+
}
|
|
58
|
+
const session = createNativeSession(input, 'Swap', `slippage-${process.pid}`);
|
|
59
|
+
try {
|
|
60
|
+
await session.open();
|
|
61
|
+
const initial = await session.snapshot();
|
|
62
|
+
const opener = nativeNode(initial, 'edit-slippage-button')
|
|
63
|
+
? 'edit-slippage-button'
|
|
64
|
+
: 'bridge-slippage-settings-button';
|
|
65
|
+
await session.execute('ui.press', { test_id: opener, timeout_ms: timeoutMs, settle: false });
|
|
66
|
+
await session.execute('ui.press', { text: 'Custom', timeout_ms: timeoutMs, settle: false });
|
|
67
|
+
await replaceSlippage(session, percent, timeoutMs);
|
|
68
|
+
await session.execute('ui.press', { text: 'Confirm', timeout_ms: timeoutMs, settle: false });
|
|
69
|
+
|
|
70
|
+
const deadline = Date.now() + timeoutMs;
|
|
71
|
+
let observed = null;
|
|
72
|
+
let destinationAmount = null;
|
|
73
|
+
while (Date.now() <= deadline) {
|
|
74
|
+
const snapshot = await session.snapshot();
|
|
75
|
+
observed = nativeLabel(nativeNode(snapshot, 'edit-slippage-button')) || null;
|
|
76
|
+
destinationAmount = nativeLabel(nativeNode(snapshot, 'dest-token-area-input')) || null;
|
|
77
|
+
if (Number.parseFloat(observed) === percent && Number(destinationAmount) > 0) break;
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
79
|
+
}
|
|
80
|
+
if (Number.parseFloat(observed) !== percent) {
|
|
81
|
+
throw new Error(`Swap quote did not display Slippage: ${percent}%; observed ${JSON.stringify(observed)}.`);
|
|
82
|
+
}
|
|
83
|
+
if (!(Number(destinationAmount) > 0)) {
|
|
84
|
+
throw new Error('Swap quote did not finish refreshing after custom slippage was applied.');
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
action: input.action,
|
|
88
|
+
percent,
|
|
89
|
+
observed,
|
|
90
|
+
destinationAmount,
|
|
91
|
+
proofPath: 'native-accessibility-custom-slippage',
|
|
92
|
+
};
|
|
93
|
+
} finally {
|
|
94
|
+
await closeNativeSession(session);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { runAdapter } from '../platform/bridge.mjs';
|
|
2
|
+
import {
|
|
3
|
+
closeNativeSession,
|
|
4
|
+
createNativeSession,
|
|
5
|
+
nativeLabel,
|
|
6
|
+
nativeNode,
|
|
7
|
+
} from '../platform/native-session.mjs';
|
|
8
|
+
|
|
9
|
+
function visibleLabels(snapshot) {
|
|
10
|
+
return snapshot.nodes
|
|
11
|
+
.filter((node) => node.visibleToUser !== false)
|
|
12
|
+
.map(nativeLabel)
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function quote(snapshot) {
|
|
17
|
+
const source = nativeNode(snapshot, 'source-token-area');
|
|
18
|
+
const destination = nativeNode(snapshot, 'dest-token-area');
|
|
19
|
+
const sourceAmount = nativeLabel(nativeNode(snapshot, 'source-token-area-input'));
|
|
20
|
+
const destinationAmount = nativeLabel(nativeNode(snapshot, 'dest-token-area-input'));
|
|
21
|
+
const button = nativeNode(snapshot, 'bridge-confirm-button');
|
|
22
|
+
const labels = visibleLabels(snapshot);
|
|
23
|
+
return {
|
|
24
|
+
source: nativeLabel(source),
|
|
25
|
+
destination: nativeLabel(destination),
|
|
26
|
+
sourceAmount,
|
|
27
|
+
destinationAmount,
|
|
28
|
+
buttonEnabled: button?.enabled === true,
|
|
29
|
+
warning: labels.find((label) => /insufficient|no quotes|unavailable|may fail|might fail/iu.test(label)) ?? null,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function waitForQuote(session, expected, timeoutMs) {
|
|
34
|
+
const deadline = Date.now() + Math.min(timeoutMs, 60_000);
|
|
35
|
+
let stable = null;
|
|
36
|
+
let stableSince = 0;
|
|
37
|
+
while (Date.now() <= deadline) {
|
|
38
|
+
const snapshot = await session.snapshot();
|
|
39
|
+
const current = quote(snapshot);
|
|
40
|
+
const matches = current.source.toUpperCase().includes(expected.sourceSymbol) &&
|
|
41
|
+
current.destination.toUpperCase().includes(expected.destinationSymbol) &&
|
|
42
|
+
Number(current.sourceAmount) === expected.amount &&
|
|
43
|
+
Number(current.destinationAmount) > 0 &&
|
|
44
|
+
current.buttonEnabled &&
|
|
45
|
+
!current.warning;
|
|
46
|
+
const identity = JSON.stringify(current);
|
|
47
|
+
if (!matches) {
|
|
48
|
+
stable = null;
|
|
49
|
+
stableSince = 0;
|
|
50
|
+
} else if (stable !== identity) {
|
|
51
|
+
stable = identity;
|
|
52
|
+
stableSince = Date.now();
|
|
53
|
+
} else if (Date.now() - stableSince >= 1500) {
|
|
54
|
+
return current;
|
|
55
|
+
}
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
57
|
+
}
|
|
58
|
+
throw new Error('The Mobile Swap quote did not remain submittable for the requested pair and amount.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function pressIfVisible(session, testId, timeoutMs) {
|
|
62
|
+
if (!nativeNode(await session.snapshot(), testId)) return false;
|
|
63
|
+
await session.execute('ui.press', { test_id: testId, timeout_ms: timeoutMs, settle: false });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function waitForTerminalStatus(session, timeoutMs) {
|
|
68
|
+
const deadline = Date.now() + timeoutMs;
|
|
69
|
+
while (Date.now() <= deadline) {
|
|
70
|
+
const snapshot = await session.snapshot();
|
|
71
|
+
const labels = visibleLabels(snapshot);
|
|
72
|
+
const failed = labels.find((label) => /^(?:Swap|Bridge) failed$/iu.test(label));
|
|
73
|
+
if (failed) throw new Error(`Mobile ${failed}.`);
|
|
74
|
+
const success = labels.find((label) => /^(?:Swap|Bridge) complete$/iu.test(label));
|
|
75
|
+
if (success) return { snapshot, status: success };
|
|
76
|
+
if (nativeNode(snapshot, 'price-impact-footer-confirm')) {
|
|
77
|
+
await session.execute('ui.press', {
|
|
78
|
+
test_id: 'price-impact-footer-confirm',
|
|
79
|
+
timeout_ms: Math.max(1, deadline - Date.now()),
|
|
80
|
+
settle: false,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
84
|
+
}
|
|
85
|
+
throw new Error('Mobile Swap did not reach a terminal post-trade status.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
runAdapter(async (input) => {
|
|
89
|
+
const sourceSymbol = String(input.node?.source_symbol ?? '').trim().toUpperCase();
|
|
90
|
+
const destinationSymbol = String(input.node?.destination_symbol ?? '').trim().toUpperCase();
|
|
91
|
+
const amount = Number(input.node?.amount);
|
|
92
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 300_000);
|
|
93
|
+
if (!sourceSymbol || !destinationSymbol || !(amount > 0)) {
|
|
94
|
+
throw new Error('metamask.swap_bridge.submit_transaction requires source_symbol, destination_symbol, and positive amount.');
|
|
95
|
+
}
|
|
96
|
+
if (input.node?.confirm_live !== true) {
|
|
97
|
+
throw new Error('metamask.swap_bridge.submit_transaction requires confirm_live=true.');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const session = createNativeSession(input, 'Swap', `submit-${process.pid}`);
|
|
101
|
+
try {
|
|
102
|
+
await session.open();
|
|
103
|
+
const before = await waitForQuote(session, { sourceSymbol, destinationSymbol, amount }, timeoutMs);
|
|
104
|
+
await session.execute('ui.press', {
|
|
105
|
+
test_id: 'bridge-confirm-button',
|
|
106
|
+
timeout_ms: Math.min(timeoutMs, 60_000),
|
|
107
|
+
settle: false,
|
|
108
|
+
});
|
|
109
|
+
const terminal = await waitForTerminalStatus(session, timeoutMs);
|
|
110
|
+
await pressIfVisible(session, 'post-trade-bottom-sheet-view-activity-button', timeoutMs);
|
|
111
|
+
let activity = null;
|
|
112
|
+
const activityDeadline = Date.now() + Math.min(timeoutMs, 60_000);
|
|
113
|
+
while (Date.now() <= activityDeadline) {
|
|
114
|
+
const snapshot = await session.snapshot();
|
|
115
|
+
if (nativeNode(snapshot, 'activity-screen-safe-area-view')) {
|
|
116
|
+
const labels = visibleLabels(snapshot);
|
|
117
|
+
activity = labels.find((label) => {
|
|
118
|
+
const upper = label.toUpperCase();
|
|
119
|
+
return upper.includes(sourceSymbol) && upper.includes(destinationSymbol);
|
|
120
|
+
}) ?? labels.find((label) => /swap|bridge/iu.test(label)) ?? null;
|
|
121
|
+
if (activity) break;
|
|
122
|
+
}
|
|
123
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
124
|
+
}
|
|
125
|
+
if (!activity) throw new Error('Confirmed Mobile Swap did not appear in visible Activity.');
|
|
126
|
+
return {
|
|
127
|
+
action: input.action,
|
|
128
|
+
sourceSymbol,
|
|
129
|
+
destinationSymbol,
|
|
130
|
+
amount,
|
|
131
|
+
destinationAmount: before.destinationAmount,
|
|
132
|
+
submitted: true,
|
|
133
|
+
confirmed: true,
|
|
134
|
+
status: terminal.status,
|
|
135
|
+
activity,
|
|
136
|
+
proofPath: 'native-accessibility-swap-post-trade-activity',
|
|
137
|
+
};
|
|
138
|
+
} finally {
|
|
139
|
+
await closeNativeSession(session);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
@@ -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 IDS = {
|
|
6
9
|
wallet: ['wallet-safe-area', 'homepage-container'],
|
|
@@ -9,12 +12,16 @@ const IDS = {
|
|
|
9
12
|
perpsEntry: 'homepage-balance-breakdown-row-perps',
|
|
10
13
|
perpsTradeEntry: 'wallet-perps-action',
|
|
11
14
|
swapTradeEntry: 'wallet-actions-bottom-sheet-swap-button',
|
|
15
|
+
swapBack: 'button-icon',
|
|
12
16
|
swapSourceInput: 'source-token-area-input',
|
|
13
17
|
swapSourceToken: 'source-token-area',
|
|
14
18
|
swapDestinationToken: 'dest-token-area',
|
|
15
19
|
perpsHome: 'perps-home',
|
|
16
20
|
perpsHomeBack: 'perps-home-back-button',
|
|
21
|
+
perpsSearch: 'perps-home-search-toggle',
|
|
17
22
|
tutorialSkip: 'perps-tutorial-skip-button',
|
|
23
|
+
modeLite: 'perps-mode-selection-lite-option',
|
|
24
|
+
modePro: 'perps-mode-selection-pro-option',
|
|
18
25
|
market: 'perps-market-details-view',
|
|
19
26
|
marketBack: 'perps-market-header-back-button',
|
|
20
27
|
marketSubtitle: 'perps-market-header-subtitle',
|
|
@@ -23,6 +30,11 @@ const IDS = {
|
|
|
23
30
|
proMarketList: 'perps-pro-market-header-market-list-button',
|
|
24
31
|
proMarketSubtitle: 'perps-pro-market-header-subtitle',
|
|
25
32
|
marketList: 'perps-market-list',
|
|
33
|
+
marketListClose: 'perps-market-list-close-button-back-button',
|
|
34
|
+
tokenList: 'token-list',
|
|
35
|
+
tokenListBack: 'back-button',
|
|
36
|
+
tokenDetails: 'token-asset-overview',
|
|
37
|
+
tokenDetailsBack: 'back-arrow-button',
|
|
26
38
|
};
|
|
27
39
|
|
|
28
40
|
export async function navigateOpaqueMobile(input, destination) {
|
|
@@ -33,8 +45,10 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
33
45
|
}
|
|
34
46
|
const app = String(env.ANDROID_PACKAGE_ID ?? 'io.metamask');
|
|
35
47
|
const session = nativeSessionName(env, device, app);
|
|
48
|
+
const stateDir = nativeAgentDeviceStateDir(env, input.context.projectRoot, device);
|
|
36
49
|
const client = createAgentDeviceClient({
|
|
37
50
|
session,
|
|
51
|
+
stateDir,
|
|
38
52
|
lockPolicy: 'reject',
|
|
39
53
|
lockPlatform: 'android',
|
|
40
54
|
});
|
|
@@ -44,6 +58,7 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
44
58
|
device,
|
|
45
59
|
app,
|
|
46
60
|
session,
|
|
61
|
+
stateDir,
|
|
47
62
|
client,
|
|
48
63
|
});
|
|
49
64
|
const context = {
|
|
@@ -52,9 +67,11 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
52
67
|
artifactsDir: input.context.artifactsDir,
|
|
53
68
|
env,
|
|
54
69
|
};
|
|
70
|
+
const perpsMode = destination.params?.mode === 'pro' ? 'pro' : 'lite';
|
|
55
71
|
const press = (testId) => transport.execute('ui.press', {
|
|
56
72
|
test_id: testId,
|
|
57
73
|
timeout_ms: Number(input.node?.timeout_ms ?? 30_000),
|
|
74
|
+
settle: false,
|
|
58
75
|
}, context);
|
|
59
76
|
const wait = (testId) => transport.execute('ui.wait_for', {
|
|
60
77
|
test_id: testId,
|
|
@@ -80,7 +97,9 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
80
97
|
forceFull: true,
|
|
81
98
|
});
|
|
82
99
|
const identifiers = async () => new Set(
|
|
83
|
-
(await snapshot()).nodes
|
|
100
|
+
(await snapshot()).nodes
|
|
101
|
+
.filter((node) => node.visibleToUser !== false)
|
|
102
|
+
.map((node) => String(node.identifier ?? '')),
|
|
84
103
|
);
|
|
85
104
|
|
|
86
105
|
try {
|
|
@@ -91,25 +110,25 @@ export async function navigateOpaqueMobile(input, destination) {
|
|
|
91
110
|
} else if (destination.page === 'swap') {
|
|
92
111
|
observedScreen = await navigateSwap({ identifiers, press, wait, back });
|
|
93
112
|
} else if (destination.page === 'perps-market-list') {
|
|
94
|
-
observedScreen = await navigatePerpsMarketList({ identifiers, press, wait, back, timeoutMs: Number(input.node?.timeout_ms ?? 30_000) });
|
|
113
|
+
observedScreen = await navigatePerpsMarketList({ identifiers, press, wait, back, timeoutMs: Number(input.node?.timeout_ms ?? 30_000), perpsMode });
|
|
95
114
|
} else {
|
|
96
115
|
const timeoutMs = Number(input.node?.timeout_ms ?? 30_000);
|
|
97
116
|
const initialIds = await identifiers();
|
|
98
117
|
observedScreen = destination.page === 'perps-market' && initialIds.has(IDS.marketList)
|
|
99
|
-
? await selectMarket({ destination, identifiers, press, snapshot, timeoutMs })
|
|
100
|
-
: await navigatePerps({ identifiers, press, wait, back, timeoutMs });
|
|
118
|
+
? await selectMarket({ destination, identifiers, press, snapshot, timeoutMs, perpsMode })
|
|
119
|
+
: await navigatePerps({ identifiers, press, wait, back, timeoutMs, perpsMode });
|
|
101
120
|
if (destination.page === 'perps-market') {
|
|
102
121
|
const market = String(destination.params.market.symbol).toUpperCase();
|
|
103
122
|
const ids = await identifiers();
|
|
104
123
|
if (ids.has(IDS.proMarket) && !await marketMatches(snapshot, market)) {
|
|
105
124
|
await press(IDS.proMarketList);
|
|
106
|
-
observedScreen = await selectMarket({ destination, identifiers, press, snapshot, timeoutMs });
|
|
125
|
+
observedScreen = await selectMarket({ destination, identifiers, press, snapshot, timeoutMs, perpsMode });
|
|
107
126
|
} else if (!ids.has(IDS.proMarket) && (!ids.has(IDS.market) || !await marketMatches(snapshot, market))) {
|
|
108
127
|
if (ids.has(IDS.market)) await press(IDS.marketBack);
|
|
109
128
|
await press(`perps-market-row-item-${market}`);
|
|
110
129
|
observedScreen = IDS.market;
|
|
111
130
|
}
|
|
112
|
-
await waitForPerpsDestination(identifiers, timeoutMs);
|
|
131
|
+
await waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode);
|
|
113
132
|
if (!await marketMatches(snapshot, market)) {
|
|
114
133
|
throw new Error(`Opaque Mobile navigation opened a different Perps market than ${market}.`);
|
|
115
134
|
}
|
|
@@ -147,10 +166,10 @@ async function navigateSwap({ identifiers, press, wait, back }) {
|
|
|
147
166
|
return IDS.swapSourceInput;
|
|
148
167
|
}
|
|
149
168
|
|
|
150
|
-
async function selectMarket({ destination, identifiers, press, snapshot, timeoutMs }) {
|
|
169
|
+
async function selectMarket({ destination, identifiers, press, snapshot, timeoutMs, perpsMode }) {
|
|
151
170
|
const market = String(destination.params.market.symbol).toUpperCase();
|
|
152
171
|
await press(`perps-market-row-item-${market}`);
|
|
153
|
-
const observed = await waitForPerpsDestination(identifiers, timeoutMs);
|
|
172
|
+
const observed = await waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode);
|
|
154
173
|
if (!await marketMatches(snapshot, market)) {
|
|
155
174
|
throw new Error(`Opaque Mobile navigation opened a different Perps market than ${market}.`);
|
|
156
175
|
}
|
|
@@ -162,32 +181,56 @@ async function navigateHome({ identifiers, press, wait, back }) {
|
|
|
162
181
|
const ids = await identifiers();
|
|
163
182
|
if (IDS.wallet.some((id) => ids.has(id))) return;
|
|
164
183
|
if (ids.has(IDS.walletTab)) {
|
|
165
|
-
await press
|
|
166
|
-
|
|
167
|
-
|
|
184
|
+
if (await pressCurrent(press, IDS.walletTab)) {
|
|
185
|
+
await wait(IDS.wallet[0]);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
168
189
|
}
|
|
169
190
|
if (ids.has(IDS.marketBack)) {
|
|
170
|
-
await press
|
|
191
|
+
await pressCurrent(press, IDS.marketBack);
|
|
171
192
|
continue;
|
|
172
193
|
}
|
|
173
194
|
if (ids.has(IDS.proMarketBack)) {
|
|
174
|
-
await press
|
|
195
|
+
await pressCurrent(press, IDS.proMarketBack);
|
|
175
196
|
continue;
|
|
176
197
|
}
|
|
177
198
|
if (ids.has(IDS.perpsHomeBack)) {
|
|
178
|
-
await press
|
|
199
|
+
await pressCurrent(press, IDS.perpsHomeBack);
|
|
179
200
|
continue;
|
|
180
201
|
}
|
|
181
|
-
if (ids.has(IDS.
|
|
182
|
-
await
|
|
202
|
+
if (ids.has(IDS.marketListClose)) {
|
|
203
|
+
await pressCurrent(press, IDS.marketListClose);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (ids.has(IDS.tokenList) && ids.has(IDS.tokenListBack)) {
|
|
207
|
+
await pressCurrent(press, IDS.tokenListBack);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (ids.has(IDS.tokenDetails) && ids.has(IDS.tokenDetailsBack)) {
|
|
211
|
+
await pressCurrent(press, IDS.tokenDetailsBack);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (ids.has(IDS.swapSourceInput) && ids.has(IDS.swapBack)) {
|
|
215
|
+
await pressCurrent(press, IDS.swapBack);
|
|
183
216
|
continue;
|
|
184
217
|
}
|
|
185
|
-
|
|
218
|
+
await back();
|
|
186
219
|
}
|
|
187
220
|
throw new Error('Opaque Mobile navigation did not reach wallet home.');
|
|
188
221
|
}
|
|
189
222
|
|
|
190
|
-
async function
|
|
223
|
+
async function pressCurrent(press, testId) {
|
|
224
|
+
try {
|
|
225
|
+
await press(testId);
|
|
226
|
+
return true;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (!/Selector did not match/iu.test(String(error?.message ?? error))) throw error;
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function navigatePerps({ identifiers, press, wait, back, timeoutMs, perpsMode }) {
|
|
191
234
|
let ids = await identifiers();
|
|
192
235
|
if (ids.has(IDS.proMarket)) return IDS.proMarket;
|
|
193
236
|
if (ids.has(IDS.market)) {
|
|
@@ -219,15 +262,20 @@ async function navigatePerps({ identifiers, press, wait, back, timeoutMs }) {
|
|
|
219
262
|
ids = await identifiers();
|
|
220
263
|
}
|
|
221
264
|
if (ids.has(IDS.tutorialSkip)) await press(IDS.tutorialSkip);
|
|
222
|
-
return waitForPerpsDestination(identifiers, timeoutMs);
|
|
265
|
+
return waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode);
|
|
223
266
|
}
|
|
224
267
|
|
|
225
|
-
async function navigatePerpsMarketList({ identifiers, press, wait, back, timeoutMs }) {
|
|
268
|
+
async function navigatePerpsMarketList({ identifiers, press, wait, back, timeoutMs, perpsMode }) {
|
|
226
269
|
let ids = await identifiers();
|
|
227
270
|
if (ids.has(IDS.marketList)) return IDS.marketList;
|
|
228
|
-
const observed = await navigatePerps({ identifiers, press, wait, back, timeoutMs });
|
|
271
|
+
const observed = await navigatePerps({ identifiers, press, wait, back, timeoutMs, perpsMode });
|
|
229
272
|
ids = await identifiers();
|
|
230
273
|
if (ids.has(IDS.marketList)) return IDS.marketList;
|
|
274
|
+
if (ids.has(IDS.perpsSearch)) {
|
|
275
|
+
await press(IDS.perpsSearch);
|
|
276
|
+
await wait(IDS.marketList);
|
|
277
|
+
return IDS.marketList;
|
|
278
|
+
}
|
|
231
279
|
if (ids.has(IDS.proMarketList)) {
|
|
232
280
|
await press(IDS.proMarketList);
|
|
233
281
|
await wait(IDS.marketList);
|
|
@@ -236,10 +284,14 @@ async function navigatePerpsMarketList({ identifiers, press, wait, back, timeout
|
|
|
236
284
|
throw new Error(`Opaque Mobile navigation reached ${observed}, but no visible market-list control was available.`);
|
|
237
285
|
}
|
|
238
286
|
|
|
239
|
-
async function waitForPerpsDestination(identifiers, timeoutMs) {
|
|
287
|
+
async function waitForPerpsDestination(identifiers, timeoutMs, press, perpsMode = 'lite') {
|
|
240
288
|
const deadline = Date.now() + timeoutMs;
|
|
241
289
|
while (Date.now() <= deadline) {
|
|
242
290
|
const ids = await identifiers();
|
|
291
|
+
if (ids.has(IDS.modeLite) || ids.has(IDS.modePro)) {
|
|
292
|
+
await press(perpsMode === 'pro' ? IDS.modePro : IDS.modeLite);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
243
295
|
for (const id of [IDS.perpsHome, IDS.market, IDS.proMarket]) {
|
|
244
296
|
if (ids.has(id)) return id;
|
|
245
297
|
}
|
|
@@ -15,11 +15,22 @@ function text(value) {
|
|
|
15
15
|
function pageRoute(node) {
|
|
16
16
|
const page = text(node?.page);
|
|
17
17
|
if (!page) return undefined;
|
|
18
|
-
|
|
18
|
+
const mode = text(node?.mode)?.toLowerCase() ?? 'lite';
|
|
19
|
+
if (!['lite', 'pro'].includes(mode)) {
|
|
20
|
+
throw new Error('mobile ui.navigate mode must be lite or pro.');
|
|
21
|
+
}
|
|
22
|
+
if (PAGE_ROUTES[page]) {
|
|
23
|
+
const route = PAGE_ROUTES[page];
|
|
24
|
+
return {
|
|
25
|
+
page,
|
|
26
|
+
...route,
|
|
27
|
+
params: page.startsWith('perps') ? { ...route.params, mode } : route.params,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
19
30
|
if (page === 'perps-market') {
|
|
20
31
|
const market = text(node.market) ?? text(node.symbol) ?? text(node.params?.market?.symbol);
|
|
21
32
|
if (!market) throw new Error('mobile ui.navigate page=perps-market requires market or symbol.');
|
|
22
|
-
return { page, route: 'PerpsMarketDetails', params: { market: { symbol: market } } };
|
|
33
|
+
return { page, route: 'PerpsMarketDetails', params: { market: { symbol: market }, mode } };
|
|
23
34
|
}
|
|
24
35
|
throw new Error('mobile ui.navigate supported page intents: home, perps, perps-market-list, perps-market, swap.');
|
|
25
36
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runAdapter } from '../platform/bridge.mjs';
|
|
2
|
+
import { isOpaqueMobileRuntime, lockWalletThroughNativeUi } from './native-ui.mjs';
|
|
3
|
+
|
|
4
|
+
runAdapter(async (input) => {
|
|
5
|
+
if (!isOpaqueMobileRuntime(input)) {
|
|
6
|
+
throw new Error('metamask.wallet.lock requires an opaque Mobile runtime.');
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
action: input.action,
|
|
10
|
+
...await lockWalletThroughNativeUi(input),
|
|
11
|
+
locked: true,
|
|
12
|
+
proofPath: 'mobile-visible-wallet-lock-native',
|
|
13
|
+
};
|
|
14
|
+
});
|