@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
461 lines (382 loc) • 16.2 kB
text/typescript
/**
* @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');
});
});
});