9636b3659f
- Move all bilingual text from templates to UI JSON (labels, buttons, modals) - Move skills summary paragraph to CV JSON with HTML support - Add new UI sections: navigation, viewControls, sections, footer, portfolio, pdfModal, shortcutsModal, infoModal, widgets - Update Go structs to match expanded JSON structure - Add template.HTML type for CV.SkillsSummary field - Add JSON content validation test (70-json-content-validation.test.mjs) Templates now contain only structural logic (CSS classes, HTML attributes) while all user-visible text loads from JSON files for proper i18n support.
256 lines
12 KiB
JavaScript
Executable File
256 lines
12 KiB
JavaScript
Executable File
#!/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();
|