339 lines
14 KiB
JavaScript
339 lines
14 KiB
JavaScript
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);
|
|
});
|