@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.
433 lines (349 loc) • 12.9 kB
text/typescript
/**
* FluidTypography Test Suite
* Comprehensive tests for fluid typography system
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { FluidTypography, FluidConfig, ContainerBasedConfig } from '../FluidTypography';
// Mock ResizeObserver
global.ResizeObserver = vi.fn().mockImplementation((callback) => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
describe('FluidTypography', () => {
let fluidTypography: FluidTypography;
let testElement: HTMLElement;
beforeEach(() => {
// Clean up DOM
document.body.innerHTML = '';
// Create test element
testElement = document.createElement('p');
testElement.textContent = 'Test typography content';
testElement.style.fontSize = '16px';
document.body.appendChild(testElement);
fluidTypography = new FluidTypography();
// Mock getComputedStyle
vi.spyOn(window, 'getComputedStyle').mockReturnValue({
fontSize: '16px',
fontFamily: 'Arial, sans-serif',
fontWeight: 'normal',
fontStyle: 'normal'
} as CSSStyleDeclaration);
});
afterEach(() => {
fluidTypography.destroy();
document.body.innerHTML = '';
vi.restoreAllMocks();
});
describe('Fluid Scaling', () => {
it('should apply fluid scaling with clamp()', () => {
const config: FluidConfig = {
minSize: 14,
maxSize: 24,
minViewport: 320,
maxViewport: 1200
};
fluidTypography.applyFluidScaling(testElement, config);
expect(testElement.style.fontSize).toContain('clamp(');
expect(testElement.getAttribute('data-proteus-fluid')).toBe('true');
expect(testElement.getAttribute('data-proteus-min-size')).toBe('14');
expect(testElement.getAttribute('data-proteus-max-size')).toBe('24');
});
it('should enforce accessibility constraints', () => {
const config: FluidConfig = {
minSize: 10, // Below WCAG AA minimum
maxSize: 20,
accessibility: 'AA',
enforceAccessibility: true
};
fluidTypography.applyFluidScaling(testElement, config);
// Should be adjusted to meet WCAG AA minimum (14px)
expect(testElement.getAttribute('data-proteus-min-size')).toBe('14');
});
it('should respect user preferences', () => {
// Mock root font size to simulate user preference
vi.spyOn(window, 'getComputedStyle').mockImplementation((element) => {
if (element === document.documentElement) {
return { fontSize: '20px' } as CSSStyleDeclaration; // User increased font size
}
return {
fontSize: '16px',
fontFamily: 'Arial, sans-serif',
fontWeight: 'normal',
fontStyle: 'normal'
} as CSSStyleDeclaration;
});
const config: FluidConfig = {
minSize: 16,
maxSize: 24,
respectUserPreferences: true
};
fluidTypography.applyFluidScaling(testElement, config);
// Font sizes should be scaled by user preference (20/16 = 1.25)
expect(testElement.getAttribute('data-proteus-min-size')).toBe('20'); // 16 * 1.25
expect(testElement.getAttribute('data-proteus-max-size')).toBe('30'); // 24 * 1.25
});
it('should generate linear clamp values correctly', () => {
const config: FluidConfig = {
minSize: 16,
maxSize: 24,
minViewport: 320,
maxViewport: 1200,
scalingFunction: 'linear'
};
fluidTypography.applyFluidScaling(testElement, config);
const fontSize = testElement.style.fontSize;
expect(fontSize).toMatch(/clamp\(16px,.*,24px\)/);
});
it('should handle exponential scaling', () => {
const config: FluidConfig = {
minSize: 16,
maxSize: 32,
minViewport: 320,
maxViewport: 1200,
scalingFunction: 'exponential'
};
fluidTypography.applyFluidScaling(testElement, config);
const fontSize = testElement.style.fontSize;
expect(fontSize).toContain('clamp(');
expect(fontSize).toContain('16px');
expect(fontSize).toContain('32px');
});
});
describe('Container-Based Scaling', () => {
let containerElement: HTMLElement;
beforeEach(() => {
containerElement = document.createElement('div');
containerElement.style.width = '600px';
containerElement.style.height = '400px';
containerElement.appendChild(testElement);
document.body.appendChild(containerElement);
// Mock getBoundingClientRect for container
vi.spyOn(containerElement, 'getBoundingClientRect').mockReturnValue({
width: 600,
height: 400,
top: 0,
left: 0,
bottom: 400,
right: 600,
x: 0,
y: 0,
toJSON: () => ({})
} as DOMRect);
});
it('should apply container-based scaling', () => {
const config: ContainerBasedConfig = {
minSize: 14,
maxSize: 24,
containerElement,
minContainerWidth: 300,
maxContainerWidth: 800
};
fluidTypography.applyContainerBasedScaling(testElement, config);
// Container width is 600px, should scale between min and max
const fontSize = parseFloat(testElement.style.fontSize);
expect(fontSize).toBeGreaterThan(14);
expect(fontSize).toBeLessThan(24);
});
it('should find nearest container automatically', () => {
const config: ContainerBasedConfig = {
minSize: 14,
maxSize: 24,
minContainerWidth: 300,
maxContainerWidth: 800
};
fluidTypography.applyContainerBasedScaling(testElement, config);
// Should find the container element automatically
expect(testElement.style.fontSize).toBeTruthy();
});
it('should update on container resize', () => {
const config: ContainerBasedConfig = {
minSize: 14,
maxSize: 24,
containerElement,
minContainerWidth: 300,
maxContainerWidth: 800
};
fluidTypography.applyContainerBasedScaling(testElement, config);
const initialFontSize = testElement.style.fontSize;
// Simulate container resize
vi.spyOn(containerElement, 'getBoundingClientRect').mockReturnValue({
width: 400,
height: 400,
top: 0,
left: 0,
bottom: 400,
right: 400,
x: 0,
y: 0,
toJSON: () => ({})
} as DOMRect);
// Trigger resize update
fluidTypography['handleContainerResize'](containerElement);
expect(testElement.style.fontSize).not.toBe(initialFontSize);
});
});
describe('Text Fitting', () => {
beforeEach(() => {
// Mock getBoundingClientRect for text measurement
const mockGetBoundingClientRect = vi.fn();
Element.prototype.getBoundingClientRect = mockGetBoundingClientRect;
// Return different widths based on font size
mockGetBoundingClientRect.mockImplementation(function(this: Element) {
const fontSize = parseFloat(this.style.fontSize || '16');
const textLength = this.textContent?.length || 0;
return {
width: fontSize * textLength * 0.6, // Approximate character width
height: fontSize * 1.2,
top: 0,
left: 0,
bottom: fontSize * 1.2,
right: fontSize * textLength * 0.6,
x: 0,
y: 0,
toJSON: () => ({})
} as DOMRect;
});
});
it('should fit text to container width', () => {
const config = {
maxWidth: 200,
minSize: 12,
maxSize: 24
};
fluidTypography.fitTextToContainer(testElement, config);
const fontSize = parseFloat(testElement.style.fontSize);
expect(fontSize).toBeGreaterThanOrEqual(12);
expect(fontSize).toBeLessThanOrEqual(24);
});
it('should handle overflow settings', () => {
const config = {
maxWidth: 100,
minSize: 12,
maxSize: 24,
allowOverflow: false,
wordBreak: 'break-all' as const
};
fluidTypography.fitTextToContainer(testElement, config);
expect(testElement.style.overflow).toBe('hidden');
expect(testElement.style.textOverflow).toBe('ellipsis');
expect(testElement.style.wordBreak).toBe('break-all');
});
it('should calculate optimal text size correctly', () => {
testElement.textContent = 'Short text';
const config = {
maxWidth: 300,
minSize: 12,
maxSize: 48
};
fluidTypography.fitTextToContainer(testElement, config);
// Should find a size that fits within the width
const fontSize = parseFloat(testElement.style.fontSize);
expect(fontSize).toBeGreaterThan(12);
});
});
describe('Element Management', () => {
it('should remove fluid scaling', () => {
const config: FluidConfig = {
minSize: 14,
maxSize: 24
};
fluidTypography.applyFluidScaling(testElement, config);
expect(testElement.style.fontSize).toBeTruthy();
expect(testElement.getAttribute('data-proteus-fluid')).toBe('true');
fluidTypography.removeFluidScaling(testElement);
expect(testElement.style.fontSize).toBeFalsy();
expect(testElement.getAttribute('data-proteus-fluid')).toBeNull();
});
it('should handle elements without applied scaling', () => {
expect(() => {
fluidTypography.removeFluidScaling(testElement);
}).not.toThrow();
});
it('should clean up resources on destroy', () => {
const config: FluidConfig = {
minSize: 14,
maxSize: 24
};
fluidTypography.applyFluidScaling(testElement, config);
expect(() => {
fluidTypography.destroy();
}).not.toThrow();
});
});
describe('Error Handling', () => {
it('should handle missing container gracefully', () => {
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
// Remove element from DOM to simulate missing container
testElement.remove();
const config: ContainerBasedConfig = {
minSize: 14,
maxSize: 24
};
expect(() => {
fluidTypography.applyContainerBasedScaling(testElement, config);
}).not.toThrow();
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('No container found')
);
consoleSpy.mockRestore();
});
it('should handle errors in scaling gracefully', () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
// Force an error by providing invalid config
const config = null as any;
expect(() => {
fluidTypography.applyFluidScaling(testElement, config);
}).not.toThrow();
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
it('should handle empty text content', () => {
testElement.textContent = '';
const config = {
maxWidth: 200,
minSize: 12,
maxSize: 24
};
expect(() => {
fluidTypography.fitTextToContainer(testElement, config);
}).not.toThrow();
expect(parseFloat(testElement.style.fontSize)).toBe(12); // Should use minimum
});
});
describe('Accessibility Compliance', () => {
it('should enforce WCAG AA compliance', () => {
const config: FluidConfig = {
minSize: 10, // Below WCAG AA minimum
maxSize: 20,
accessibility: 'AA',
enforceAccessibility: true
};
fluidTypography.applyFluidScaling(testElement, config);
const minSize = parseFloat(testElement.getAttribute('data-proteus-min-size') || '0');
expect(minSize).toBeGreaterThanOrEqual(14); // WCAG AA minimum
});
it('should enforce WCAG AAA compliance', () => {
const config: FluidConfig = {
minSize: 12, // Below WCAG AAA minimum
maxSize: 20,
accessibility: 'AAA',
enforceAccessibility: true
};
fluidTypography.applyFluidScaling(testElement, config);
const minSize = parseFloat(testElement.getAttribute('data-proteus-min-size') || '0');
expect(minSize).toBeGreaterThanOrEqual(16); // WCAG AAA minimum
});
it('should allow bypassing accessibility constraints', () => {
const config: FluidConfig = {
minSize: 10,
maxSize: 20,
accessibility: 'AA',
enforceAccessibility: false
};
fluidTypography.applyFluidScaling(testElement, config);
const minSize = parseFloat(testElement.getAttribute('data-proteus-min-size') || '0');
expect(minSize).toBe(10); // Should not be enforced
});
});
});