harb/tmp/usertest-visual.mjs

340 lines
14 KiB
JavaScript
Raw Normal View History

2026-02-18 00:19:05 +01:00
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);
});