#!/usr/bin/env bun /** * JSON CONTENT VALIDATION TEST * ============================ * Tests that CV content is loaded from JSON files, not hardcoded in templates. * Validates: * - Title badges rendered from CV JSON * - SEO meta tags from CV JSON * - Widget labels from UI JSON * - Both EN and ES languages */ import { chromium } from 'playwright'; import { readFileSync } from 'fs'; import { join } from 'path'; const URL = "http://localhost:1999"; // Load JSON files for comparison const dataDir = join(process.cwd(), 'data'); const cvEN = JSON.parse(readFileSync(join(dataDir, 'cv-en.json'), 'utf-8')); const cvES = JSON.parse(readFileSync(join(dataDir, 'cv-es.json'), 'utf-8')); const uiEN = JSON.parse(readFileSync(join(dataDir, 'ui-en.json'), 'utf-8')); const uiES = JSON.parse(readFileSync(join(dataDir, 'ui-es.json'), 'utf-8')); async function testJSONContentValidation() { console.log('📋 JSON CONTENT VALIDATION TEST\n'); console.log('='.repeat(70)); const browser = await chromium.launch({ headless: true }); const testResults = []; // ======================================================================== // TEST 1: English - Title Badges from JSON // ======================================================================== console.log("\n1️⃣ Testing English Title Badges..."); const pageEN = await browser.newPage(); await pageEN.goto(`${URL}/?lang=en`); await pageEN.waitForTimeout(1000); const titleBadgesEN = await pageEN.evaluate(() => { const badges = document.querySelectorAll('.title-badge'); return Array.from(badges).map(b => b.textContent.trim()); }); // Compare with JSON (CSS makes them uppercase, so compare case-insensitively) const expectedBadgesEN = cvEN.personal.titleBadges.map(b => b.toUpperCase()); const actualBadgesEN = titleBadgesEN.map(b => b.toUpperCase()); const badgesMatchEN = expectedBadgesEN.every((badge, i) => actualBadgesEN[i] === badge); console.log(` Expected: ${expectedBadgesEN.join(' | ')}`); console.log(` Actual: ${actualBadgesEN.join(' | ')}`); console.log(` ${badgesMatchEN ? '✅ PASS' : '❌ FAIL'} - Title badges match JSON`); testResults.push({ test: 'EN Title Badges', passed: badgesMatchEN }); // ======================================================================== // TEST 2: English - SEO Meta Tags from JSON // ======================================================================== console.log("\n2️⃣ Testing English SEO Meta Tags..."); const metaEN = await pageEN.evaluate(() => { return { title: document.title, description: document.querySelector('meta[name="description"]')?.content, keywords: document.querySelector('meta[name="keywords"]')?.content, ogTitle: document.querySelector('meta[property="og:title"]')?.content, ogDescription: document.querySelector('meta[property="og:description"]')?.content, firstName: document.querySelector('meta[property="profile:first_name"]')?.content, lastName: document.querySelector('meta[property="profile:last_name"]')?.content, username: document.querySelector('meta[property="profile:username"]')?.content, }; }); const seoTestsEN = [ { name: 'Page title contains SEO pageTitle', passed: metaEN.title.includes(cvEN.seo.pageTitle) }, { name: 'Description contains SEO metaDescription', passed: metaEN.description.includes(cvEN.seo.metaDescription) }, { name: 'Keywords contain SEO keywords', passed: metaEN.keywords.includes(cvEN.seo.keywords.split(',')[0].trim()) }, { name: 'OG description contains SEO ogDescription', passed: metaEN.ogDescription.includes(cvEN.seo.ogDescription) }, { name: 'First name from JSON', passed: metaEN.firstName === cvEN.personal.firstName }, { name: 'Last name from JSON', passed: metaEN.lastName === cvEN.personal.lastName }, { name: 'Username from JSON', passed: metaEN.username === cvEN.personal.username }, ]; seoTestsEN.forEach(t => { console.log(` ${t.passed ? '✅' : '❌'} ${t.name}`); testResults.push({ test: `EN SEO: ${t.name}`, passed: t.passed }); }); // ======================================================================== // TEST 3: English - Widget Labels from UI JSON // ======================================================================== console.log("\n3️⃣ Testing English Widget Labels..."); const widgetsEN = await pageEN.evaluate(() => { return { backToTop: document.querySelector('#back-to-top')?.getAttribute('aria-label'), infoButton: document.querySelector('#info-button')?.getAttribute('aria-label'), downloadButton: document.querySelector('#download-button')?.getAttribute('aria-label'), printButton: document.querySelector('#print-friendly-button')?.getAttribute('aria-label'), shortcutsButton: document.querySelector('#shortcuts-button')?.getAttribute('aria-label'), zoomToggle: document.querySelector('#zoom-toggle-button')?.getAttribute('aria-label'), zoomControl: document.querySelector('#zoom-control')?.getAttribute('aria-label'), }; }); const widgetTestsEN = [ { name: 'Back to top label', passed: widgetsEN.backToTop === uiEN.widgets.backToTop.ariaLabel }, { name: 'Info button label', passed: widgetsEN.infoButton === uiEN.widgets.info.ariaLabel }, { name: 'Download button label', passed: widgetsEN.downloadButton === uiEN.widgets.download.ariaLabel }, { name: 'Print button label', passed: widgetsEN.printButton === uiEN.widgets.print.ariaLabel }, { name: 'Shortcuts button label', passed: widgetsEN.shortcutsButton === uiEN.widgets.shortcuts.ariaLabel }, { name: 'Zoom toggle label', passed: widgetsEN.zoomToggle === uiEN.widgets.zoomToggle.ariaLabel }, { name: 'Zoom control label', passed: widgetsEN.zoomControl === uiEN.widgets.zoomControl.groupLabel }, ]; widgetTestsEN.forEach(t => { console.log(` ${t.passed ? '✅' : '❌'} ${t.name}: "${t.passed ? 'matches' : 'MISMATCH'}"`); testResults.push({ test: `EN Widget: ${t.name}`, passed: t.passed }); }); await pageEN.close(); // ======================================================================== // TEST 4: Spanish - Title Badges from JSON // ======================================================================== console.log("\n4️⃣ Testing Spanish Title Badges..."); const pageES = await browser.newPage(); await pageES.goto(`${URL}/?lang=es`); await pageES.waitForTimeout(1000); const titleBadgesES = await pageES.evaluate(() => { const badges = document.querySelectorAll('.title-badge'); return Array.from(badges).map(b => b.textContent.trim()); }); const expectedBadgesES = cvES.personal.titleBadges.map(b => b.toUpperCase()); const actualBadgesES = titleBadgesES.map(b => b.toUpperCase()); const badgesMatchES = expectedBadgesES.every((badge, i) => actualBadgesES[i] === badge); console.log(` Expected: ${expectedBadgesES.join(' | ')}`); console.log(` Actual: ${actualBadgesES.join(' | ')}`); console.log(` ${badgesMatchES ? '✅ PASS' : '❌ FAIL'} - Title badges match JSON`); testResults.push({ test: 'ES Title Badges', passed: badgesMatchES }); // ======================================================================== // TEST 5: Spanish - SEO Meta Tags from JSON // ======================================================================== console.log("\n5️⃣ Testing Spanish SEO Meta Tags..."); const metaES = await pageES.evaluate(() => { return { title: document.title, description: document.querySelector('meta[name="description"]')?.content, ogDescription: document.querySelector('meta[property="og:description"]')?.content, }; }); const seoTestsES = [ { name: 'Page title contains SEO pageTitle', passed: metaES.title.includes(cvES.seo.pageTitle) }, { name: 'Description contains SEO metaDescription', passed: metaES.description.includes(cvES.seo.metaDescription) }, { name: 'OG description contains SEO ogDescription', passed: metaES.ogDescription.includes(cvES.seo.ogDescription) }, ]; seoTestsES.forEach(t => { console.log(` ${t.passed ? '✅' : '❌'} ${t.name}`); testResults.push({ test: `ES SEO: ${t.name}`, passed: t.passed }); }); // ======================================================================== // TEST 6: Spanish - Widget Labels from UI JSON // ======================================================================== console.log("\n6️⃣ Testing Spanish Widget Labels..."); const widgetsES = await pageES.evaluate(() => { return { backToTop: document.querySelector('#back-to-top')?.getAttribute('aria-label'), infoButton: document.querySelector('#info-button')?.getAttribute('aria-label'), downloadButton: document.querySelector('#download-button')?.getAttribute('aria-label'), printButton: document.querySelector('#print-friendly-button')?.getAttribute('aria-label'), }; }); const widgetTestsES = [ { name: 'Back to top label', passed: widgetsES.backToTop === uiES.widgets.backToTop.ariaLabel }, { name: 'Info button label', passed: widgetsES.infoButton === uiES.widgets.info.ariaLabel }, { name: 'Download button label', passed: widgetsES.downloadButton === uiES.widgets.download.ariaLabel }, { name: 'Print button label', passed: widgetsES.printButton === uiES.widgets.print.ariaLabel }, ]; widgetTestsES.forEach(t => { console.log(` ${t.passed ? '✅' : '❌'} ${t.name}: "${t.passed ? 'matches' : 'MISMATCH'}"`); testResults.push({ test: `ES Widget: ${t.name}`, passed: t.passed }); }); // ======================================================================== // TEST 7: Verify NO hardcoded language conditionals in rendered output // ======================================================================== console.log("\n7️⃣ Testing for hardcoded content elimination..."); // Check that title badges don't contain "if eq .Lang" template artifacts const noTemplateArtifacts = await pageES.evaluate(() => { const html = document.body.innerHTML; return !html.includes('{{if eq .Lang') && !html.includes('{{else}}'); }); console.log(` ${noTemplateArtifacts ? '✅' : '❌'} No template artifacts in rendered HTML`); testResults.push({ test: 'No template artifacts', passed: noTemplateArtifacts }); await pageES.close(); await browser.close(); // ======================================================================== // FINAL SUMMARY // ======================================================================== console.log("\n" + "=".repeat(70)); console.log("📊 TEST SUMMARY\n"); const passedCount = testResults.filter(r => r.passed).length; const totalCount = testResults.length; // Group by category const categories = { 'EN Title Badges': testResults.filter(r => r.test.includes('EN Title')), 'EN SEO': testResults.filter(r => r.test.includes('EN SEO')), 'EN Widgets': testResults.filter(r => r.test.includes('EN Widget')), 'ES Title Badges': testResults.filter(r => r.test.includes('ES Title')), 'ES SEO': testResults.filter(r => r.test.includes('ES SEO')), 'ES Widgets': testResults.filter(r => r.test.includes('ES Widget')), 'Other': testResults.filter(r => !r.test.includes('EN ') && !r.test.includes('ES ')), }; for (const [category, tests] of Object.entries(categories)) { if (tests.length === 0) continue; const catPassed = tests.filter(t => t.passed).length; const icon = catPassed === tests.length ? '✅' : '❌'; console.log(` ${icon} ${category}: ${catPassed}/${tests.length}`); } console.log(`\n Total: ${passedCount}/${totalCount} tests passed`); console.log("=".repeat(70) + "\n"); if (passedCount === totalCount) { console.log("🎉 JSON CONTENT VALIDATION PASSED!"); console.log(" All content is correctly loaded from JSON files."); process.exit(0); } else { console.log("⚠️ SOME TESTS FAILED"); console.log(" Check that templates use JSON data instead of hardcoded values."); process.exit(1); } } await testJSONContentValidation();