Files
ARIA df7d3b3a38 Fixes #13: CSS performance optimizations
- 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
2026-07-12 14:55:36 +02:00

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');
});
});