@sc4rfurryx/proteusjs
Version:
The Modern Web Development Framework for Accessible, Responsive, and High-Performance Applications. Intelligent container queries, fluid typography, WCAG compliance, and performance optimization.
310 lines (248 loc) • 9.47 kB
text/typescript
/**
* Tests for Batch DOM Operations
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { BatchDOMOperations } from '../BatchDOMOperations';
// Mock requestAnimationFrame
global.requestAnimationFrame = vi.fn((callback) => {
setTimeout(callback, 16);
return 1;
});
global.cancelAnimationFrame = vi.fn();
describe('BatchDOMOperations', () => {
let batchDOM: BatchDOMOperations;
let testElement: HTMLElement;
beforeEach(() => {
batchDOM = new BatchDOMOperations({
maxBatchSize: 10,
frameTimeLimit: 16,
separateReadWrite: true,
autoFlush: false // Disable for testing
});
testElement = document.createElement('div');
testElement.style.width = '100px';
testElement.style.height = '100px';
document.body.appendChild(testElement);
});
afterEach(() => {
batchDOM.destroy();
document.body.removeChild(testElement);
vi.clearAllMocks();
});
describe('Read Operations', () => {
it('should queue and execute read operations', async () => {
const result = await batchDOM.queueRead(
testElement,
() => testElement.offsetWidth
);
expect(result).toBe(100);
});
it('should batch multiple reads efficiently', async () => {
const reads = [
batchDOM.queueRead(testElement, () => testElement.offsetWidth),
batchDOM.queueRead(testElement, () => testElement.offsetHeight),
batchDOM.queueRead(testElement, () => testElement.offsetTop)
];
const results = await Promise.all(reads);
expect(results[0]).toBe(100); // width
expect(results[1]).toBe(100); // height
expect(typeof results[2]).toBe('number'); // top
});
it('should handle read operation errors', async () => {
await expect(
batchDOM.queueRead(testElement, () => {
throw new Error('Test error');
})
).rejects.toThrow('Test error');
});
});
describe('Write Operations', () => {
it('should queue and execute write operations', async () => {
await batchDOM.queueWrite(
testElement,
() => {
testElement.style.backgroundColor = 'red';
}
);
expect(testElement.style.backgroundColor).toBe('red');
});
it('should batch multiple writes efficiently', async () => {
const writes = [
batchDOM.queueWrite(testElement, () => {
testElement.style.color = 'blue';
}),
batchDOM.queueWrite(testElement, () => {
testElement.style.fontSize = '20px';
})
];
await Promise.all(writes);
expect(testElement.style.color).toBe('blue');
expect(testElement.style.fontSize).toBe('20px');
});
});
describe('Batch Style Operations', () => {
it('should batch multiple style changes', async () => {
await batchDOM.batchStyles(testElement, {
'background-color': 'green',
'border': '1px solid black',
'padding': '10px'
});
expect(testElement.style.backgroundColor).toBe('green');
expect(testElement.style.border).toBe('1px solid black');
expect(testElement.style.padding).toBe('10px');
});
});
describe('Batch Class Operations', () => {
it('should batch class changes', async () => {
testElement.className = 'initial-class';
await batchDOM.batchClasses(testElement, {
add: ['new-class', 'another-class'],
remove: ['initial-class'],
toggle: ['toggle-class']
});
expect(testElement.classList.contains('new-class')).toBe(true);
expect(testElement.classList.contains('another-class')).toBe(true);
expect(testElement.classList.contains('initial-class')).toBe(false);
expect(testElement.classList.contains('toggle-class')).toBe(true);
});
});
describe('Batch Attribute Operations', () => {
it('should batch attribute changes', async () => {
await batchDOM.batchAttributes(testElement, {
'data-test': 'value',
'aria-label': 'Test element',
'title': null // Should remove
});
expect(testElement.getAttribute('data-test')).toBe('value');
expect(testElement.getAttribute('aria-label')).toBe('Test element');
expect(testElement.hasAttribute('title')).toBe(false);
});
});
describe('Batch Read Operations', () => {
it('should batch multiple property reads', async () => {
const results = await batchDOM.batchReads(testElement, {
width: () => testElement.offsetWidth,
height: () => testElement.offsetHeight,
tagName: () => testElement.tagName
});
expect(results.width).toBe(100);
expect(results.height).toBe(100);
expect(results.tagName).toBe('DIV');
});
});
describe('Element Measurement', () => {
it('should measure element dimensions', async () => {
const measurements = await batchDOM.measureElement(
testElement,
['width', 'height', 'top', 'left']
);
expect(measurements.width).toBe(100);
expect(measurements.height).toBe(100);
expect(typeof measurements.top).toBe('number');
expect(typeof measurements.left).toBe('number');
});
it('should measure only requested dimensions', async () => {
const measurements = await batchDOM.measureElement(
testElement,
['width']
);
expect(measurements.width).toBe(100);
expect(measurements.height).toBeUndefined();
});
});
describe('Priority Handling', () => {
it('should respect operation priorities', async () => {
const executionOrder: string[] = [];
// Add operations with different priorities
const lowPriority = batchDOM.queueWrite(testElement, () => {
executionOrder.push('low');
}, 'low');
const highPriority = batchDOM.queueWrite(testElement, () => {
executionOrder.push('high');
}, 'high');
const normalPriority = batchDOM.queueWrite(testElement, () => {
executionOrder.push('normal');
}, 'normal');
await Promise.all([lowPriority, highPriority, normalPriority]);
// High priority should execute first
expect(executionOrder[0]).toBe('high');
});
});
describe('Read/Write Separation', () => {
it('should separate reads and writes to prevent layout thrashing', async () => {
const executionOrder: string[] = [];
// Mix reads and writes
const read1 = batchDOM.queueRead(testElement, () => {
executionOrder.push('read1');
return testElement.offsetWidth;
});
const write1 = batchDOM.queueWrite(testElement, () => {
executionOrder.push('write1');
testElement.style.width = '200px';
});
const read2 = batchDOM.queueRead(testElement, () => {
executionOrder.push('read2');
return testElement.offsetHeight;
});
const write2 = batchDOM.queueWrite(testElement, () => {
executionOrder.push('write2');
testElement.style.height = '200px';
});
await Promise.all([read1, write1, read2, write2]);
// All reads should come before writes
const readIndices = executionOrder
.map((op, index) => op.startsWith('read') ? index : -1)
.filter(index => index !== -1);
const writeIndices = executionOrder
.map((op, index) => op.startsWith('write') ? index : -1)
.filter(index => index !== -1);
const lastReadIndex = Math.max(...readIndices);
const firstWriteIndex = Math.min(...writeIndices);
expect(lastReadIndex).toBeLessThan(firstWriteIndex);
});
});
describe('Performance Metrics', () => {
it('should track performance metrics', async () => {
const initialMetrics = batchDOM.getMetrics();
await batchDOM.queueRead(testElement, () => testElement.offsetWidth);
await batchDOM.queueWrite(testElement, () => {
testElement.style.color = 'red';
});
const updatedMetrics = batchDOM.getMetrics();
expect(updatedMetrics.totalOperations).toBeGreaterThan(initialMetrics.totalOperations);
expect(updatedMetrics.readOperations).toBeGreaterThan(initialMetrics.readOperations);
expect(updatedMetrics.writeOperations).toBeGreaterThan(initialMetrics.writeOperations);
});
});
describe('Flush Operations', () => {
it('should flush all queued operations', async () => {
let executed = false;
// Queue operation without auto-flush
batchDOM.queueWrite(testElement, () => {
executed = true;
});
// Should not be executed yet
expect(executed).toBe(false);
// Flush manually
await batchDOM.flush();
// Should be executed now
expect(executed).toBe(true);
});
});
describe('Cleanup', () => {
it('should clear all queues', () => {
batchDOM.queueRead(testElement, () => testElement.offsetWidth);
batchDOM.queueWrite(testElement, () => {
testElement.style.color = 'red';
});
const metricsBeforeClear = batchDOM.getMetrics();
expect(metricsBeforeClear.totalOperations).toBeGreaterThan(0);
batchDOM.clear();
// Queues should be empty after clear
// Note: We can't directly test queue size, but we can test that no new operations are processed
});
it('should destroy properly', () => {
expect(() => batchDOM.destroy()).not.toThrow();
});
});
});