UNPKG

jsdom-testing-mocks

Version:

A set of tools for emulating browser behavior in jsdom environment

1,081 lines (913 loc) 34.8 kB
import { isJsdomEnv, WrongEnvironmentError } from '../../helper'; /** * CSS Typed OM Implementation * * A comprehensive polyfill for CSS Typed Object Model Level 1 * Based on the W3C specification: https://www.w3.org/TR/css-typed-om-1/ * * This implementation includes: * - CSSNumericValue (base class) * - CSSUnitValue (single unit values) * - CSSMathValue and subclasses (math expressions) * - Unit conversion and type checking * - Full arithmetic operations */ // ============================================================================ // Types and Constants // ============================================================================ /** * CSS numeric base types as defined in the specification */ type CSSNumericBaseType = 'length' | 'angle' | 'time' | 'frequency' | 'resolution' | 'flex' | 'percent'; /** * CSS math operators */ type CSSMathOperator = 'sum' | 'product' | 'negate' | 'invert' | 'min' | 'max' | 'clamp'; /** * CSS numeric type representing the dimensional analysis of a value */ interface CSSNumericType { length: number; angle: number; time: number; frequency: number; resolution: number; flex: number; percent: number; percentHint?: CSSNumericBaseType; } /** * Unit conversion table for compatible units */ const UNIT_CONVERSIONS: Record<string, { canonical: string; factor: number }> = { // Length units (canonical: px) 'px': { canonical: 'px', factor: 1 }, 'in': { canonical: 'px', factor: 96 }, 'cm': { canonical: 'px', factor: 96 / 2.54 }, 'mm': { canonical: 'px', factor: 96 / 25.4 }, 'pt': { canonical: 'px', factor: 96 / 72 }, 'pc': { canonical: 'px', factor: 96 / 6 }, 'Q': { canonical: 'px', factor: 96 / 101.6 }, // Angle units (canonical: deg) 'deg': { canonical: 'deg', factor: 1 }, 'rad': { canonical: 'deg', factor: 180 / Math.PI }, 'grad': { canonical: 'deg', factor: 0.9 }, 'turn': { canonical: 'deg', factor: 360 }, // Time units (canonical: ms) 'ms': { canonical: 'ms', factor: 1 }, 's': { canonical: 'ms', factor: 1000 }, // Frequency units (canonical: Hz) 'Hz': { canonical: 'Hz', factor: 1 }, 'kHz': { canonical: 'Hz', factor: 1000 }, // Resolution units (canonical: dppx) 'dppx': { canonical: 'dppx', factor: 1 }, 'dpi': { canonical: 'dppx', factor: 1 / 96 }, 'dpcm': { canonical: 'dppx', factor: 2.54 / 96 }, }; // ============================================================================ // Utility Functions // ============================================================================ /** * Gets the base type for a given unit */ function getBaseType(unit: string): CSSNumericBaseType | null { if (unit === 'number') return null; if (unit === 'percent') return 'percent'; // Length units - comprehensive list const lengthUnits = [ // Absolute 'px', 'in', 'cm', 'mm', 'pt', 'pc', 'Q', // Font-relative 'em', 'rem', 'ex', 'ch', 'cap', 'ic', 'lh', 'rlh', // Viewport-relative (original) 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax', // Viewport-relative (small) 'svw', 'svh', 'svi', 'svb', 'svmin', 'svmax', // Viewport-relative (large) 'lvw', 'lvh', 'lvi', 'lvb', 'lvmin', 'lvmax', // Viewport-relative (dynamic) 'dvw', 'dvh', 'dvi', 'dvb', 'dvmin', 'dvmax', // Container query 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax' ]; if (lengthUnits.includes(unit)) return 'length'; if (['deg', 'rad', 'grad', 'turn'].includes(unit)) return 'angle'; if (['ms', 's'].includes(unit)) return 'time'; if (['Hz', 'kHz'].includes(unit)) return 'frequency'; if (['dppx', 'dpi', 'dpcm'].includes(unit)) return 'resolution'; if (unit === 'fr') return 'flex'; return null; } /** * Creates a CSS numeric type from a unit (sparse - only non-zero properties) */ function createTypeFromUnit(unit: string): CSSNumericType { const baseType = getBaseType(unit); if (baseType && baseType !== 'percent') { const result: Partial<CSSNumericType> = {}; result[baseType] = 1; return result as CSSNumericType; } else if (baseType === 'percent') { return { percent: 1 } as CSSNumericType; } return {} as CSSNumericType; // Empty object for dimensionless numbers } /** * Adds two CSS numeric types */ function addTypes(type1: CSSNumericType, type2: CSSNumericType): CSSNumericType | null { // For addition, types must be compatible (same dimensions) const result: CSSNumericType = { ...type1 }; // Check if both are numbers (empty objects or all zeros) const isType1Number = Object.keys(type1).length === 0 || Object.values(type1).every(v => v === 0 || v === undefined); const isType2Number = Object.keys(type2).length === 0 || Object.values(type2).every(v => v === 0 || v === undefined); // Numbers can only be added to other numbers if (isType1Number && isType2Number) { return {} as CSSNumericType; // Return empty object for numbers } // If one is number and other isn't, it's incompatible for addition if (isType1Number || isType2Number) { return null; } // Check if types are compatible for addition (same dimensions) const keys: Array<keyof CSSNumericType> = ['length', 'angle', 'time', 'frequency', 'resolution', 'flex', 'percent']; for (const key of keys) { if (key === 'percentHint') continue; if ((type1[key] || 0) !== (type2[key] || 0)) { return null; } } return result; } /** * Multiplies two CSS numeric types */ function multiplyTypes(type1: CSSNumericType, type2: CSSNumericType): CSSNumericType { const result: CSSNumericType = { length: type1.length + type2.length, angle: type1.angle + type2.angle, time: type1.time + type2.time, frequency: type1.frequency + type2.frequency, resolution: type1.resolution + type2.resolution, flex: type1.flex + type2.flex, percent: type1.percent + type2.percent }; return result; } /** * Converts a unit to its canonical form if possible */ function convertUnit(value: number, fromUnit: string, toUnit: string): number | null { if (fromUnit === toUnit) return value; const fromConversion = UNIT_CONVERSIONS[fromUnit]; const toConversion = UNIT_CONVERSIONS[toUnit]; if (!fromConversion || !toConversion) return null; if (fromConversion.canonical !== toConversion.canonical) return null; // Convert: value * fromFactor / toFactor const result = value * fromConversion.factor / toConversion.factor; // Round to match browser precision - use fewer decimal places to match browser behavior return Math.round(result * 10000) / 10000; } /** * Gets the canonical unit for a given unit */ function getCanonicalUnit(unit: string): string { const conversion = UNIT_CONVERSIONS[unit]; return conversion ? conversion.canonical : unit; } /** * Converts a CSSNumberish value to a CSSNumericValue */ function rectifyNumberish(value: CSSNumberish, allowDirectNumbers = false): MockedCSSNumericValue { if (typeof value === 'number') { if (allowDirectNumbers) { return new MockedCSSUnitValue(value, 'number'); } throw new TypeError('Numbers cannot be used directly in CSS Typed OM operations. Use CSS.number() to create a CSSNumericValue.'); } if (value instanceof MockedCSSNumericValue) { return value; } throw new TypeError('Invalid CSSNumberish value'); } // ============================================================================ // CSSNumericValue (Base Class) // ============================================================================ /** * Base class for all CSS numeric values */ abstract class MockedCSSNumericValue implements CSSNumericValue { constructor() { if (new.target === MockedCSSNumericValue) { throw new TypeError('Cannot instantiate abstract class MockedCSSNumericValue directly'); } } /** * Returns the type of this numeric value */ abstract type(): CSSNumericType; /** * Adds one or more values to this value */ add(...values: CSSNumberish[]): CSSNumericValue { if (values.length === 0) return this; // Check for raw numbers - they should throw if (values.some(v => typeof v === 'number')) { throw new TypeError('Cannot add numbers directly. Use CSS.number() instead.'); } const operands = [this, ...values.map(v => rectifyNumberish(v))]; // Try to simplify if all are CSSUnitValue with same unit if (operands.every(op => op instanceof MockedCSSUnitValue)) { const unitValues = operands as MockedCSSUnitValue[]; const firstUnit = unitValues[0].unit; if (unitValues.every(uv => uv.unit === firstUnit)) { const sum = unitValues.reduce((acc, uv) => acc + uv.value, 0); return new MockedCSSUnitValue(sum, firstUnit); } } // Special case: if all are numbers, simplify if (operands.every(op => op instanceof MockedCSSUnitValue && op.unit === 'number')) { const sum = operands.reduce((acc, op) => acc + (op as MockedCSSUnitValue).value, 0); return new MockedCSSUnitValue(sum, 'number'); } // Check type compatibility let resultType = this.type(); for (let i = 1; i < operands.length; i++) { const opType = operands[i].type(); const addedType = addTypes(resultType, opType); if (addedType === null) { throw new TypeError('Incompatible types for addition'); } resultType = addedType; } return new MockedCSSMathSum(operands); } /** * Subtracts one or more values from this value */ sub(...values: CSSNumberish[]): CSSNumericValue { if (values.length === 0) return this; // Check for raw numbers - they should throw if (values.some(v => typeof v === 'number')) { throw new TypeError('Cannot subtract numbers directly. Use CSS.number() instead.'); } const negatedValues = values.map(v => { const numeric = rectifyNumberish(v); return numeric.negate(); }); return this.add(...negatedValues); } /** * Multiplies this value by one or more values */ mul(...values: CSSNumberish[]): CSSNumericValue { if (values.length === 0) return this; const operands = [this, ...values.map(v => rectifyNumberish(v, true))]; // Try to simplify if all are numbers or one unit with numbers if (operands.every(op => op instanceof MockedCSSUnitValue)) { const unitValues = operands as MockedCSSUnitValue[]; const numbers = unitValues.filter(uv => uv.unit === 'number'); const nonNumbers = unitValues.filter(uv => uv.unit !== 'number'); if (nonNumbers.length <= 1) { const numberProduct = numbers.reduce((acc, uv) => acc * uv.value, 1); if (nonNumbers.length === 0) { return new MockedCSSUnitValue(numberProduct, 'number'); } else { const singleUnit = nonNumbers[0]; return new MockedCSSUnitValue(numberProduct * singleUnit.value, singleUnit.unit); } } } return new MockedCSSMathProduct(operands); } /** * Divides this value by one or more values */ div(...values: CSSNumberish[]): CSSNumericValue { if (values.length === 0) return this; const invertedValues = values.map(v => { const numeric = rectifyNumberish(v, true); if (numeric instanceof MockedCSSUnitValue && numeric.value === 0) { throw new RangeError('Cannot divide by zero'); } return numeric.invert(); }); return this.mul(...invertedValues); } /** * Returns the minimum of this value and one or more other values */ min(...values: CSSNumberish[]): CSSNumericValue { if (values.length === 0) return this; // Per CSS Typed OM spec, throw TypeError for raw numbers instead of returning null // This follows the spec rather than quirky browser behavior, providing better type safety if (values.some(v => typeof v === 'number')) { throw new TypeError('Cannot use raw numbers in min(). Use CSS.number() instead.'); } const operands = [this, ...values.map(v => rectifyNumberish(v))]; // Try to simplify if all are CSSUnitValue with same unit if (operands.every(op => op instanceof MockedCSSUnitValue)) { const unitValues = operands as MockedCSSUnitValue[]; const firstUnit = unitValues[0].unit; if (unitValues.every(uv => uv.unit === firstUnit)) { const minValue = Math.min(...unitValues.map(uv => uv.value)); return new MockedCSSUnitValue(minValue, firstUnit); } } return new MockedCSSMathMin(operands); } /** * Returns the maximum of this value and one or more other values */ max(...values: CSSNumberish[]): CSSNumericValue { if (values.length === 0) return this; // Per CSS Typed OM spec, throw TypeError for raw numbers instead of returning null // This follows the spec rather than quirky browser behavior, providing better type safety if (values.some(v => typeof v === 'number')) { throw new TypeError('Cannot use raw numbers in max(). Use CSS.number() instead.'); } const operands = [this, ...values.map(v => rectifyNumberish(v))]; // Try to simplify if all are CSSUnitValue with same unit if (operands.every(op => op instanceof MockedCSSUnitValue)) { const unitValues = operands as MockedCSSUnitValue[]; const firstUnit = unitValues[0].unit; if (unitValues.every(uv => uv.unit === firstUnit)) { const maxValue = Math.max(...unitValues.map(uv => uv.value)); return new MockedCSSUnitValue(maxValue, firstUnit); } } return new MockedCSSMathMax(operands); } /** * Checks if this value equals one or more other values */ equals(...values: CSSNumberish[]): boolean { for (const value of values) { const numeric = rectifyNumberish(value); if (!this.isEqualTo(numeric)) { return false; } } return true; } /** * Converts this value to the specified unit */ to(unit: string): CSSUnitValue { if (!(this instanceof MockedCSSUnitValue)) { throw new TypeError('Cannot convert complex values to a single unit'); } const converted = convertUnit(this.value, this.unit, unit); if (converted === null) { throw new TypeError(`Cannot convert from '${this.unit}' to '${unit}'`); } return new MockedCSSUnitValue(converted, unit); } /** * Converts this value to a sum of the specified units */ toSum(...units: string[]): CSSMathSum { // For simplicity, if no units specified, return this as a sum if (units.length === 0) { if (this instanceof MockedCSSMathSum) { return this; } return new MockedCSSMathSum([this]); } // If this is a simple unit value, try to convert to requested units if (this instanceof MockedCSSUnitValue) { const results: MockedCSSUnitValue[] = []; const sourceUnit = this.unit; let foundCompatible = false; for (const targetUnit of units) { // Check if units are compatible const converted = convertUnit(this.value, sourceUnit, targetUnit); if (converted !== null && !foundCompatible) { // Put all value in the first compatible unit results.push(new MockedCSSUnitValue(converted, targetUnit)); foundCompatible = true; } else { // Incompatible unit or already used compatible unit, add zero results.push(new MockedCSSUnitValue(0, targetUnit)); } } if (!foundCompatible) { throw new TypeError(`Cannot convert ${sourceUnit} to any of the specified units`); } return new MockedCSSMathSum(results); } // For complex values, try to simplify first if (this instanceof MockedCSSMathSum) { // Group values by compatible units const groups = new Map<string, MockedCSSUnitValue[]>(); for (const value of this.values) { if (value instanceof MockedCSSUnitValue) { const key = getCanonicalUnit(value.unit); if (!groups.has(key)) { groups.set(key, []); } groups.get(key)?.push(value); } } const results: MockedCSSUnitValue[] = []; for (const targetUnit of units) { const canonicalUnit = getCanonicalUnit(targetUnit); const compatibleValues = groups.get(canonicalUnit) || []; if (compatibleValues.length > 0) { // Sum all compatible values and convert to target unit let sum = 0; for (const val of compatibleValues) { const converted = convertUnit(val.value, val.unit, targetUnit); if (converted !== null) { sum += converted; } } results.push(new MockedCSSUnitValue(sum, targetUnit)); groups.delete(canonicalUnit); } else { results.push(new MockedCSSUnitValue(0, targetUnit)); } } // Check if any groups remain (incompatible units) if (groups.size > 0) { throw new TypeError('Some values cannot be converted to the specified units'); } return new MockedCSSMathSum(results); } throw new TypeError('Cannot convert complex math expressions to sum'); } /** * Negates this value */ negate(): CSSNumericValue { if (this instanceof MockedCSSUnitValue) { return new MockedCSSUnitValue(-this.value, this.unit); } return new MockedCSSMathNegate(this); } /** * Inverts this value (1/value) */ invert(): CSSNumericValue { if (this instanceof MockedCSSUnitValue && this.unit === 'number') { if (this.value === 0) { throw new RangeError('Cannot invert zero'); } return new MockedCSSUnitValue(1 / this.value, 'number'); } return new MockedCSSMathInvert(this); } /** * Checks if this value is equal to another */ protected isEqualTo(other: MockedCSSNumericValue): boolean { if (this.constructor !== other.constructor) { return false; } if (this instanceof MockedCSSUnitValue && other instanceof MockedCSSUnitValue) { return this.value === other.value && this.unit === other.unit; } // For math values, check structure if (this instanceof MockedCSSMathSum && other instanceof MockedCSSMathSum) { return this.values.length === other.values.length && this.values.every((v, i) => v.isEqualTo(other.values[i])); } // Add more comparisons for other math types as needed return false; } // eslint-disable-next-line @typescript-eslint/no-unused-vars static parse(_cssText: string): MockedCSSNumericValue { throw new TypeError('CSS.parse is not available in browsers'); } } // ============================================================================ // CSSUnitValue // ============================================================================ /** * Represents a CSS value with a single numeric value and unit */ class MockedCSSUnitValue extends MockedCSSNumericValue implements CSSUnitValue { constructor(public value: number, public unit: string) { super(); // Validate numeric value - throw for NaN, Infinity, -Infinity if (typeof value !== 'number' || !isFinite(value)) { throw new TypeError('Invalid numeric value'); } // Map % to percent if (unit === '%') { this.unit = 'percent'; } // Validate unit if (this.unit !== 'number' && this.unit !== 'percent' && getBaseType(this.unit) === null) { throw new TypeError(`Invalid unit: ${unit}`); } } type(): CSSNumericType { return createTypeFromUnit(this.unit); } toString(): string { if (this.unit === 'number') { return String(this.value); } // Handle scientific notation for large numbers like browser if (Math.abs(this.value) >= 1000000) { const exp = this.value.toExponential(); // Pad exponent with zero to match browser format (e+06 instead of e+6) const formatted = exp.replace(/e\+(\d)$/, 'e+0$1'); return `${formatted}${this.unit}`; } return `${this.value}${this.unit}`; } // eslint-disable-next-line @typescript-eslint/no-unused-vars static parse(_cssText: string): MockedCSSUnitValue { // _cssText parameter required for interface compatibility throw new TypeError('CSS.parse is not available in browsers'); } } // ============================================================================ // CSSMathValue (Base for Math Operations) // ============================================================================ /** * Base class for CSS math expressions */ abstract class MockedCSSMathValue extends MockedCSSNumericValue implements CSSMathValue { abstract get operator(): CSSMathOperator; toString(): string { return `calc(${this.toCSSString()})`; } protected abstract toCSSString(): string; } // ============================================================================ // CSSMathSum // ============================================================================ /** * Represents a CSS calc() sum expression */ class MockedCSSMathSum extends MockedCSSMathValue implements CSSMathSum { readonly operator: CSSMathOperator = 'sum'; constructor(public values: MockedCSSNumericValue[]) { super(); if (values.length === 0) { throw new SyntaxError('CSSMathSum requires at least one value'); } } type(): CSSNumericType { let result = this.values[0].type(); for (let i = 1; i < this.values.length; i++) { const addedType = addTypes(result, this.values[i].type()); if (addedType === null) { throw new TypeError('Incompatible types in sum'); } result = addedType; } return result; } protected toCSSString(): string { return this.values.map((v, i) => { const str = v.toString(); if (i > 0) { if (str.startsWith('-')) { return `+ ${str}`; } else { return `+ ${str}`; } } return str; }).join(' '); } } // ============================================================================ // CSSMathProduct // ============================================================================ /** * Represents a CSS calc() product expression */ class MockedCSSMathProduct extends MockedCSSMathValue implements CSSMathProduct { readonly operator: CSSMathOperator = 'product'; constructor(public values: MockedCSSNumericValue[]) { super(); if (values.length === 0) { throw new SyntaxError('CSSMathProduct requires at least one value'); } } type(): CSSNumericType { let result = this.values[0].type(); for (let i = 1; i < this.values.length; i++) { result = multiplyTypes(result, this.values[i].type()); } return result; } protected toCSSString(): string { return this.values.map(v => v.toString()).join(' * '); } } // ============================================================================ // CSSMathNegate // ============================================================================ /** * Represents a CSS calc() negation */ class MockedCSSMathNegate extends MockedCSSMathValue implements CSSMathNegate { readonly operator: CSSMathOperator = 'negate'; constructor(public value: MockedCSSNumericValue) { super(); } type(): CSSNumericType { return this.value.type(); } protected toCSSString(): string { return `-${this.value.toString()}`; } } // ============================================================================ // CSSMathInvert // ============================================================================ /** * Represents a CSS calc() inversion (1/value) */ class MockedCSSMathInvert extends MockedCSSMathValue implements CSSMathInvert { readonly operator: CSSMathOperator = 'invert'; constructor(public value: MockedCSSNumericValue) { super(); } type(): CSSNumericType { const valueType = this.value.type(); const result: CSSNumericType = { length: -valueType.length, angle: -valueType.angle, time: -valueType.time, frequency: -valueType.frequency, resolution: -valueType.resolution, flex: -valueType.flex, percent: -valueType.percent }; return result; } protected toCSSString(): string { return `1 / ${this.value.toString()}`; } } // ============================================================================ // CSSMathMin // ============================================================================ /** * Represents a CSS min() expression */ class MockedCSSMathMin extends MockedCSSMathValue implements CSSMathMin { readonly operator: CSSMathOperator = 'min'; constructor(public values: MockedCSSNumericValue[]) { super(); if (values.length === 0) { throw new SyntaxError('CSSMathMin requires at least one value'); } } type(): CSSNumericType { let result = this.values[0].type(); for (let i = 1; i < this.values.length; i++) { const addedType = addTypes(result, this.values[i].type()); if (addedType === null) { throw new TypeError('Incompatible types in min()'); } result = addedType; } return result; } protected toCSSString(): string { return `min(${this.values.map(v => v.toString()).join(', ')})`; } toString(): string { return this.toCSSString(); } } // ============================================================================ // CSSMathMax // ============================================================================ /** * Represents a CSS max() expression */ class MockedCSSMathMax extends MockedCSSMathValue implements CSSMathMax { readonly operator: CSSMathOperator = 'max'; constructor(public values: MockedCSSNumericValue[]) { super(); if (values.length === 0) { throw new SyntaxError('CSSMathMax requires at least one value'); } } type(): CSSNumericType { let result = this.values[0].type(); for (let i = 1; i < this.values.length; i++) { const addedType = addTypes(result, this.values[i].type()); if (addedType === null) { throw new TypeError('Incompatible types in max()'); } result = addedType; } return result; } protected toCSSString(): string { return `max(${this.values.map(v => v.toString()).join(', ')})`; } toString(): string { return this.toCSSString(); } } // ============================================================================ // CSSMathClamp // ============================================================================ /** * Represents a CSS clamp() expression */ class MockedCSSMathClamp extends MockedCSSMathValue implements CSSMathClamp { readonly operator: CSSMathOperator = 'clamp'; constructor( public lower: MockedCSSNumericValue, public value: MockedCSSNumericValue, public upper: MockedCSSNumericValue ) { super(); // Type check compatibility const lowerType = lower.type(); const valueType = value.type(); const upperType = upper.type(); const resultType = addTypes(lowerType, valueType); if (resultType === null) { throw new TypeError('Incompatible types in clamp() lower and value'); } const finalType = addTypes(resultType, upperType); if (finalType === null) { throw new TypeError('Incompatible types in clamp()'); } } type(): CSSNumericType { // All three values must be compatible, so return the type of any of them return this.lower.type(); } protected toCSSString(): string { return `clamp(${this.lower.toString()}, ${this.value.toString()}, ${this.upper.toString()})`; } toString(): string { return this.toCSSString(); } } // ============================================================================ // CSS Factory Functions // ============================================================================ export const MockedCSS = { // Numbers and percentages number: (value: number) => new MockedCSSUnitValue(value, 'number'), percent: (value: number) => new MockedCSSUnitValue(value, 'percent'), // Length units - Absolute px: (value: number) => new MockedCSSUnitValue(value, 'px'), cm: (value: number) => new MockedCSSUnitValue(value, 'cm'), mm: (value: number) => new MockedCSSUnitValue(value, 'mm'), in: (value: number) => new MockedCSSUnitValue(value, 'in'), pt: (value: number) => new MockedCSSUnitValue(value, 'pt'), pc: (value: number) => new MockedCSSUnitValue(value, 'pc'), Q: (value: number) => new MockedCSSUnitValue(value, 'Q'), // Length units - Font-relative em: (value: number) => new MockedCSSUnitValue(value, 'em'), rem: (value: number) => new MockedCSSUnitValue(value, 'rem'), ex: (value: number) => new MockedCSSUnitValue(value, 'ex'), ch: (value: number) => new MockedCSSUnitValue(value, 'ch'), cap: (value: number) => new MockedCSSUnitValue(value, 'cap'), ic: (value: number) => new MockedCSSUnitValue(value, 'ic'), lh: (value: number) => new MockedCSSUnitValue(value, 'lh'), rlh: (value: number) => new MockedCSSUnitValue(value, 'rlh'), // Length units - Viewport-relative (original) vw: (value: number) => new MockedCSSUnitValue(value, 'vw'), vh: (value: number) => new MockedCSSUnitValue(value, 'vh'), vi: (value: number) => new MockedCSSUnitValue(value, 'vi'), vb: (value: number) => new MockedCSSUnitValue(value, 'vb'), vmin: (value: number) => new MockedCSSUnitValue(value, 'vmin'), vmax: (value: number) => new MockedCSSUnitValue(value, 'vmax'), // Length units - Viewport-relative (small) svw: (value: number) => new MockedCSSUnitValue(value, 'svw'), svh: (value: number) => new MockedCSSUnitValue(value, 'svh'), svi: (value: number) => new MockedCSSUnitValue(value, 'svi'), svb: (value: number) => new MockedCSSUnitValue(value, 'svb'), svmin: (value: number) => new MockedCSSUnitValue(value, 'svmin'), svmax: (value: number) => new MockedCSSUnitValue(value, 'svmax'), // Length units - Viewport-relative (large) lvw: (value: number) => new MockedCSSUnitValue(value, 'lvw'), lvh: (value: number) => new MockedCSSUnitValue(value, 'lvh'), lvi: (value: number) => new MockedCSSUnitValue(value, 'lvi'), lvb: (value: number) => new MockedCSSUnitValue(value, 'lvb'), lvmin: (value: number) => new MockedCSSUnitValue(value, 'lvmin'), lvmax: (value: number) => new MockedCSSUnitValue(value, 'lvmax'), // Length units - Viewport-relative (dynamic) dvw: (value: number) => new MockedCSSUnitValue(value, 'dvw'), dvh: (value: number) => new MockedCSSUnitValue(value, 'dvh'), dvi: (value: number) => new MockedCSSUnitValue(value, 'dvi'), dvb: (value: number) => new MockedCSSUnitValue(value, 'dvb'), dvmin: (value: number) => new MockedCSSUnitValue(value, 'dvmin'), dvmax: (value: number) => new MockedCSSUnitValue(value, 'dvmax'), // Length units - Container query cqw: (value: number) => new MockedCSSUnitValue(value, 'cqw'), cqh: (value: number) => new MockedCSSUnitValue(value, 'cqh'), cqi: (value: number) => new MockedCSSUnitValue(value, 'cqi'), cqb: (value: number) => new MockedCSSUnitValue(value, 'cqb'), cqmin: (value: number) => new MockedCSSUnitValue(value, 'cqmin'), cqmax: (value: number) => new MockedCSSUnitValue(value, 'cqmax'), // Angle units deg: (value: number) => new MockedCSSUnitValue(value, 'deg'), rad: (value: number) => new MockedCSSUnitValue(value, 'rad'), grad: (value: number) => new MockedCSSUnitValue(value, 'grad'), turn: (value: number) => new MockedCSSUnitValue(value, 'turn'), // Time units s: (value: number) => new MockedCSSUnitValue(value, 's'), ms: (value: number) => new MockedCSSUnitValue(value, 'ms'), // Frequency units Hz: (value: number) => new MockedCSSUnitValue(value, 'Hz'), kHz: (value: number) => new MockedCSSUnitValue(value, 'kHz'), // Resolution units dpi: (value: number) => new MockedCSSUnitValue(value, 'dpi'), dpcm: (value: number) => new MockedCSSUnitValue(value, 'dpcm'), dppx: (value: number) => new MockedCSSUnitValue(value, 'dppx'), // Flex units fr: (value: number) => new MockedCSSUnitValue(value, 'fr'), }; // ============================================================================ // Initialization Function // ============================================================================ /** * Initializes CSS Typed OM mocks in the global environment */ export function initCSSTypedOM(): void { if (!isJsdomEnv()) { throw new WrongEnvironmentError(); } // Install the classes on globalThis Object.defineProperty(globalThis, 'CSSNumericValue', { writable: true, configurable: true, value: MockedCSSNumericValue, }); Object.defineProperty(globalThis, 'CSSUnitValue', { writable: true, configurable: true, value: MockedCSSUnitValue, }); Object.defineProperty(globalThis, 'CSSMathValue', { writable: true, configurable: true, value: MockedCSSMathValue, }); Object.defineProperty(globalThis, 'CSSMathSum', { writable: true, configurable: true, value: MockedCSSMathSum, }); Object.defineProperty(globalThis, 'CSSMathProduct', { writable: true, configurable: true, value: MockedCSSMathProduct, }); Object.defineProperty(globalThis, 'CSSMathNegate', { writable: true, configurable: true, value: MockedCSSMathNegate, }); Object.defineProperty(globalThis, 'CSSMathInvert', { writable: true, configurable: true, value: MockedCSSMathInvert, }); Object.defineProperty(globalThis, 'CSSMathMin', { writable: true, configurable: true, value: MockedCSSMathMin, }); Object.defineProperty(globalThis, 'CSSMathMax', { writable: true, configurable: true, value: MockedCSSMathMax, }); Object.defineProperty(globalThis, 'CSSMathClamp', { writable: true, configurable: true, value: MockedCSSMathClamp, }); // Install CSS factory functions Object.defineProperty(globalThis, 'CSS', { writable: true, configurable: true, value: MockedCSS, }); } // ============================================================================ // Exports // ============================================================================ export { MockedCSSNumericValue, MockedCSSUnitValue, MockedCSSMathValue, MockedCSSMathSum, MockedCSSMathProduct, MockedCSSMathNegate, MockedCSSMathInvert, MockedCSSMathMin, MockedCSSMathMax, MockedCSSMathClamp, }; // Re-export for compatibility with existing code export { initCSSTypedOM as mockCSSTypedOM };