@deeeed/metamask-harness 0.36.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/adapters/extension/artifact-runtime-state.cjs +138 -0
- package/adapters/extension/inject.mjs +2 -0
- package/adapters/extension/launch-browser.cjs +24 -0
- package/adapters/extension/live.sh +82 -12
- package/adapters/extension/readiness.mjs +77 -36
- package/adapters/extension/snapshot-dist.sh +88 -3
- package/adapters/extension/verify.sh +6 -2
- package/adapters/manifest.json +17 -1
- package/adapters/shared/log-tui.mjs +1 -1
- package/dist/adapters/extension/artifact-integrity.js +38 -0
- package/dist/adapters/extension/extension-id.js +23 -4
- package/dist/adapters/extension/release-artifact.js +386 -0
- package/dist/adapters/extension/runtime-decision.js +164 -20
- package/dist/adapters/extension/runtime.js +169 -3
- package/dist/adapters/mobile/provision.js +38 -3
- package/dist/adapters/mobile/release-artifact-state.js +124 -0
- package/dist/adapters/mobile/release-artifact.js +295 -0
- package/dist/adapters.js +78 -6
- package/dist/app-lifecycle.js +4 -3
- package/dist/command-contract.js +16 -0
- package/dist/commands/call.js +6 -2
- package/dist/commands/launch/extension.js +74 -9
- package/dist/commands/launch/index.js +3 -0
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/provision.js +2 -0
- package/dist/commands/run-engine.js +109 -14
- package/dist/commands/run.js +4 -2
- package/dist/commands/runtime-launch.js +178 -10
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +3 -1
- package/dist/metamask-action-validation.js +47 -1
- package/dist/mm-harness-cli.js +31 -1
- package/dist/recipe-security.js +40 -0
- package/dist/run-diagnostics.js +1 -1
- package/dist/runner.js +3 -0
- package/docs/RELEASE-QA-CAPABILITY-MAP.md +125 -0
- package/library/actions/extension/assets/finish_send.mjs +128 -0
- package/library/actions/extension/assets/import_custom_token.mjs +227 -0
- package/library/actions/extension/assets/prepare_send.mjs +53 -0
- package/library/actions/extension/assets/read_details.mjs +57 -0
- package/library/actions/extension/assets/read_visible_state.mjs +50 -0
- package/library/actions/extension/assets/review_send.mjs +23 -0
- package/library/actions/extension/assets/select_network_scope.mjs +30 -0
- package/library/actions/extension/assets/send.mjs +104 -0
- package/library/actions/extension/assets/set_custom_gas.mjs +56 -0
- package/library/actions/extension/assets/set_token_visibility.mjs +129 -0
- package/library/actions/extension/assets/verify_sorting.mjs +89 -0
- package/library/actions/extension/networks/add_chainlist.mjs +94 -0
- package/library/actions/extension/networks/add_custom.mjs +123 -0
- package/library/actions/extension/networks/custom_network.mjs +103 -0
- package/library/actions/extension/networks/read_visible_state.mjs +50 -0
- package/library/actions/extension/networks/remove_custom.mjs +68 -0
- package/library/actions/extension/perps/close_orders.mjs +1 -0
- package/library/actions/extension/perps/close_visible_position.mjs +133 -0
- package/library/actions/extension/perps/compare_provider_market.mjs +109 -0
- package/library/actions/extension/perps/edit_margin.mjs +82 -0
- package/library/actions/extension/perps/open_balance_action.mjs +58 -0
- package/library/actions/extension/perps/open_position_action.mjs +80 -0
- package/library/actions/extension/perps/perps.mjs +196 -2
- package/library/actions/extension/perps/place_order.mjs +5 -2
- package/library/actions/extension/perps/read_eligibility.mjs +34 -0
- package/library/actions/extension/perps/read_funds_confirmation.mjs +69 -0
- package/library/actions/extension/perps/read_snapshot.mjs +470 -0
- package/library/actions/extension/perps/read_visible_state.mjs +98 -0
- package/library/actions/extension/perps/select_activity_filter.mjs +44 -0
- package/library/actions/extension/perps/select_market_filter.mjs +71 -0
- package/library/actions/extension/perps/set_market_favorite.mjs +47 -0
- package/library/actions/extension/perps/update_position_tpsl.mjs +106 -0
- package/library/actions/extension/platform/cdp.mjs +109 -3
- package/library/actions/extension/settings/set_basic_functionality.mjs +58 -0
- package/library/actions/extension/swap_bridge/read_visible_state.mjs +71 -0
- package/library/actions/extension/swap_bridge/select_assets.mjs +103 -0
- package/library/actions/extension/swap_bridge/set_amount.mjs +36 -0
- package/library/actions/extension/swap_bridge/set_max_amount.mjs +58 -0
- package/library/actions/extension/swap_bridge/set_slippage.mjs +121 -0
- package/library/actions/extension/swap_bridge/submit_transaction.mjs +363 -0
- package/library/actions/extension/ui/activate_and_observe.mjs +96 -0
- package/library/actions/extension/ui/navigate.mjs +264 -8
- package/library/actions/extension/wallet/ensure_unlocked.mjs +2 -6
- package/library/actions/extension/wallet/import.mjs +13 -46
- package/library/actions/extension/wallet/lock.mjs +35 -0
- package/library/actions/extension/wallet/secret-input.mjs +98 -0
- package/library/actions/extension/wallet/select_account.mjs +71 -7
- package/library/actions/mobile/assets/read_visible_state.mjs +37 -0
- package/library/actions/mobile/perps/read_visible_state.mjs +31 -0
- package/library/actions/mobile/platform/native-session-name.mjs +5 -0
- package/library/actions/mobile/platform/observe-ui.mjs +86 -2
- package/library/actions/mobile/swap_bridge/native-session.mjs +45 -0
- package/library/actions/mobile/swap_bridge/read_visible_state.mjs +31 -0
- package/library/actions/mobile/swap_bridge/select_assets.mjs +94 -0
- package/library/actions/mobile/swap_bridge/set_amount.mjs +73 -0
- package/library/actions/mobile/ui/native-navigation.mjs +265 -0
- package/library/actions/mobile/ui/navigate.mjs +10 -1
- package/library/actions/mobile/wallet/ensure_unlocked.mjs +17 -0
- package/library/actions/mobile/wallet/import.mjs +71 -2
- package/library/actions/mobile/wallet/native-ui.mjs +512 -0
- package/library/actions/mobile/wallet/read_state.mjs +16 -0
- package/library/actions/mobile/wallet/reset.mjs +17 -4
- package/library/actions/shared/assets/visible-state.mjs +176 -0
- package/library/actions/shared/networks/visible-state.mjs +70 -0
- package/library/actions/shared/perps/visible-state.mjs +354 -0
- package/library/actions/shared/swap-bridge/transaction.mjs +54 -0
- package/library/actions/shared/swap-bridge/visible-state.mjs +97 -0
- package/library/actions/shared/ui/locators.mjs +7 -0
- package/library/manifests/extension.action-manifest.json +1366 -184
- package/library/manifests/mobile.action-manifest.json +235 -1
- package/library/recipes/assets/release-custom-gas-send.recipe.json +88 -0
- package/library/recipes/assets/release-custom-network-token-persistence.recipe.json +106 -0
- package/library/recipes/assets/release-native-max-cancel.recipe.json +70 -0
- package/library/recipes/assets/release-native-max-send.recipe.json +78 -0
- package/library/recipes/assets/release-own-account-send.recipe.json +80 -0
- package/library/recipes/assets/release-token-address-import.recipe.json +47 -0
- package/library/recipes/assets/release-token-details.recipe.json +48 -0
- package/library/recipes/assets/release-token-hide-readd.recipe.json +55 -0
- package/library/recipes/assets/release-token-list.recipe.json +75 -0
- package/library/recipes/assets/release-token-persistence.recipe.json +73 -0
- package/library/recipes/assets/release-token-search-import.recipe.json +40 -0
- package/library/recipes/assets/release-token-sorting.recipe.json +29 -0
- package/library/recipes/extension/networks/release-balance-stability.recipe.json +51 -0
- package/library/recipes/extension/perps/release-live-limit-order.recipe.json +104 -0
- package/library/recipes/extension/perps/release-order-entry.recipe.json +155 -0
- package/library/recipes/extension/perps/release-order-validation.recipe.json +220 -0
- package/library/recipes/extension/runner/action-validation.recipe.json +12 -1
- package/library/recipes/perps/release-activity-filters.recipe.json +51 -0
- package/library/recipes/perps/release-funds-flows.recipe.json +76 -0
- package/library/recipes/perps/release-live-market-order.recipe.json +122 -0
- package/library/recipes/perps/release-market-details.recipe.json +83 -0
- package/library/recipes/perps/release-market-search.recipe.json +79 -0
- package/library/recipes/perps/release-watchlist.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-custom-slippage.recipe.json +90 -0
- package/library/recipes/swap-bridge/release-live-max-swap.recipe.json +99 -0
- package/library/recipes/swap-bridge/release-live-swap.recipe.json +101 -0
- package/library/recipes/swap-bridge/release-max-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-quote.recipe.json +72 -0
- package/library/recipes/swap-bridge/release-surface.recipe.json +43 -0
- package/library/recipes/wallet/import.recipe.json +20 -1
- package/library/recipes/wallet/reset-import.recipe.json +20 -1
- package/package.json +6 -5
- package/scripts/validate-release-report.mjs +319 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { dataTestId } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
async function visible(page, selector) {
|
|
4
|
+
return page.evaluate(`(() => {
|
|
5
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
6
|
+
if (!element) return false;
|
|
7
|
+
const style = getComputedStyle(element);
|
|
8
|
+
const rect = element.getBoundingClientRect();
|
|
9
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
10
|
+
})()`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function waitFor(page, predicate, message, timeoutMs) {
|
|
14
|
+
const deadline = Date.now() + timeoutMs;
|
|
15
|
+
while (Date.now() < deadline) {
|
|
16
|
+
const value = await predicate();
|
|
17
|
+
if (value) return value;
|
|
18
|
+
await page.evaluate('new Promise((resolve) => setTimeout(resolve, 150))');
|
|
19
|
+
}
|
|
20
|
+
throw new Error(message);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function semanticButtonOrNull(page, label) {
|
|
24
|
+
const marker = `mm-harness-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
25
|
+
const found = await page.evaluate(`(() => {
|
|
26
|
+
const label = ${JSON.stringify(label)};
|
|
27
|
+
const marker = ${JSON.stringify(marker)};
|
|
28
|
+
for (const element of document.querySelectorAll('button')) {
|
|
29
|
+
const text = String(element.getAttribute('aria-label') || element.innerText || element.textContent || '').replace(/\\s+/gu, ' ').trim();
|
|
30
|
+
const style = getComputedStyle(element);
|
|
31
|
+
const rect = element.getBoundingClientRect();
|
|
32
|
+
if (text === label && !element.disabled && element.getAttribute('aria-disabled') !== 'true' && style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0) {
|
|
33
|
+
element.setAttribute('data-mm-harness-target', marker);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
})()`);
|
|
39
|
+
return found ? `[data-mm-harness-target="${marker}"]` : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function semanticButton(page, label) {
|
|
43
|
+
const selector = await semanticButtonOrNull(page, label);
|
|
44
|
+
if (!selector) throw new Error(`Visible button ${JSON.stringify(label)} was not found.`);
|
|
45
|
+
return selector;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function openNetworkManager(page, timeoutMs) {
|
|
49
|
+
const menu = dataTestId('account-options-menu-button');
|
|
50
|
+
const networks = dataTestId('global-menu-networks');
|
|
51
|
+
await page.click(menu);
|
|
52
|
+
await waitFor(page, () => visible(page, networks), 'Networks did not become visible in the account menu.', timeoutMs);
|
|
53
|
+
await page.click(networks);
|
|
54
|
+
await waitFor(
|
|
55
|
+
page,
|
|
56
|
+
() => visible(page, dataTestId('networks-page-add-custom-network-button')),
|
|
57
|
+
'Network manager did not open.',
|
|
58
|
+
timeoutMs,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function findNetworkItem(page, network, chainId) {
|
|
63
|
+
return page.evaluate(`(() => {
|
|
64
|
+
const network = ${JSON.stringify(network.toLowerCase())};
|
|
65
|
+
const chainIds = new Set(${JSON.stringify([
|
|
66
|
+
String(chainId),
|
|
67
|
+
`0x${chainId.toString(16)}`,
|
|
68
|
+
`eip155:${chainId}`,
|
|
69
|
+
].map((value) => value.toLowerCase()))});
|
|
70
|
+
for (const element of document.querySelectorAll('[data-testid^="network-list-item-"]')) {
|
|
71
|
+
const testId = String(element.getAttribute('data-testid') || '');
|
|
72
|
+
if (testId.includes('-options-')) continue;
|
|
73
|
+
const suffix = testId.slice('network-list-item-'.length).toLowerCase();
|
|
74
|
+
const label = String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500);
|
|
75
|
+
const nameMatches = [...element.querySelectorAll('[data-testid]')]
|
|
76
|
+
.some((child) => String(child.getAttribute('data-testid') || '').toLowerCase() === network);
|
|
77
|
+
if (chainIds.has(suffix) && nameMatches) return { testId, label, suffix };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
})()`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function selectHomeNetwork(page, network, chainId, timeoutMs) {
|
|
84
|
+
const sort = dataTestId('sort-by-networks');
|
|
85
|
+
const chainHex = `0x${chainId.toString(16)}`;
|
|
86
|
+
const choices = [
|
|
87
|
+
dataTestId(`home-network-filter-network-${chainHex}`),
|
|
88
|
+
dataTestId(`home-network-filter-custom-${chainHex}`),
|
|
89
|
+
dataTestId(`home-network-filter-additional-${chainHex}`),
|
|
90
|
+
];
|
|
91
|
+
await page.click(sort);
|
|
92
|
+
const choice = await waitFor(page, async () => {
|
|
93
|
+
for (const candidate of choices) {
|
|
94
|
+
if (await visible(page, candidate)) return candidate;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}, `${network} did not appear in the home network filter.`, timeoutMs);
|
|
98
|
+
await page.click(choice);
|
|
99
|
+
await waitFor(page, async () => {
|
|
100
|
+
const label = await page.evaluate(`String(document.querySelector(${JSON.stringify(sort)})?.innerText || '')`);
|
|
101
|
+
return label.toLowerCase().includes(network.toLowerCase());
|
|
102
|
+
}, `${network} did not become the selected home network.`, timeoutMs);
|
|
103
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
normalizeVisibleNetworkState,
|
|
4
|
+
resolveVisibleNetworkOptions,
|
|
5
|
+
} from '../../shared/networks/visible-state.mjs';
|
|
6
|
+
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function capture(page, maximum) {
|
|
12
|
+
return page.evaluate(`(() => {
|
|
13
|
+
const rows = [...document.querySelectorAll('[data-testid="multichain-token-list-button"]')]
|
|
14
|
+
.filter((element) => {
|
|
15
|
+
const style = getComputedStyle(element);
|
|
16
|
+
const rect = element.getBoundingClientRect();
|
|
17
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
18
|
+
})
|
|
19
|
+
.slice(0, ${JSON.stringify(maximum)})
|
|
20
|
+
.map((element) => {
|
|
21
|
+
const change = element.querySelector('[data-testid^="token-increase-decrease-percentage-"]');
|
|
22
|
+
return {
|
|
23
|
+
testId: element.getAttribute('data-testid'),
|
|
24
|
+
label: String(element.getAttribute('aria-label') || element.innerText || '').replace(/\\s+/gu, ' ').trim(),
|
|
25
|
+
name: element.querySelector('[data-testid="multichain-token-list-item-token-name"]')?.innerText,
|
|
26
|
+
fiat: element.querySelector('[data-testid="multichain-token-list-item-secondary-value"]')?.innerText,
|
|
27
|
+
amount: element.querySelector('[data-testid="multichain-token-list-item-value"]')?.innerText,
|
|
28
|
+
priceChange: change?.innerText,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
network: document.querySelector('[data-testid="sort-by-networks"]')?.innerText,
|
|
33
|
+
errorText: document.body?.innerText,
|
|
34
|
+
items: rows,
|
|
35
|
+
truncated: document.querySelectorAll('[data-testid="multichain-token-list-button"]').length > rows.length,
|
|
36
|
+
};
|
|
37
|
+
})()`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
41
|
+
const options = resolveVisibleNetworkOptions(input.node);
|
|
42
|
+
const before = await capture(page, options.assets.maxItems);
|
|
43
|
+
if (options.settleMs > 0) await sleep(options.settleMs);
|
|
44
|
+
const after = options.settleMs > 0 ? await capture(page, options.assets.maxItems) : before;
|
|
45
|
+
return {
|
|
46
|
+
action: input.action,
|
|
47
|
+
...normalizeVisibleNetworkState(before, after, options, 'extension'),
|
|
48
|
+
proofPath: 'visible-dom-accessibility',
|
|
49
|
+
};
|
|
50
|
+
}));
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
import {
|
|
3
|
+
findNetworkItem,
|
|
4
|
+
openNetworkManager,
|
|
5
|
+
selectHomeNetwork,
|
|
6
|
+
semanticButtonOrNull,
|
|
7
|
+
waitFor,
|
|
8
|
+
} from './custom_network.mjs';
|
|
9
|
+
|
|
10
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
11
|
+
const network = String(input.node?.network || '').trim();
|
|
12
|
+
const chainId = Number(input.node?.chain_id);
|
|
13
|
+
const timeoutMs = Number(input.node?.timeout_ms ?? 30_000);
|
|
14
|
+
if (!network || !Number.isInteger(chainId) || chainId <= 0) {
|
|
15
|
+
throw new Error('metamask.networks.remove_custom requires network and positive chain_id.');
|
|
16
|
+
}
|
|
17
|
+
await selectHomeNetwork(page, 'Ethereum', 1, timeoutMs);
|
|
18
|
+
await openNetworkManager(page, timeoutMs);
|
|
19
|
+
const item = await findNetworkItem(page, network, chainId);
|
|
20
|
+
if (!item) throw new Error(`${network} (${chainId}) is not present for removal.`);
|
|
21
|
+
await page.click(dataTestId(`network-list-item-options-button-${item.suffix}`));
|
|
22
|
+
const remove = await waitFor(
|
|
23
|
+
page,
|
|
24
|
+
() => semanticButtonOrNull(page, 'Delete'),
|
|
25
|
+
'Custom network Delete action did not become visible.',
|
|
26
|
+
timeoutMs,
|
|
27
|
+
);
|
|
28
|
+
await page.click(remove);
|
|
29
|
+
await waitFor(
|
|
30
|
+
page,
|
|
31
|
+
() => page.evaluate(`Boolean(document.querySelector(${JSON.stringify(dataTestId('confirm-delete-network-modal'))}))`),
|
|
32
|
+
'Custom network delete confirmation did not appear.',
|
|
33
|
+
timeoutMs,
|
|
34
|
+
);
|
|
35
|
+
await page.click(dataTestId('confirm-delete-network-modal-delete-button'));
|
|
36
|
+
await waitFor(
|
|
37
|
+
page,
|
|
38
|
+
() => page.evaluate(`!document.querySelector(${JSON.stringify(dataTestId('confirm-delete-network-modal'))})`),
|
|
39
|
+
'Custom network delete confirmation did not close.',
|
|
40
|
+
timeoutMs,
|
|
41
|
+
);
|
|
42
|
+
await waitFor(
|
|
43
|
+
page,
|
|
44
|
+
async () => !(await findNetworkItem(page, network, chainId)),
|
|
45
|
+
`${network} remained in the network manager after deletion.`,
|
|
46
|
+
timeoutMs,
|
|
47
|
+
);
|
|
48
|
+
const back = await waitFor(
|
|
49
|
+
page,
|
|
50
|
+
() => semanticButtonOrNull(page, 'Back'),
|
|
51
|
+
'Network manager Back button did not become visible.',
|
|
52
|
+
timeoutMs,
|
|
53
|
+
);
|
|
54
|
+
await page.click(back);
|
|
55
|
+
await waitFor(
|
|
56
|
+
page,
|
|
57
|
+
() => page.evaluate(`Boolean(document.querySelector(${JSON.stringify(dataTestId('sort-by-networks'))}))`),
|
|
58
|
+
'Wallet home did not reopen after network deletion.',
|
|
59
|
+
timeoutMs,
|
|
60
|
+
);
|
|
61
|
+
return {
|
|
62
|
+
action: input.action,
|
|
63
|
+
network,
|
|
64
|
+
chainId,
|
|
65
|
+
removed: true,
|
|
66
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
67
|
+
};
|
|
68
|
+
}));
|
|
@@ -3,6 +3,7 @@ import { assertOrders, closeOrders } from './perps.mjs';
|
|
|
3
3
|
|
|
4
4
|
runAdapter(async (input) => {
|
|
5
5
|
const close = await closeOrders(input);
|
|
6
|
+
if (close.proofPath === 'visible-product-controls') return close;
|
|
6
7
|
const assertion = await assertOrders(input, false);
|
|
7
8
|
return { ...assertion, close };
|
|
8
9
|
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
function marketSymbol(value) {
|
|
4
|
+
const result = String(value ?? '').trim().toUpperCase();
|
|
5
|
+
if (!/^[A-Z0-9:_-]{2,20}$/u.test(result)) {
|
|
6
|
+
throw new Error('metamask.perps.close_visible_position requires a valid market symbol.');
|
|
7
|
+
}
|
|
8
|
+
return result;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
12
|
+
const market = marketSymbol(input.node?.market);
|
|
13
|
+
const partialAmount = input.node?.amount == null ? null : Number(input.node.amount);
|
|
14
|
+
if (partialAmount != null && (!Number.isFinite(partialAmount) || partialAmount <= 0)) {
|
|
15
|
+
throw new Error('metamask.perps.close_visible_position amount must be positive.');
|
|
16
|
+
}
|
|
17
|
+
if (input.node?.confirm_live !== true) {
|
|
18
|
+
throw new Error('metamask.perps.close_visible_position requires confirm_live=true.');
|
|
19
|
+
}
|
|
20
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
|
|
21
|
+
await page.waitForSelector(dataTestId('perps-position-size-value'), { timeoutMs: 20_000 });
|
|
22
|
+
const before = await page.evaluate(`(() => {
|
|
23
|
+
const market = ${JSON.stringify(market)};
|
|
24
|
+
const route = String(location.hash);
|
|
25
|
+
const symbol = String(document.querySelector('[data-testid="perps-market-detail-pair"]')?.innerText || '')
|
|
26
|
+
.trim()
|
|
27
|
+
.toUpperCase();
|
|
28
|
+
if (!route.includes('/perps/market/') || !symbol.startsWith(market + '-')) {
|
|
29
|
+
throw new Error('Visible Perps market does not match the requested close market.');
|
|
30
|
+
}
|
|
31
|
+
const read = (testId) => String(document.querySelector('[data-testid="' + testId + '"]')?.innerText || '')
|
|
32
|
+
.replace(/\\s+/gu, ' ')
|
|
33
|
+
.trim()
|
|
34
|
+
.slice(0, 500);
|
|
35
|
+
const size = read('perps-position-size-value');
|
|
36
|
+
if (!size) throw new Error('No visible Perps position is available to close.');
|
|
37
|
+
return {
|
|
38
|
+
route,
|
|
39
|
+
market,
|
|
40
|
+
size,
|
|
41
|
+
direction: read('perps-position-leverage'),
|
|
42
|
+
margin: read('perps-position-margin-value'),
|
|
43
|
+
entryPrice: read('perps-position-entry-value'),
|
|
44
|
+
liquidationPrice: read('perps-position-liquidation-value'),
|
|
45
|
+
};
|
|
46
|
+
})()`);
|
|
47
|
+
let confirmation;
|
|
48
|
+
for (let attempt = 0; attempt < 3 && !confirmation; attempt += 1) {
|
|
49
|
+
await page.click(dataTestId('perps-close-cta-button'));
|
|
50
|
+
const deadline = Date.now() + 3_000;
|
|
51
|
+
while (Date.now() <= deadline) {
|
|
52
|
+
confirmation = await page.evaluate(`(() => {
|
|
53
|
+
const modal = document.querySelector('[data-testid="perps-close-position-modal"]');
|
|
54
|
+
const submit = document.querySelector('[data-testid="perps-close-position-modal-submit"]');
|
|
55
|
+
if (!modal || !submit) return null;
|
|
56
|
+
const read = (testId) => String(document.querySelector('[data-testid="' + testId + '"]')?.innerText || '')
|
|
57
|
+
.replace(/\\s+/gu, ' ')
|
|
58
|
+
.trim()
|
|
59
|
+
.slice(0, 500);
|
|
60
|
+
return {
|
|
61
|
+
amount: String(document.querySelector('[data-testid="close-amount-value"] input')?.value || read('close-amount-value')),
|
|
62
|
+
fullSizeSelected: Boolean(document.querySelector('[data-testid="close-amount-slider-pct-100"]')),
|
|
63
|
+
margin: read('perps-close-summary-margin-value'),
|
|
64
|
+
fees: read('perps-close-summary-fees-value'),
|
|
65
|
+
receive: read('perps-close-summary-receive-value'),
|
|
66
|
+
submitEnabled: !submit.disabled && submit.getAttribute('aria-disabled') !== 'true',
|
|
67
|
+
};
|
|
68
|
+
})()`);
|
|
69
|
+
if (confirmation) break;
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!confirmation) throw new Error('Perps close confirmation did not become visible.');
|
|
74
|
+
if (partialAmount != null) {
|
|
75
|
+
await page.setInput('input[data-testid="close-amount-value"], [data-testid="close-amount-value"] input', String(partialAmount));
|
|
76
|
+
const readyDeadline = Date.now() + 10_000;
|
|
77
|
+
while (Date.now() <= readyDeadline) {
|
|
78
|
+
confirmation = await page.evaluate(`(() => {
|
|
79
|
+
const submit = document.querySelector('[data-testid="perps-close-position-modal-submit"]');
|
|
80
|
+
return {
|
|
81
|
+
amount: document.querySelector('input[data-testid="close-amount-value"], [data-testid="close-amount-value"] input')?.value ?? null,
|
|
82
|
+
margin: String(document.querySelector('[data-testid="perps-close-summary-margin-value"]')?.innerText || '').trim(),
|
|
83
|
+
fees: String(document.querySelector('[data-testid="perps-close-summary-fees-value"]')?.innerText || '').trim(),
|
|
84
|
+
receive: String(document.querySelector('[data-testid="perps-close-summary-receive-value"]')?.innerText || '').trim(),
|
|
85
|
+
submitEnabled: Boolean(submit && !submit.disabled && submit.getAttribute('aria-disabled') !== 'true'),
|
|
86
|
+
warnings: [...document.querySelectorAll('[role="alert"]')]
|
|
87
|
+
.map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim())
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.slice(0, 20),
|
|
90
|
+
};
|
|
91
|
+
})()`);
|
|
92
|
+
if (Number(confirmation.amount) === partialAmount && confirmation.submitEnabled) break;
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!confirmation.submitEnabled) throw new Error('Perps close confirmation is not submit-ready.');
|
|
97
|
+
await page.click(dataTestId('perps-close-position-modal-submit'));
|
|
98
|
+
const closeDeadline = Date.now() + 45_000;
|
|
99
|
+
let finalState;
|
|
100
|
+
while (Date.now() <= closeDeadline) {
|
|
101
|
+
finalState = await page.evaluate(`(() => ({
|
|
102
|
+
route: String(location.hash),
|
|
103
|
+
positionVisible: Boolean(document.querySelector('[data-testid="perps-position-size-value"]')),
|
|
104
|
+
positionSize: String(document.querySelector('[data-testid="perps-position-size-value"]')?.innerText || '').trim(),
|
|
105
|
+
modalVisible: Boolean(document.querySelector('[data-testid="perps-close-position-modal"]')),
|
|
106
|
+
error: String(document.querySelector('[data-testid="perps-order-submit-error"]')?.innerText || '').trim(),
|
|
107
|
+
activity: [...document.querySelectorAll('[data-testid^="transaction-card-"]')]
|
|
108
|
+
.slice(0, 5)
|
|
109
|
+
.map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 500)),
|
|
110
|
+
}))()`);
|
|
111
|
+
if (finalState.error) throw new Error(`Perps close failed: ${finalState.error}`);
|
|
112
|
+
if (partialAmount != null) {
|
|
113
|
+
if (!finalState.modalVisible && finalState.positionVisible && finalState.positionSize !== before.size) break;
|
|
114
|
+
} else if (!finalState.positionVisible && !finalState.modalVisible) break;
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
116
|
+
}
|
|
117
|
+
const incomplete = partialAmount != null
|
|
118
|
+
? finalState?.modalVisible || !finalState?.positionVisible || finalState?.positionSize === before.size
|
|
119
|
+
: finalState?.positionVisible || finalState?.modalVisible;
|
|
120
|
+
if (incomplete) {
|
|
121
|
+
throw new Error('Perps position did not reach the requested close state after the timeout.');
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
action: input.action,
|
|
125
|
+
market,
|
|
126
|
+
closed: partialAmount == null,
|
|
127
|
+
partial: partialAmount != null,
|
|
128
|
+
positionBefore: before,
|
|
129
|
+
confirmation,
|
|
130
|
+
finalState,
|
|
131
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
132
|
+
};
|
|
133
|
+
}));
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { dataTestId, marketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
function numeric(value) {
|
|
4
|
+
const match = String(value ?? '').replace(/[$,]/gu, '').match(/-?[0-9]+(?:\.[0-9]+)?/u);
|
|
5
|
+
if (!match) return null;
|
|
6
|
+
const parsed = Number(match[0]);
|
|
7
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function compactUsd(value) {
|
|
11
|
+
const parsed = numeric(value);
|
|
12
|
+
if (parsed == null) return null;
|
|
13
|
+
const suffix = String(value).trim().match(/[KMBT](?=$|\s)/u)?.[0];
|
|
14
|
+
return parsed * ({ K: 1e3, M: 1e6, B: 1e9, T: 1e12 }[suffix] ?? 1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function relativeDifference(actual, expected) {
|
|
18
|
+
return Math.abs(actual - expected) / Math.max(Math.abs(expected), 1e-12);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
runAdapter(async (input) => {
|
|
22
|
+
const market = marketSymbol(input);
|
|
23
|
+
if (!market) throw new Error('metamask.perps.compare_provider_market requires market=<symbol>.');
|
|
24
|
+
const endpoint = String(input.node?.endpoint ?? 'https://api.hyperliquid.xyz/info');
|
|
25
|
+
if (endpoint !== 'https://api.hyperliquid.xyz/info') {
|
|
26
|
+
throw new Error('metamask.perps.compare_provider_market endpoint must be the Hyperliquid production info API.');
|
|
27
|
+
}
|
|
28
|
+
const response = await fetch(endpoint, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'content-type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({ type: 'metaAndAssetCtxs' }),
|
|
32
|
+
signal: AbortSignal.timeout(Number(input.node?.timeout_ms ?? 15_000)),
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) throw new Error(`Hyperliquid market request failed with HTTP ${response.status}.`);
|
|
35
|
+
const payload = await response.json();
|
|
36
|
+
const universe = payload?.[0]?.universe;
|
|
37
|
+
const contexts = payload?.[1];
|
|
38
|
+
const index = Array.isArray(universe) ? universe.findIndex((entry) => entry?.name === market) : -1;
|
|
39
|
+
if (index < 0 || !Array.isArray(contexts) || !contexts[index]) {
|
|
40
|
+
throw new Error(`Hyperliquid production response did not contain ${market}.`);
|
|
41
|
+
}
|
|
42
|
+
const metadata = universe[index];
|
|
43
|
+
const context = contexts[index];
|
|
44
|
+
|
|
45
|
+
return withExtensionPage(input, async (page) => {
|
|
46
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
|
|
47
|
+
await page.waitForSelector(dataTestId('perps-market-detail-pair'), { timeoutMs: 20_000 });
|
|
48
|
+
const visible = await page.evaluate(`(() => {
|
|
49
|
+
const read = (testId) => String(document.querySelector('[data-testid="' + testId + '"]')?.innerText || '').replace(/\\s+/gu, ' ').trim();
|
|
50
|
+
const text = read('perps-market-detail-page');
|
|
51
|
+
const after = (label) => text.match(new RegExp(label + '\\\\s+([^\\\\s]+)', 'u'))?.[1] ?? null;
|
|
52
|
+
return {
|
|
53
|
+
route: String(location.hash),
|
|
54
|
+
name: read('perps-market-detail-name'),
|
|
55
|
+
pair: read('perps-market-detail-pair'),
|
|
56
|
+
maxLeverage: read('perps-market-max-leverage'),
|
|
57
|
+
price: read('perps-market-detail-price'),
|
|
58
|
+
volume: after('24h Volume'),
|
|
59
|
+
openInterest: after('Open interest'),
|
|
60
|
+
fundingRate: after('Funding rate'),
|
|
61
|
+
oraclePrice: read('perps-market-detail-oracle-price'),
|
|
62
|
+
favoriteLabel: String(document.querySelector('[data-testid="perps-market-detail-favorite-button"]')?.getAttribute('aria-label') || '').trim(),
|
|
63
|
+
};
|
|
64
|
+
})()`);
|
|
65
|
+
const provider = {
|
|
66
|
+
symbol: metadata.name,
|
|
67
|
+
maxLeverage: Number(metadata.maxLeverage),
|
|
68
|
+
markPrice: Number(context.markPx),
|
|
69
|
+
oraclePrice: Number(context.oraclePx),
|
|
70
|
+
volume24hUsd: Number(context.dayNtlVlm),
|
|
71
|
+
openInterestUsd: Number(context.openInterest) * Number(context.markPx),
|
|
72
|
+
fundingRate: Number(context.funding),
|
|
73
|
+
};
|
|
74
|
+
const observed = {
|
|
75
|
+
price: numeric(visible.price),
|
|
76
|
+
oraclePrice: numeric(visible.oraclePrice),
|
|
77
|
+
volume24hUsd: compactUsd(visible.volume),
|
|
78
|
+
openInterestUsd: compactUsd(visible.openInterest),
|
|
79
|
+
fundingRate: numeric(visible.fundingRate) == null ? null : numeric(visible.fundingRate) / 100,
|
|
80
|
+
maxLeverage: numeric(visible.maxLeverage),
|
|
81
|
+
};
|
|
82
|
+
const checks = {
|
|
83
|
+
route: visible.route === `#/perps/market/${market}`,
|
|
84
|
+
symbol: visible.pair.startsWith(`${market}-`),
|
|
85
|
+
name: visible.name.length > 0,
|
|
86
|
+
maxLeverage: observed.maxLeverage === provider.maxLeverage,
|
|
87
|
+
price: observed.price != null && relativeDifference(observed.price, provider.markPrice) <= 0.02,
|
|
88
|
+
oraclePrice: observed.oraclePrice != null && relativeDifference(observed.oraclePrice, provider.oraclePrice) <= 0.02,
|
|
89
|
+
volume24hUsd: observed.volume24hUsd != null && relativeDifference(observed.volume24hUsd, provider.volume24hUsd) <= 0.05,
|
|
90
|
+
openInterestUsd: observed.openInterestUsd != null && relativeDifference(observed.openInterestUsd, provider.openInterestUsd) <= 0.05,
|
|
91
|
+
fundingRate: observed.fundingRate != null && Math.abs(observed.fundingRate - provider.fundingRate) <= 0.00001,
|
|
92
|
+
favoriteState: /favorites?/iu.test(visible.favoriteLabel),
|
|
93
|
+
};
|
|
94
|
+
const failed = Object.entries(checks).filter(([, passed]) => !passed).map(([name]) => name);
|
|
95
|
+
if (failed.length > 0) {
|
|
96
|
+
throw new Error(`Visible ${market} market differs from the production provider for: ${failed.join(', ')}.`);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
action: input.action,
|
|
100
|
+
market,
|
|
101
|
+
endpoint,
|
|
102
|
+
visible,
|
|
103
|
+
observed,
|
|
104
|
+
provider,
|
|
105
|
+
checks,
|
|
106
|
+
proofPath: 'visible-ui-plus-live-provider',
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { dataTestId, marketSymbol, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
4
|
+
const market = marketSymbol(input);
|
|
5
|
+
const mode = String(input.node?.margin_action ?? 'add').trim().toLowerCase();
|
|
6
|
+
const amount = Number(input.node?.amount);
|
|
7
|
+
if (!market) throw new Error('metamask.perps.edit_margin requires market=<symbol>.');
|
|
8
|
+
if (mode !== 'add' && mode !== 'remove') {
|
|
9
|
+
throw new Error(`metamask.perps.edit_margin received unsupported margin_action ${JSON.stringify(mode)}.`);
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
12
|
+
throw new Error('metamask.perps.edit_margin requires a positive amount.');
|
|
13
|
+
}
|
|
14
|
+
if (input.node?.confirm_live !== true) {
|
|
15
|
+
throw new Error('metamask.perps.edit_margin requires confirm_live=true for live submission.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
await page.navigateHash(`#/perps/market/${encodeURIComponent(market)}`);
|
|
19
|
+
await page.waitForSelector(dataTestId('perps-position-margin-value'), { timeoutMs: 20_000 });
|
|
20
|
+
const before = await page.evaluate(`(() => ({
|
|
21
|
+
margin: String(document.querySelector('[data-testid="perps-position-margin-value"]')?.textContent || '').trim(),
|
|
22
|
+
liquidation: String(document.querySelector('[data-testid="perps-position-liquidation-value"]')?.textContent || '').trim(),
|
|
23
|
+
}))()`);
|
|
24
|
+
await page.click(dataTestId('perps-margin-card'));
|
|
25
|
+
const menuItem = mode === 'add' ? 'perps-margin-menu-add' : 'perps-margin-menu-remove';
|
|
26
|
+
await page.waitForSelector(dataTestId(menuItem), { timeoutMs: 10_000 });
|
|
27
|
+
await page.click(dataTestId(menuItem));
|
|
28
|
+
const modal = mode === 'add' ? 'perps-add-margin-modal' : 'perps-decrease-margin-modal';
|
|
29
|
+
await page.waitForSelector(dataTestId(modal), { timeoutMs: 10_000 });
|
|
30
|
+
await page.setInput(`${dataTestId('perps-edit-margin-amount-input')} input`, String(amount));
|
|
31
|
+
|
|
32
|
+
const saveDeadline = Date.now() + 10_000;
|
|
33
|
+
let ready;
|
|
34
|
+
while (Date.now() <= saveDeadline) {
|
|
35
|
+
ready = await page.evaluate(`(() => {
|
|
36
|
+
const input = document.querySelector('[data-testid="perps-edit-margin-amount-input"] input');
|
|
37
|
+
const save = document.querySelector('[data-testid="perps-edit-margin-modal-save"]');
|
|
38
|
+
return {
|
|
39
|
+
amount: input?.value ?? null,
|
|
40
|
+
saveEnabled: Boolean(save && !save.disabled && save.getAttribute('aria-disabled') !== 'true'),
|
|
41
|
+
available: String(document.querySelector('[data-testid="perps-edit-margin-available-value"]')?.textContent || '').trim(),
|
|
42
|
+
liquidation: String(document.querySelector('[data-testid="perps-edit-margin-liquidation-price-value"]')?.textContent || '').trim(),
|
|
43
|
+
};
|
|
44
|
+
})()`);
|
|
45
|
+
if (ready.saveEnabled) break;
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
47
|
+
}
|
|
48
|
+
if (Number(ready?.amount) !== amount || !ready?.saveEnabled) {
|
|
49
|
+
throw new Error(`Perps margin update is not ready: ${JSON.stringify(ready)}.`);
|
|
50
|
+
}
|
|
51
|
+
await page.click(dataTestId('perps-edit-margin-modal-save'));
|
|
52
|
+
|
|
53
|
+
const resultDeadline = Date.now() + Number(input.node?.timeout_ms ?? 30_000);
|
|
54
|
+
let after;
|
|
55
|
+
while (Date.now() <= resultDeadline) {
|
|
56
|
+
after = await page.evaluate(`(() => ({
|
|
57
|
+
modalVisible: Boolean(document.querySelector(${JSON.stringify(dataTestId(modal))})),
|
|
58
|
+
margin: String(document.querySelector('[data-testid="perps-position-margin-value"]')?.textContent || '').trim(),
|
|
59
|
+
liquidation: String(document.querySelector('[data-testid="perps-position-liquidation-value"]')?.textContent || '').trim(),
|
|
60
|
+
errors: [...document.querySelectorAll('[role="alert"]')]
|
|
61
|
+
.map((element) => String(element.innerText || '').replace(/\\s+/gu, ' ').trim())
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.slice(0, 20),
|
|
64
|
+
}))()`);
|
|
65
|
+
if (!after.modalVisible && after.margin && after.margin !== before.margin) break;
|
|
66
|
+
if (after.errors.length > 0) throw new Error(`Perps margin update failed: ${after.errors.join(' | ')}`);
|
|
67
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
68
|
+
}
|
|
69
|
+
if (!after || after.modalVisible || after.margin === before.margin) {
|
|
70
|
+
throw new Error(`Perps margin did not reconcile after submission: ${JSON.stringify({ before, after })}.`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
action: input.action,
|
|
74
|
+
market,
|
|
75
|
+
marginAction: mode,
|
|
76
|
+
amount,
|
|
77
|
+
before,
|
|
78
|
+
ready,
|
|
79
|
+
after,
|
|
80
|
+
proofPath: 'visible-product-controls',
|
|
81
|
+
};
|
|
82
|
+
}));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { dataTestId, runAdapter, withExtensionPage } from '../platform/cdp.mjs';
|
|
2
|
+
|
|
3
|
+
const ACTIONS = new Map([
|
|
4
|
+
['add-funds', 'perps-balance-dropdown-add-funds'],
|
|
5
|
+
['withdraw', 'perps-balance-dropdown-withdraw'],
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
runAdapter(async (input) => withExtensionPage(input, async (page) => {
|
|
9
|
+
const balanceAction = String(input.node?.balance_action ?? '').trim().toLowerCase();
|
|
10
|
+
const actionTestId = ACTIONS.get(balanceAction);
|
|
11
|
+
if (!actionTestId) {
|
|
12
|
+
throw new Error(`metamask.perps.open_balance_action received unsupported balance_action ${JSON.stringify(balanceAction)}.`);
|
|
13
|
+
}
|
|
14
|
+
await page.click(dataTestId('perps-balance-dropdown-balance'));
|
|
15
|
+
const actionSelector = dataTestId(actionTestId);
|
|
16
|
+
const deadline = Date.now() + 10_000;
|
|
17
|
+
while (Date.now() <= deadline) {
|
|
18
|
+
const present = await page.evaluate(`(() => Boolean(document.querySelector(${JSON.stringify(actionSelector)})))()`);
|
|
19
|
+
if (present) break;
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
21
|
+
}
|
|
22
|
+
await page.click(actionSelector);
|
|
23
|
+
const expected = balanceAction === 'withdraw'
|
|
24
|
+
? ['perps-withdraw-page', 'perps-withdraw-container', 'perps-withdraw-view']
|
|
25
|
+
: ['confirm-page-container-content', 'confirm-page-container', 'transaction-confirmation'];
|
|
26
|
+
const resultDeadline = Date.now() + 15_000;
|
|
27
|
+
let observed;
|
|
28
|
+
while (Date.now() <= resultDeadline) {
|
|
29
|
+
observed = await page.evaluate(`(() => {
|
|
30
|
+
const expected = ${JSON.stringify(expected)};
|
|
31
|
+
const visibleTestIds = [...document.querySelectorAll('[data-testid]')]
|
|
32
|
+
.filter((element) => {
|
|
33
|
+
const style = getComputedStyle(element);
|
|
34
|
+
const rect = element.getBoundingClientRect();
|
|
35
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
|
|
36
|
+
})
|
|
37
|
+
.map((element) => element.getAttribute('data-testid'))
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
return {
|
|
40
|
+
route: String(location.hash),
|
|
41
|
+
matched: expected.filter((testId) => visibleTestIds.includes(testId)),
|
|
42
|
+
visibleTestIds: visibleTestIds.filter((testId) => /(?:perps|confirm|transaction|amount|token|network)/u.test(testId)).slice(0, 150),
|
|
43
|
+
text: String(document.body?.innerText || '').replace(/\\s+/gu, ' ').trim().slice(0, 10_000),
|
|
44
|
+
};
|
|
45
|
+
})()`);
|
|
46
|
+
if (observed.matched.length > 0 || observed.route !== '#/perps-home') break;
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
48
|
+
}
|
|
49
|
+
if (!observed || (observed.matched.length === 0 && observed.route === '#/perps-home')) {
|
|
50
|
+
throw new Error(`Perps ${balanceAction} did not open: ${JSON.stringify(observed)}.`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
action: input.action,
|
|
54
|
+
balanceAction,
|
|
55
|
+
...observed,
|
|
56
|
+
proofPath: 'trusted-pointer-visible-ui',
|
|
57
|
+
};
|
|
58
|
+
}));
|