Extract reusable helpers from sovereign-exit/always-leave.spec.ts into three focused modules under scripts/harb-evaluator/helpers/: - helpers/wallet.ts: connectWallet, disconnectWallet, getEthBalance, getKrkBalance — UI connect/disconnect flow and on-chain balance reads. - helpers/swap.ts: buyKrk (navigates to the real /app/get-krk page and drives the LocalSwapWidget, now that #393 fill() fix is in), sellAllKrk (approve + exactInputSingle via window.ethereum, no UI dependency). - helpers/assertions.ts: expectBalanceIncrease (snapshot/action/assert pattern for any token or ETH), expectPoolHasLiquidity (slot0 + liquidity sanity check on a Uniswap V3 pool). always-leave.spec.ts is refactored to use these helpers and to navigate to /app/get-krk instead of the /app/cheats workaround introduced before the #393 fix landed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
105 lines
4.7 KiB
TypeScript
105 lines
4.7 KiB
TypeScript
/**
|
|
* Shared wallet helpers for holdout scenarios.
|
|
*
|
|
* Functions here operate in the Playwright Node.js context (not the browser).
|
|
* UI interactions use Playwright locators; on-chain reads use direct RPC calls
|
|
* so that tests do not depend on the app's wallet state for balance queries.
|
|
*/
|
|
import type { Page } from '@playwright/test';
|
|
import { expect } from '@playwright/test';
|
|
|
|
// ── RPC helpers ─────────────────────────────────────────────────────────────
|
|
|
|
async function rpcCall(rpcUrl: string, method: string, params: unknown[]): Promise<unknown> {
|
|
const resp = await fetch(rpcUrl, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }),
|
|
});
|
|
const payload = await resp.json();
|
|
if (payload.error) throw new Error(`RPC ${method}: ${payload.error.message}`);
|
|
return payload.result;
|
|
}
|
|
|
|
// ── Balance readers ──────────────────────────────────────────────────────────
|
|
|
|
/** Return the native ETH balance (in wei) of `address`. */
|
|
export async function getEthBalance(rpcUrl: string, address: string): Promise<bigint> {
|
|
const result = (await rpcCall(rpcUrl, 'eth_getBalance', [address, 'latest'])) as string;
|
|
return BigInt(result);
|
|
}
|
|
|
|
/** Return the ERC-20 KRK balance (in wei) of `address` on the given token contract. */
|
|
export async function getKrkBalance(rpcUrl: string, krkAddress: string, address: string): Promise<bigint> {
|
|
const selector = '0x70a08231'; // balanceOf(address)
|
|
const data = selector + address.slice(2).padStart(64, '0');
|
|
const result = (await rpcCall(rpcUrl, 'eth_call', [{ to: krkAddress, data }, 'latest'])) as string;
|
|
return BigInt(result);
|
|
}
|
|
|
|
// ── UI helpers ───────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Connect the test wallet via the desktop UI flow.
|
|
*
|
|
* Expects the page to already be on the web app with the navbar visible.
|
|
* The injected wallet provider must already be set up by createWalletContext.
|
|
* Verifies connection by waiting for the connected button to appear in the navbar.
|
|
*/
|
|
export async function connectWallet(page: Page): Promise<void> {
|
|
// Trigger resize so Vue's useMobile composable re-evaluates with screen.width=1280.
|
|
await page.evaluate(() => window.dispatchEvent(new Event('resize')));
|
|
await page.waitForTimeout(2_000);
|
|
|
|
let panelOpened = false;
|
|
|
|
const connectButton = page.locator('.connect-button--disconnected').first();
|
|
if (await connectButton.isVisible({ timeout: 5_000 })) {
|
|
console.log('[wallet] Found desktop Connect button, clicking...');
|
|
await connectButton.click();
|
|
panelOpened = true;
|
|
} else {
|
|
// Fallback: mobile login icon. Dead code when screen.width=1280 but kept for safety.
|
|
const mobileLoginIcon = page.locator('.navbar-end svg').first();
|
|
if (await mobileLoginIcon.isVisible({ timeout: 2_000 })) {
|
|
console.log('[wallet] Found mobile login icon, clicking...');
|
|
await mobileLoginIcon.click();
|
|
panelOpened = true;
|
|
}
|
|
}
|
|
|
|
if (panelOpened) {
|
|
await page.waitForTimeout(1_000);
|
|
const injectedConnector = page.locator('.connectors-element').first();
|
|
if (await injectedConnector.isVisible({ timeout: 5_000 })) {
|
|
console.log('[wallet] Clicking wallet connector...');
|
|
await injectedConnector.click();
|
|
await page.waitForTimeout(2_000);
|
|
} else {
|
|
console.log('[wallet] WARNING: No wallet connector found in panel');
|
|
}
|
|
}
|
|
|
|
// The navbar shows .connect-button--connected once wagmi reports status=connected.
|
|
await expect(page.locator('.connect-button--connected').first()).toBeVisible({ timeout: 15_000 });
|
|
console.log('[wallet] Wallet connected');
|
|
}
|
|
|
|
/**
|
|
* Disconnect the wallet by opening the connected panel and clicking the logout icon.
|
|
*
|
|
* Verifies disconnection by waiting for the Connect button to reappear.
|
|
*/
|
|
export async function disconnectWallet(page: Page): Promise<void> {
|
|
const connectedButton = page.locator('.connect-button--connected').first();
|
|
await expect(connectedButton).toBeVisible({ timeout: 5_000 });
|
|
await connectedButton.click();
|
|
|
|
// Panel opens showing .connected-header-logout (img alt="Logout").
|
|
const logoutIcon = page.locator('.connected-header-logout').first();
|
|
await expect(logoutIcon).toBeVisible({ timeout: 5_000 });
|
|
await logoutIcon.click();
|
|
|
|
await expect(page.locator('.connect-button--disconnected').first()).toBeVisible({ timeout: 10_000 });
|
|
console.log('[wallet] Wallet disconnected');
|
|
}
|