2026-03-03 20:58:01 +00:00
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for Ponder indexing lag and UI re-renders after on-chain transactions in E2E tests. See AGENTS.md #Engineering Principles. */
2025-10-05 19:40:14 +02:00
import { expect , test , type APIRequestContext } from '@playwright/test' ;
import { Wallet } from 'ethers' ;
import { createWalletContext } from '../setup/wallet-provider' ;
2025-10-07 21:57:32 +00:00
import { getStackConfig , validateStackHealthy } from '../setup/stack' ;
2026-02-20 17:28:59 +01:00
import { navigateSPA } from '../setup/navigate' ;
2025-10-05 19:40:14 +02:00
const ACCOUNT_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' ;
const ACCOUNT_ADDRESS = new Wallet ( ACCOUNT_PRIVATE_KEY ) . address . toLowerCase ( ) ;
2025-10-07 21:57:32 +00:00
// Get stack configuration from environment (or defaults)
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 ;
2025-10-05 19:40:14 +02:00
async function fetchPositions ( request : APIRequestContext , owner : string ) {
const response = await request . post ( STACK_GRAPHQL_URL , {
data : {
query : `
query PositionsByOwner ( $owner : String ! ) {
positionss ( where : { owner : $owner } , limit : 5 ) {
items {
id
owner
taxRate
stakeDeposit
status
}
}
}
` ,
variables : { owner } ,
} ,
headers : { 'content-type' : 'application/json' } ,
} ) ;
expect ( response . ok ( ) ) . toBeTruthy ( ) ;
const payload = await response . json ( ) ;
return ( payload ? . data ? . positionss ? . items ? ? [ ] ) as Array < {
id : string ;
owner : string ;
taxRate : number ;
stakeDeposit : string ;
status : string ;
} > ;
}
test . describe ( 'Acquire & Stake' , ( ) = > {
2025-10-07 21:57:32 +00:00
// Validate that a healthy stack exists before running tests
// Tests do NOT start their own stack - stack must be running already
2025-10-05 19:40:14 +02:00
test . beforeAll ( async ( ) = > {
2025-10-07 21:57:32 +00:00
await validateStackHealthy ( STACK_CONFIG ) ;
2025-10-05 19:40:14 +02:00
} ) ;
test ( 'users can swap KRK via UI' , async ( { browser , request } ) = > {
console . log ( '[TEST] Creating wallet context...' ) ;
const context = await createWalletContext ( browser , {
privateKey : ACCOUNT_PRIVATE_KEY ,
rpcUrl : STACK_RPC_URL ,
} ) ;
const page = await context . newPage ( ) ;
// Log browser console messages
page . on ( 'console' , msg = > console . log ( ` [BROWSER] ${ msg . type ( ) } : ${ msg . text ( ) } ` ) ) ;
page . on ( 'pageerror' , error = > console . log ( ` [BROWSER ERROR] ${ error . message } ` ) ) ;
try {
console . log ( '[TEST] Loading app...' ) ;
await page . goto ( ` ${ STACK_WEBAPP_URL } /app/ ` , { waitUntil : 'domcontentloaded' } ) ;
2026-02-02 19:24:57 +01:00
console . log ( '[TEST] App loaded, waiting for Vue app to mount...' ) ;
2025-10-05 19:40:14 +02:00
2026-02-02 19:24:57 +01:00
// Wait for the Vue app to fully mount by waiting for a key element
// The navbar-title is always present regardless of connection state
const navbarTitle = page . locator ( '.navbar-title' ) . first ( ) ;
await expect ( navbarTitle ) . toBeVisible ( { timeout : 30_000 } ) ;
console . log ( '[TEST] Vue app mounted, navbar is visible' ) ;
// Trigger a resize event to force Vue's useMobile composable to recalculate
// This ensures the app recognizes the desktop screen width set by wallet-provider
await page . evaluate ( ( ) = > {
window . dispatchEvent ( new Event ( 'resize' ) ) ;
} ) ;
await page . waitForTimeout ( 500 ) ;
// Give extra time for wallet connectors to initialize
await page . waitForTimeout ( 2 _000 ) ;
// Connect wallet flow:
// The wallet-provider sets screen.width to 1280 to ensure desktop mode.
// We expect the desktop Connect button to be visible.
console . log ( '[TEST] Looking for Connect button...' ) ;
// Desktop Connect button
const connectButton = page . locator ( '.connect-button--disconnected' ) . first ( ) ;
let panelOpened = false ;
// Wait for the Connect button with a reasonable timeout
if ( await connectButton . isVisible ( { timeout : 5_000 } ) ) {
console . log ( '[TEST] Found desktop Connect button, clicking...' ) ;
await connectButton . click ( ) ;
panelOpened = true ;
} else {
// Debug: Log current screen.width and navbar-end contents
const screenWidth = await page . evaluate ( ( ) = > window . screen . width ) ;
const navbarEndHtml = await page . locator ( '.navbar-end' ) . innerHTML ( ) . catch ( ( ) = > 'not found' ) ;
console . log ( ` [TEST] DEBUG: screen.width = ${ screenWidth } ` ) ;
console . log ( ` [TEST] DEBUG: navbar-end HTML = ${ navbarEndHtml . substring ( 0 , 500 ) } ` ) ;
console . log ( '[TEST] Connect button not visible - checking for mobile fallback...' ) ;
// Fallback to mobile login icon (SVG in navbar-end when disconnected)
const mobileLoginIcon = page . locator ( '.navbar-end svg' ) . first ( ) ;
if ( await mobileLoginIcon . isVisible ( { timeout : 2_000 } ) ) {
console . log ( '[TEST] Found mobile login icon, clicking...' ) ;
await mobileLoginIcon . click ( ) ;
panelOpened = true ;
} else {
console . log ( '[TEST] No Connect button or mobile icon visible - wallet may already be connected' ) ;
}
}
if ( panelOpened ) {
await page . waitForTimeout ( 1 _000 ) ;
// Look for the injected wallet connector in the slideout panel
console . log ( '[TEST] Looking for wallet connector in panel...' ) ;
const injectedConnector = page . locator ( '.connectors-element' ) . first ( ) ;
if ( await injectedConnector . isVisible ( { timeout : 5_000 } ) ) {
console . log ( '[TEST] Clicking first wallet connector...' ) ;
await injectedConnector . click ( ) ;
await page . waitForTimeout ( 2 _000 ) ;
} else {
console . log ( '[TEST] WARNING: No wallet connector found in panel' ) ;
}
}
2025-10-05 19:40:14 +02:00
// Check if wallet shows as connected in UI
console . log ( '[TEST] Checking for wallet display...' ) ;
const walletDisplay = page . getByText ( /0xf39F/i ) . first ( ) ;
await expect ( walletDisplay ) . toBeVisible ( { timeout : 15_000 } ) ;
console . log ( '[TEST] Wallet connected successfully!' ) ;
2025-10-11 17:20:30 +00:00
console . log ( '[TEST] Verifying stack version footer...' ) ;
const versionFooter = page . getByTestId ( 'stack-version-footer' ) ;
await expect ( versionFooter ) . toBeVisible ( { timeout : 15_000 } ) ;
await expect ( page . getByTestId ( 'stack-version-contracts' ) ) . not . toHaveText ( /Loading|—/i ) ;
await expect ( page . getByTestId ( 'stack-version-ponder' ) ) . not . toHaveText ( /Loading|—/i ) ;
await expect ( page . getByTestId ( 'stack-version-kraiken-lib' ) ) . toHaveText ( /^v\d+/i ) ;
await expect ( page . getByTestId ( 'stack-version-web-app' ) ) . toHaveText ( /^v/i ) ;
const warningBanner = page . getByTestId ( 'stack-version-warning' ) ;
await expect ( warningBanner ) . toHaveCount ( 0 ) ;
console . log ( '[TEST] Stack version footer verified.' ) ;
2025-10-05 19:40:14 +02:00
console . log ( '[TEST] Navigating to cheats page...' ) ;
2026-02-20 17:28:59 +01:00
await navigateSPA ( page , '/app/cheats' ) ;
await expect ( page . getByRole ( 'heading' , { name : 'Cheat Console' } ) ) . toBeVisible ( { timeout : 10_000 } ) ;
2025-10-05 19:40:14 +02:00
console . log ( '[TEST] Minting test ETH...' ) ;
await page . getByLabel ( 'RPC URL' ) . fill ( STACK_RPC_URL ) ;
await page . getByLabel ( 'Recipient' ) . fill ( ACCOUNT_ADDRESS ) ;
const mintButton = page . getByRole ( 'button' , { name : 'Mint' } ) ;
await expect ( mintButton ) . toBeEnabled ( ) ;
await mintButton . click ( ) ;
await page . waitForTimeout ( 3 _000 ) ;
2026-03-02 06:32:48 +00:00
console . log ( '[TEST] Navigating to get-krk page to buy KRK...' ) ;
await navigateSPA ( page , '/app/get-krk' ) ;
await expect ( page . getByRole ( 'heading' , { name : 'Get $KRK Tokens' } ) ) . toBeVisible ( { timeout : 10_000 } ) ;
2025-10-05 19:40:14 +02:00
2026-03-02 06:32:48 +00:00
await page . screenshot ( { path : 'test-results/before-swap.png' } ) ;
2025-10-05 19:40:14 +02:00
2026-03-02 23:13:05 +00:00
const ethToSpendInput = page . getByTestId ( 'swap-amount-input' ) ;
2026-03-02 06:32:48 +00:00
await expect ( ethToSpendInput ) . toBeVisible ( { timeout : 15_000 } ) ;
2025-10-05 19:40:14 +02:00
await ethToSpendInput . fill ( '0.05' ) ;
2026-03-02 23:13:05 +00:00
const buyButton = page . getByTestId ( 'swap-buy-button' ) ;
2025-10-05 19:40:14 +02:00
await expect ( buyButton ) . toBeVisible ( ) ;
2026-03-02 06:32:48 +00:00
console . log ( '[TEST] Clicking Buy KRK button...' ) ;
2025-10-05 19:40:14 +02:00
await buyButton . click ( ) ;
2026-03-02 06:32:48 +00:00
// Wait for button to show "Submitting..." then return to "Buy KRK"
2025-10-05 19:40:14 +02:00
console . log ( '[TEST] Waiting for swap to process...' ) ;
try {
await page . getByRole ( 'button' , { name : /Submitting/i } ) . waitFor ( { state : 'visible' , timeout : 5_000 } ) ;
console . log ( '[TEST] Swap initiated, waiting for completion...' ) ;
2026-03-02 23:47:53 +00:00
await expect ( page . getByTestId ( 'swap-buy-button' ) ) . toHaveText ( 'Buy KRK' , { timeout : 60_000 } ) ;
2025-10-05 19:40:14 +02:00
console . log ( '[TEST] Swap completed!' ) ;
} catch ( e ) {
console . log ( '[TEST] No "Submitting" state detected, swap may have completed instantly' ) ;
}
await page . waitForTimeout ( 2 _000 ) ;
console . log ( '[TEST] Verifying swap via RPC...' ) ;
// Query the blockchain directly to verify KRK balance increased
const balanceResponse = await fetch ( STACK_RPC_URL , {
method : 'POST' ,
headers : { 'content-type' : 'application/json' } ,
body : JSON.stringify ( {
jsonrpc : '2.0' ,
id : 1 ,
method : 'eth_call' ,
params : [ {
2025-11-20 18:54:53 +01:00
to : STACK_CONFIG.contracts.Kraiken , // KRK token from deployments
2025-10-05 19:40:14 +02:00
data : ` 0x70a08231000000000000000000000000 ${ ACCOUNT_ADDRESS . slice ( 2 ) } ` // balanceOf(address)
} , 'latest' ]
} )
} ) ;
const balanceData = await balanceResponse . json ( ) ;
2025-11-20 18:54:53 +01:00
const balance = BigInt ( balanceData . result || '0x0' ) ;
2025-10-05 19:40:14 +02:00
console . log ( ` [TEST] KRK balance: ${ balance . toString ( ) } wei ` ) ;
expect ( balance ) . toBeGreaterThan ( 0 n ) ;
console . log ( '[TEST] ✅ Swap successful! KRK balance > 0' ) ;
2025-10-07 15:06:38 +02:00
// Now test staking via UI
console . log ( '[TEST] Navigating to stake page...' ) ;
2026-02-20 17:28:59 +01:00
await navigateSPA ( page , '/app/stake' ) ;
2025-10-07 15:06:38 +02:00
await page . waitForTimeout ( 2 _000 ) ;
// Wait for the stake form to be initialized
console . log ( '[TEST] Waiting for stake form to load...' ) ;
2025-10-07 21:57:32 +00:00
const tokenAmountSlider = page . getByRole ( 'slider' , { name : 'Token Amount' } ) ;
await expect ( tokenAmountSlider ) . toBeVisible ( { timeout : 15_000 } ) ;
2025-10-07 15:06:38 +02:00
2025-10-07 21:57:32 +00:00
console . log ( '[TEST] Filling stake form via accessible controls...' ) ;
// Take screenshot before interaction
await page . screenshot ( { path : 'test-results/before-stake-fill.png' } ) ;
// Check if input is visible and enabled
const stakeAmountInput = page . getByLabel ( 'Staking Amount' ) ;
console . log ( '[TEST] Checking if staking amount input is visible...' ) ;
await expect ( stakeAmountInput ) . toBeVisible ( { timeout : 10_000 } ) ;
console . log ( '[TEST] Staking amount input is visible, filling value...' ) ;
2026-02-26 19:37:08 +00:00
await stakeAmountInput . fill ( '1000' ) ;
2025-10-07 21:57:32 +00:00
console . log ( '[TEST] Filled staking amount!' ) ;
const taxSelect = page . getByRole ( 'combobox' , { name : 'Tax' } ) ;
console . log ( '[TEST] Selecting tax rate...' ) ;
await taxSelect . selectOption ( { value : '2' } ) ;
console . log ( '[TEST] Tax rate selected!' ) ;
2025-10-07 15:06:38 +02:00
console . log ( '[TEST] Clicking stake button...' ) ;
2025-10-07 21:57:32 +00:00
// Use the main form's submit button (large/block), not the small position card buttons
const stakeButton = page . getByRole ( 'main' ) . getByRole ( 'button' , { name : /Stake|Snatch and Stake/i } ) ;
2025-10-07 15:06:38 +02:00
await expect ( stakeButton ) . toBeVisible ( { timeout : 5_000 } ) ;
await stakeButton . click ( ) ;
// Wait for transaction to process
console . log ( '[TEST] Waiting for stake transaction...' ) ;
try {
await page . getByRole ( 'button' , { name : /Sign Transaction|Waiting/i } ) . waitFor ( { state : 'visible' , timeout : 5_000 } ) ;
console . log ( '[TEST] Transaction initiated, waiting for completion...' ) ;
await page . getByRole ( 'button' , { name : /Stake|Snatch and Stake/i } ) . waitFor ( { state : 'visible' , timeout : 60_000 } ) ;
console . log ( '[TEST] Stake transaction completed!' ) ;
} catch ( e ) {
console . log ( '[TEST] Transaction may have completed instantly' ) ;
}
2026-02-26 15:18:03 +00:00
// Poll for Ponder to index the staking transaction (Ponder has indexing latency)
console . log ( '[TEST] Polling GraphQL for staking position (Ponder indexing latency)...' ) ;
let positions : Awaited < ReturnType < typeof fetchPositions > > = [ ] ;
for ( let attempt = 0 ; attempt < 15 ; attempt ++ ) {
await page . waitForTimeout ( 2 _000 ) ;
positions = await fetchPositions ( request , ACCOUNT_ADDRESS ) ;
if ( positions . length > 0 ) break ;
console . log ( ` [TEST] Ponder not yet indexed (attempt ${ attempt + 1 } /15), retrying... ` ) ;
}
2025-10-07 15:06:38 +02:00
console . log ( ` [TEST] Found ${ positions . length } position(s) ` ) ;
expect ( positions . length ) . toBeGreaterThan ( 0 ) ;
2025-10-07 21:57:32 +00:00
const activePositions = positions . filter ( p = > p . status === 'Active' ) ;
2025-10-07 15:06:38 +02:00
expect ( activePositions . length ) . toBeGreaterThan ( 0 ) ;
console . log ( ` [TEST] ✅ Staking successful! Created ${ activePositions . length } active position(s) ` ) ;
console . log ( '[TEST] ✅ E2E test complete: Full journey verified (Mint → Swap → Stake)' ) ;
2025-10-05 19:40:14 +02:00
} finally {
await context . close ( ) ;
}
} ) ;
} ) ;