95de841e14
- Add Lightning CSS integration for CSS bundling and minification - Create Makefile targets: css-dev, css-prod, css-watch, css-clean - Implement conditional CSS loading based on GO_ENV (dev=modular, prod=bundled) - Add IsProduction template variable for environment-aware rendering - Keep print.css separate with media="print" for PDF export - Add static/dist/ to .gitignore (generated bundles) - Fix Go template syntax in _cv-header.css - Remove redundant font @import in _typography.css Performance gains: - 27 HTTP requests → 1 (96% reduction) - 188KB → 86KB CSS (54% reduction) - ~15KB gzip network transfer Documentation: - Update 12-CSS-ARCHITECTURE.md with bundling section - Add Phase 9 to 2-MODERN-WEB-TECHNIQUES.md - Add css-bundling.test.mjs Playwright test (8/8 pass)
206 lines
6.7 KiB
JavaScript
206 lines
6.7 KiB
JavaScript
/**
|
|
* CSS Bundling Test
|
|
*
|
|
* Tests for Lightning CSS bundling in production vs development mode.
|
|
* Verifies correct CSS loading based on GO_ENV environment variable.
|
|
*/
|
|
|
|
import { chromium } from 'playwright';
|
|
|
|
const BASE_URL = process.env.BASE_URL || 'http://localhost:1999';
|
|
|
|
console.log('\n============================================================');
|
|
console.log('CSS BUNDLING TEST');
|
|
console.log('============================================================\n');
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext();
|
|
const page = await context.newPage();
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
const results = [];
|
|
|
|
function logResult(name, success, details = '') {
|
|
const status = success ? '✅ PASS' : '❌ FAIL';
|
|
console.log(`${status} - ${name}${details ? ': ' + details : ''}`);
|
|
results.push({ name, success, details });
|
|
if (success) passed++;
|
|
else failed++;
|
|
}
|
|
|
|
try {
|
|
// Test 1: Check CSS files are loading (development mode)
|
|
console.log('1️⃣ Testing CSS Loading...\n');
|
|
|
|
await page.goto(BASE_URL, { waitUntil: 'networkidle' });
|
|
|
|
// Get all CSS links
|
|
const cssLinks = await page.evaluate(() => {
|
|
return Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
.map(link => link.href);
|
|
});
|
|
|
|
console.log(' CSS Links found:');
|
|
cssLinks.forEach(link => console.log(` - ${link}`));
|
|
console.log('');
|
|
|
|
// Check if using bundled or modular CSS
|
|
const hasBundledCSS = cssLinks.some(href => href.includes('/dist/bundle'));
|
|
const hasModularCSS = cssLinks.some(href => href.includes('/css/main.css'));
|
|
const hasPrintCSS = cssLinks.some(href => href.includes('/css/print.css'));
|
|
|
|
logResult(
|
|
'CSS Loading Strategy',
|
|
hasBundledCSS || hasModularCSS,
|
|
hasBundledCSS ? 'Production bundle' : 'Development modular'
|
|
);
|
|
|
|
logResult(
|
|
'Print CSS Separate',
|
|
hasPrintCSS,
|
|
hasPrintCSS ? 'Loaded separately with media="print"' : 'Missing!'
|
|
);
|
|
|
|
// Test 2: Verify print.css has media="print" attribute
|
|
console.log('\n2️⃣ Testing Print CSS Media Attribute...\n');
|
|
|
|
const printLinkMedia = await page.evaluate(() => {
|
|
const printLink = document.querySelector('link[href*="print.css"]');
|
|
return printLink ? printLink.media : null;
|
|
});
|
|
|
|
logResult(
|
|
'Print CSS media="print"',
|
|
printLinkMedia === 'print',
|
|
printLinkMedia ? `media="${printLinkMedia}"` : 'No media attribute'
|
|
);
|
|
|
|
// Test 3: Verify CSS is rendering correctly (basic visual check)
|
|
console.log('\n3️⃣ Testing CSS Rendering...\n');
|
|
|
|
const bodyStyles = await page.evaluate(() => {
|
|
const body = document.body;
|
|
const computed = window.getComputedStyle(body);
|
|
return {
|
|
fontFamily: computed.fontFamily,
|
|
color: computed.color,
|
|
backgroundColor: window.getComputedStyle(document.documentElement).getPropertyValue('--page-bg')
|
|
};
|
|
});
|
|
|
|
const hasQuicksandFont = bodyStyles.fontFamily.toLowerCase().includes('quicksand');
|
|
logResult(
|
|
'Font Family Applied',
|
|
hasQuicksandFont || bodyStyles.fontFamily.includes('system-ui'),
|
|
bodyStyles.fontFamily.substring(0, 50) + '...'
|
|
);
|
|
|
|
const hasThemeColor = bodyStyles.backgroundColor && bodyStyles.backgroundColor !== '';
|
|
logResult(
|
|
'CSS Variables Working',
|
|
hasThemeColor,
|
|
`--page-bg: ${bodyStyles.backgroundColor || 'not set'}`
|
|
);
|
|
|
|
// Test 4: Verify no CSS 404 errors
|
|
console.log('\n4️⃣ Testing CSS Resources Load Successfully...\n');
|
|
|
|
const cssResponses = [];
|
|
page.on('response', response => {
|
|
if (response.url().includes('.css')) {
|
|
cssResponses.push({
|
|
url: response.url(),
|
|
status: response.status()
|
|
});
|
|
}
|
|
});
|
|
|
|
// Reload to capture all CSS requests
|
|
await page.reload({ waitUntil: 'networkidle' });
|
|
|
|
const all200 = cssResponses.every(r => r.status === 200);
|
|
const css404s = cssResponses.filter(r => r.status === 404);
|
|
|
|
logResult(
|
|
'All CSS Resources Load (200)',
|
|
all200,
|
|
css404s.length > 0 ? `404s: ${css404s.map(r => r.url).join(', ')}` : `${cssResponses.length} CSS files OK`
|
|
);
|
|
|
|
// Test 5: Check CSS size (if bundled)
|
|
console.log('\n5️⃣ Testing CSS Size...\n');
|
|
|
|
let totalCSSSize = 0;
|
|
for (const cssLink of cssLinks) {
|
|
try {
|
|
const response = await page.evaluate(async (url) => {
|
|
const res = await fetch(url);
|
|
const text = await res.text();
|
|
return text.length;
|
|
}, cssLink);
|
|
totalCSSSize += response;
|
|
} catch (e) {
|
|
// External CSS (like Google Fonts) won't be fetchable
|
|
}
|
|
}
|
|
|
|
const sizeKB = (totalCSSSize / 1024).toFixed(1);
|
|
const sizeOK = totalCSSSize > 0 && totalCSSSize < 500 * 1024; // Under 500KB is reasonable
|
|
|
|
logResult(
|
|
'CSS Size Reasonable',
|
|
sizeOK,
|
|
`${sizeKB} KB total (local CSS)`
|
|
);
|
|
|
|
// Test 6: Verify ITCSS layers (check for specific class names)
|
|
console.log('\n6️⃣ Testing CSS Architecture (ITCSS Layers)...\n');
|
|
|
|
const cssClasses = await page.evaluate(() => {
|
|
const classes = {
|
|
foundation: document.querySelector('[class*="cv-"]') !== null,
|
|
layout: document.querySelector('.cv-container') !== null,
|
|
components: document.querySelector('.cv-header') !== null ||
|
|
document.querySelector('.cv-paper') !== null,
|
|
interactive: document.querySelector('.modal') !== null ||
|
|
document.querySelector('.toggle-switch') !== null,
|
|
responsive: window.matchMedia('(max-width: 768px)').matches !== undefined
|
|
};
|
|
return classes;
|
|
});
|
|
|
|
const layersPresent = Object.values(cssClasses).filter(v => v).length;
|
|
logResult(
|
|
'ITCSS Layers Present',
|
|
layersPresent >= 3,
|
|
`${layersPresent}/5 layers detected`
|
|
);
|
|
|
|
} catch (error) {
|
|
console.error('Test error:', error.message);
|
|
logResult('Test Execution', false, error.message);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
// Summary
|
|
console.log('\n============================================================');
|
|
console.log('TEST SUMMARY');
|
|
console.log('============================================================\n');
|
|
|
|
results.forEach(r => {
|
|
const icon = r.success ? '✅' : '❌';
|
|
console.log(` ${icon} ${r.name}`);
|
|
});
|
|
|
|
console.log(`\n Total: ${passed}/${passed + failed} tests passed\n`);
|
|
|
|
if (failed > 0) {
|
|
console.log('❌ SOME TESTS FAILED\n');
|
|
process.exit(1);
|
|
} else {
|
|
console.log('✅ ALL TESTS PASSED\n');
|
|
process.exit(0);
|
|
}
|