harb/tests/e2e/06-dashboard-pages.spec.ts
2026-03-06 01:12:26 +00:00

374 lines
15 KiB
TypeScript

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;
}
// eslint-disable-next-line no-restricted-syntax -- no event source exists for Ponder indexing lag and UI re-renders; Promise+setTimeout used for polling Ponder over HTTP where no push subscription is available. See AGENTS.md #Engineering Principles.
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');
// eslint-disable-next-line no-restricted-syntax -- no event source exists for Ponder indexing lag and UI re-renders; Promise+setTimeout used for polling Ponder over HTTP where no push subscription is available. See AGENTS.md #Engineering Principles.
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');
// eslint-disable-next-line no-restricted-syntax -- no event source exists for Ponder indexing lag and UI re-renders; Promise+setTimeout used for polling Ponder over HTTP where no push subscription is available. See AGENTS.md #Engineering Principles.
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');
// eslint-disable-next-line no-restricted-syntax -- no event source exists for Ponder indexing lag and UI re-renders; Promise+setTimeout used for polling Ponder over HTTP where no push subscription is available. See AGENTS.md #Engineering Principles.
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');
// eslint-disable-next-line no-restricted-syntax -- no event source exists for Ponder indexing lag and UI re-renders; Promise+setTimeout used for polling Ponder over HTTP where no push subscription is available. See AGENTS.md #Engineering Principles.
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');
// eslint-disable-next-line no-restricted-syntax -- no event source exists for Ponder indexing lag and UI re-renders; Promise+setTimeout used for polling Ponder over HTTP where no push subscription is available. See AGENTS.md #Engineering Principles.
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');
// eslint-disable-next-line no-restricted-syntax -- no event source exists for Ponder indexing lag and UI re-renders; Promise+setTimeout used for polling Ponder over HTTP where no push subscription is available. See AGENTS.md #Engineering Principles.
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');
// eslint-disable-next-line no-restricted-syntax -- no event source exists for Ponder indexing lag and UI re-renders; Promise+setTimeout used for polling Ponder over HTTP where no push subscription is available. See AGENTS.md #Engineering Principles.
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();
}
});
});
});