feat: add gh-dashboard project, language switch partial, header alignment fix
- Add gh-dashboard (debba/gh-dashboard) as collaboration project in EN/ES CV data - Add head-language-switch.html template partial with test - Change CV header text-align-last from justify to left - Update .gitignore to exclude prompt symlinks and cv-site binary
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* HEAD-SUPPORT EXTENSION TEST
|
||||
* ============================
|
||||
* Tests that the head-support extension updates <head> tags
|
||||
* and <html lang> on language switching via HTMX
|
||||
* - Verifies <html lang> updates on language switch
|
||||
* - Verifies <title> updates on language switch
|
||||
* - Verifies <meta description> updates
|
||||
* - Verifies no duplicate tags after multiple switches
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const URL = "http://localhost:1999";
|
||||
|
||||
async function testHeadSupport() {
|
||||
console.log('🏷️ HEAD-SUPPORT EXTENSION TEST\n');
|
||||
console.log('='.repeat(70));
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
|
||||
|
||||
const errors = [];
|
||||
const testResults = [];
|
||||
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
console.log("\n1️⃣ Loading page (English default)...");
|
||||
await page.goto(URL);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// ========================================================================
|
||||
// TEST 1: Head-support extension is loaded
|
||||
// ========================================================================
|
||||
console.log("\n2️⃣ Testing head-support extension loaded...");
|
||||
const extLoaded = await page.evaluate(() => {
|
||||
const body = document.body;
|
||||
const hxExt = body.getAttribute('hx-ext');
|
||||
return {
|
||||
hasHxExt: hxExt !== null && hxExt.includes('head-support'),
|
||||
hxExtValue: hxExt
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` hx-ext attribute: ${extLoaded.hxExtValue}`);
|
||||
console.log(` ${extLoaded.hasHxExt ? '✅ PASS' : '❌ FAIL'} - head-support extension activated`);
|
||||
testResults.push({ test: 'Head-support Extension Loaded', passed: extLoaded.hasHxExt });
|
||||
|
||||
// ========================================================================
|
||||
// TEST 2: Initial state - English
|
||||
// ========================================================================
|
||||
console.log("\n3️⃣ Testing initial state (English)...");
|
||||
const initialState = await page.evaluate(() => {
|
||||
return {
|
||||
htmlLang: document.documentElement.getAttribute('lang'),
|
||||
title: document.title,
|
||||
metaDesc: document.querySelector('meta[name="description"]')?.getAttribute('content') || '',
|
||||
canonical: document.querySelector('link[rel="canonical"]')?.getAttribute('href') || ''
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` <html lang>: ${initialState.htmlLang}`);
|
||||
console.log(` <title>: ${initialState.title}`);
|
||||
console.log(` canonical: ${initialState.canonical}`);
|
||||
const initialOk = initialState.htmlLang === 'en';
|
||||
console.log(` ${initialOk ? '✅ PASS' : '❌ FAIL'} - Initial state is English`);
|
||||
testResults.push({ test: 'Initial English State', passed: initialOk });
|
||||
|
||||
// ========================================================================
|
||||
// TEST 3: Switch to Spanish via HTMX button
|
||||
// ========================================================================
|
||||
console.log("\n4️⃣ Switching to Spanish via HTMX...");
|
||||
|
||||
// Click the Spanish button
|
||||
const esButton = await page.$('button[aria-label="Español"]');
|
||||
if (esButton) {
|
||||
await esButton.click();
|
||||
await page.waitForTimeout(3000); // Wait for HTMX swap + head-support processing
|
||||
|
||||
const spanishState = await page.evaluate(() => {
|
||||
return {
|
||||
htmlLang: document.documentElement.getAttribute('lang'),
|
||||
title: document.title,
|
||||
metaDesc: document.querySelector('meta[name="description"]')?.getAttribute('content') || '',
|
||||
canonical: document.querySelector('link[rel="canonical"]')?.getAttribute('href') || '',
|
||||
ogLocale: document.querySelector('meta[property="og:locale"]')?.getAttribute('content') || ''
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` <html lang>: ${spanishState.htmlLang}`);
|
||||
console.log(` <title>: ${spanishState.title}`);
|
||||
console.log(` canonical: ${spanishState.canonical}`);
|
||||
console.log(` og:locale: ${spanishState.ogLocale}`);
|
||||
|
||||
const langChanged = spanishState.htmlLang === 'es';
|
||||
const canonicalChanged = spanishState.canonical.includes('lang=es');
|
||||
const descChanged = spanishState.metaDesc.length > 0;
|
||||
|
||||
console.log(` ${langChanged ? '✅ PASS' : '❌ FAIL'} - <html lang> changed to "es"`);
|
||||
console.log(` ${canonicalChanged ? '✅ PASS' : '❌ FAIL'} - canonical URL updated to Spanish`);
|
||||
console.log(` ${descChanged ? '✅ PASS' : '❌ FAIL'} - meta description present`);
|
||||
|
||||
testResults.push({ test: 'HTML Lang Updated to ES', passed: langChanged });
|
||||
testResults.push({ test: 'Canonical URL Updated to ES', passed: canonicalChanged });
|
||||
testResults.push({ test: 'Meta Description Present', passed: descChanged });
|
||||
} else {
|
||||
console.log(' ❌ FAIL - Spanish button not found');
|
||||
testResults.push({ test: 'HTML Lang Updated to ES', passed: false });
|
||||
testResults.push({ test: 'Canonical URL Updated to ES', passed: false });
|
||||
testResults.push({ test: 'Meta Description Present', passed: false });
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TEST 4: Switch back to English
|
||||
// ========================================================================
|
||||
console.log("\n5️⃣ Switching back to English...");
|
||||
|
||||
const enButton = await page.$('button[aria-label="English"]');
|
||||
if (enButton) {
|
||||
await enButton.click();
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const englishState = await page.evaluate(() => {
|
||||
return {
|
||||
htmlLang: document.documentElement.getAttribute('lang'),
|
||||
title: document.title,
|
||||
canonical: document.querySelector('link[rel="canonical"]')?.getAttribute('href') || '',
|
||||
ogLocale: document.querySelector('meta[property="og:locale"]')?.getAttribute('content') || ''
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` <html lang>: ${englishState.htmlLang}`);
|
||||
console.log(` <title>: ${englishState.title}`);
|
||||
console.log(` canonical: ${englishState.canonical}`);
|
||||
console.log(` og:locale: ${englishState.ogLocale}`);
|
||||
|
||||
const langBack = englishState.htmlLang === 'en';
|
||||
const canonicalBack = englishState.canonical.includes('lang=en');
|
||||
|
||||
console.log(` ${langBack ? '✅ PASS' : '❌ FAIL'} - <html lang> back to "en"`);
|
||||
console.log(` ${canonicalBack ? '✅ PASS' : '❌ FAIL'} - canonical URL back to English`);
|
||||
|
||||
testResults.push({ test: 'HTML Lang Restored to EN', passed: langBack });
|
||||
testResults.push({ test: 'Canonical URL Restored to EN', passed: canonicalBack });
|
||||
} else {
|
||||
console.log(' ❌ FAIL - English button not found');
|
||||
testResults.push({ test: 'HTML Lang Restored to EN', passed: false });
|
||||
testResults.push({ test: 'Canonical URL Restored to EN', passed: false });
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// TEST 5: No duplicate tags after multiple switches
|
||||
// ========================================================================
|
||||
console.log("\n6️⃣ Testing no duplicate tags after multiple switches...");
|
||||
|
||||
const duplicateCheck = await page.evaluate(() => {
|
||||
const titles = document.querySelectorAll('title').length;
|
||||
const descriptions = document.querySelectorAll('meta[name="description"]').length;
|
||||
const canonicals = document.querySelectorAll('link[rel="canonical"]').length;
|
||||
const ogLocales = document.querySelectorAll('meta[property="og:locale"]').length;
|
||||
|
||||
return {
|
||||
titles,
|
||||
descriptions,
|
||||
canonicals,
|
||||
ogLocales
|
||||
};
|
||||
});
|
||||
|
||||
console.log(` <title> tags: ${duplicateCheck.titles}`);
|
||||
console.log(` <meta description> tags: ${duplicateCheck.descriptions}`);
|
||||
console.log(` <link canonical> tags: ${duplicateCheck.canonicals}`);
|
||||
console.log(` og:locale tags: ${duplicateCheck.ogLocales}`);
|
||||
|
||||
const noDuplicates = duplicateCheck.titles === 1 &&
|
||||
duplicateCheck.descriptions === 1 &&
|
||||
duplicateCheck.canonicals === 1 &&
|
||||
duplicateCheck.ogLocales === 1;
|
||||
|
||||
console.log(` ${noDuplicates ? '✅ PASS' : '❌ FAIL'} - No duplicate tags`);
|
||||
testResults.push({ test: 'No Duplicate Tags', passed: noDuplicates });
|
||||
|
||||
// ========================================================================
|
||||
// FINAL SUMMARY
|
||||
// ========================================================================
|
||||
console.log("\n" + "=".repeat(70));
|
||||
console.log("📊 TEST SUMMARY\n");
|
||||
|
||||
const totalTests = testResults.length;
|
||||
const passedTests = testResults.filter(r => r.passed).length;
|
||||
const failedTests = totalTests - passedTests;
|
||||
|
||||
testResults.forEach(result => {
|
||||
console.log(` ${result.passed ? '✅' : '❌'} ${result.test}`);
|
||||
});
|
||||
|
||||
console.log(`\n Total: ${passedTests}/${totalTests} tests passed`);
|
||||
|
||||
if (errors.length === 0) {
|
||||
console.log("\n✅ NO CONSOLE ERRORS");
|
||||
} else {
|
||||
console.log(`\n⚠️ ${errors.length} CONSOLE ERRORS`);
|
||||
}
|
||||
|
||||
console.log("=".repeat(70) + "\n");
|
||||
|
||||
await browser.close();
|
||||
|
||||
if (failedTests === 0) {
|
||||
console.log("🎉 HEAD-SUPPORT EXTENSION VALIDATED!\n");
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log("⚠️ SOME TESTS FAILED - See details above\n");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
await testHeadSupport();
|
||||
Reference in New Issue
Block a user