UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

612 lines (508 loc) 19.3 kB
/** * @fileoverview Tests for OrdoJS DOM Optimizer */ import { beforeEach, describe, expect, it } from 'vitest'; import { DirectiveType, ExpressionType, type AttributeNode, type ClientBlockNode, type ComponentAST, type ComponentNode, type ExpressionNode, type HTMLElementNode, type InterpolationNode, type MarkupBlockNode, type ReactiveVariableNode, type SourcePosition, type SourceRange } from '../types/index.js'; import { DependencyAnalyzer } from './dependency-analyzer.js'; import { DOMOptimizer } from './dom-optimizer.js'; describe('DOMOptimizer', () => { let optimizer: DOMOptimizer; let analyzer: DependencyAnalyzer; let mockSourceRange: SourceRange; let mockSourcePosition: SourcePosition; beforeEach(() => { optimizer = new DOMOptimizer(); analyzer = new DependencyAnalyzer(); mockSourcePosition = { line: 1, column: 1, offset: 0 }; mockSourceRange = { start: mockSourcePosition, end: mockSourcePosition }; }); // Helper function to create a mock expression function createMockExpression( type: ExpressionType, identifier?: string, value?: any, left?: ExpressionNode, right?: ExpressionNode, operator?: string ): ExpressionNode { return { type: 'Expression', expressionType: type, identifier, value, left, right, operator, range: mockSourceRange }; } // Helper function to create a mock reactive variable function createMockReactiveVariable( name: string, initialValue: ExpressionNode, isConst = false ): ReactiveVariableNode { return { type: 'ReactiveVariable', name, initialValue, dataType: { name: 'any', isArray: false, isOptional: false, genericTypes: [] }, isConst, range: mockSourceRange }; } // Helper function to create a mock interpolation function createMockInterpolation(expression: ExpressionNode): InterpolationNode { return { type: 'Interpolation', expression, range: mockSourceRange }; } // Helper function to create a mock attribute function createMockAttribute( name: string, value: string | ExpressionNode, isDirective = false, directiveType?: DirectiveType ): AttributeNode { return { type: 'Attribute', name, value, isDirective, directiveType, range: mockSourceRange }; } // Helper function to create a mock HTML element function createMockHTMLElement( tagName: string, attributes: AttributeNode[] = [], children: any[] = [] ): HTMLElementNode { return { type: 'HTMLElement', tagName, attributes, children, isSelfClosing: false, isVoidElement: false, range: mockSourceRange }; } // Helper function to create a complete component AST function createMockComponentAST( reactiveVariables: ReactiveVariableNode[], elements: HTMLElementNode[], interpolations: InterpolationNode[] = [] ): ComponentAST { const clientBlock: ClientBlockNode = { type: 'ClientBlock', reactiveVariables, computedValues: [], eventHandlers: [], functions: [], lifecycle: [], range: mockSourceRange, children: reactiveVariables }; const markupBlock: MarkupBlockNode = { type: 'MarkupBlock', elements, textNodes: [], interpolations, range: mockSourceRange, children: [...elements, ...interpolations] }; const component: ComponentNode = { type: 'Component', name: 'TestComponent', props: [], clientBlock, markupBlock, range: mockSourceRange, children: [clientBlock, markupBlock] }; return { component, dependencies: [], exports: [], sourceMap: { version: 3, sources: [], names: [], mappings: '', sourcesContent: [] } }; } describe('Basic DOM Optimization', () => { it('should generate update code for text interpolations', () => { const countVar = createMockReactiveVariable( 'count', createMockExpression(ExpressionType.LITERAL, undefined, 0) ); const interpolation = createMockInterpolation( createMockExpression(ExpressionType.IDENTIFIER, 'count') ); const ast = createMockComponentAST([countVar], [], [interpolation]); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); expect(result.updateCode).toContain('updateFunctions'); expect(result.updateCode).toContain('textContent'); expect(result.updateCode).toContain('component.state.count'); }); it('should generate update code for attribute bindings', () => { const titleVar = createMockReactiveVariable( 'title', createMockExpression(ExpressionType.LITERAL, undefined, 'Hello') ); const titleAttr = createMockAttribute( 'title', createMockExpression(ExpressionType.IDENTIFIER, 'title'), true ); const element = createMockHTMLElement('div', [titleAttr]); const ast = createMockComponentAST([titleVar], [element]); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); expect(result.updateCode).toContain('setAttribute'); expect(result.updateCode).toContain('title'); expect(result.updateCode).toContain('component.state.title'); }); it('should generate setup code for two-way bindings', () => { const valueVar = createMockReactiveVariable( 'value', createMockExpression(ExpressionType.LITERAL, undefined, '') ); const bindAttr = createMockAttribute( 'bind:value', createMockExpression(ExpressionType.IDENTIFIER, 'value'), true, DirectiveType.BIND ); const element = createMockHTMLElement('input', [bindAttr]); const ast = createMockComponentAST([valueVar], [element]); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); expect(result.setupCode).toContain('setupTwoWayBindings'); expect(result.setupCode).toContain('addEventListener'); expect(result.setupCode).toContain('input'); expect(result.setupCode).toContain('component.state.value'); }); it('should generate class toggle updates', () => { const activeVar = createMockReactiveVariable( 'active', createMockExpression(ExpressionType.LITERAL, undefined, false) ); const classAttr = createMockAttribute( 'class:active', createMockExpression(ExpressionType.IDENTIFIER, 'active'), true, DirectiveType.CLASS ); const element = createMockHTMLElement('div', [classAttr]); const ast = createMockComponentAST([activeVar], [element]); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); expect(result.updateCode).toContain('classList.toggle'); expect(result.updateCode).toContain('active'); expect(result.updateCode).toContain('component.state.active'); }); it('should generate style updates', () => { const colorVar = createMockReactiveVariable( 'color', createMockExpression(ExpressionType.LITERAL, undefined, 'red') ); const styleAttr = createMockAttribute( 'style:color', createMockExpression(ExpressionType.IDENTIFIER, 'color'), true, DirectiveType.STYLE ); const element = createMockHTMLElement('div', [styleAttr]); const ast = createMockComponentAST([colorVar], [element]); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); expect(result.updateCode).toContain('el.style.color'); expect(result.updateCode).toContain('component.state.color'); }); }); describe('Selective Updates', () => { it('should generate selective updates for specific variables', () => { const countVar = createMockReactiveVariable( 'count', createMockExpression(ExpressionType.LITERAL, undefined, 0) ); const nameVar = createMockReactiveVariable( 'name', createMockExpression(ExpressionType.LITERAL, undefined, 'test') ); const countInterpolation = createMockInterpolation( createMockExpression(ExpressionType.IDENTIFIER, 'count') ); const nameInterpolation = createMockInterpolation( createMockExpression(ExpressionType.IDENTIFIER, 'name') ); const ast = createMockComponentAST( [countVar, nameVar], [], [countInterpolation, nameInterpolation] ); const dependencyGraph = analyzer.analyze(ast); // Generate selective updates for only 'count' const selectiveCode = optimizer.generateSelectiveUpdates(ast, dependencyGraph, ['count']); expect(selectiveCode).toContain('component.state.count'); expect(selectiveCode).not.toContain('component.state.name'); }); it('should handle complex expressions in selective updates', () => { const aVar = createMockReactiveVariable( 'a', createMockExpression(ExpressionType.LITERAL, undefined, 1) ); const bVar = createMockReactiveVariable( 'b', createMockExpression(ExpressionType.LITERAL, undefined, 2) ); // Create expression: a + b const complexInterpolation = createMockInterpolation( createMockExpression( ExpressionType.BINARY, undefined, undefined, createMockExpression(ExpressionType.IDENTIFIER, 'a'), createMockExpression(ExpressionType.IDENTIFIER, 'b'), '+' ) ); const ast = createMockComponentAST([aVar, bVar], [], [complexInterpolation]); const dependencyGraph = analyzer.analyze(ast); // Generate selective updates for only 'a' const selectiveCode = optimizer.generateSelectiveUpdates(ast, dependencyGraph, ['a']); expect(selectiveCode).toContain('component.state.a'); expect(selectiveCode).toContain('component.state.b'); expect(selectiveCode).toContain('+'); }); }); describe('Batched Updates', () => { it('should batch similar update operations', () => { const var1 = createMockReactiveVariable( 'var1', createMockExpression(ExpressionType.LITERAL, undefined, 'value1') ); const var2 = createMockReactiveVariable( 'var2', createMockExpression(ExpressionType.LITERAL, undefined, 'value2') ); const attr1 = createMockAttribute( 'title', createMockExpression(ExpressionType.IDENTIFIER, 'var1'), true ); const attr2 = createMockAttribute( 'alt', createMockExpression(ExpressionType.IDENTIFIER, 'var2'), true ); const element1 = createMockHTMLElement('div', [attr1]); const element2 = createMockHTMLElement('img', [attr2]); const ast = createMockComponentAST([var1, var2], [element1, element2]); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); // Should have separate batches for each variable expect(result.updateCode).toContain('batch_0'); expect(result.updateCode).toContain('batch_1'); expect(result.updateCode).toContain('updateFunctions'); }); it('should prioritize update operations correctly', () => { const visibleVar = createMockReactiveVariable( 'visible', createMockExpression(ExpressionType.LITERAL, undefined, true) ); const colorVar = createMockReactiveVariable( 'color', createMockExpression(ExpressionType.LITERAL, undefined, 'red') ); const textVar = createMockReactiveVariable( 'text', createMockExpression(ExpressionType.LITERAL, undefined, 'Hello') ); const styleAttr = createMockAttribute( 'style:color', createMockExpression(ExpressionType.IDENTIFIER, 'color'), true, DirectiveType.STYLE ); const element = createMockHTMLElement('div', [styleAttr]); const textInterpolation = createMockInterpolation( createMockExpression(ExpressionType.IDENTIFIER, 'text') ); const ast = createMockComponentAST( [visibleVar, colorVar, textVar], [element], [textInterpolation] ); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); // Should contain different types of updates with proper prioritization expect(result.updateCode).toContain('el.style.color'); expect(result.updateCode).toContain('textContent'); }); }); describe('Complex Scenarios', () => { it('should handle nested elements with multiple bindings', () => { // Simplify the test to use direct variable references instead of member expressions const activeVar = createMockReactiveVariable( 'active', createMockExpression(ExpressionType.LITERAL, undefined, true) ); const colorVar = createMockReactiveVariable( 'color', createMockExpression(ExpressionType.LITERAL, undefined, 'blue') ); const classAttr = createMockAttribute( 'class:active', createMockExpression(ExpressionType.IDENTIFIER, 'active'), true, DirectiveType.CLASS ); const styleAttr = createMockAttribute( 'style:color', createMockExpression(ExpressionType.IDENTIFIER, 'color'), true, DirectiveType.STYLE ); const parentElement = createMockHTMLElement('div', [classAttr]); const childElement = createMockHTMLElement('span', [styleAttr]); parentElement.children = [childElement]; const ast = createMockComponentAST([activeVar, colorVar], [parentElement]); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); expect(result.updateCode).toContain('classList.toggle'); expect(result.updateCode).toContain('el.style.color'); expect(result.updateCode).toContain('component.state.active'); expect(result.updateCode).toContain('component.state.color'); }); it('should handle multiple two-way bindings', () => { const nameVar = createMockReactiveVariable( 'name', createMockExpression(ExpressionType.LITERAL, undefined, '') ); const emailVar = createMockReactiveVariable( 'email', createMockExpression(ExpressionType.LITERAL, undefined, '') ); const checkedVar = createMockReactiveVariable( 'checked', createMockExpression(ExpressionType.LITERAL, undefined, false) ); const nameBinding = createMockAttribute( 'bind:value', createMockExpression(ExpressionType.IDENTIFIER, 'name'), true, DirectiveType.BIND ); const emailBinding = createMockAttribute( 'bind:value', createMockExpression(ExpressionType.IDENTIFIER, 'email'), true, DirectiveType.BIND ); const checkedBinding = createMockAttribute( 'bind:checked', createMockExpression(ExpressionType.IDENTIFIER, 'checked'), true, DirectiveType.BIND ); const nameInput = createMockHTMLElement('input', [nameBinding]); const emailInput = createMockHTMLElement('input', [emailBinding]); const checkbox = createMockHTMLElement('input', [checkedBinding]); const ast = createMockComponentAST( [nameVar, emailVar, checkedVar], [nameInput, emailInput, checkbox] ); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); expect(result.setupCode).toContain('component.state.name'); expect(result.setupCode).toContain('component.state.email'); expect(result.setupCode).toContain('component.state.checked'); expect(result.setupCode).toContain('input'); expect(result.setupCode).toContain('change'); }); it('should generate efficient code for large numbers of bindings', () => { const variables: ReactiveVariableNode[] = []; const elements: HTMLElementNode[] = []; // Create 10 reactive variables and corresponding elements for (let i = 0; i < 10; i++) { const variable = createMockReactiveVariable( `var${i}`, createMockExpression(ExpressionType.LITERAL, undefined, `value${i}`) ); variables.push(variable); const attr = createMockAttribute( 'title', createMockExpression(ExpressionType.IDENTIFIER, `var${i}`), true ); const element = createMockHTMLElement('div', [attr]); elements.push(element); } const ast = createMockComponentAST(variables, elements); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); // Should generate efficient batched updates expect(result.updateCode).toContain('updateFunctions'); // Each variable creates both a property update and an attribute update, so we expect 20 batches expect(result.updateCode.split('batch_').length - 1).toBe(20); // 2 batches per variable (property + attribute) expect(result.updateCode).toContain('querySelectorAll'); expect(result.updateCode).toContain('forEach'); }); }); describe('Error Handling and Edge Cases', () => { it('should handle components with no reactive variables', () => { const element = createMockHTMLElement('div'); const ast = createMockComponentAST([], [element]); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); expect(result.updateCode).toContain('updateFunctions'); expect(result.setupCode).toBe(''); expect(result.cleanupCode).toBe(''); }); it('should handle empty markup blocks', () => { const variable = createMockReactiveVariable( 'test', createMockExpression(ExpressionType.LITERAL, undefined, 'value') ); const ast = createMockComponentAST([variable], []); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); expect(result.updateCode).toContain('updateFunctions'); expect(result.setupCode).toBe(''); }); it('should handle expressions with no dependencies', () => { const literalInterpolation = createMockInterpolation( createMockExpression(ExpressionType.LITERAL, undefined, 'static text') ); const ast = createMockComponentAST([], [], [literalInterpolation]); const dependencyGraph = analyzer.analyze(ast); const result = optimizer.optimize(ast, dependencyGraph); // Should still generate update code, but with no variable dependencies expect(result.updateCode).toContain('updateFunctions'); }); }); });