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