681 lines
29 KiB
TypeScript
681 lines
29 KiB
TypeScript
/* eslint-disable no-restricted-syntax -- waitForTimeout: no event source exists for animation settling, wallet connector UI transitions, and debounced state updates in user-simulation tests. See AGENTS.md #Engineering Principles. */
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
});
|