Root cause: the test wallet provider's eth_accounts and getProviderState always returned the account address regardless of connection state. This caused wagmi to auto-connect via EIP-6963 provider discovery, skipping the 'disconnected' status entirely. As a result, .connect-button--disconnected never rendered and .connectors-element was never shown. Changes: - wallet-provider: eth_accounts returns [] when not connected (EIP-1193 compliant) - wallet-provider: getProviderState returns empty accounts when not connected - All wallet connection helpers: handle auto-reconnect case, increase timeout for wagmi to settle into disconnected state (5s → 10s) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
300 lines
15 KiB
TypeScript
300 lines
15 KiB
TypeScript
import { expect, test, type APIRequestContext } from '@playwright/test';
|
|
import { Wallet } from 'ethers';
|
|
import { createWalletContext } from '../setup/wallet-provider';
|
|
import { getStackConfig, validateStackHealthy } from '../setup/stack';
|
|
import { navigateSPA } from '../setup/navigate';
|
|
|
|
const ACCOUNT_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
|
|
const ACCOUNT_ADDRESS = new Wallet(ACCOUNT_PRIVATE_KEY).address.toLowerCase();
|
|
|
|
// Get stack configuration from environment (or defaults)
|
|
const STACK_CONFIG = getStackConfig();
|
|
const STACK_RPC_URL = STACK_CONFIG.rpcUrl;
|
|
const STACK_WEBAPP_URL = STACK_CONFIG.webAppUrl;
|
|
const STACK_GRAPHQL_URL = STACK_CONFIG.graphqlUrl;
|
|
|
|
async function fetchPositions(request: APIRequestContext, owner: string) {
|
|
const response = await request.post(STACK_GRAPHQL_URL, {
|
|
data: {
|
|
query: `
|
|
query PositionsByOwner($owner: String!) {
|
|
positionss(where: { owner: $owner }, limit: 5) {
|
|
items {
|
|
id
|
|
owner
|
|
taxRate
|
|
stakeDeposit
|
|
status
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
variables: { owner },
|
|
},
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
|
|
expect(response.ok()).toBeTruthy();
|
|
const payload = await response.json();
|
|
return (payload?.data?.positionss?.items ?? []) as Array<{
|
|
id: string;
|
|
owner: string;
|
|
taxRate: number;
|
|
stakeDeposit: string;
|
|
status: string;
|
|
}>;
|
|
}
|
|
|
|
test.describe('Acquire & Stake', () => {
|
|
// Validate that a healthy stack exists before running tests
|
|
// Tests do NOT start their own stack - stack must be running already
|
|
test.beforeAll(async () => {
|
|
await validateStackHealthy(STACK_CONFIG);
|
|
});
|
|
|
|
test('users can swap KRK via UI', async ({ browser, request }) => {
|
|
console.log('[TEST] Creating wallet context...');
|
|
const context = await createWalletContext(browser, {
|
|
privateKey: ACCOUNT_PRIVATE_KEY,
|
|
rpcUrl: STACK_RPC_URL,
|
|
});
|
|
|
|
const page = await context.newPage();
|
|
|
|
// Log browser console messages
|
|
page.on('console', msg => console.log(`[BROWSER] ${msg.type()}: ${msg.text()}`));
|
|
page.on('pageerror', error => console.log(`[BROWSER ERROR] ${error.message}`));
|
|
|
|
try {
|
|
console.log('[TEST] Loading app...');
|
|
await page.goto(`${STACK_WEBAPP_URL}/app/`, { waitUntil: 'domcontentloaded' });
|
|
console.log('[TEST] App loaded, waiting for Vue app to mount...');
|
|
|
|
// Wait for the Vue app to fully mount by waiting for a key element
|
|
// The navbar-title is always present regardless of connection state
|
|
const navbarTitle = page.locator('.navbar-title').first();
|
|
await expect(navbarTitle).toBeVisible({ timeout: 30_000 });
|
|
console.log('[TEST] Vue app mounted, navbar is visible');
|
|
|
|
// Trigger a resize event to force Vue's useMobile composable to recalculate
|
|
// This ensures the app recognizes the desktop screen width set by wallet-provider
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new Event('resize'));
|
|
});
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(500);
|
|
|
|
// Give extra time for wallet connectors to initialize
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
// Connect wallet flow:
|
|
// The wallet-provider sets screen.width to 1280 to ensure desktop mode.
|
|
// We expect the desktop Connect button to be visible.
|
|
console.log('[TEST] Looking for Connect button...');
|
|
|
|
// Check if wallet already connected (wagmi may auto-reconnect from storage)
|
|
const alreadyConnected = page.locator('.connect-button--connected').first();
|
|
if (await alreadyConnected.isVisible({ timeout: 1_000 }).catch(() => false)) {
|
|
console.log('[TEST] Wallet already connected (auto-reconnect), skipping connect flow');
|
|
} else {
|
|
// Desktop Connect button — wait up to 10s for wagmi to settle into disconnected state
|
|
const connectButton = page.locator('.connect-button--disconnected').first();
|
|
|
|
let panelOpened = false;
|
|
|
|
if (await connectButton.isVisible({ timeout: 10_000 })) {
|
|
console.log('[TEST] Found desktop Connect button, clicking...');
|
|
await connectButton.click();
|
|
panelOpened = true;
|
|
} else {
|
|
// Debug: Log current screen.width and navbar-end contents
|
|
const screenWidth = await page.evaluate(() => window.screen.width);
|
|
const navbarEndHtml = await page.locator('.navbar-end').innerHTML().catch(() => 'not found');
|
|
console.log(`[TEST] DEBUG: screen.width = ${screenWidth}`);
|
|
console.log(`[TEST] DEBUG: navbar-end HTML = ${navbarEndHtml.substring(0, 500)}`);
|
|
console.log('[TEST] Connect button not visible - checking for mobile fallback...');
|
|
|
|
// Fallback to mobile login icon (SVG in navbar-end when disconnected)
|
|
const mobileLoginIcon = page.locator('.navbar-end svg').first();
|
|
if (await mobileLoginIcon.isVisible({ timeout: 2_000 })) {
|
|
console.log('[TEST] Found mobile login icon, clicking...');
|
|
await mobileLoginIcon.click();
|
|
panelOpened = true;
|
|
} else {
|
|
console.log('[TEST] No Connect button or mobile icon visible - wallet may already be connected');
|
|
}
|
|
}
|
|
|
|
if (panelOpened) {
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(1_000);
|
|
|
|
// Look for the injected wallet connector in the slideout panel
|
|
console.log('[TEST] Looking for wallet connector in panel...');
|
|
const injectedConnector = page.locator('.connectors-element').first();
|
|
if (await injectedConnector.isVisible({ timeout: 5_000 })) {
|
|
console.log('[TEST] Clicking first wallet connector...');
|
|
await injectedConnector.click();
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
} else {
|
|
console.log('[TEST] WARNING: No wallet connector found in panel');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if wallet shows as connected in UI
|
|
console.log('[TEST] Checking for wallet display...');
|
|
const walletDisplay = page.getByText(/0xf39F/i).first();
|
|
await expect(walletDisplay).toBeVisible({ timeout: 15_000 });
|
|
console.log('[TEST] Wallet connected successfully!');
|
|
|
|
console.log('[TEST] Verifying stack version footer...');
|
|
const versionFooter = page.getByTestId('stack-version-footer');
|
|
await expect(versionFooter).toBeVisible({ timeout: 15_000 });
|
|
await expect(page.getByTestId('stack-version-contracts')).not.toHaveText(/Loading|—/i);
|
|
await expect(page.getByTestId('stack-version-ponder')).not.toHaveText(/Loading|—/i);
|
|
await expect(page.getByTestId('stack-version-kraiken-lib')).toHaveText(/^v\d+/i);
|
|
await expect(page.getByTestId('stack-version-web-app')).toHaveText(/^v/i);
|
|
const warningBanner = page.getByTestId('stack-version-warning');
|
|
await expect(warningBanner).toHaveCount(0);
|
|
console.log('[TEST] Stack version footer verified.');
|
|
|
|
console.log('[TEST] Navigating to cheats page...');
|
|
await navigateSPA(page, '/app/cheats');
|
|
await expect(page.getByRole('heading', { name: 'Cheat Console' })).toBeVisible({ timeout: 10_000 });
|
|
|
|
console.log('[TEST] Minting test ETH...');
|
|
await page.getByLabel('RPC URL').fill(STACK_RPC_URL);
|
|
await page.getByLabel('Recipient').fill(ACCOUNT_ADDRESS);
|
|
const mintButton = page.getByRole('button', { name: 'Mint' });
|
|
await expect(mintButton).toBeEnabled();
|
|
await mintButton.click();
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(3_000);
|
|
|
|
console.log('[TEST] Navigating to get-krk page to buy KRK...');
|
|
await navigateSPA(page, '/app/get-krk');
|
|
await expect(page.getByRole('heading', { name: 'Get $KRK Tokens' })).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.screenshot({ path: 'test-results/before-swap.png' });
|
|
|
|
const ethToSpendInput = page.getByTestId('swap-amount-input');
|
|
await expect(ethToSpendInput).toBeVisible({ timeout: 15_000 });
|
|
await ethToSpendInput.fill('0.05');
|
|
|
|
const buyButton = page.getByTestId('swap-buy-button');
|
|
await expect(buyButton).toBeVisible();
|
|
console.log('[TEST] Clicking Buy KRK button...');
|
|
await buyButton.click();
|
|
|
|
// Wait for swap to complete. The button becomes disabled (swapping=true) while the
|
|
// transaction is in flight and re-enables (swapping=false) when it finishes.
|
|
// Try to observe the disabled state first; if the RPC responds so fast that the
|
|
// disabled state cycles before we can observe it, the try throws and we fall through.
|
|
// Either way, the unconditional toBeEnabled() call below waits for the final ready
|
|
// state, covering both fast-RPC (already enabled) and slow-RPC (waiting to enable) paths.
|
|
console.log('[TEST] Waiting for swap to process...');
|
|
try {
|
|
await expect(buyButton).toBeDisabled({ timeout: 5_000 });
|
|
console.log('[TEST] Swap initiated...');
|
|
} catch {
|
|
console.log('[TEST] Swap completed before disabled state was observable (fast RPC).');
|
|
}
|
|
await expect(buyButton).toBeEnabled({ timeout: 60_000 });
|
|
console.log('[TEST] Swap completed!');
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
console.log('[TEST] Verifying swap via RPC...');
|
|
// Query the blockchain directly to verify KRK balance increased
|
|
const balanceResponse = await fetch(STACK_RPC_URL, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
id: 1,
|
|
method: 'eth_call',
|
|
params: [{
|
|
to: STACK_CONFIG.contracts.Kraiken, // KRK token from deployments
|
|
data: `0x70a08231000000000000000000000000${ACCOUNT_ADDRESS.slice(2)}` // balanceOf(address)
|
|
}, 'latest']
|
|
})
|
|
});
|
|
|
|
const balanceData = await balanceResponse.json();
|
|
const balance = BigInt(balanceData.result || '0x0');
|
|
console.log(`[TEST] KRK balance: ${balance.toString()} wei`);
|
|
|
|
expect(balance).toBeGreaterThan(0n);
|
|
console.log('[TEST] ✅ Swap successful! KRK balance > 0');
|
|
|
|
// Now test staking via UI
|
|
console.log('[TEST] Navigating to stake page...');
|
|
await navigateSPA(page, '/app/stake');
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
|
|
// Wait for the stake form to be initialized
|
|
console.log('[TEST] Waiting for stake form to load...');
|
|
const tokenAmountSlider = page.getByRole('slider', { name: 'Token Amount' });
|
|
await expect(tokenAmountSlider).toBeVisible({ timeout: 15_000 });
|
|
|
|
console.log('[TEST] Filling stake form via accessible controls...');
|
|
|
|
// Take screenshot before interaction
|
|
await page.screenshot({ path: 'test-results/before-stake-fill.png' });
|
|
|
|
// Check if input is visible and enabled
|
|
const stakeAmountInput = page.getByLabel('Staking Amount');
|
|
console.log('[TEST] Checking if staking amount input is visible...');
|
|
await expect(stakeAmountInput).toBeVisible({ timeout: 10_000 });
|
|
console.log('[TEST] Staking amount input is visible, filling value...');
|
|
await stakeAmountInput.fill('1000');
|
|
console.log('[TEST] Filled staking amount!');
|
|
|
|
const taxSelect = page.getByRole('combobox', { name: 'Position Cost (Tax Rate)' });
|
|
console.log('[TEST] Selecting tax rate...');
|
|
await taxSelect.selectOption({ value: '2' });
|
|
console.log('[TEST] Tax rate selected!');
|
|
|
|
console.log('[TEST] Clicking stake button...');
|
|
// Use the main form's submit button (large/block), not the small position card buttons
|
|
const stakeButton = page.getByRole('main').getByRole('button', { name: /Stake|Snatch and Stake/i });
|
|
await expect(stakeButton).toBeVisible({ timeout: 5_000 });
|
|
await stakeButton.click();
|
|
|
|
// Wait for transaction to process
|
|
console.log('[TEST] Waiting for stake transaction...');
|
|
try {
|
|
await page.getByRole('button', { name: /Sign Transaction|Waiting/i }).waitFor({ state: 'visible', timeout: 5_000 });
|
|
console.log('[TEST] Transaction initiated, waiting for completion...');
|
|
await page.getByRole('button', { name: /Stake|Snatch and Stake/i }).waitFor({ state: 'visible', timeout: 60_000 });
|
|
console.log('[TEST] Stake transaction completed!');
|
|
} catch (e) {
|
|
console.log('[TEST] Transaction may have completed instantly');
|
|
}
|
|
// Poll for Ponder to index the staking transaction (Ponder has indexing latency)
|
|
console.log('[TEST] Polling GraphQL for staking position (Ponder indexing latency)...');
|
|
let positions: Awaited<ReturnType<typeof fetchPositions>> = [];
|
|
for (let attempt = 0; attempt < 15; attempt++) {
|
|
// eslint-disable-next-line no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles.
|
|
await page.waitForTimeout(2_000);
|
|
positions = await fetchPositions(request, ACCOUNT_ADDRESS);
|
|
if (positions.length > 0) break;
|
|
console.log(`[TEST] Ponder not yet indexed (attempt ${attempt + 1}/15), retrying...`);
|
|
}
|
|
console.log(`[TEST] Found ${positions.length} position(s)`);
|
|
|
|
expect(positions.length).toBeGreaterThan(0);
|
|
const activePositions = positions.filter(p => p.status === 'Active');
|
|
expect(activePositions.length).toBeGreaterThan(0);
|
|
|
|
console.log(`[TEST] ✅ Staking successful! Created ${activePositions.length} active position(s)`);
|
|
console.log('[TEST] ✅ E2E test complete: Full journey verified (Mint → Swap → Stake)');
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
});
|