367 lines
13 KiB
TypeScript
367 lines
13 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;
|
||
|
|
}
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|