UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

461 lines (382 loc) 16.2 kB
/** * @fileoverview Tests for CSS Optimizer */ import { describe, expect, it } from 'vitest'; import type { CSSDeclarationNode, CSSRuleNode, ComponentAST, StyleBlockNode } from '../types/index.js'; import { OrdoJSCSSOptimizer } from './css-optimizer.js'; // Helper function to create a CSS declaration function createDeclaration(property: string, value: string): CSSDeclarationNode { return { type: 'CSSDeclaration', property, value, important: false, range: { start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 0, offset: 0 } } }; } // Helper function to create a CSS rule function createRule(selector: string, declarations: CSSDeclarationNode[]): CSSRuleNode { return { type: 'CSSRule', selector, declarations, range: { start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 0, offset: 0 } } }; } // Helper function to create a style block function createStyleBlock(rules: CSSRuleNode[], scoped: boolean = true): StyleBlockNode { return { type: 'StyleBlock', rules, scoped, range: { start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 0, offset: 0 } } }; } // Helper function to create a mock component AST function createMockComponentAST(usedElements: string[] = [], usedClasses: string[] = []): ComponentAST { const mockElements = usedElements.map(tagName => ({ type: 'HTMLElement', tagName, attributes: usedClasses.length > 0 ? [ { name: 'class', value: usedClasses.join(' ') } ] : [], children: [] })); // Also create elements for each class (in case no elements are specified) if (usedElements.length === 0 && usedClasses.length > 0) { mockElements.push({ type: 'HTMLElement', tagName: 'div', attributes: [ { name: 'class', value: usedClasses.join(' ') } ], children: [] }); } return { component: { type: 'Component', name: 'TestComponent', props: [], markupBlock: { type: 'MarkupBlock', elements: mockElements, textNodes: [], interpolations: [], range: { start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 0, offset: 0 } } }, range: { start: { line: 1, column: 0, offset: 0 }, end: { line: 1, column: 0, offset: 0 } } }, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } } as ComponentAST; } describe('OrdoJSCSSOptimizer', () => { let optimizer: OrdoJSCSSOptimizer; beforeEach(() => { optimizer = new OrdoJSCSSOptimizer(); }); describe('Basic Optimization', () => { it('should optimize CSS with default options', () => { const styleBlock = createStyleBlock([ createRule('.button', [ createDeclaration('color', 'red'), createDeclaration('font-size', '16px') ]) ]); const result = optimizer.optimize(styleBlock); expect(result.optimizedCSS).toBeTruthy(); expect(result.originalSize).toBeGreaterThan(0); expect(result.optimizedSize).toBeGreaterThan(0); expect(result.compressionRatio).toBeGreaterThanOrEqual(0); }); it('should minify CSS when minification is enabled', () => { const optimizer = new OrdoJSCSSOptimizer({ minify: true }); const styleBlock = createStyleBlock([ createRule('.button', [ createDeclaration('color', 'red'), createDeclaration('font-size', '16px') ]) ]); const result = optimizer.optimize(styleBlock); // Minified CSS should not contain newlines or extra spaces expect(result.optimizedCSS).not.toContain('\n'); expect(result.optimizedCSS).not.toContain(' '); expect(result.optimizedCSS).toContain('.button{color:red;font-size:16px}'); }); it('should preserve CSS when minification is disabled', () => { const optimizer = new OrdoJSCSSOptimizer({ minify: false }); const styleBlock = createStyleBlock([ createRule('.button', [ createDeclaration('color', 'red') ]) ]); const result = optimizer.optimize(styleBlock); // Non-minified CSS should contain newlines and proper formatting expect(result.optimizedCSS).toContain('\n'); expect(result.optimizedCSS).toContain('.button {\n color: red;\n}'); }); }); describe('Dead Code Elimination', () => { it('should remove unused CSS rules when component AST is provided', () => { const styleBlock = createStyleBlock([ createRule('.used-class', [createDeclaration('color', 'red')]), createRule('.unused-class', [createDeclaration('color', 'blue')]), createRule('div', [createDeclaration('margin', '10px')]) ]); const componentAST = createMockComponentAST(['div'], ['used-class']); const result = optimizer.optimize(styleBlock, componentAST); expect(result.removedRules).toBe(1); expect(result.optimizedCSS).toContain('used-class'); expect(result.optimizedCSS).toContain('div'); expect(result.optimizedCSS).not.toContain('unused-class'); }); it('should preserve global selectors', () => { const styleBlock = createStyleBlock([ createRule(':root', [createDeclaration('--primary-color', 'blue')]), createRule('html', [createDeclaration('font-size', '16px')]), createRule('body', [createDeclaration('margin', '0')]), createRule('.unused-class', [createDeclaration('color', 'red')]) ]); const componentAST = createMockComponentAST([], []); const result = optimizer.optimize(styleBlock, componentAST); expect(result.optimizedCSS).toContain(':root'); expect(result.optimizedCSS).toContain('html'); expect(result.optimizedCSS).toContain('body'); expect(result.optimizedCSS).not.toContain('unused-class'); }); it('should handle complex selectors correctly', () => { const styleBlock = createStyleBlock([ createRule('.parent .child', [createDeclaration('color', 'red')]), createRule('.parent > .child', [createDeclaration('color', 'blue')]), createRule('.unused .child', [createDeclaration('color', 'green')]) ]); const componentAST = createMockComponentAST([], ['parent', 'child']); const result = optimizer.optimize(styleBlock, componentAST); expect(result.optimizedCSS).toContain('.parent .child'); expect(result.optimizedCSS).toContain('.parent > .child'); expect(result.optimizedCSS).not.toContain('.unused .child'); }); }); describe('Rule Merging', () => { it('should merge duplicate selectors', () => { const styleBlock = createStyleBlock([ createRule('.button', [createDeclaration('color', 'red')]), createRule('.button', [createDeclaration('font-size', '16px')]), createRule('.button', [createDeclaration('color', 'blue')]) // Should override red ]); const result = optimizer.optimize(styleBlock); expect(result.mergedRules).toBe(2); expect(result.optimizedCSS).toContain('color:blue'); // Should use the last value expect(result.optimizedCSS).toContain('font-size:16px'); // Should only have one .button rule expect((result.optimizedCSS.match(/\.button/g) || []).length).toBe(1); }); it('should not merge different selectors', () => { const styleBlock = createStyleBlock([ createRule('.button', [createDeclaration('color', 'red')]), createRule('.link', [createDeclaration('color', 'blue')]) ]); const result = optimizer.optimize(styleBlock); expect(result.mergedRules).toBe(0); expect(result.optimizedCSS).toContain('.button'); expect(result.optimizedCSS).toContain('.link'); }); }); describe('Empty Rule Removal', () => { it('should remove rules with no declarations', () => { const styleBlock = createStyleBlock([ createRule('.button', [createDeclaration('color', 'red')]), createRule('.empty', []), createRule('.link', [createDeclaration('color', 'blue')]) ]); const result = optimizer.optimize(styleBlock); expect(result.removedRules).toBe(1); expect(result.optimizedCSS).toContain('.button'); expect(result.optimizedCSS).toContain('.link'); expect(result.optimizedCSS).not.toContain('.empty'); }); }); describe('Redundant Declaration Removal', () => { it('should remove duplicate declarations within the same rule', () => { const styleBlock = createStyleBlock([ createRule('.button', [ createDeclaration('color', 'red'), createDeclaration('font-size', '16px'), createDeclaration('color', 'blue'), // Should override red createDeclaration('font-size', '18px') // Should override 16px ]) ]); const result = optimizer.optimize(styleBlock); expect(result.optimizedCSS).toContain('color:blue'); expect(result.optimizedCSS).toContain('font-size:18px'); expect(result.optimizedCSS).not.toContain('color:red'); expect(result.optimizedCSS).not.toContain('font-size:16px'); }); }); describe('Shorthand Optimization', () => { it('should optimize margin properties to shorthand', () => { const styleBlock = createStyleBlock([ createRule('.box', [ createDeclaration('margin-top', '10px'), createDeclaration('margin-right', '10px'), createDeclaration('margin-bottom', '10px'), createDeclaration('margin-left', '10px') ]) ]); const result = optimizer.optimize(styleBlock); expect(result.optimizedDeclarations).toBe(1); expect(result.optimizedCSS).toContain('margin:10px'); expect(result.optimizedCSS).not.toContain('margin-top'); expect(result.optimizedCSS).not.toContain('margin-right'); expect(result.optimizedCSS).not.toContain('margin-bottom'); expect(result.optimizedCSS).not.toContain('margin-left'); }); it('should optimize padding properties to shorthand', () => { const styleBlock = createStyleBlock([ createRule('.box', [ createDeclaration('padding-top', '5px'), createDeclaration('padding-right', '10px'), createDeclaration('padding-bottom', '5px'), createDeclaration('padding-left', '10px') ]) ]); const result = optimizer.optimize(styleBlock); expect(result.optimizedDeclarations).toBe(1); expect(result.optimizedCSS).toContain('padding:5px 10px'); }); it('should create four-value shorthand when all values are different', () => { const styleBlock = createStyleBlock([ createRule('.box', [ createDeclaration('margin-top', '1px'), createDeclaration('margin-right', '2px'), createDeclaration('margin-bottom', '3px'), createDeclaration('margin-left', '4px') ]) ]); const result = optimizer.optimize(styleBlock); expect(result.optimizedCSS).toContain('margin:1px 2px 3px 4px'); }); it('should not create shorthand when not all properties are present', () => { const styleBlock = createStyleBlock([ createRule('.box', [ createDeclaration('margin-top', '10px'), createDeclaration('margin-right', '10px'), createDeclaration('color', 'red') ]) ]); const result = optimizer.optimize(styleBlock); expect(result.optimizedCSS).toContain('margin-top:10px'); expect(result.optimizedCSS).toContain('margin-right:10px'); expect(result.optimizedCSS).not.toContain('margin:'); }); }); describe('Declaration Sorting', () => { it('should sort declarations alphabetically', () => { const styleBlock = createStyleBlock([ createRule('.button', [ createDeclaration('z-index', '1'), createDeclaration('color', 'red'), createDeclaration('background', 'blue'), createDeclaration('font-size', '16px') ]) ]); const result = optimizer.optimize(styleBlock); const css = result.optimizedCSS; const backgroundIndex = css.indexOf('background'); const colorIndex = css.indexOf('color'); const fontSizeIndex = css.indexOf('font-size'); const zIndexIndex = css.indexOf('z-index'); expect(backgroundIndex).toBeLessThan(colorIndex); expect(colorIndex).toBeLessThan(fontSizeIndex); expect(fontSizeIndex).toBeLessThan(zIndexIndex); }); }); describe('Bundle Optimization', () => { it('should optimize multiple style blocks into a single bundle', () => { const styleBlock1 = createStyleBlock([ createRule('.button', [createDeclaration('color', 'red')]) ]); const styleBlock2 = createStyleBlock([ createRule('.link', [createDeclaration('color', 'blue')]), createRule('.button', [createDeclaration('font-size', '16px')]) ]); const result = optimizer.optimizeBundle([styleBlock1, styleBlock2]); expect(result.optimizedCSS).toContain('.button'); expect(result.optimizedCSS).toContain('.link'); expect(result.mergedRules).toBe(1); // .button rules should be merged }); }); describe('Compression Metrics', () => { it('should calculate compression ratio correctly', () => { const styleBlock = createStyleBlock([ createRule('.very-long-class-name-that-will-be-compressed', [ createDeclaration('background-color', 'rgba(255, 255, 255, 0.5)'), createDeclaration('border-radius', '10px'), createDeclaration('box-shadow', '0 2px 4px rgba(0, 0, 0, 0.1)') ]) ]); const result = optimizer.optimize(styleBlock); expect(result.originalSize).toBeGreaterThan(result.optimizedSize); expect(result.compressionRatio).toBeGreaterThan(0); expect(result.compressionRatio).toBeLessThanOrEqual(1); }); it('should report optimization statistics', () => { const styleBlock = createStyleBlock([ createRule('.button', [createDeclaration('color', 'red')]), createRule('.button', [createDeclaration('font-size', '16px')]), createRule('.empty', []), createRule('.unused', [createDeclaration('color', 'blue')]) ]); const componentAST = createMockComponentAST([], ['button']); const result = optimizer.optimize(styleBlock, componentAST); expect(result.removedRules).toBe(2); // .empty and .unused expect(result.mergedRules).toBe(1); // .button rules merged expect(typeof result.originalSize).toBe('number'); expect(typeof result.optimizedSize).toBe('number'); expect(typeof result.compressionRatio).toBe('number'); }); }); describe('Edge Cases', () => { it('should handle empty style blocks', () => { const styleBlock = createStyleBlock([]); const result = optimizer.optimize(styleBlock); expect(result.optimizedCSS).toBe(''); expect(result.originalSize).toBe(0); expect(result.optimizedSize).toBe(0); expect(result.compressionRatio).toBe(0); }); it('should handle style blocks with only empty rules', () => { const styleBlock = createStyleBlock([ createRule('.empty1', []), createRule('.empty2', []) ]); const result = optimizer.optimize(styleBlock); expect(result.optimizedCSS).toBe(''); expect(result.removedRules).toBe(2); }); it('should handle pseudo-classes and pseudo-elements', () => { const styleBlock = createStyleBlock([ createRule('.button:hover', [createDeclaration('color', 'blue')]), createRule('.button::before', [createDeclaration('content', '""')]) ]); const componentAST = createMockComponentAST([], ['button']); const result = optimizer.optimize(styleBlock, componentAST); expect(result.optimizedCSS).toContain('.button:hover'); expect(result.optimizedCSS).toContain('.button::before'); }); }); });