- Add CSS minification support to build script (--minify flag)
- Pure JS minifier: removes comments, whitespace, redundant chars
- Reports size reduction percentage
- Preserves CSS custom properties, data URIs, strings
- Refactor _08_themes.css: extend CSS custom properties pattern
- Define theme-specific properties per theme (--rpg-content-bg,
--rpg-content-box-shadow, --rpg-header-text-shadow, etc.)
- Replace triple-theme selector duplication with single [data-theme]
selectors — children inherit variables automatically
- Reduces themes.css from 379 to ~220 lines
- Reduces style.css from 305KB to 294KB (3.8% reduction)
- Add build:css:min npm script for production builds
- Add 9 tests for CSS build: minification, size reduction, CSS
variable preservation, data URI preservation, theme refactoring
148 lines
6.3 KiB
JavaScript
148 lines
6.3 KiB
JavaScript
/**
|
|
* Tests for CSS build script (scripts/build-css.js)
|
|
* Tests minification logic and build output.
|
|
*/
|
|
|
|
const fs = require('fs-extra');
|
|
const path = require('path');
|
|
const { execSync } = require('child_process');
|
|
|
|
const rootDir = path.resolve(__dirname, '..');
|
|
const stylesDir = path.join(rootDir, 'src', 'styles');
|
|
const outputFile = path.join(rootDir, 'style.css');
|
|
|
|
describe('CSS Build Script', () => {
|
|
let originalContent;
|
|
|
|
beforeAll(async () => {
|
|
// Save original style.css for restoration
|
|
if (await fs.pathExists(outputFile)) {
|
|
originalContent = await fs.readFile(outputFile, 'utf-8');
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
// Restore original style.css
|
|
if (originalContent) {
|
|
await fs.writeFile(outputFile, originalContent, 'utf-8');
|
|
}
|
|
});
|
|
|
|
test('styles directory contains CSS files', async () => {
|
|
const files = await fs.readdir(stylesDir);
|
|
const cssFiles = files.filter(f => f.endsWith('.css'));
|
|
expect(cssFiles.length).toBeGreaterThan(0);
|
|
expect(cssFiles).toContain('_variables.css');
|
|
expect(cssFiles).toContain('_08_themes.css');
|
|
});
|
|
|
|
test('build:css produces readable output with comments', () => {
|
|
const result = execSync('node scripts/build-css.js', { cwd: rootDir, encoding: 'utf-8' });
|
|
|
|
expect(result).toContain('Building CSS from src/styles/');
|
|
|
|
const content = fs.readFileSync(outputFile, 'utf-8');
|
|
// Readable build should contain section comments
|
|
expect(content).toContain('/* ============================================ */');
|
|
expect(content).toContain('/* _variables.css */');
|
|
});
|
|
|
|
test('build:css --minify produces minified output', () => {
|
|
const result = execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
|
|
|
|
expect(result).toContain('minified');
|
|
expect(result).toContain('Minification:');
|
|
|
|
const content = fs.readFileSync(outputFile, 'utf-8');
|
|
// Minified build should NOT contain section comments
|
|
expect(content).not.toContain('/* ============================================ */');
|
|
// Should not have multiple consecutive spaces (except in strings)
|
|
expect(content).not.toMatch(/[^\S\r\n]{2,}/);
|
|
// Should not have newlines between rules
|
|
const lines = content.split('\n');
|
|
expect(lines.length).toBeLessThan(1000); // Original is ~12000 lines
|
|
});
|
|
|
|
test('minified output is smaller than readable output', () => {
|
|
// Build readable
|
|
execSync('node scripts/build-css.js', { cwd: rootDir, encoding: 'utf-8' });
|
|
const readableStats = fs.statSync(outputFile);
|
|
const readableSize = readableStats.size;
|
|
|
|
// Build minified
|
|
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
|
|
const minifiedStats = fs.statSync(outputFile);
|
|
const minifiedSize = minifiedStats.size;
|
|
|
|
expect(minifiedSize).toBeLessThan(readableSize);
|
|
// Expect at least 30% reduction (comments + whitespace)
|
|
const reduction = ((readableSize - minifiedSize) / readableSize) * 100;
|
|
expect(reduction).toBeGreaterThan(30);
|
|
|
|
// Restore readable version for other tests
|
|
execSync('node scripts/build-css.js', { cwd: rootDir, encoding: 'utf-8' });
|
|
});
|
|
|
|
test('minified output preserves CSS custom properties', () => {
|
|
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
|
|
|
|
const content = fs.readFileSync(outputFile, 'utf-8');
|
|
// CSS variables should be preserved
|
|
expect(content).toContain('--rpg-bg');
|
|
expect(content).toContain('--rpg-accent');
|
|
expect(content).toContain('--rpg-text');
|
|
expect(content).toContain('--rpg-highlight');
|
|
expect(content).toContain('--rpg-border');
|
|
expect(content).toContain('--rpg-shadow');
|
|
});
|
|
|
|
test('minified output preserves data URIs', () => {
|
|
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
|
|
|
|
const content = fs.readFileSync(outputFile, 'utf-8');
|
|
// Fantasy theme has SVG data URI — should be preserved
|
|
expect(content).toContain('data:image/svg+xml');
|
|
});
|
|
|
|
test('minified output preserves data-theme selectors', () => {
|
|
execSync('node scripts/build-css.js --minify', { cwd: rootDir, encoding: 'utf-8' });
|
|
|
|
const content = fs.readFileSync(outputFile, 'utf-8');
|
|
expect(content).toContain('[data-theme="sci-fi"]');
|
|
expect(content).toContain('[data-theme="fantasy"]');
|
|
expect(content).toContain('[data-theme="cyberpunk"]');
|
|
});
|
|
|
|
test('themes.css uses grouped [data-theme] selectors (no duplication)', async () => {
|
|
const themesContent = await fs.readFile(path.join(stylesDir, '_08_themes.css'), 'utf-8');
|
|
|
|
// Should use [data-theme] grouped selectors instead of repeating all 3 themes
|
|
expect(themesContent).toContain('.rpg-panel[data-theme] .rpg-tabs-nav');
|
|
expect(themesContent).toContain('.rpg-panel[data-theme] .rpg-tab-btn');
|
|
|
|
// Should NOT have the old pattern of listing all 3 themes for every element
|
|
const oldPattern = /\.rpg-panel\[data-theme="sci-fi"\] \.rpg-tabs-nav,\s*\.rpg-panel\[data-theme="fantasy"\] \.rpg-tabs-nav,\s*\.rpg-panel\[data-theme="cyberpunk"\] \.rpg-tabs-nav/;
|
|
expect(themesContent).not.toMatch(oldPattern);
|
|
});
|
|
|
|
test('themes.css defines extended custom properties per theme', async () => {
|
|
const themesContent = await fs.readFile(path.join(stylesDir, '_08_themes.css'), 'utf-8');
|
|
|
|
// Extended properties for content box
|
|
expect(themesContent).toContain('--rpg-content-bg');
|
|
expect(themesContent).toContain('--rpg-content-box-shadow');
|
|
expect(themesContent).toContain('--rpg-content-border');
|
|
|
|
// Extended properties for headers
|
|
expect(themesContent).toContain('--rpg-header-text-shadow');
|
|
expect(themesContent).toContain('--rpg-header-font-family');
|
|
|
|
// Extended properties for dividers
|
|
expect(themesContent).toContain('--rpg-divider-bg');
|
|
expect(themesContent).toContain('--rpg-divider-content');
|
|
|
|
// Extended properties for thoughts
|
|
expect(themesContent).toContain('--rpg-thoughts-bg');
|
|
});
|
|
});
|