import { expect, test } from '@playwright/test'; import { Wallet } from 'ethers'; import { createWalletContext } from '../../setup/wallet-provider'; import { getStackConfig, validateStackHealthy } from '../../setup/stack'; import { createPersonaFeedback, addFeedbackStep, writePersonaFeedback, mintEth, buyKrk, resetChainState, type PersonaFeedback, } from './helpers'; import { mkdirSync, readFileSync } from 'fs'; import { join } from 'path'; const KRK_ADDRESS = JSON.parse(readFileSync(join(process.cwd(), 'onchain', 'deployments-local.json'), 'utf-8')).contracts.Kraiken.toLowerCase(); const STACK_CONFIG = getStackConfig(); const STACK_RPC_URL = STACK_CONFIG.rpcUrl; const STACK_WEBAPP_URL = STACK_CONFIG.webAppUrl; const LANDING_PAGE_URL = 'http://localhost:8081/'; // Account for tests const ACCOUNT_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; const ACCOUNT_ADDRESS = new Wallet(ACCOUNT_PRIVATE_KEY).address; test.describe('Test A: Passive Holder Journey', () => { test.beforeAll(async () => { await resetChainState(STACK_RPC_URL); await validateStackHealthy(STACK_CONFIG); }); test.describe.serial('Tyler - Retail Degen ("sell me in 30 seconds")', () => { let feedback: PersonaFeedback; test.beforeAll(() => { feedback = createPersonaFeedback('tyler', 'A', 'passive-holder'); }); test('Tyler evaluates landing page value prop', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const observations: string[] = []; try { // Step 1: Navigate to landing page await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2_000); // Tyler's quick evaluation observations.push('Scanning page... do I see APY numbers? Big buttons? What\'s the hook?'); // Check for value prop clarity const hasGetKrkButton = await page.getByRole('button', { name: /get.*krk/i }).isVisible().catch(() => false); if (hasGetKrkButton) { observations.push('✓ "Get KRK" button is visible and prominent - good CTA'); } else { observations.push('✗ No clear "Get KRK" button visible - where do I start?'); } // Check for stats/numbers that catch attention const hasStats = await page.locator('text=/\\d+%|\\$\\d+|APY/i').first().isVisible().catch(() => false); if (hasStats) { observations.push('✓ Numbers visible - I see stats, that\'s good for credibility'); } else { observations.push('✗ No flashy APY or TVL numbers - nothing to grab my attention'); } // Crypto jargon check const pageText = await page.textContent('body') || ''; const jargonWords = ['harberger', 'vwap', 'tokenomics', 'liquidity', 'leverage']; const foundJargon = jargonWords.filter(word => pageText.toLowerCase().includes(word)); if (foundJargon.length > 2) { observations.push(`⚠ Too much jargon: ${foundJargon.join(', ')} - might scare normies away`); } else { observations.push('✓ Copy is relatively clean, not too technical'); } // Protocol Health section const hasProtocolHealth = await page.getByText(/protocol health|system status/i).isVisible().catch(() => false); if (hasProtocolHealth) { observations.push('✓ Protocol Health section builds trust - shows transparency'); } else { observations.push('Missing: No visible protocol health/stats - how do I know this isn\'t rugpull?'); } // Screenshot const screenshotDir = join('test-results', 'usertest', 'tyler-a'); mkdirSync(screenshotDir, { recursive: true }); const screenshotPath = join(screenshotDir, `landing-page-${Date.now()}.png`); await page.screenshot({ path: screenshotPath, fullPage: true }); addFeedbackStep(feedback, 'landing-page', observations, screenshotPath); // Tyler's 30-second verdict const verdict = hasGetKrkButton && hasStats ? 'PASS: Clear CTA, visible stats. I\'d click through to learn more.' : 'FAIL: Not sold in 30 seconds. Needs bigger numbers and clearer value prop.'; observations.push(`Tyler\'s verdict: ${verdict}`); } finally { await context.close(); } }); test('Tyler clicks Get KRK and checks Uniswap link', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const observations: string[] = []; try { await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(1_000); // Click Get KRK button const getKrkButton = page.getByRole('button', { name: /get.*krk/i }).first(); const buttonVisible = await getKrkButton.isVisible({ timeout: 5_000 }).catch(() => false); if (buttonVisible) { await getKrkButton.click(); await page.waitForTimeout(2_000); // Check if navigated to web-app const currentUrl = page.url(); if (currentUrl.includes('get-krk') || currentUrl.includes('5173')) { observations.push('✓ Get KRK button navigated to web-app'); } else { observations.push(`✗ Get KRK button went to wrong place: ${currentUrl}`); } // Check for Uniswap link with correct token address const uniswapLink = await page.locator(`a[href*="uniswap"][href*="${KRK_ADDRESS}"]`).isVisible().catch(() => false); if (uniswapLink) { observations.push('✓ Uniswap link exists with correct KRK token address'); } else { const anyUniswapLink = await page.locator('a[href*="uniswap"]').isVisible().catch(() => false); if (anyUniswapLink) { observations.push('⚠ Uniswap link exists but may have wrong token address'); } else { observations.push('✗ No Uniswap link found - how do I actually get KRK?'); } } // Screenshot const screenshotDir = join('test-results', 'usertest', 'tyler-a'); const screenshotPath = join(screenshotDir, `get-krk-page-${Date.now()}.png`); await page.screenshot({ path: screenshotPath, fullPage: true }); addFeedbackStep(feedback, 'get-krk', observations, screenshotPath); } else { observations.push('✗ CRITICAL: Get KRK button not found on landing page'); addFeedbackStep(feedback, 'get-krk', observations); } } finally { await context.close(); } }); test('Tyler simulates having KRK and checks return value', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const observations: string[] = []; try { // Navigate to web-app to connect wallet await page.goto(`${STACK_WEBAPP_URL}/app/`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2_000); // Mint ETH and buy KRK programmatically observations.push('Buying KRK via on-chain swap...'); await mintEth(page, STACK_RPC_URL, ACCOUNT_ADDRESS, '10'); try { await buyKrk(page, '1', STACK_RPC_URL, ACCOUNT_PRIVATE_KEY); observations.push('✓ Successfully acquired KRK via swap'); } catch (error: any) { observations.push(`✗ KRK purchase failed: ${error.message}`); feedback.overall.friction.push('Cannot acquire KRK through documented flow'); } // Navigate back to landing page await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2_000); // Check for reasons to return observations.push('Now I have KRK... why would I come back to landing page?'); const hasStatsSection = await page.getByText(/stats|protocol health|dashboard/i).isVisible().catch(() => false); const hasPriceInfo = await page.locator('text=/price|\\$[0-9]/i').isVisible().catch(() => false); const hasAPY = await page.locator('text=/APY|%/i').isVisible().catch(() => false); if (hasStatsSection || hasPriceInfo || hasAPY) { observations.push('✓ Landing page has stats/info - gives me reason to check back'); } else { observations.push('✗ No compelling reason to return to landing page - just a static ad'); feedback.overall.friction.push('Landing page offers no ongoing value for holders'); } // Screenshot const screenshotDir = join('test-results', 'usertest', 'tyler-a'); const screenshotPath = join(screenshotDir, `return-check-${Date.now()}.png`); await page.screenshot({ path: screenshotPath, fullPage: true }); addFeedbackStep(feedback, 'return-value', observations, screenshotPath); // Tyler's overall assessment const wouldReturn = hasStatsSection || hasPriceInfo || hasAPY; feedback.overall.wouldBuy = observations.some(o => o.includes('✓ Successfully acquired KRK')); feedback.overall.wouldReturn = wouldReturn; if (!wouldReturn) { feedback.overall.friction.push('Landing page is one-time conversion, no repeat visit value'); } } finally { await context.close(); writePersonaFeedback(feedback); } }); }); test.describe.serial('Alex - Newcomer ("what even is this?")', () => { let feedback: PersonaFeedback; test.beforeAll(() => { feedback = createPersonaFeedback('alex', 'A', 'passive-holder'); }); test('Alex tries to understand the landing page', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const observations: string[] = []; try { await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2_000); observations.push('Reading the page... trying to understand what this protocol does'); // Check for explanatory content const hasExplainer = await page.getByText(/how it works|what is|getting started/i).isVisible().catch(() => false); if (hasExplainer) { observations.push('✓ Found "How it works" or explainer section - helpful!'); } else { observations.push('✗ No clear explainer - I\'m lost and don\'t know what this is'); feedback.overall.friction.push('No beginner-friendly explanation on landing page'); } // Jargon overload check const pageText = await page.textContent('body') || ''; const complexTerms = ['harberger', 'vwap', 'amm', 'liquidity pool', 'tokenomics', 'leverage', 'tax rate']; const foundTerms = complexTerms.filter(term => pageText.toLowerCase().includes(term)); if (foundTerms.length > 3) { observations.push(`⚠ Jargon overload (${foundTerms.length} complex terms): ${foundTerms.join(', ')}`); observations.push('As a newcomer, this is intimidating and confusing'); feedback.overall.friction.push('Too much unexplained crypto jargon'); } else { observations.push('✓ Language is relatively accessible'); } // Check for Get KRK button clarity const getKrkButton = await page.getByRole('button', { name: /get.*krk/i }).isVisible().catch(() => false); if (getKrkButton) { observations.push('✓ "Get KRK" button is clear - I understand that\'s the next step'); } else { observations.push('✗ Not sure how to start or what to do first'); } // Trust signals const hasTrustSignals = await page.getByText(/audit|secure|safe|verified/i).isVisible().catch(() => false); if (hasTrustSignals) { observations.push('✓ Trust signals present (audit/secure) - makes me feel safer'); } else { observations.push('⚠ No visible security/audit info - how do I know this is safe?'); feedback.overall.friction.push('Lack of trust signals for newcomers'); } // Screenshot const screenshotDir = join('test-results', 'usertest', 'alex-a'); mkdirSync(screenshotDir, { recursive: true }); const screenshotPath = join(screenshotDir, `landing-confusion-${Date.now()}.png`); await page.screenshot({ path: screenshotPath, fullPage: true }); addFeedbackStep(feedback, 'landing-page', observations, screenshotPath); } finally { await context.close(); } }); test('Alex explores Get KRK page and looks for guidance', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const observations: string[] = []; try { await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(1_000); // Try to click Get KRK const getKrkButton = page.getByRole('button', { name: /get.*krk/i }).first(); const buttonVisible = await getKrkButton.isVisible({ timeout: 5_000 }).catch(() => false); if (buttonVisible) { await getKrkButton.click(); await page.waitForTimeout(2_000); observations.push('Clicked "Get KRK" - now what?'); // Look for step-by-step instructions const hasInstructions = await page.getByText(/step|how to|tutorial|guide/i).isVisible().catch(() => false); if (hasInstructions) { observations.push('✓ Found step-by-step instructions - very helpful for newcomer'); } else { observations.push('✗ No clear instructions on how to proceed'); feedback.overall.friction.push('Get KRK page lacks step-by-step guide'); } // Check for Uniswap link explanation const uniswapLink = await page.locator('a[href*="uniswap"]').first().isVisible().catch(() => false); if (uniswapLink) { // Check if there's explanatory text near the link const hasContext = await page.getByText(/swap|exchange|buy on uniswap/i).isVisible().catch(() => false); if (hasContext) { observations.push('✓ Uniswap link has context/explanation'); } else { observations.push('⚠ Uniswap link present but no explanation - what is Uniswap?'); feedback.overall.friction.push('No explanation of external links (Uniswap)'); } } else { observations.push('✗ No Uniswap link found - how do I get KRK?'); } // Screenshot const screenshotDir = join('test-results', 'usertest', 'alex-a'); const screenshotPath = join(screenshotDir, `get-krk-page-${Date.now()}.png`); await page.screenshot({ path: screenshotPath, fullPage: true }); addFeedbackStep(feedback, 'get-krk', observations, screenshotPath); } else { observations.push('✗ Could not find Get KRK button'); addFeedbackStep(feedback, 'get-krk', observations); } } finally { await context.close(); } }); test('Alex simulates getting KRK and evaluates next steps', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const observations: string[] = []; try { await page.goto(`${STACK_WEBAPP_URL}/app/`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2_000); // Simulate getting KRK observations.push('Pretending I figured out Uniswap and bought KRK...'); await mintEth(page, STACK_RPC_URL, ACCOUNT_ADDRESS, '10'); try { await buyKrk(page, '1', STACK_RPC_URL, ACCOUNT_PRIVATE_KEY); observations.push('✓ Somehow managed to get KRK'); } catch (error: any) { observations.push(`✗ Failed to get KRK: ${error.message}`); } // Navigate back to landing await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2_000); observations.push('Okay, I have KRK now... what should I do with it?'); // Look for holder guidance const hasHolderInfo = await page.getByText(/hold|stake|earn|now what/i).isVisible().catch(() => false); if (hasHolderInfo) { observations.push('✓ Found guidance for what to do after getting KRK'); } else { observations.push('✗ No clear next steps - just have tokens sitting in wallet'); feedback.overall.friction.push('No guidance for new holders on what to do next'); } // Check for ongoing value const hasReasonToReturn = await page.getByText(/dashboard|stats|price|track/i).isVisible().catch(() => false); if (hasReasonToReturn) { observations.push('✓ Landing page has info worth checking regularly'); } else { observations.push('✗ No reason to come back to landing page'); } // Screenshot const screenshotDir = join('test-results', 'usertest', 'alex-a'); const screenshotPath = join(screenshotDir, `after-purchase-${Date.now()}.png`); await page.screenshot({ path: screenshotPath, fullPage: true }); addFeedbackStep(feedback, 'post-purchase', observations, screenshotPath); // Alex's verdict const understandsValueProp = observations.some(o => o.includes('✓ Found "How it works"')); const knowsNextSteps = hasHolderInfo; feedback.overall.wouldBuy = understandsValueProp && observations.some(o => o.includes('✓ Somehow managed to get KRK')); feedback.overall.wouldReturn = hasReasonToReturn; if (!understandsValueProp) { feedback.overall.friction.push('Value proposition unclear to crypto newcomers'); } if (!knowsNextSteps) { feedback.overall.friction.push('Post-purchase journey undefined'); } } finally { await context.close(); writePersonaFeedback(feedback); } }); }); test.describe.serial('Sarah - Yield Farmer ("is this worth my time?")', () => { let feedback: PersonaFeedback; test.beforeAll(() => { feedback = createPersonaFeedback('sarah', 'A', 'passive-holder'); }); test('Sarah analyzes landing page metrics and credibility', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const observations: string[] = []; try { await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2_000); observations.push('Scanning for key metrics: APY, TVL, risk factors...'); // Check for APY/yield info const hasAPY = await page.locator('text=/\\d+%|APY|yield/i').isVisible().catch(() => false); if (hasAPY) { observations.push('✓ APY or yield percentage visible - good, I can compare to other protocols'); } else { observations.push('✗ No clear APY shown - can\'t evaluate if this is competitive'); feedback.overall.friction.push('No yield/APY displayed on landing page'); } // Check for TVL const hasTVL = await page.locator('text=/TVL|total value locked|\\$[0-9]+[kmb]/i').isVisible().catch(() => false); if (hasTVL) { observations.push('✓ TVL visible - helps me assess protocol size and safety'); } else { observations.push('⚠ No TVL shown - harder to gauge protocol maturity'); } // Protocol Health section const hasProtocolHealth = await page.getByText(/protocol health|health|status/i).isVisible().catch(() => false); if (hasProtocolHealth) { observations.push('✓ Protocol Health section present - shows transparency and confidence'); } else { observations.push('⚠ No protocol health metrics - how do I assess risk?'); feedback.overall.friction.push('Missing protocol health/risk indicators'); } // Audit info const hasAudit = await page.getByText(/audit|audited|security/i).isVisible().catch(() => false); if (hasAudit) { observations.push('✓ Audit information visible - critical for serious yield farmers'); } else { observations.push('✗ No audit badge or security info - major red flag'); feedback.overall.friction.push('No visible audit/security credentials'); } // Smart contract addresses const hasContracts = await page.locator('text=/0x[a-fA-F0-9]{40}|contract address/i').isVisible().catch(() => false); if (hasContracts) { observations.push('✓ Contract addresses visible - I can verify on Etherscan'); } else { observations.push('⚠ No contract addresses - want to verify before committing capital'); } // Screenshot const screenshotDir = join('test-results', 'usertest', 'sarah-a'); mkdirSync(screenshotDir, { recursive: true }); const screenshotPath = join(screenshotDir, `landing-metrics-${Date.now()}.png`); await page.screenshot({ path: screenshotPath, fullPage: true }); addFeedbackStep(feedback, 'landing-page', observations, screenshotPath); } finally { await context.close(); } }); test('Sarah evaluates Get KRK flow efficiency', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const observations: string[] = []; try { await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(1_000); const getKrkButton = page.getByRole('button', { name: /get.*krk/i }).first(); const buttonVisible = await getKrkButton.isVisible({ timeout: 5_000 }).catch(() => false); if (buttonVisible) { await getKrkButton.click(); await page.waitForTimeout(2_000); observations.push('Evaluating acquisition flow - time is money'); // Check for direct swap vs external redirect const currentUrl = page.url(); const hasDirectSwap = await page.locator('input[placeholder*="amount" i]').isVisible({ timeout: 3_000 }).catch(() => false); if (hasDirectSwap) { observations.push('✓ Direct swap interface - efficient, no external redirects'); } else { observations.push('⚠ Redirects to external swap - adds friction and gas costs'); } // Uniswap link check const uniswapLink = await page.locator(`a[href*="uniswap"][href*="${KRK_ADDRESS}"]`).isVisible().catch(() => false); if (uniswapLink) { observations.push('✓ Uniswap link with correct token address - can verify liquidity'); } else { observations.push('✗ No Uniswap link or wrong address - can\'t verify DEX liquidity'); feedback.overall.friction.push('Cannot verify DEX liquidity before buying'); } // Price impact warning const hasPriceImpact = await page.getByText(/price impact|slippage/i).isVisible().catch(() => false); if (hasPriceImpact) { observations.push('✓ Price impact/slippage shown - good UX for larger trades'); } else { observations.push('⚠ No price impact warning - could be surprised by slippage'); } // Screenshot const screenshotDir = join('test-results', 'usertest', 'sarah-a'); const screenshotPath = join(screenshotDir, `get-krk-flow-${Date.now()}.png`); await page.screenshot({ path: screenshotPath, fullPage: true }); addFeedbackStep(feedback, 'get-krk', observations, screenshotPath); } else { observations.push('✗ Get KRK button not found'); addFeedbackStep(feedback, 'get-krk', observations); } } finally { await context.close(); } }); test('Sarah checks for holder value and monitoring tools', async ({ browser }) => { const context = await createWalletContext(browser, { privateKey: ACCOUNT_PRIVATE_KEY, rpcUrl: STACK_RPC_URL, }); const page = await context.newPage(); const observations: string[] = []; try { await page.goto(`${STACK_WEBAPP_URL}/app/`, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2_000); // Acquire KRK observations.push('Acquiring KRK to evaluate holder experience...'); await mintEth(page, STACK_RPC_URL, ACCOUNT_ADDRESS, '10'); try { await buyKrk(page, '2', STACK_RPC_URL, ACCOUNT_PRIVATE_KEY); observations.push('✓ KRK acquired'); } catch (error: any) { observations.push(`✗ Acquisition failed: ${error.message}`); feedback.overall.friction.push('Programmatic acquisition flow broken'); } // Return to landing page await page.goto(LANDING_PAGE_URL, { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(2_000); observations.push('Now holding KRK - what ongoing value does landing page provide?'); // Real-time stats const hasRealtimeStats = await page.locator('text=/live|24h|volume|price/i').isVisible().catch(() => false); if (hasRealtimeStats) { observations.push('✓ Real-time stats visible - makes landing page a monitoring dashboard'); } else { observations.push('✗ No real-time data - no reason to return to landing page'); feedback.overall.friction.push('Landing page provides no ongoing value for holders'); } // Protocol health tracking const hasHealthMetrics = await page.getByText(/protocol health|system status|health score/i).isVisible().catch(() => false); if (hasHealthMetrics) { observations.push('✓ Protocol health tracking - helps me monitor risk'); } else { observations.push('⚠ No protocol health dashboard - can\'t monitor protocol risk'); } // Links to analytics const hasAnalytics = await page.locator('a[href*="dune"][href*="dexscreener"]').or(page.getByText(/analytics|charts/i)).isVisible().catch(() => false); if (hasAnalytics) { observations.push('✓ Links to analytics platforms - good for research'); } else { observations.push('⚠ No links to Dune/DexScreener - harder to do deep analysis'); } // Screenshot const screenshotDir = join('test-results', 'usertest', 'sarah-a'); const screenshotPath = join(screenshotDir, `holder-dashboard-${Date.now()}.png`); await page.screenshot({ path: screenshotPath, fullPage: true }); addFeedbackStep(feedback, 'holder-experience', observations, screenshotPath); // Sarah's ROI assessment const hasCompetitiveAPY = observations.some(o => o.includes('✓ APY or yield percentage visible')); const hasMonitoringTools = hasRealtimeStats || hasHealthMetrics; const lowFriction = feedback.overall.friction.length < 3; feedback.overall.wouldBuy = hasCompetitiveAPY && lowFriction; feedback.overall.wouldReturn = hasMonitoringTools; if (!hasMonitoringTools) { feedback.overall.friction.push('Insufficient monitoring/analytics tools for active yield farmers'); } observations.push(`Sarah's verdict: ${feedback.overall.wouldBuy ? 'Worth allocating capital' : 'Not competitive enough'}`); observations.push(`Would return: ${feedback.overall.wouldReturn ? 'Yes, for monitoring' : 'No, one-time interaction only'}`); } finally { await context.close(); writePersonaFeedback(feedback); } }); }); });