taxPaidGes = taxDue + taxPaid, so the displayed value includes both outstanding tax and historically paid tax. Rename the UI label from 'Tax Paid' to 'Tax Cost' to accurately reflect the combined amount. Update the matching E2E test selector accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
374 lines
15 KiB
TypeScript
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 cost
|
|
const taxPaid = page.locator('text=/Tax Cost/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();
|
|
}
|
|
});
|
|
});
|
|
});
|