2026-02-19 14:47:15 +01:00
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 ;
}
2026-03-06 01:12:26 +00:00
// 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.
2026-02-19 14:47:15 +01:00
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 {
2026-02-20 17:28:59 +01:00
await page . goto ( ` ${ STACK_WEBAPP_URL } /app/wallet/ ${ ACCOUNT_ADDRESS } ` , {
2026-02-19 14:47:15 +01:00
waitUntil : 'domcontentloaded' ,
} ) ;
await page . waitForLoadState ( 'networkidle' ) ;
2026-03-06 01:12:26 +00:00
// 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.
2026-02-19 14:47:15 +01:00
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 {
2026-02-20 17:28:59 +01:00
await page . goto ( ` ${ STACK_WEBAPP_URL } /app/wallet/ ${ ACCOUNT_ADDRESS } ` , {
2026-02-19 14:47:15 +01:00
waitUntil : 'domcontentloaded' ,
} ) ;
await page . waitForLoadState ( 'networkidle' ) ;
2026-03-06 01:12:26 +00:00
// 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.
2026-02-19 14:47:15 +01:00
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' ;
2026-02-20 17:28:59 +01:00
await page . goto ( ` ${ STACK_WEBAPP_URL } /app/wallet/ ${ unknownAddr } ` , {
2026-02-19 14:47:15 +01:00
waitUntil : 'domcontentloaded' ,
} ) ;
await page . waitForLoadState ( 'networkidle' ) ;
2026-03-06 01:12:26 +00:00
// 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.
2026-02-19 14:47:15 +01:00
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 {
2026-02-20 17:28:59 +01:00
await page . goto ( ` ${ STACK_WEBAPP_URL } /app/position/ ${ positionId } ` , {
2026-02-19 14:47:15 +01:00
waitUntil : 'domcontentloaded' ,
} ) ;
await page . waitForLoadState ( 'networkidle' ) ;
2026-03-06 01:12:26 +00:00
// 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.
2026-02-19 14:47:15 +01:00
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 } ) ;
2026-03-06 11:51:45 +00:00
// Should show tax cost
const taxPaid = page . locator ( 'text=/Tax Cost/i' ) . first ( ) ;
2026-02-19 14:47:15 +01:00
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 {
2026-02-20 17:28:59 +01:00
await page . goto ( ` ${ STACK_WEBAPP_URL } /app/position/999999999 ` , {
2026-02-19 14:47:15 +01:00
waitUntil : 'domcontentloaded' ,
} ) ;
await page . waitForLoadState ( 'networkidle' ) ;
2026-03-06 01:12:26 +00:00
// 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.
2026-02-19 14:47:15 +01:00
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 {
2026-02-20 17:28:59 +01:00
await page . goto ( ` ${ STACK_WEBAPP_URL } /app/position/ ${ positionId } ` , {
2026-02-19 14:47:15 +01:00
waitUntil : 'domcontentloaded' ,
} ) ;
await page . waitForLoadState ( 'networkidle' ) ;
2026-03-06 01:12:26 +00:00
// 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.
2026-02-19 14:47:15 +01:00
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' ) ;
2026-03-06 01:12:26 +00:00
// 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.
2026-02-19 14:47:15 +01:00
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 ( ) ;
}
} ) ;
} ) ;
} ) ;