import { test, expect, type APIRequestContext } from '@playwright/test'; import { Wallet } from 'ethers'; import { createWalletContext } from '../setup/wallet-provider'; import { getStackConfig, validateStackHealthy } from '../setup/stack'; const ACCOUNT_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; const ACCOUNT_ADDRESS = new Wallet(ACCOUNT_PRIVATE_KEY).address; 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; /** * Fetch holder data from GraphQL */ async function fetchHolder(request: APIRequestContext, address: string) { const response = await request.post(STACK_GRAPHQL_URL, { data: { query: `query { holders(address: "${address.toLowerCase()}") { address balance } }`, }, headers: { 'content-type': 'application/json' }, }); const payload = await response.json(); return payload?.data?.holders; } /** * Fetch active positions for an owner from GraphQL */ async function fetchPositions(request: APIRequestContext, owner: string) { const response = await request.post(STACK_GRAPHQL_URL, { data: { query: `query { positionss(where: { owner: "${owner.toLowerCase()}", status: "Active" }, limit: 5) { items { id owner taxRate kraikenDeposit status share } } }`, }, headers: { 'content-type': 'application/json' }, }); const payload = await response.json(); return payload?.data?.positionss?.items ?? []; } test.describe('Dashboard Pages', () => { test.beforeAll(async ({ request }) => { await validateStackHealthy(STACK_CONFIG); // Wait for ponder to index positions created by earlier tests (01-05). // Ponder runs in realtime mode but may lag a few seconds behind the chain. const maxWaitMs = 30_000; const pollMs = 1_000; const start = Date.now(); let found = false; while (Date.now() - start < maxWaitMs) { const positions = await fetchPositions(request, ACCOUNT_ADDRESS); if (positions.length > 0) { found = true; console.log(`[TEST] Ponder has ${positions.length} positions after ${Date.now() - start}ms`); break; } await new Promise(r => setTimeout(r, pollMs)); } if (!found) { console.log('[TEST] WARNING: No positions found in ponder after 30s — tests may fail'); } }); test.describe('Wallet Dashboard', () => { test('renders wallet page with balance and protocol stats', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const errors: string[] = []; page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); }); try { await page.goto(`${STACK_WEBAPP_URL}/app/#/wallet/${ACCOUNT_ADDRESS}`, { waitUntil: 'domcontentloaded', }); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); // Should show the address (truncated) const addressText = await page.textContent('body'); expect(addressText).toContain(ACCOUNT_ADDRESS.slice(0, 6)); // Should show KRK balance (non-zero after test 01 mints + swaps) const balanceEl = page.locator('text=/\\d+.*KRK/i').first(); await expect(balanceEl).toBeVisible({ timeout: 10_000 }); // Should show ETH backing card const ethBacking = page.locator('text=/ETH Backing/i').first(); await expect(ethBacking).toBeVisible({ timeout: 5_000 }); // Should show floor value card const floorValue = page.locator('text=/Floor Value/i').first(); await expect(floorValue).toBeVisible({ timeout: 5_000 }); // Should show protocol health metrics const ethReserve = page.locator('text=/ETH Reserve/i').first(); await expect(ethReserve).toBeVisible({ timeout: 5_000 }); // Take screenshot await page.screenshot({ path: 'test-results/dashboard-wallet.png', fullPage: true, }); // No console errors const realErrors = errors.filter( e => !e.includes('favicon') && !e.includes('DevTools') ); expect(realErrors).toHaveLength(0); console.log('[TEST] ✅ Wallet dashboard renders correctly'); } finally { await page.close(); await context.close(); } }); test('wallet page shows staking positions when they exist', async ({ browser, request }) => { // First verify positions exist (created by test 01) const positions = await fetchPositions(request, ACCOUNT_ADDRESS); console.log(`[TEST] Found ${positions.length} positions for ${ACCOUNT_ADDRESS}`); if (positions.length === 0) { console.log('[TEST] ⚠️ No positions found — skipping position list check'); test.skip(); return; } const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); try { await page.goto(`${STACK_WEBAPP_URL}/app/#/wallet/${ACCOUNT_ADDRESS}`, { waitUntil: 'domcontentloaded', }); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); // Should show position entries with links to position detail const positionLink = page.locator(`a[href*="/position/"]`).first(); await expect(positionLink).toBeVisible({ timeout: 10_000 }); console.log('[TEST] ✅ Wallet dashboard shows staking positions'); } finally { await page.close(); await context.close(); } }); test('wallet page handles unknown address gracefully', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const errors: string[] = []; page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); }); try { // Navigate to a wallet with no balance const unknownAddr = '0x0000000000000000000000000000000000000001'; await page.goto(`${STACK_WEBAPP_URL}/app/#/wallet/${unknownAddr}`, { waitUntil: 'domcontentloaded', }); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); // Page should render without crashing const body = await page.textContent('body'); expect(body).toBeTruthy(); // Should show zero or empty state (not crash) const realErrors = errors.filter( e => !e.includes('favicon') && !e.includes('DevTools') ); expect(realErrors).toHaveLength(0); console.log('[TEST] ✅ Wallet page handles unknown address gracefully'); } finally { await page.close(); await context.close(); } }); }); test.describe('Position Dashboard', () => { test('renders position page with valid position data', async ({ browser, request }) => { // Find a real position ID from GraphQL const positions = await fetchPositions(request, ACCOUNT_ADDRESS); console.log(`[TEST] Found ${positions.length} positions`); if (positions.length === 0) { console.log('[TEST] ⚠️ No positions found — skipping'); test.skip(); return; } const positionId = positions[0].id; console.log(`[TEST] Testing position #${positionId}`); const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const errors: string[] = []; page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); }); try { await page.goto(`${STACK_WEBAPP_URL}/app/#/position/${positionId}`, { waitUntil: 'domcontentloaded', }); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); // Should show position ID const body = await page.textContent('body'); expect(body).toContain(positionId); // Should show deposit amount const deposited = page.locator('text=/Deposited/i').first(); await expect(deposited).toBeVisible({ timeout: 10_000 }); // Should show current value const currentValue = page.locator('text=/Current Value/i').first(); await expect(currentValue).toBeVisible({ timeout: 5_000 }); // Should show tax paid const taxPaid = page.locator('text=/Tax Paid/i').first(); await expect(taxPaid).toBeVisible({ timeout: 5_000 }); // Should show net return const netReturn = page.locator('text=/Net Return/i').first(); await expect(netReturn).toBeVisible({ timeout: 5_000 }); // Should show tax rate const taxRate = page.locator('text=/Tax Rate/i').first(); await expect(taxRate).toBeVisible({ timeout: 5_000 }); // Should show snatch risk indicator const snatchRisk = page.locator('text=/Snatch Risk/i').first(); await expect(snatchRisk).toBeVisible({ timeout: 5_000 }); // Should show daily tax cost const dailyTax = page.locator('text=/Daily Tax/i').first(); await expect(dailyTax).toBeVisible({ timeout: 5_000 }); // Should show owner link to wallet page const ownerLink = page.locator('a[href*="/wallet/"]').first(); await expect(ownerLink).toBeVisible({ timeout: 5_000 }); // Take screenshot await page.screenshot({ path: 'test-results/dashboard-position.png', fullPage: true, }); // No console errors const realErrors = errors.filter( e => !e.includes('favicon') && !e.includes('DevTools') ); expect(realErrors).toHaveLength(0); console.log('[TEST] ✅ Position dashboard renders correctly'); } finally { await page.close(); await context.close(); } }); test('position page handles non-existent position gracefully', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const errors: string[] = []; page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); }); try { await page.goto(`${STACK_WEBAPP_URL}/app/#/position/999999999`, { waitUntil: 'domcontentloaded', }); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); // Should show "not found" state without crashing const body = await page.textContent('body'); expect(body).toBeTruthy(); // Look for not-found or error messaging const hasNotFound = body?.toLowerCase().includes('not found') || body?.toLowerCase().includes('no position') || body?.toLowerCase().includes('does not exist'); expect(hasNotFound).toBeTruthy(); const realErrors = errors.filter( e => !e.includes('favicon') && !e.includes('DevTools') ); expect(realErrors).toHaveLength(0); console.log('[TEST] ✅ Position page handles non-existent ID gracefully'); } finally { await page.close(); await context.close(); } }); test('position page links back to wallet dashboard', async ({ browser, request }) => { const positions = await fetchPositions(request, ACCOUNT_ADDRESS); if (positions.length === 0) { console.log('[TEST] ⚠️ No positions — skipping'); test.skip(); return; } const positionId = positions[0].id; const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); try { await page.goto(`${STACK_WEBAPP_URL}/app/#/position/${positionId}`, { waitUntil: 'domcontentloaded', }); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); // Click owner link → should navigate to wallet page const ownerLink = page.locator('a[href*="/wallet/"]').first(); await expect(ownerLink).toBeVisible({ timeout: 10_000 }); await ownerLink.click(); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); // Should now be on the wallet page expect(page.url()).toContain('/wallet/'); console.log('[TEST] ✅ Position → Wallet navigation works'); } finally { await page.close(); await context.close(); } }); }); });