refactor: Rename 'extended' → 'long' + add compact sidebar fonts
BREAKING CHANGE: API parameter renamed from 'extended' to 'long' ## Breaking Change: Terminology Standardization Renamed 'extended' to 'long' across entire codebase for consistency: **Backend (Go):** - internal/handlers/cv.go (7 locations) - Migration logic to auto-convert 'extended' → 'long' cookies - API validation now rejects 'extended', requires 'long' - Toggle state logic updated - internal/handlers/pdf_test.go (17 occurrences) - Test function renamed: TestExportPDF_ExtendedWithSkills → TestExportPDF_LongWithSkills - All test cases, parameters, and expected filenames updated - internal/pdf/generator.go (2 comment updates) **Frontend:** - PDF-EXPORT-FEATURE.md (3 occurrences) - doc/3-API.md (parameter documentation) - doc/7-CUSTOMIZATION.md (examples updated) - templates/partials/modals/pdf-modal.html (button text, URLs) - static/js/main.js (migration logic) - static/hyperscript/toggles._hs (toggle logic) - tests/mjs/24-pdf-download-params.test.mjs (test expectations) - tests/mjs/test-preference-migration.test.mjs (NEW) - tests/mjs/verify-migration.test.mjs (NEW) **PDFs Renamed:** - cv-extended-with_skills-jamr-2025-en.pdf → cv-long-with_skills-jamr-2025-en.pdf - cv-extended-with_skills-jamr-2025-es.pdf → cv-long-with_skills-jamr-2025-es.pdf **Migration:** Automatic cookie migration from 'extended' → 'long' for seamless UX ## New Feature: Compact Sidebar Fonts Reduces page count for short CV with skills from 6 → 5 pages: **Implementation:** - Location: internal/pdf/generator.go (lines 154-215) - Cookie detection: `cookies["cv-length"] == "short"` - Font reduction: 2-6% (0.94-0.98em) - very subtle - Only activates for: `length=short` + `version=with_skills` - Long version: Always uses full-size fonts **Impact:** - Page count: 6 pages → 5 pages (16.7% reduction) - Readability: Maintained - fonts remain professional - Design philosophy: Subtle, natural content flow **Testing:** - New test: TestPDFGenerator_CompactSidebarFonts - Comprehensive coverage of cookie detection and PDF generation - Manual verification: 5-page PDF with compact but readable fonts **Documentation:** - doc/LONG-PDF-GENERATION.md (NEW, 13 KB) - Complete feature documentation - Implementation details with code examples - Font size breakdown table - Testing and troubleshooting guides - Compact sidebar fonts section (comprehensive) **Files Changed:** - 11 modified (backend + frontend + docs) - 5 new files (2 PDFs, 1 doc, 2 tests) - 2 files renamed (PDFs) **Tests:** All Go tests passing, API validation verified, PDF generation tested
This commit is contained in:
@@ -181,7 +181,7 @@ async function testPDFDownload() {
|
||||
console.log(` ${longParamsValid ? '✅ PASS' : '❌ FAIL'} - Long CV parameters correct`);
|
||||
testResults.push({ test: 'Long CV Parameters', passed: longParamsValid });
|
||||
|
||||
console.log(` Expected filename: cv-extended-with_skills-jamr-${new Date().getFullYear()}-${currentLang}.pdf`);
|
||||
console.log(` Expected filename: cv-long-with_skills-jamr-${new Date().getFullYear()}-${currentLang}.pdf`);
|
||||
} else {
|
||||
console.log(` ❌ FAIL - Download button not clickable`);
|
||||
testResults.push({ test: 'Long CV Parameters', passed: false });
|
||||
@@ -259,10 +259,10 @@ async function testPDFDownload() {
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
const expectedPatterns = [
|
||||
{ format: 'detailed (clean)', pattern: new RegExp(`cv-detailed-[a-z]+-${currentYear}-(en|es)\\.pdf`) },
|
||||
{ format: 'detailed-with_skills', pattern: new RegExp(`cv-detailed-with_skills-[a-z]+-${currentYear}-(en|es)\\.pdf`) },
|
||||
{ format: 'extended (clean)', pattern: new RegExp(`cv-extended-[a-z]+-${currentYear}-(en|es)\\.pdf`) },
|
||||
{ format: 'extended-with_skills', pattern: new RegExp(`cv-extended-with_skills-[a-z]+-${currentYear}-(en|es)\\.pdf`) }
|
||||
{ format: 'long (clean)', pattern: new RegExp(`cv-long-[a-z]+-${currentYear}-(en|es)\\.pdf`) },
|
||||
{ format: 'long-with_skills', pattern: new RegExp(`cv-long-with_skills-[a-z]+-${currentYear}-(en|es)\\.pdf`) },
|
||||
{ format: 'short (clean)', pattern: new RegExp(`cv-short-[a-z]+-${currentYear}-(en|es)\\.pdf`) },
|
||||
{ format: 'short-with_skills', pattern: new RegExp(`cv-short-with_skills-[a-z]+-${currentYear}-(en|es)\\.pdf`) }
|
||||
];
|
||||
|
||||
console.log(` Expected filename format: cv-{length}[-{version}]-{initials}-{year}-{lang}.pdf`);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
/**
|
||||
* Test preference migration from old to new values
|
||||
*
|
||||
* Tests:
|
||||
* 1. Old 'long' → migrates to 'extended'
|
||||
* 2. Old 'true'/'false' → migrates to 'show'/'hide'
|
||||
* 3. Toggles work correctly with new values
|
||||
*/
|
||||
|
||||
async function testPreferenceMigration() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
console.log('\n=== Testing Preference Migration ===\n');
|
||||
|
||||
// Test 1: Set old values and verify migration
|
||||
console.log('Test 1: Setting old localStorage values...');
|
||||
await page.goto('http://localhost:1999/?lang=en');
|
||||
|
||||
// Set OLD values
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('cv-length', 'extended');
|
||||
localStorage.setItem('cv-icons', 'true');
|
||||
});
|
||||
|
||||
console.log(' ✓ Old values set: length=extended, icons=true');
|
||||
|
||||
// Reload page to trigger migration
|
||||
console.log('\nTest 2: Reloading page to trigger migration...');
|
||||
await page.reload();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check that values were migrated
|
||||
const migratedLength = await page.evaluate(() => localStorage.getItem('cv-length'));
|
||||
const migratedIcons = await page.evaluate(() => localStorage.getItem('cv-icons'));
|
||||
|
||||
console.log(` Migration result: length="${migratedLength}", icons="${migratedIcons}"`);
|
||||
|
||||
if (migratedLength === 'long' && migratedIcons === 'show') {
|
||||
console.log(' ✅ Migration successful!');
|
||||
} else {
|
||||
console.error(` ❌ Migration failed! Expected: length="long", icons="show"`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Test 3: Verify UI state matches migrated values
|
||||
console.log('\nTest 3: Verifying UI state...');
|
||||
const hasLongClass = await page.evaluate(() => {
|
||||
return document.querySelector('.cv-paper')?.classList.contains('cv-long');
|
||||
});
|
||||
const hasIconsClass = await page.evaluate(() => {
|
||||
return document.querySelector('.cv-paper')?.classList.contains('show-icons');
|
||||
});
|
||||
|
||||
console.log(` UI state: cv-long=${hasLongClass}, show-icons=${hasIconsClass}`);
|
||||
|
||||
if (hasLongClass && hasIconsClass) {
|
||||
console.log(' ✅ UI state correct!');
|
||||
} else {
|
||||
console.error(' ❌ UI state incorrect!');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Test 4: Toggle and verify new values are used
|
||||
console.log('\nTest 4: Testing toggles with new values...');
|
||||
|
||||
// Toggle length
|
||||
const lengthToggle = await page.$('#lengthToggle');
|
||||
if (lengthToggle) {
|
||||
await lengthToggle.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const newLength = await page.evaluate(() => localStorage.getItem('cv-length'));
|
||||
console.log(` Length toggle clicked, new value: "${newLength}"`);
|
||||
|
||||
if (newLength === 'short') {
|
||||
console.log(' ✅ Length toggle works correctly!');
|
||||
} else {
|
||||
console.error(` ❌ Length toggle failed! Expected "short", got "${newLength}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle icons
|
||||
const iconToggle = await page.$('#iconToggle');
|
||||
if (iconToggle) {
|
||||
await iconToggle.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const newIcons = await page.evaluate(() => localStorage.getItem('cv-icons'));
|
||||
console.log(` Icon toggle clicked, new value: "${newIcons}"`);
|
||||
|
||||
if (newIcons === 'hide') {
|
||||
console.log(' ✅ Icon toggle works correctly!');
|
||||
} else {
|
||||
console.error(` ❌ Icon toggle failed! Expected "hide", got "${newIcons}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 5: Test with 'false' old value
|
||||
console.log('\nTest 5: Testing migration of "false" value...');
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('cv-icons', 'false');
|
||||
});
|
||||
await page.reload();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const migratedFalse = await page.evaluate(() => localStorage.getItem('cv-icons'));
|
||||
console.log(` Migration of "false": "${migratedFalse}"`);
|
||||
|
||||
if (migratedFalse === 'hide') {
|
||||
console.log(' ✅ "false" migrated to "hide" correctly!');
|
||||
} else {
|
||||
console.error(` ❌ "false" migration failed! Expected "hide", got "${migratedFalse}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\n=== ✅ All tests passed! ===\n');
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ Test failed with error:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
testPreferenceMigration();
|
||||
@@ -0,0 +1,44 @@
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
async function verifyMigration() {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
console.log('\n=== Quick Migration Verification ===\n');
|
||||
|
||||
// Set old values
|
||||
await page.goto('http://localhost:1999/?lang=en');
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('cv-length', 'extended');
|
||||
localStorage.setItem('cv-icons', 'true');
|
||||
});
|
||||
console.log('✓ Set old values: length="extended", icons="true"');
|
||||
|
||||
// Reload to trigger migration
|
||||
await page.reload();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Check migration
|
||||
const length = await page.evaluate(() => localStorage.getItem('cv-length'));
|
||||
const icons = await page.evaluate(() => localStorage.getItem('cv-icons'));
|
||||
|
||||
console.log(`✓ After migration: length="${length}", icons="${icons}"`);
|
||||
|
||||
if (length === 'long' && icons === 'show') {
|
||||
console.log('\n✅ Migration SUCCESS!\n');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error('\n❌ Migration FAILED!\n');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('\n❌ Error:', error.message, '\n');
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
verifyMigration();
|
||||
Reference in New Issue
Block a user