import { chromium } from 'playwright'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const variants = [ { id: 'a', url: 'http://127.0.0.1:8081/#/', name: 'defensive' }, { id: 'b', url: 'http://127.0.0.1:8081/#/offensive', name: 'offensive' }, { id: 'c', url: 'http://127.0.0.1:8081/#/mixed', name: 'mixed' } ]; const personas = [ { id: 'marcus', name: 'Marcus (degen)', profile: 'CT native, trades memecoins, responds to: hype, FOMO, edge, "alpha". Hates: corporate speak, "safe" language. Wants: to ape fast. Benchmark: "would I RT this?"', evaluator: (text) => evaluateMarcus(text) }, { id: 'sarah', name: 'Sarah (yield farmer)', profile: 'Uses Aave/Compound, wants 8%+ yield. Responds to: numbers, APY, risk metrics. Hates: vague promises, no data. Benchmark: "better risk-adjusted than my Aave position?"', evaluator: (text) => evaluateSarah(text) }, { id: 'alex', name: 'Alex (newcomer)', profile: 'First DeFi exposure, scared of scams. Responds to: clarity, trust signals, simplicity. Hates: jargon, hype. Benchmark: "do I understand what this does and trust it?"', evaluator: (text) => evaluateAlex(text) } ]; function evaluateMarcus(text) { const textLower = text.toLowerCase(); // Check for hype/edge language const hypeWords = ['alpha', 'moon', 'ape', 'degen', 'chad', 'gm', 'ngmi', 'wagmi', 'lfg', 'fomo']; const hypeCount = hypeWords.filter(w => textLower.includes(w)).length; // Check for corporate/safe language (negative) const corporateWords = ['compliance', 'regulation', 'safe', 'secure', 'trusted', 'enterprise']; const corporateCount = corporateWords.filter(w => textLower.includes(w)).length; // Look for action-oriented CTAs const hasStrongCTA = /launch|trade|ape|buy|get in|join/i.test(text); // Check for AI/trading edge mentions const hasEdge = /ai|bot|automated|trades.*sleep|24\/7|algorithm/i.test(text); // Extract specific compelling phrases const compellingPhrases = []; if (text.includes("can't be rugged")) compellingPhrases.push("can't be rugged"); if (text.includes("trades while you sleep")) compellingPhrases.push("trades while you sleep"); if (text.includes("without the rug pull")) compellingPhrases.push("without the rug pull"); // Scoring let firstImpression = 5; if (hypeCount > 2) firstImpression += 2; if (hasEdge) firstImpression += 2; if (corporateCount > 2) firstImpression -= 2; firstImpression = Math.max(1, Math.min(10, firstImpression)); let excitement = 5; if (hypeCount > 1) excitement += 2; if (hasEdge) excitement += 2; if (corporateCount > 1) excitement -= 2; excitement = Math.max(1, Math.min(10, excitement)); let trustLevel = 6; // Degens trust the memes if (textLower.includes('rug')) trustLevel += 1; if (textLower.includes('ai')) trustLevel += 1; trustLevel = Math.max(1, Math.min(10, trustLevel)); const wouldClickCTA = hasStrongCTA && (hypeCount > 0 || hasEdge); const wouldShare = excitement >= 7; let topComplaint = "Not enough hype"; if (corporateCount > 2) topComplaint = "Too corporate, not degen enough"; if (!hasStrongCTA) topComplaint = "Weak CTA - where's the 'ape in' button?"; let whatWouldMakeThemBuy = "More FOMO triggers, clearer edge/alpha signal"; if (compellingPhrases.length > 0) whatWouldMakeThemBuy = "Already has some good hooks, needs more social proof (TVL, user count)"; return { firstImpression: `${firstImpression}/10 - ${hypeCount > 1 ? 'Has some edge/hype vibes' : 'Needs more degen energy'}`, wouldClickCTA: wouldClickCTA ? `yes - ${hasStrongCTA ? 'CTA speaks my language' : 'curious about the tech'}` : `no - ${topComplaint}`, trustLevel: `${trustLevel}/10 - ${textLower.includes('rug') ? 'Anti-rug messaging resonates' : 'Needs more proof it works'}`, excitement: `${excitement}/10 - ${excitement >= 7 ? 'This could moon' : 'Meh, seen similar'}`, topComplaint, whatWouldMakeThemBuy, wouldShare: wouldShare ? `yes - ${compellingPhrases.length > 0 ? `Good RT material: "${compellingPhrases[0]}"` : 'Interesting enough'}` : 'no - not spicy enough for CT', specificCopyFeedback: compellingPhrases.length > 0 ? `Strong: "${compellingPhrases.join('", "')}" ${corporateCount > 1 ? '| Weak: too much safe/corporate language' : ''}` : `Needs more edge. ${hypeWords.slice(0, 3).join(', ')} would hit better. ${corporateCount > 1 ? 'Ditch the corporate speak.' : ''}` }; } function evaluateSarah(text) { const textLower = text.toLowerCase(); // Check for numbers/metrics const hasAPY = /\d+%|\d+\s*percent|apy|yield|return/i.test(text); const hasNumbers = (text.match(/\d+/g) || []).length; // Check for risk/safety mentions const hasRiskInfo = /risk|audit|security|transparent|verified/i.test(text); // Check for DeFi comparisons const mentionsCompetitors = /aave|compound|curve|convex/i.test(text); // Check for vague promises (negative) const vaguePromises = /revolutionary|game-changing|disrupting|amazing|incredible/i.test(text); // Extract specific numbers const numbers = text.match(/\d+\.?\d*%?/g) || []; // Scoring let firstImpression = 5; if (hasAPY) firstImpression += 2; if (hasNumbers > 3) firstImpression += 1; if (vaguePromises) firstImpression -= 2; firstImpression = Math.max(1, Math.min(10, firstImpression)); let trustLevel = 5; if (hasRiskInfo) trustLevel += 2; if (hasAPY) trustLevel += 1; if (vaguePromises) trustLevel -= 2; trustLevel = Math.max(1, Math.min(10, trustLevel)); let excitement = 4; // Skeptical by default if (hasAPY && hasNumbers > 5) excitement += 3; if (hasRiskInfo) excitement += 1; excitement = Math.max(1, Math.min(10, excitement)); const wouldClickCTA = hasAPY && hasRiskInfo; const wouldShare = excitement >= 7 && hasAPY; let topComplaint = "No yield numbers - what's the APY?"; if (vaguePromises) topComplaint = "Too much hype, not enough data"; if (!hasRiskInfo) topComplaint = "No risk metrics or audit info"; let whatWouldMakeThemBuy = "Show me the APY, risk-adjusted returns, and audit reports"; if (hasAPY) whatWouldMakeThemBuy = "More detail on how yields are generated and sustained"; return { firstImpression: `${firstImpression}/10 - ${hasAPY ? 'Has some numbers' : 'No yield data visible'}`, wouldClickCTA: wouldClickCTA ? 'yes - enough data to explore further' : `no - ${topComplaint}`, trustLevel: `${trustLevel}/10 - ${hasRiskInfo ? 'Shows some risk awareness' : 'Missing critical risk/audit info'}`, excitement: `${excitement}/10 - ${excitement >= 7 ? 'Numbers look interesting' : 'Need more concrete data'}`, topComplaint, whatWouldMakeThemBuy, wouldShare: wouldShare ? 'yes - solid risk-adjusted opportunity' : 'no - not enough data to recommend', specificCopyFeedback: hasAPY ? `Good: ${numbers.slice(0, 3).join(', ')} shown | Needs: more breakdown of yield sources and risks` : `Missing: APY/yield numbers, risk metrics, comparison to Aave/Compound rates. ${vaguePromises ? 'Remove vague promises, add hard data.' : ''}` }; } function evaluateAlex(text) { const textLower = text.toLowerCase(); // Check for clarity const hasSimpleExplanation = /what is|how it works|step|simple|easy/i.test(text); const hasClearValue = /earn|make money|profit|income|passive/i.test(text); // Check for jargon (negative) const jargonWords = ['liquidity', 'amm', 'tvl', 'dex', 'yield farming', 'impermanent loss', 'slippage']; const jargonCount = jargonWords.filter(w => textLower.includes(w)).length; // Check for trust signals const trustSignals = ['audit', 'secure', 'safe', 'protected', 'insurance', 'verified']; const trustCount = trustSignals.filter(w => textLower.includes(w)).length; // Check for excessive hype (negative) const hypeWords = ['moon', 'ape', 'degen', 'chad', 'gm']; const hypeCount = hypeWords.filter(w => textLower.includes(w)).length; // Scoring let firstImpression = 5; if (hasSimpleExplanation) firstImpression += 2; if (jargonCount > 3) firstImpression -= 2; if (hypeCount > 0) firstImpression -= 1; firstImpression = Math.max(1, Math.min(10, firstImpression)); let trustLevel = 5; if (trustCount > 1) trustLevel += 2; if (textLower.includes('rug') && textLower.includes("can't")) trustLevel += 2; if (hypeCount > 1) trustLevel -= 2; trustLevel = Math.max(1, Math.min(10, trustLevel)); let excitement = 5; if (hasClearValue) excitement += 2; if (jargonCount > 3) excitement -= 2; excitement = Math.max(1, Math.min(10, excitement)); const wouldClickCTA = hasSimpleExplanation && trustCount > 0 && jargonCount < 3; const wouldShare = trustLevel >= 7 && excitement >= 6; let topComplaint = "Too much jargon - I don't understand"; if (hypeCount > 1) topComplaint = "Feels scammy with all the hype"; if (!hasSimpleExplanation) topComplaint = "Doesn't explain what it actually does"; let whatWouldMakeThemBuy = "Clear explanation of what it does, how I make money, and why it's safe"; if (hasSimpleExplanation) whatWouldMakeThemBuy = "More trust signals and real user testimonials"; return { firstImpression: `${firstImpression}/10 - ${hasSimpleExplanation ? 'Seems understandable' : 'Confused about what this does'}`, wouldClickCTA: wouldClickCTA ? 'yes - feels safe enough to learn more' : `no - ${topComplaint}`, trustLevel: `${trustLevel}/10 - ${trustCount > 1 ? 'Has some trust signals' : 'Worried about scams'}`, excitement: `${excitement}/10 - ${excitement >= 6 ? 'Interested if it\'s real' : 'Skeptical and confused'}`, topComplaint, whatWouldMakeThemBuy, wouldShare: wouldShare ? 'yes - would recommend to friends' : 'no - too risky/confusing to recommend', specificCopyFeedback: jargonCount > 2 ? `Too much jargon: ${jargonWords.filter(w => textLower.includes(w)).slice(0, 3).join(', ')}. Explain in plain English. ${hypeCount > 0 ? 'Hype language makes it feel less trustworthy.' : ''}` : `Good: ${hasSimpleExplanation ? 'has clear explanations' : 'not too technical'}. ${trustCount > 0 ? `Trust signals present (${trustSignals.filter(w => textLower.includes(w)).slice(0, 2).join(', ')})` : 'Needs more safety assurances'}` }; } async function captureScreenshotCDP(page, filepath) { const cdp = await page.context().newCDPSession(page); const { data } = await cdp.send('Page.captureScreenshot', { format: 'png' }); fs.writeFileSync(filepath, Buffer.from(data, 'base64')); console.log(` āœ“ Screenshot saved: ${filepath}`); } async function testVariant(browser, variant) { console.log(`\n=== Testing variant ${variant.id.toUpperCase()}: ${variant.name} (${variant.url}) ===`); const page = await browser.newPage(); // Block fonts to avoid hangs await page.route('**/*fonts*', r => r.abort()); await page.route('**/*analytics*', r => r.abort()); await page.route('**/*gtag*', r => r.abort()); // Navigate with commit waitUntil console.log(`Navigating to ${variant.url}...`); await page.goto(variant.url, { waitUntil: 'commit' }); // Wait for Vue to render console.log('Waiting 6 seconds for Vue to render...'); await page.waitForTimeout(6000); // Create screenshots directory const screenshotDir = path.join(__dirname, 'usertest-results', 'screenshots', variant.id); fs.mkdirSync(screenshotDir, { recursive: true }); // Take screenshots at different scroll positions const scrollPositions = [ { name: 'hero', y: 0 }, { name: 'scroll-800', y: 800 }, { name: 'scroll-1600', y: 1600 }, { name: 'scroll-2400', y: 2400 } ]; for (const pos of scrollPositions) { console.log(`Taking screenshot at ${pos.name} (y=${pos.y})...`); await page.evaluate((y) => window.scrollTo(0, y), pos.y); await page.waitForTimeout(500); // Let scroll settle const filepath = path.join(screenshotDir, `${pos.name}.png`); await captureScreenshotCDP(page, filepath); } // Scroll back to top await page.evaluate(() => window.scrollTo(0, 0)); await page.waitForTimeout(500); // Extract all visible text console.log('Extracting visible text...'); const pageText = await page.evaluate(() => document.body.innerText); // Save extracted text const textPath = path.join(__dirname, 'usertest-results', `text-${variant.id}.txt`); fs.writeFileSync(textPath, pageText); console.log(` āœ“ Text saved: ${textPath}`); console.log(` Text length: ${pageText.length} chars`); await page.close(); return pageText; } async function generateEvaluations(variant, pageText) { console.log(`\n=== Generating evaluations for variant ${variant.id.toUpperCase()} ===`); const resultsDir = path.join(__dirname, 'usertest-results'); fs.mkdirSync(resultsDir, { recursive: true }); for (const persona of personas) { console.log(`Evaluating for ${persona.name}...`); const evaluation = persona.evaluator(pageText); const result = { persona: persona.id, variant: variant.id, ...evaluation }; const filepath = path.join(resultsDir, `visual-feedback-${persona.id}-${variant.id}.json`); fs.writeFileSync(filepath, JSON.stringify(result, null, 2)); console.log(` āœ“ Saved: ${filepath}`); } } async function main() { console.log('Starting Playwright user testing...'); console.log(`Chromium path: ${process.env.HOME}/.cache/ms-playwright/chromium-1209/chrome-linux64/chrome`); const browser = await chromium.launch({ headless: true, executablePath: `${process.env.HOME}/.cache/ms-playwright/chromium-1209/chrome-linux64/chrome` }); console.log('Browser launched successfully'); try { // Test each variant for (const variant of variants) { const pageText = await testVariant(browser, variant); await generateEvaluations(variant, pageText); } console.log('\nāœ… All tests completed successfully!'); console.log(`Results saved to: ${path.join(__dirname, 'usertest-results')}`); } finally { await browser.close(); console.log('Browser closed'); } } main().catch(error => { console.error('Error:', error); process.exit(1); });