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:
+255
@@ -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();
|
||||
Reference in New Issue
Block a user