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