refactor: Extract all hardcoded content to JSON files

- 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.
This commit is contained in:
juanatsap
2025-11-30 10:13:37 +00:00
parent c834919a3c
commit 9636b3659f
36 changed files with 806 additions and 168 deletions
+255
View File
@@ -0,0 +1,255 @@
#!/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();