diff --git a/packages/happy-dom/README.md b/packages/happy-dom/README.md index 53415248d..3a34e699f 100644 --- a/packages/happy-dom/README.md +++ b/packages/happy-dom/README.md @@ -258,7 +258,11 @@ const window = new Window({ disableJavaScriptEvaluation: true, disableCSSFileLoading: true, disableIframePageLoading: true, - enableFileSystemHttpRequests: true + enableFileSystemHttpRequests: true, + device: { + mediaType: 'print', + prefersColorScheme = 'dark + } } }); ``` @@ -273,6 +277,8 @@ window.happyDOM.settings.disableJavaScriptEvaluation = true; window.happyDOM.settings.disableCSSFileLoading = true; window.happyDOM.settings.disableIframePageLoading = true; window.happyDOM.settings.enableFileSystemHttpRequests = true; +window.happyDOM.settings.device.mediaType = 'print'; +window.happyDOM.settings.device.prefersColorScheme = 'dark'; ``` **disableJavaScriptFileLoading** @@ -295,6 +301,14 @@ Set it to "true" to disable page loading in HTMLIFrameElement. Defaults to "fals Set it to "true" to enable file system HTTP requests using XMLHttpRequest. Defaults to "false". +**device.mediaType** + +Used by media queries. Acceptable values are "screen" or "print". Defaults to "screen". + +**device.prefersColorScheme** + +Used by media queries. Acceptable values are "light" or "dark". Defaults to "dark". + # Performance | Operation | JSDOM | Happy DOM | diff --git a/packages/happy-dom/src/css/CSSParser.ts b/packages/happy-dom/src/css/CSSParser.ts index d96a78fae..adb5a5be0 100644 --- a/packages/happy-dom/src/css/CSSParser.ts +++ b/packages/happy-dom/src/css/CSSParser.ts @@ -33,10 +33,13 @@ export default class CSSParser { if (match[0] === '{') { const selectorText = css.substring(lastIndex, match.index).trim(); - if (selectorText.startsWith('@keyframes')) { + if ( + selectorText.startsWith('@keyframes') || + selectorText.startsWith('@-webkit-keyframes') + ) { const newRule = new CSSKeyframesRule(); - (newRule.name) = selectorText.replace('@keyframes ', ''); + (newRule.name) = selectorText.replace(/@(-webkit-){0,1}keyframes +/, ''); newRule.parentStyleSheet = parentStyleSheet; cssRules.push(newRule); parentRule = newRule; @@ -51,22 +54,34 @@ export default class CSSParser { newRule.parentStyleSheet = parentStyleSheet; cssRules.push(newRule); parentRule = newRule; - } else if (selectorText.startsWith('@container')) { - const conditionText = selectorText.replace(/@container */, ''); + } else if ( + selectorText.startsWith('@container') || + selectorText.startsWith('@-webkit-container') + ) { + const conditionText = selectorText.replace(/@(-webkit-){0,1}container +/, ''); const newRule = new CSSContainerRule(); (newRule.conditionText) = conditionText; newRule.parentStyleSheet = parentStyleSheet; cssRules.push(newRule); parentRule = newRule; - } else if (selectorText.startsWith('@supports')) { - const conditionText = selectorText.replace(/@supports */, ''); + } else if ( + selectorText.startsWith('@supports') || + selectorText.startsWith('@-webkit-supports') + ) { + const conditionText = selectorText.replace(/@(-webkit-){0,1}supports +/, ''); const newRule = new CSSSupportsRule(); (newRule.conditionText) = conditionText; newRule.parentStyleSheet = parentStyleSheet; cssRules.push(newRule); parentRule = newRule; + } else if (selectorText.startsWith('@')) { + // Unknown rule. + // We will create a new rule to let it grab its content, but we will not add it to the cssRules array. + const newRule = new CSSRule(); + newRule.parentStyleSheet = parentStyleSheet; + parentRule = newRule; } else if (parentRule && parentRule.type === CSSRule.KEYFRAMES_RULE) { const newRule = new CSSKeyframeRule(); (newRule.keyText) = selectorText.trim(); diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index 22547789d..573fb523b 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -3,8 +3,8 @@ import Attr from '../../nodes/attr/Attr'; import CSSRule from '../CSSRule'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum'; import DOMException from '../../exception/DOMException'; -import CSSStyleDeclarationElementStyle from './utilities/CSSStyleDeclarationElementStyle'; -import CSSStyleDeclarationPropertyManager from './utilities/CSSStyleDeclarationPropertyManager'; +import CSSStyleDeclarationElementStyle from './element-style/CSSStyleDeclarationElementStyle'; +import CSSStyleDeclarationPropertyManager from './property-manager/CSSStyleDeclarationPropertyManager'; /** * CSS Style Declaration. diff --git a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationCSSParser.ts b/packages/happy-dom/src/css/declaration/css-parser/CSSStyleDeclarationCSSParser.ts similarity index 100% rename from packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationCSSParser.ts rename to packages/happy-dom/src/css/declaration/css-parser/CSSStyleDeclarationCSSParser.ts diff --git a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts similarity index 59% rename from packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts rename to packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 37091db6d..edbc726a2 100644 --- a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -3,18 +3,22 @@ import IElement from '../../../nodes/element/IElement'; import IDocument from '../../../nodes/document/IDocument'; import IHTMLStyleElement from '../../../nodes/html-style-element/IHTMLStyleElement'; import INodeList from '../../../nodes/node/INodeList'; -import CSSStyleDeclarationPropertyManager from './CSSStyleDeclarationPropertyManager'; +import CSSStyleDeclarationPropertyManager from '../property-manager/CSSStyleDeclarationPropertyManager'; import NodeTypeEnum from '../../../nodes/node/NodeTypeEnum'; import CSSRuleTypeEnum from '../../CSSRuleTypeEnum'; import CSSMediaRule from '../../rules/CSSMediaRule'; import CSSRule from '../../CSSRule'; import CSSStyleRule from '../../rules/CSSStyleRule'; -import CSSStyleDeclarationElementDefaultCSS from './CSSStyleDeclarationElementDefaultCSS'; -import CSSStyleDeclarationElementInheritedProperties from './CSSStyleDeclarationElementInheritedProperties'; -import CSSStyleDeclarationCSSParser from './CSSStyleDeclarationCSSParser'; +import CSSStyleDeclarationElementDefaultCSS from './config/CSSStyleDeclarationElementDefaultCSS'; +import CSSStyleDeclarationElementInheritedProperties from './config/CSSStyleDeclarationElementInheritedProperties'; +import CSSStyleDeclarationElementMeasurementProperties from './config/CSSStyleDeclarationElementMeasurementProperties'; +import CSSStyleDeclarationCSSParser from '../css-parser/CSSStyleDeclarationCSSParser'; import QuerySelector from '../../../query-selector/QuerySelector'; +import CSSMeasurementConverter from '../measurement-converter/CSSMeasurementConverter'; +import MediaQueryList from '../../../match-media/MediaQueryList'; const CSS_VARIABLE_REGEXP = /var\( *(--[^) ]+)\)/g; +const CSS_MEASUREMENT_REGEXP = /[0-9.]+(px|rem|em|vw|vh|%|vmin|vmax|cm|mm|in|pt|pc|Q)/g; type IStyleAndElement = { element: IElement | IShadowRoot | IDocument; @@ -160,98 +164,79 @@ export default class CSSStyleDeclarationElementStyle { // Concatenates all parent element CSS to one string. const targetElement = parentElements[parentElements.length - 1]; - let inheritedCSSText = ''; + const propertyManager = new CSSStyleDeclarationPropertyManager(); + const cssVariables: { [k: string]: string } = {}; + let rootFontSize: string | number = 16; + let parentFontSize: string | number = 16; for (const parentElement of parentElements) { - if (parentElement !== targetElement) { - parentElement.cssTexts.sort((a, b) => a.priorityWeight - b.priorityWeight); - - if (CSSStyleDeclarationElementDefaultCSS[(parentElement.element).tagName]) { - inheritedCSSText += - CSSStyleDeclarationElementDefaultCSS[(parentElement.element).tagName]; - } + parentElement.cssTexts.sort((a, b) => a.priorityWeight - b.priorityWeight); - for (const cssText of parentElement.cssTexts) { - inheritedCSSText += cssText.cssText; - } - - if (parentElement.element['_attributes']['style']?.value) { - inheritedCSSText += parentElement.element['_attributes']['style'].value; - } + let elementCSSText = ''; + if (CSSStyleDeclarationElementDefaultCSS[(parentElement.element).tagName]) { + elementCSSText += + CSSStyleDeclarationElementDefaultCSS[(parentElement.element).tagName]; } - } - - const cssVariables: { [k: string]: string } = {}; - const properties = {}; - let targetCSSText = - CSSStyleDeclarationElementDefaultCSS[(targetElement.element).tagName] || ''; - - targetElement.cssTexts.sort((a, b) => a.priorityWeight - b.priorityWeight); - - for (const cssText of targetElement.cssTexts) { - targetCSSText += cssText.cssText; - } - if (targetElement.element['_attributes']['style']?.value) { - targetCSSText += targetElement.element['_attributes']['style'].value; - } - - const combinedCSSText = inheritedCSSText + targetCSSText; - - if (this.cache.propertyManager && this.cache.cssText === combinedCSSText) { - return this.cache.propertyManager; - } - - // Parses the parent element CSS and stores CSS variables and inherited properties. - CSSStyleDeclarationCSSParser.parse(inheritedCSSText, (name, value, important) => { - if (name.startsWith('--')) { - const cssValue = this.getCSSValue(value, cssVariables); - if (cssValue) { - cssVariables[name] = cssValue; - } - return; + for (const cssText of parentElement.cssTexts) { + elementCSSText += cssText.cssText; } - if (CSSStyleDeclarationElementInheritedProperties[name]) { - const cssValue = this.getCSSValue(value, cssVariables); - if (cssValue && (!properties[name]?.important || important)) { - properties[name] = { - value: cssValue, - important - }; - } + if (parentElement.element['_attributes']['style']?.value) { + elementCSSText += parentElement.element['_attributes']['style'].value; } - }); - - // Parses the target element CSS. - CSSStyleDeclarationCSSParser.parse(targetCSSText, (name, value, important) => { - if (name.startsWith('--')) { - const cssValue = this.getCSSValue(value, cssVariables); - if (cssValue && (!properties[name]?.important || important)) { - cssVariables[name] = cssValue; - properties[name] = { - value, - important - }; + + CSSStyleDeclarationCSSParser.parse(elementCSSText, (name, value, important) => { + if (name.startsWith('--')) { + const cssValue = this.parseCSSVariablesInValue(value, cssVariables); + if (cssValue) { + cssVariables[name] = cssValue; + } + return; } - } else { - const cssValue = this.getCSSValue(value, cssVariables); - if (cssValue && (!properties[name]?.important || important)) { - properties[name] = { - value: cssValue, - important - }; + + if ( + CSSStyleDeclarationElementInheritedProperties[name] || + parentElement === targetElement + ) { + const cssValue = this.parseCSSVariablesInValue(value, cssVariables); + if (cssValue && (!propertyManager.get(name)?.important || important)) { + propertyManager.set(name, cssValue, important); + if (name === 'font' || name === 'font-size') { + const fontSize = propertyManager.properties['font-size']; + if (fontSize !== null) { + const parsedValue = this.parseMeasurementsInValue({ + value: fontSize.value, + rootFontSize, + parentFontSize, + parentSize: parentFontSize + }); + if ((parentElement.element).tagName === 'HTML') { + rootFontSize = parsedValue; + } else if (parentElement !== targetElement) { + parentFontSize = parsedValue; + } + } + } + } } - } - }); + }); + } - const propertyManager = new CSSStyleDeclarationPropertyManager(); + for (const name of CSSStyleDeclarationElementMeasurementProperties) { + const property = propertyManager.properties[name]; + if (property) { + property.value = this.parseMeasurementsInValue({ + value: property.value, + rootFontSize, + parentFontSize, - for (const name of Object.keys(properties)) { - propertyManager.set(name, properties[name].value, properties[name].important); + // TODO: Only "font-size" is supported when using percentage values. Add support for other properties. + parentSize: name === 'font-size' ? parentFontSize : 0 + }); + } } - this.cache.cssText = combinedCSSText; this.cache.propertyManager = propertyManager; return propertyManager; @@ -274,7 +259,7 @@ export default class CSSStyleDeclarationElementStyle { return; } - const defaultView = options.elements[0].element.ownerDocument.defaultView; + const ownerWindow = this.element.ownerDocument.defaultView; for (const rule of options.cssRules) { if (rule.type === CSSRuleTypeEnum.styleRule) { @@ -289,10 +274,6 @@ export default class CSSStyleDeclarationElementStyle { } } else { for (const element of options.elements) { - // Skip @-rules. - if (selectorText.startsWith('@')) { - continue; - } const matchResult = QuerySelector.match(element.element, selectorText); if (matchResult) { element.cssTexts.push({ @@ -305,7 +286,12 @@ export default class CSSStyleDeclarationElementStyle { } } else if ( rule.type === CSSRuleTypeEnum.mediaRule && - defaultView.matchMedia((rule).conditionText).matches + // TODO: We need to send in a predfined root font size as it will otherwise be calculated using Window.getComputedStyle(), which will cause a never ending loop. Is there another solution? + new MediaQueryList({ + ownerWindow, + media: (rule).conditionText, + rootFontSize: this.element.tagName === 'HTML' ? 16 : null + }).matches ) { this.parseCSSRules({ elements: options.elements, @@ -317,23 +303,60 @@ export default class CSSStyleDeclarationElementStyle { } /** - * Returns CSS value. + * Parses CSS variables in a value. * * @param value Value. * @param cssVariables CSS variables. * @returns CSS value. */ - private getCSSValue(value: string, cssVariables: { [k: string]: string }): string { + private parseCSSVariablesInValue(value: string, cssVariables: { [k: string]: string }): string { const regexp = new RegExp(CSS_VARIABLE_REGEXP); let newValue = value; let match; + while ((match = regexp.exec(value)) !== null) { - const cssVariableValue = cssVariables[match[1]]; - if (!cssVariableValue) { - return null; + newValue = newValue.replace(match[0], cssVariables[match[1]] || ''); + } + + return newValue; + } + + /** + * Parses measurements in a value. + * + * @param options Options. + * @param options.value Value. + * @param options.rootFontSize Root font size. + * @param options.parentFontSize Parent font size. + * @param [options.parentSize] Parent width. + * @returns CSS value. + */ + private parseMeasurementsInValue(options: { + value: string; + rootFontSize: string | number; + parentFontSize: string | number; + parentSize: string | number; + }): string { + const regexp = new RegExp(CSS_MEASUREMENT_REGEXP); + let newValue = options.value; + let match; + + while ((match = regexp.exec(options.value)) !== null) { + if (match[1] !== 'px') { + const valueInPixels = CSSMeasurementConverter.toPixels({ + ownerWindow: this.element.ownerDocument.defaultView, + value: match[0], + rootFontSize: options.rootFontSize, + parentFontSize: options.parentFontSize, + parentSize: options.parentSize + }); + + if (valueInPixels !== null) { + newValue = newValue.replace(match[0], valueInPixels + 'px'); + } } - newValue = newValue.replace(match[0], cssVariableValue); } + return newValue; } } diff --git a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementDefaultCSS.ts b/packages/happy-dom/src/css/declaration/element-style/config/CSSStyleDeclarationElementDefaultCSS.ts similarity index 97% rename from packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementDefaultCSS.ts rename to packages/happy-dom/src/css/declaration/element-style/config/CSSStyleDeclarationElementDefaultCSS.ts index b745e01ff..fd79652b2 100644 --- a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementDefaultCSS.ts +++ b/packages/happy-dom/src/css/declaration/element-style/config/CSSStyleDeclarationElementDefaultCSS.ts @@ -69,7 +69,7 @@ export default { HEADER: 'display: block;', HGROUP: 'display: block;', HR: 'display: block;', - HTML: 'display: block;direction: ltr;', + HTML: 'display: block;direction: ltr;font: 16px "Times New Roman";', I: '', IFRAME: '', INS: '', diff --git a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementInheritedProperties.ts b/packages/happy-dom/src/css/declaration/element-style/config/CSSStyleDeclarationElementInheritedProperties.ts similarity index 100% rename from packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementInheritedProperties.ts rename to packages/happy-dom/src/css/declaration/element-style/config/CSSStyleDeclarationElementInheritedProperties.ts diff --git a/packages/happy-dom/src/css/declaration/element-style/config/CSSStyleDeclarationElementMeasurementProperties.ts b/packages/happy-dom/src/css/declaration/element-style/config/CSSStyleDeclarationElementMeasurementProperties.ts new file mode 100644 index 000000000..dc70b04b8 --- /dev/null +++ b/packages/happy-dom/src/css/declaration/element-style/config/CSSStyleDeclarationElementMeasurementProperties.ts @@ -0,0 +1,41 @@ +export default [ + 'background-position-x', + 'background-position-y', + 'background-size', + 'border-image-outset', + 'border-top-width', + 'border-right-width', + 'border-bottom-width', + 'border-left-width', + 'border-top-left-radius', + 'border-top-right-radius', + 'border-bottom-right-radius', + 'border-bottom-left-radius', + 'border-image-width', + 'clip', + 'font-size', + 'padding-top', + 'padding-right', + 'padding-bottom', + 'padding-left', + 'margin-top', + 'margin-right', + 'margin-bottom', + 'margin-left', + 'width', + 'height', + 'min-width', + 'min-height', + 'max-width', + 'max-height', + 'top', + 'right', + 'bottom', + 'left', + 'outline-width', + 'outline-offset', + 'letter-spacing', + 'word-spacing', + 'text-indent', + 'line-height' +]; diff --git a/packages/happy-dom/src/css/declaration/measurement-converter/CSSMeasurementConverter.ts b/packages/happy-dom/src/css/declaration/measurement-converter/CSSMeasurementConverter.ts new file mode 100644 index 000000000..626e02095 --- /dev/null +++ b/packages/happy-dom/src/css/declaration/measurement-converter/CSSMeasurementConverter.ts @@ -0,0 +1,81 @@ +import IWindow from '../../../window/IWindow'; + +/** + * CSS Measurement Converter. + */ +export default class CSSMeasurementConverter { + /** + * Returns measurement in pixels. + * + * @param options Options. + * @param options.ownerWindow Owner window. + * @param options.value Measurement (e.g. "10px", "10rem" or "10em"). + * @param options.rootFontSize Root font size in pixels. + * @param options.parentFontSize Parent font size in pixels. + * @param [options.parentSize] Parent size in pixels. + * @returns Measurement in pixels. + */ + public static toPixels(options: { + ownerWindow: IWindow; + value: string; + rootFontSize: string | number; + parentFontSize: string | number; + parentSize?: string | number | null; + }): number | null { + const value = parseFloat(options.value); + const unit = options.value.replace(value.toString(), ''); + + if (isNaN(value)) { + return null; + } + + switch (unit) { + case 'px': + return value; + case 'rem': + return this.round(value * parseFloat(options.rootFontSize)); + case 'em': + return this.round(value * parseFloat(options.parentFontSize)); + case 'vw': + return this.round((value * options.ownerWindow.innerWidth) / 100); + case 'vh': + return this.round((value * options.ownerWindow.innerHeight) / 100); + case '%': + return options.parentSize !== undefined && options.parentSize !== null + ? this.round((value * parseFloat(options.parentSize)) / 100) + : null; + case 'vmin': + return this.round( + (value * Math.min(options.ownerWindow.innerWidth, options.ownerWindow.innerHeight)) / 100 + ); + case 'vmax': + return ( + (value * Math.max(options.ownerWindow.innerWidth, options.ownerWindow.innerHeight)) / 100 + ); + case 'cm': + return this.round(value * 37.7812); + case 'mm': + return this.round(value * 3.7781); + case 'in': + return this.round(value * 96); + case 'pt': + return this.round(value * 1.3281); + case 'pc': + return this.round(value * 16); + case 'Q': + return this.round(value * 0.945); + default: + return null; + } + } + + /** + * Rounds the number with 4 decimals. + * + * @param value Value. + * @returns Rounded value. + */ + public static round(value: number): number { + return Math.round(value * 10000) / 10000; + } +} diff --git a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationPropertyGetParser.ts b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyGetParser.ts similarity index 94% rename from packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationPropertyGetParser.ts rename to packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyGetParser.ts index 1270d51ae..36d9a4d6c 100644 --- a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationPropertyGetParser.ts +++ b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyGetParser.ts @@ -35,6 +35,57 @@ export default class CSSStyleDeclarationPropertyGetParser { ); } + /** + * Returns outline. + * + * @param properties Properties. + * @returns Property value + */ + public static getOutline(properties: { + [k: string]: ICSSStyleDeclarationPropertyValue; + }): ICSSStyleDeclarationPropertyValue { + if ( + !properties['outline-color']?.value || + !properties['outline-style']?.value || + !properties['outline-width']?.value + ) { + return null; + } + + const important = + properties['outline-color'].important && + properties['outline-style'].important && + properties['outline-width'].important; + + if ( + CSSStyleDeclarationValueParser.getGlobalExceptInitial(properties['outline-width'].value) && + properties['outline-width'].value === properties['outline-style'].value && + properties['outline-width'].value === properties['outline-color'].value + ) { + return { + important, + value: properties['outline-width'].value + }; + } + + const values = []; + + if (!CSSStyleDeclarationValueParser.getInitial(properties['outline-color']?.value)) { + values.push(properties['outline-color'].value); + } + if (!CSSStyleDeclarationValueParser.getInitial(properties['outline-style']?.value)) { + values.push(properties['outline-style'].value); + } + if (!CSSStyleDeclarationValueParser.getInitial(properties['outline-width'].value)) { + values.push(properties['outline-width'].value); + } + + return { + important, + value: values.join(' ') + }; + } + /** * Returns border. * diff --git a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationPropertyManager.ts b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyManager.ts similarity index 93% rename from packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationPropertyManager.ts rename to packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyManager.ts index 2be3afc71..4b4e12a39 100644 --- a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationPropertyManager.ts +++ b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertyManager.ts @@ -2,7 +2,7 @@ import ICSSStyleDeclarationPropertyValue from './ICSSStyleDeclarationPropertyVal import CSSStyleDeclarationPropertySetParser from './CSSStyleDeclarationPropertySetParser'; import CSSStyleDeclarationValueParser from './CSSStyleDeclarationValueParser'; import CSSStyleDeclarationPropertyGetParser from './CSSStyleDeclarationPropertyGetParser'; -import CSSStyleDeclarationCSSParser from './CSSStyleDeclarationCSSParser'; +import CSSStyleDeclarationCSSParser from '../css-parser/CSSStyleDeclarationCSSParser'; const TO_STRING_SHORTHAND_PROPERTIES = [ ['margin'], @@ -44,7 +44,7 @@ export default class CSSStyleDeclarationPropertyManager { * @param name Property name. * @returns Property value. */ - public get(name: string): ICSSStyleDeclarationPropertyValue { + public get(name: string): ICSSStyleDeclarationPropertyValue | null { if (this.properties[name]) { return this.properties[name]; } @@ -73,6 +73,8 @@ export default class CSSStyleDeclarationPropertyManager { return CSSStyleDeclarationPropertyGetParser.getBorderRadius(this.properties); case 'border-image': return CSSStyleDeclarationPropertyGetParser.getBorderImage(this.properties); + case 'outline': + return CSSStyleDeclarationPropertyGetParser.getOutline(this.properties); case 'background': return CSSStyleDeclarationPropertyGetParser.getBackground(this.properties); case 'background-position': @@ -186,6 +188,11 @@ export default class CSSStyleDeclarationPropertyManager { delete this.properties['border-bottom-right-radius']; delete this.properties['border-bottom-left-radius']; break; + case 'outline': + delete this.properties['outline-color']; + delete this.properties['outline-style']; + delete this.properties['outline-width']; + break; case 'background': delete this.properties['background-color']; delete this.properties['background-image']; @@ -348,6 +355,24 @@ export default class CSSStyleDeclarationPropertyManager { case 'border-collapse': properties = CSSStyleDeclarationPropertySetParser.getBorderCollapse(value, important); break; + case 'outline': + properties = CSSStyleDeclarationPropertySetParser.getOutline(value, important); + break; + case 'outline-width': + properties = CSSStyleDeclarationPropertySetParser.getOutlineWidth(value, important); + break; + case 'outline-style': + properties = CSSStyleDeclarationPropertySetParser.getOutlineStyle(value, important); + break; + case 'outline-color': + properties = CSSStyleDeclarationPropertySetParser.getOutlineColor(value, important); + break; + case 'letter-spacing': + properties = CSSStyleDeclarationPropertySetParser.getLetterSpacing(value, important); + break; + case 'word-spacing': + properties = CSSStyleDeclarationPropertySetParser.getWordSpacing(value, important); + break; case 'clear': properties = CSSStyleDeclarationPropertySetParser.getClear(value, important); break; @@ -429,6 +454,9 @@ export default class CSSStyleDeclarationPropertyManager { case 'width': properties = CSSStyleDeclarationPropertySetParser.getWidth(value, important); break; + case 'height': + properties = CSSStyleDeclarationPropertySetParser.getHeight(value, important); + break; case 'top': properties = CSSStyleDeclarationPropertySetParser.getTop(value, important); break; @@ -462,6 +490,9 @@ export default class CSSStyleDeclarationPropertyManager { case 'line-height': properties = CSSStyleDeclarationPropertySetParser.getLineHeight(value, important); break; + case 'text-indent': + properties = CSSStyleDeclarationPropertySetParser.getTextIndent(value, important); + break; case 'font-family': properties = CSSStyleDeclarationPropertySetParser.getFontFamily(value, important); break; @@ -477,6 +508,7 @@ export default class CSSStyleDeclarationPropertyManager { case 'visibility': properties = CSSStyleDeclarationPropertySetParser.getVisibility(value, important); break; + default: const trimmedValue = value.trim(); if (trimmedValue) { diff --git a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationPropertySetParser.ts b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts similarity index 92% rename from packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationPropertySetParser.ts rename to packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts index 9ba8c931a..48f28e2d4 100644 --- a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationPropertySetParser.ts +++ b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationPropertySetParser.ts @@ -174,6 +174,66 @@ export default class CSSStyleDeclarationPropertySetParser { return null; } + /** + * Returns letter spacing. + * + * @param value Value. + * @param important Important. + * @returns Property values + */ + public static getLetterSpacing( + value: string, + important: boolean + ): { + [key: string]: ICSSStyleDeclarationPropertyValue; + } { + const parsedValue = + CSSStyleDeclarationValueParser.getVariable(value) || + CSSStyleDeclarationValueParser.getGlobal(value) || + CSSStyleDeclarationValueParser.getContentMeasurement(value); + return parsedValue ? { 'letter-spacing': { value: parsedValue, important } } : null; + } + + /** + * Returns word spacing. + * + * @param value Value. + * @param important Important. + * @returns Property values + */ + public static getWordSpacing( + value: string, + important: boolean + ): { + [key: string]: ICSSStyleDeclarationPropertyValue; + } { + const parsedValue = + CSSStyleDeclarationValueParser.getVariable(value) || + CSSStyleDeclarationValueParser.getGlobal(value) || + CSSStyleDeclarationValueParser.getContentMeasurement(value); + return parsedValue ? { 'word-spacing': { value: parsedValue, important } } : null; + } + + /** + * Returns text indent. + * + * @param value Value. + * @param important Important. + * @returns Property values + */ + public static getTextIndent( + value: string, + important: boolean + ): { + [key: string]: ICSSStyleDeclarationPropertyValue; + } { + const parsedValue = + CSSStyleDeclarationValueParser.getVariable(value) || + CSSStyleDeclarationValueParser.getGlobal(value) || + CSSStyleDeclarationValueParser.getContentMeasurement(value); + return parsedValue ? { 'text-indent': { value: parsedValue, important } } : null; + } + /** * Returns width. * @@ -187,16 +247,33 @@ export default class CSSStyleDeclarationPropertySetParser { ): { [key: string]: ICSSStyleDeclarationPropertyValue; } { - const variable = CSSStyleDeclarationValueParser.getVariable(value); - if (variable) { - return { width: { value: variable, important } }; - } const parsedValue = + CSSStyleDeclarationValueParser.getVariable(value) || CSSStyleDeclarationValueParser.getGlobal(value) || CSSStyleDeclarationValueParser.getContentMeasurement(value); return parsedValue ? { width: { value: parsedValue, important } } : null; } + /** + * Returns height. + * + * @param value Value. + * @param important Important. + * @returns Property values + */ + public static getHeight( + value: string, + important: boolean + ): { + [key: string]: ICSSStyleDeclarationPropertyValue; + } { + const parsedValue = + CSSStyleDeclarationValueParser.getVariable(value) || + CSSStyleDeclarationValueParser.getGlobal(value) || + CSSStyleDeclarationValueParser.getContentMeasurement(value); + return parsedValue ? { height: { value: parsedValue, important } } : null; + } + /** * Returns top. * @@ -210,11 +287,8 @@ export default class CSSStyleDeclarationPropertySetParser { ): { [key: string]: ICSSStyleDeclarationPropertyValue; } { - const variable = CSSStyleDeclarationValueParser.getVariable(value); - if (variable) { - return { top: { value: variable, important } }; - } const parsedValue = + CSSStyleDeclarationValueParser.getVariable(value) || CSSStyleDeclarationValueParser.getGlobal(value) || CSSStyleDeclarationValueParser.getContentMeasurement(value); return parsedValue ? { top: { value: parsedValue, important } } : null; @@ -233,11 +307,8 @@ export default class CSSStyleDeclarationPropertySetParser { ): { [key: string]: ICSSStyleDeclarationPropertyValue; } { - const variable = CSSStyleDeclarationValueParser.getVariable(value); - if (variable) { - return { right: { value: variable, important } }; - } const parsedValue = + CSSStyleDeclarationValueParser.getVariable(value) || CSSStyleDeclarationValueParser.getGlobal(value) || CSSStyleDeclarationValueParser.getContentMeasurement(value); return parsedValue ? { right: { value: parsedValue, important } } : null; @@ -256,11 +327,8 @@ export default class CSSStyleDeclarationPropertySetParser { ): { [key: string]: ICSSStyleDeclarationPropertyValue; } { - const variable = CSSStyleDeclarationValueParser.getVariable(value); - if (variable) { - return { bottom: { value: variable, important } }; - } const parsedValue = + CSSStyleDeclarationValueParser.getVariable(value) || CSSStyleDeclarationValueParser.getGlobal(value) || CSSStyleDeclarationValueParser.getContentMeasurement(value); return parsedValue ? { bottom: { value: parsedValue, important } } : null; @@ -279,11 +347,8 @@ export default class CSSStyleDeclarationPropertySetParser { ): { [key: string]: ICSSStyleDeclarationPropertyValue; } { - const variable = CSSStyleDeclarationValueParser.getVariable(value); - if (variable) { - return { left: { value: variable, important } }; - } const parsedValue = + CSSStyleDeclarationValueParser.getVariable(value) || CSSStyleDeclarationValueParser.getGlobal(value) || CSSStyleDeclarationValueParser.getContentMeasurement(value); return parsedValue ? { left: { value: parsedValue, important } } : null; @@ -398,6 +463,156 @@ export default class CSSStyleDeclarationPropertySetParser { return float ? { 'css-float': float['float'] } : null; } + /** + * Returns outline. + * + * @param value Value. + * @param important Important. + * @returns Property values. + */ + public static getOutline( + value: string, + important: boolean + ): { [key: string]: ICSSStyleDeclarationPropertyValue } { + const variable = CSSStyleDeclarationValueParser.getVariable(value); + if (variable) { + return { outline: { value: variable, important } }; + } + + const globalValue = CSSStyleDeclarationValueParser.getGlobal(value); + + if (globalValue) { + return { + ...this.getOutlineColor(globalValue, important), + ...this.getOutlineStyle(globalValue, important), + ...this.getOutlineWidth(globalValue, important) + }; + } + + const properties = { + ...this.getOutlineColor('initial', important), + ...this.getOutlineStyle('initial', important), + ...this.getOutlineWidth('initial', important) + }; + + const parts = value.split(/ +/); + + for (const part of parts) { + const width = this.getOutlineWidth(part, important); + const style = this.getOutlineStyle(part, important); + const color = this.getOutlineColor(part, important); + + if (width === null && style === null && color === null) { + return null; + } + + Object.assign(properties, width, style, color); + } + + return properties; + } + + /** + * Returns outline color. + * + * @param value Value. + * @param important Important. + * @returns Property values + */ + public static getOutlineColor( + value: string, + important: boolean + ): { + [key: string]: ICSSStyleDeclarationPropertyValue; + } { + const color = + CSSStyleDeclarationValueParser.getVariable(value) || + CSSStyleDeclarationValueParser.getGlobal(value) || + CSSStyleDeclarationValueParser.getColor(value); + return color + ? { + 'outline-color': { value: color, important } + } + : null; + } + + /** + * Returns outline offset. + * + * @param value Value. + * @param important Important. + * @returns Property values + */ + public static getOutlineOffset( + value: string, + important: boolean + ): { + [key: string]: ICSSStyleDeclarationPropertyValue; + } { + const parsedValue = + CSSStyleDeclarationValueParser.getVariable(value) || + CSSStyleDeclarationValueParser.getLength(value); + return parsedValue ? { 'outline-offset': { value: parsedValue, important } } : null; + } + + /** + * Returns outline style. + * + * @param value Value. + * @param important Important. + * @returns Property values + */ + public static getOutlineStyle( + value: string, + important: boolean + ): { + [key: string]: ICSSStyleDeclarationPropertyValue; + } { + const variable = CSSStyleDeclarationValueParser.getVariable(value); + if (variable) { + return { 'outline-style': { value: variable, important } }; + } + + const lowerValue = value.toLowerCase(); + if (CSSStyleDeclarationValueParser.getGlobal(lowerValue) || BORDER_STYLE.includes(lowerValue)) { + return { + 'outline-style': { value: lowerValue, important } + }; + } + return null; + } + + /** + * Returns outline width. + * + * @param value Value. + * @param important Important. + * @returns Property values + */ + public static getOutlineWidth( + value: string, + important: boolean + ): { + [key: string]: ICSSStyleDeclarationPropertyValue; + } { + const variable = CSSStyleDeclarationValueParser.getVariable(value); + if (variable) { + return { 'outline-width': { value: variable, important } }; + } + + const lowerValue = value.toLowerCase(); + const parsedValue = + BORDER_WIDTH.includes(lowerValue) || CSSStyleDeclarationValueParser.getGlobal(lowerValue) + ? lowerValue + : CSSStyleDeclarationValueParser.getLength(value); + if (parsedValue) { + return { + 'outline-width': { value: parsedValue, important } + }; + } + return null; + } + /** * Returns border. * diff --git a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationValueParser.ts b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationValueParser.ts similarity index 98% rename from packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationValueParser.ts rename to packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationValueParser.ts index 6c508a105..d05ada12a 100644 --- a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationValueParser.ts +++ b/packages/happy-dom/src/css/declaration/property-manager/CSSStyleDeclarationValueParser.ts @@ -1,7 +1,8 @@ const COLOR_REGEXP = /^#([0-9a-fA-F]{3,4}){1,2}$|^rgb\(([^)]*)\)$|^rgba\(([^)]*)\)$|^hsla?\(\s*(-?\d+|-?\d*.\d+)\s*,\s*(-?\d+|-?\d*.\d+)%\s*,\s*(-?\d+|-?\d*.\d+)%\s*(,\s*(-?\d+|-?\d*.\d+)\s*)?\)/; -const LENGTH_REGEXP = /^(0|[-+]?[0-9]*\.?[0-9]+(in|cm|em|mm|pt|pc|px|ex|rem|vh|vw|ch))$/; +const LENGTH_REGEXP = + /^(0|[-+]?[0-9]*\.?[0-9]+(in|cm|em|mm|pt|pc|px|ex|rem|vh|vw|ch|vw|vh|vmin|vmax|Q))$/; const PERCENTAGE_REGEXP = /^[-+]?[0-9]*\.?[0-9]+%$/; const DEGREE_REGEXP = /^[0-9]+deg$/; const URL_REGEXP = /^url\(\s*([^)]*)\s*\)$/; diff --git a/packages/happy-dom/src/css/declaration/utilities/ICSSStyleDeclarationPropertyValue.ts b/packages/happy-dom/src/css/declaration/property-manager/ICSSStyleDeclarationPropertyValue.ts similarity index 53% rename from packages/happy-dom/src/css/declaration/utilities/ICSSStyleDeclarationPropertyValue.ts rename to packages/happy-dom/src/css/declaration/property-manager/ICSSStyleDeclarationPropertyValue.ts index ad636ebc3..cbcfc9b7d 100644 --- a/packages/happy-dom/src/css/declaration/utilities/ICSSStyleDeclarationPropertyValue.ts +++ b/packages/happy-dom/src/css/declaration/property-manager/ICSSStyleDeclarationPropertyValue.ts @@ -1,4 +1,4 @@ export default interface ICSSStyleDeclarationPropertyValue { - readonly value: string; - readonly important: boolean; + value: string; + important: boolean; } diff --git a/packages/happy-dom/src/match-media/IMediaQueryRange.ts b/packages/happy-dom/src/match-media/IMediaQueryRange.ts new file mode 100644 index 000000000..6989cd72c --- /dev/null +++ b/packages/happy-dom/src/match-media/IMediaQueryRange.ts @@ -0,0 +1,5 @@ +export default interface IMediaQueryRange { + before: { value: string; operator: string }; + type: string; + after: { value: string; operator: string }; +} diff --git a/packages/happy-dom/src/match-media/IMediaQueryRule.ts b/packages/happy-dom/src/match-media/IMediaQueryRule.ts new file mode 100644 index 000000000..ff9d2a860 --- /dev/null +++ b/packages/happy-dom/src/match-media/IMediaQueryRule.ts @@ -0,0 +1,4 @@ +export default interface IMediaQueryRule { + name: string; + value: string | null; +} diff --git a/packages/happy-dom/src/match-media/MediaQueryItem.ts b/packages/happy-dom/src/match-media/MediaQueryItem.ts new file mode 100644 index 000000000..95d7a841f --- /dev/null +++ b/packages/happy-dom/src/match-media/MediaQueryItem.ts @@ -0,0 +1,336 @@ +import CSSMeasurementConverter from '../css/declaration/measurement-converter/CSSMeasurementConverter'; +import IWindow from '../window/IWindow'; +import IMediaQueryRange from './IMediaQueryRange'; +import IMediaQueryRule from './IMediaQueryRule'; +import MediaQueryTypeEnum from './MediaQueryTypeEnum'; + +/** + * Media query this. + */ +export default class MediaQueryItem { + public mediaTypes: MediaQueryTypeEnum[]; + public not: boolean; + public rules: IMediaQueryRule[]; + public ranges: IMediaQueryRange[]; + private rootFontSize: string | number | null = null; + private ownerWindow: IWindow; + + /** + * Constructor. + * + * @param options Options. + * @param options.ownerWindow Owner window. + * @param [options.rootFontSize] Root font size. + * @param [options.mediaTypes] Media types. + * @param [options.not] Not. + * @param [options.rules] Rules. + * @param [options.ranges] Ranges. + */ + constructor(options: { + ownerWindow: IWindow; + rootFontSize?: string | number | null; + mediaTypes?: MediaQueryTypeEnum[]; + not?: boolean; + rules?: IMediaQueryRule[]; + ranges?: IMediaQueryRange[]; + }) { + this.ownerWindow = options.ownerWindow; + this.rootFontSize = options.rootFontSize || null; + this.mediaTypes = options.mediaTypes || []; + this.not = options.not || false; + this.rules = options.rules || []; + this.ranges = options.ranges || []; + } + + /** + * Returns media string. + */ + public toString(): string { + return `${this.not ? 'not ' : ''}${this.mediaTypes.join(', ')}${ + (this.not || this.mediaTypes.length > 0) && !!this.ranges.length ? ' and ' : '' + }${this.ranges + .map( + (range) => + `(${range.before ? `${range.before.value} ${range.before.operator} ` : ''}${range.type}${ + range.after ? ` ${range.after.operator} ${range.after.value}` : '' + })` + ) + .join(' and ')}${ + (this.not || this.mediaTypes.length > 0) && !!this.rules.length ? ' and ' : '' + }${this.rules + .map((rule) => (rule.value ? `(${rule.name}: ${rule.value})` : `(${rule.name})`)) + .join(' and ')}`; + } + + /** + * Returns "true" if the item matches. + */ + public matches(): boolean { + return this.not ? !this.matchesAll() : this.matchesAll(); + } + + /** + * Returns "true" if all matches. + * + * @returns "true" if all matches. + */ + private matchesAll(): boolean { + if (!!this.mediaTypes.length) { + let isMediaTypeMatch = false; + for (const mediaType of this.mediaTypes) { + if (this.matchesMediaType(mediaType)) { + isMediaTypeMatch = true; + break; + } + } + + if (!isMediaTypeMatch) { + return false; + } + } + + for (const rule of this.rules) { + if (!this.matchesRule(rule)) { + return false; + } + } + + for (const range of this.ranges) { + if (!this.matchesRange(range)) { + return false; + } + } + + return true; + } + + /** + * Returns "true" if the mediaType matches. + * + * @param mediaType Media type. + * @returns "true" if the mediaType matches. + */ + private matchesMediaType(mediaType: MediaQueryTypeEnum): boolean { + if (mediaType === MediaQueryTypeEnum.all) { + return true; + } + return mediaType === this.ownerWindow.happyDOM.settings.device.mediaType; + } + + /** + * Returns "true" if the range matches. + * + * @param range Range. + * @returns "true" if the range matches. + */ + private matchesRange(range: IMediaQueryRange): boolean { + const windowSize = + range.type === 'width' ? this.ownerWindow.innerWidth : this.ownerWindow.innerHeight; + + if (range.before) { + const beforeValue = this.toPixels(range.before.value); + + if (beforeValue === null) { + return false; + } + + switch (range.before.operator) { + case '<': + if (beforeValue >= windowSize) { + return false; + } + break; + case '<=': + if (beforeValue > windowSize) { + return false; + } + break; + case '>': + if (beforeValue <= windowSize) { + return false; + } + break; + case '>=': + if (beforeValue < windowSize) { + return false; + } + break; + } + } + + if (range.after) { + const afterValue = this.toPixels(range.after.value); + + if (afterValue === null) { + return false; + } + + switch (range.after.operator) { + case '<': + if (windowSize >= afterValue) { + return false; + } + break; + case '<=': + if (windowSize > afterValue) { + return false; + } + break; + case '>': + if (windowSize <= afterValue) { + return false; + } + break; + case '>=': + if (windowSize < afterValue) { + return false; + } + break; + } + } + + return true; + } + + /** + * Returns "true" if the rule matches. + * + * @param rule Rule. + * @returns "true" if the rule matches. + */ + private matchesRule(rule: IMediaQueryRule): boolean { + if (!rule.value) { + switch (rule.name) { + case 'min-width': + case 'max-width': + case 'min-height': + case 'max-height': + case 'width': + case 'height': + case 'orientation': + case 'prefers-color-scheme': + case 'hover': + case 'any-hover': + case 'any-pointer': + case 'pointer': + case 'display-mode': + case 'min-aspect-ratio': + case 'max-aspect-ratio': + case 'aspect-ratio': + return true; + } + return false; + } + + switch (rule.name) { + case 'min-width': + const minWidth = this.toPixels(rule.value); + return minWidth !== null && this.ownerWindow.innerWidth >= minWidth; + case 'max-width': + const maxWidth = this.toPixels(rule.value); + return maxWidth !== null && this.ownerWindow.innerWidth <= maxWidth; + case 'min-height': + const minHeight = this.toPixels(rule.value); + return minHeight !== null && this.ownerWindow.innerHeight >= minHeight; + case 'max-height': + const maxHeight = this.toPixels(rule.value); + return maxHeight !== null && this.ownerWindow.innerHeight <= maxHeight; + case 'width': + const width = this.toPixels(rule.value); + return width !== null && this.ownerWindow.innerWidth === width; + case 'height': + const height = this.toPixels(rule.value); + return height !== null && this.ownerWindow.innerHeight === height; + case 'orientation': + return rule.value === 'landscape' + ? this.ownerWindow.innerWidth > this.ownerWindow.innerHeight + : this.ownerWindow.innerWidth < this.ownerWindow.innerHeight; + case 'prefers-color-scheme': + return rule.value === this.ownerWindow.happyDOM.settings.device.prefersColorScheme; + case 'any-hover': + case 'hover': + if (rule.value === 'none') { + return this.ownerWindow.navigator.maxTouchPoints > 0; + } + if (rule.value === 'hover') { + return this.ownerWindow.navigator.maxTouchPoints === 0; + } + return false; + case 'any-pointer': + case 'pointer': + if (rule.value === 'none') { + return false; + } + + if (rule.value === 'coarse') { + return this.ownerWindow.navigator.maxTouchPoints > 0; + } + + if (rule.value === 'fine') { + return this.ownerWindow.navigator.maxTouchPoints === 0; + } + + return false; + case 'display-mode': + return rule.value === 'browser'; + case 'min-aspect-ratio': + case 'max-aspect-ratio': + case 'aspect-ratio': + const aspectRatio = rule.value.split('/'); + const aspectRatioWidth = parseInt(aspectRatio[0], 10); + const aspectRatioHeight = parseInt(aspectRatio[1], 10); + + if (isNaN(aspectRatioWidth) || isNaN(aspectRatioHeight)) { + return false; + } + + switch (rule.name) { + case 'min-aspect-ratio': + return ( + aspectRatioWidth / aspectRatioHeight <= + this.ownerWindow.innerWidth / this.ownerWindow.innerHeight + ); + case 'max-aspect-ratio': + return ( + aspectRatioWidth / aspectRatioHeight >= + this.ownerWindow.innerWidth / this.ownerWindow.innerHeight + ); + case 'aspect-ratio': + return ( + aspectRatioWidth / aspectRatioHeight === + this.ownerWindow.innerWidth / this.ownerWindow.innerHeight + ); + } + } + + return false; + } + + /** + * Convert to pixels. + * + * @param value Value. + * @returns Value in pixels. + */ + private toPixels(value: string): number | null { + if (value.endsWith('em')) { + this.rootFontSize = + this.rootFontSize || + parseFloat( + this.ownerWindow.getComputedStyle(this.ownerWindow.document.documentElement).fontSize + ); + return CSSMeasurementConverter.toPixels({ + ownerWindow: this.ownerWindow, + value, + rootFontSize: this.rootFontSize, + parentFontSize: this.rootFontSize + }); + } + return CSSMeasurementConverter.toPixels({ + ownerWindow: this.ownerWindow, + value, + rootFontSize: 16, + parentFontSize: 16 + }); + } +} diff --git a/packages/happy-dom/src/match-media/MediaQueryList.ts b/packages/happy-dom/src/match-media/MediaQueryList.ts index 175a5394e..020bae0ed 100644 --- a/packages/happy-dom/src/match-media/MediaQueryList.ts +++ b/packages/happy-dom/src/match-media/MediaQueryList.ts @@ -3,9 +3,8 @@ import Event from '../event/Event'; import IWindow from '../window/IWindow'; import IEventListener from '../event/IEventListener'; import MediaQueryListEvent from '../event/events/MediaQueryListEvent'; - -const MEDIA_REGEXP = - /min-width: *([0-9]+) *px|max-width: *([0-9]+) *px|min-height: *([0-9]+) *px|max-height: *([0-9]+) *px/; +import IMediaQueryItem from './MediaQueryItem'; +import MediaQueryParser from './MediaQueryParser'; /** * Media Query List. @@ -14,20 +13,42 @@ const MEDIA_REGEXP = * https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList. */ export default class MediaQueryList extends EventTarget { - public readonly media: string = ''; public onchange: (event: Event) => void = null; private _ownerWindow: IWindow; + private _items: IMediaQueryItem[] | null = null; + private _media: string; + private _rootFontSize: string | number | null = null; /** * Constructor. * - * @param ownerWindow Window. - * @param media Media. + * @param options Options. + * @param options.ownerWindow Owner window. + * @param options.media Media. + * @param [options.rootFontSize] Root font size. */ - constructor(ownerWindow: IWindow, media: string) { + constructor(options: { ownerWindow: IWindow; media: string; rootFontSize?: string | number }) { super(); - this._ownerWindow = ownerWindow; - this.media = media; + this._ownerWindow = options.ownerWindow; + this._media = options.media; + this._rootFontSize = options.rootFontSize || null; + } + + /** + * Returns media. + * + * @returns Media. + */ + public get media(): string { + this._items = + this._items || + MediaQueryParser.parse({ + ownerWindow: this._ownerWindow, + mediaQuery: this._media, + rootFontSize: this._rootFontSize + }); + + return this._items.map((item) => item.toString()).join(', '); } /** @@ -36,19 +57,21 @@ export default class MediaQueryList extends EventTarget { * @returns Matches. */ public get matches(): boolean { - const match = MEDIA_REGEXP.exec(this.media); - if (match) { - if (match[1]) { - return this._ownerWindow.innerWidth >= parseInt(match[1]); - } else if (match[2]) { - return this._ownerWindow.innerWidth <= parseInt(match[2]); - } else if (match[3]) { - return this._ownerWindow.innerHeight >= parseInt(match[3]); - } else if (match[4]) { - return this._ownerWindow.innerHeight <= parseInt(match[4]); + this._items = + this._items || + MediaQueryParser.parse({ + ownerWindow: this._ownerWindow, + mediaQuery: this._media, + rootFontSize: this._rootFontSize + }); + + for (const item of this._items) { + if (!item.matches()) { + return false; } } - return false; + + return true; } /** diff --git a/packages/happy-dom/src/match-media/MediaQueryParser.ts b/packages/happy-dom/src/match-media/MediaQueryParser.ts new file mode 100644 index 000000000..259a84c34 --- /dev/null +++ b/packages/happy-dom/src/match-media/MediaQueryParser.ts @@ -0,0 +1,113 @@ +import MediaQueryItem from './MediaQueryItem'; +import MediaQueryTypeEnum from './MediaQueryTypeEnum'; +import IWindow from '../window/IWindow'; + +/** + * Media query RegExp. + * + * Group 1: "not", "only", "all", "screen", "print". + * Group 2: Rule. + * Group 3: Rule end paranthesis (must be present if no value). + * Group 4: Comma (,). + * Group 5: "or", "and". + */ +const MEDIA_QUERY_REGEXP = /(not|only|all|screen|print)|\(([^\)]+)(\)){0,1}|(,)| +(or|and) +/g; + +/** + * Check if resolution RegExp. + */ +const IS_RESOLUTION_REGEXP = /[<>]/; + +/** + * Resolution RegExp. + * + * Group 1: First resolution value. + * Group 2: First resolution operator. + * Group 3: Resolution type. + * Group 4: Second resolution operator. + * Group 5: Second resolution value. + */ +const RESOLUTION_REGEXP = + /(?:([0-9]+[a-z]+) *(<|<=|>|=>)){0,1} *(width|height) *(?:(<|<=|>|=>) *([0-9]+[a-z]+)){0,1}/; + +/** + * Utility for parsing a query string. + */ +export default class MediaQueryParser { + /** + * Parses a media query string. + * + * @param options Options. + * @param options.ownerWindow Owner window. + * @param options.mediaQuery Media query string. + * @param [options.rootFontSize] Root font size. + * @returns Media query items. + */ + public static parse(options: { + ownerWindow: IWindow; + mediaQuery: string; + rootFontSize?: string | number | null; + }): MediaQueryItem[] { + let currentMediaQueryItem: MediaQueryItem = new MediaQueryItem({ + ownerWindow: options.ownerWindow, + rootFontSize: options.rootFontSize + }); + const mediaQueryItems: MediaQueryItem[] = [currentMediaQueryItem]; + const regexp = new RegExp(MEDIA_QUERY_REGEXP); + let match: RegExpExecArray | null = null; + + while ((match = regexp.exec(options.mediaQuery.toLowerCase()))) { + if (match[4] === ',' || match[5] === 'or') { + currentMediaQueryItem = new MediaQueryItem({ + ownerWindow: options.ownerWindow, + rootFontSize: options.rootFontSize + }); + mediaQueryItems.push(currentMediaQueryItem); + } else if (match[1] === 'all' || match[1] === 'screen' || match[1] === 'print') { + currentMediaQueryItem.mediaTypes.push(match[1]); + } else if (match[1] === 'not') { + currentMediaQueryItem.not = true; + } else if (match[2]) { + const resolutionMatch = IS_RESOLUTION_REGEXP.test(match[2]) + ? match[2].match(RESOLUTION_REGEXP) + : null; + if (resolutionMatch && (resolutionMatch[1] || resolutionMatch[5])) { + currentMediaQueryItem.ranges.push({ + before: resolutionMatch[1] + ? { + value: resolutionMatch[1], + operator: resolutionMatch[2] + } + : null, + type: resolutionMatch[3], + after: resolutionMatch[5] + ? { + value: resolutionMatch[5], + operator: resolutionMatch[4] + } + : null + }); + } else { + const [name, value] = match[2].split(':'); + const trimmedValue = value ? value.trim() : null; + if (!trimmedValue && !match[3]) { + return [ + new MediaQueryItem({ + ownerWindow: options.ownerWindow, + rootFontSize: options.rootFontSize, + not: true, + mediaTypes: [MediaQueryTypeEnum.all] + }) + ]; + } + currentMediaQueryItem.rules.push({ + name: name.trim(), + value: trimmedValue + }); + } + } + } + + return mediaQueryItems; + } +} diff --git a/packages/happy-dom/src/match-media/MediaQueryTypeEnum.ts b/packages/happy-dom/src/match-media/MediaQueryTypeEnum.ts new file mode 100644 index 000000000..87222fb3c --- /dev/null +++ b/packages/happy-dom/src/match-media/MediaQueryTypeEnum.ts @@ -0,0 +1,7 @@ +enum MediaQueryTypeEnum { + all = 'all', + print = 'print', + screen = 'screen' +} + +export default MediaQueryTypeEnum; diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index b2421a3aa..61d8445e0 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -31,6 +31,7 @@ import Event from '../../event/Event'; import ElementUtility from './ElementUtility'; import HTMLCollection from './HTMLCollection'; import EventPhaseEnum from '../../event/EventPhaseEnum'; +import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration'; /** * Element. @@ -92,6 +93,7 @@ export default class Element extends Node implements IElement { private _classList: DOMTokenList = null; public _isValue?: string; + public _computedStyle: CSSStyleDeclaration | null = null; /** * Returns class list. diff --git a/packages/happy-dom/src/window/IHappyDOMOptions.ts b/packages/happy-dom/src/window/IHappyDOMOptions.ts index 5cf962447..f8da67761 100644 --- a/packages/happy-dom/src/window/IHappyDOMOptions.ts +++ b/packages/happy-dom/src/window/IHappyDOMOptions.ts @@ -11,5 +11,9 @@ export default interface IHappyDOMOptions { disableCSSFileLoading?: boolean; disableIframePageLoading?: boolean; enableFileSystemHttpRequests?: boolean; + device?: { + prefersColorScheme?: string; + mediaType?: string; + }; }; } diff --git a/packages/happy-dom/src/window/IHappyDOMSettings.ts b/packages/happy-dom/src/window/IHappyDOMSettings.ts index 9e9490c48..d37f4381a 100644 --- a/packages/happy-dom/src/window/IHappyDOMSettings.ts +++ b/packages/happy-dom/src/window/IHappyDOMSettings.ts @@ -7,4 +7,8 @@ export default interface IHappyDOMSettings { disableCSSFileLoading: boolean; disableIframePageLoading: boolean; enableFileSystemHttpRequests: boolean; + device: { + prefersColorScheme: string; + mediaType: string; + }; } diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 66a39943f..97fb7c559 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -177,7 +177,11 @@ export default class Window extends EventTarget implements IWindow { disableJavaScriptFileLoading: false, disableCSSFileLoading: false, disableIframePageLoading: false, - enableFileSystemHttpRequests: false + enableFileSystemHttpRequests: false, + device: { + prefersColorScheme: 'light', + mediaType: 'screen' + } } }; @@ -433,7 +437,14 @@ export default class Window extends EventTarget implements IWindow { } if (options?.settings) { - this.happyDOM.settings = Object.assign(this.happyDOM.settings, options.settings); + this.happyDOM.settings = { + ...this.happyDOM.settings, + ...options.settings, + device: { + ...this.happyDOM.settings.device, + ...options.settings.device + } + }; } this._setTimeout = ORIGINAL_SET_TIMEOUT; @@ -594,7 +605,8 @@ export default class Window extends EventTarget implements IWindow { * @returns CSS style declaration. */ public getComputedStyle(element: IElement): CSSStyleDeclaration { - return new CSSStyleDeclaration(element, true); + element['_computedStyle'] = element['_computedStyle'] || new CSSStyleDeclaration(element, true); + return element['_computedStyle']; } /** @@ -657,7 +669,7 @@ export default class Window extends EventTarget implements IWindow { * @returns A new MediaQueryList. */ public matchMedia(mediaQueryString: string): MediaQueryList { - return new MediaQueryList(this, mediaQueryString); + return new MediaQueryList({ ownerWindow: this, media: mediaQueryString }); } /** diff --git a/packages/happy-dom/test/css/data/CSSParserInput.ts b/packages/happy-dom/test/css/data/CSSParserInput.ts index f2d6bdb68..797e6764d 100644 --- a/packages/happy-dom/test/css/data/CSSParserInput.ts +++ b/packages/happy-dom/test/css/data/CSSParserInput.ts @@ -31,7 +31,7 @@ export default ` } } - @keyframes keyframes2 { + @-webkit-keyframes keyframes2 { 0% { transform: rotate(0deg); } @@ -41,6 +41,12 @@ export default ` } } + @unknown-rule { + .unknown-class { + text-spacing: 1px; + } + } + @container (min-width: 36rem) { .container { color: red; diff --git a/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts b/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts index d9aade8bb..56b2454dc 100644 --- a/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts +++ b/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts @@ -113,6 +113,12 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderImageSource).toBe('inherit'); expect(declaration.borderImageWidth).toBe('inherit'); + element.setAttribute('style', 'border: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.border).toBe('var(--test-variable)'); + element.setAttribute('style', 'border: 2px solid green'); expect(declaration.border).toBe('2px solid green'); @@ -205,6 +211,12 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderTopWidth).toBe('inherit'); expect(declaration.borderTopStyle).toBe('inherit'); + element.setAttribute('style', 'border-top: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.borderTop).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-top: green 2px solid'); expect(declaration.border).toBe(''); @@ -226,11 +238,19 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'border-right: inherit'); + expect(declaration.length).toBe(3); + expect(declaration.borderRight).toBe('inherit'); expect(declaration.borderRightColor).toBe('inherit'); expect(declaration.borderRightWidth).toBe('inherit'); expect(declaration.borderRightStyle).toBe('inherit'); + element.setAttribute('style', 'border-right: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.borderRight).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-right: green solid 2px'); expect(declaration.border).toBe(''); @@ -252,11 +272,19 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'border-bottom: inherit'); + expect(declaration.length).toBe(3); + expect(declaration.borderBottom).toBe('inherit'); expect(declaration.borderBottomColor).toBe('inherit'); expect(declaration.borderBottomWidth).toBe('inherit'); expect(declaration.borderBottomStyle).toBe('inherit'); + element.setAttribute('style', 'border-bottom: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.borderBottom).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-bottom: green solid 2px'); expect(declaration.border).toBe(''); @@ -278,11 +306,19 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'border-left: inherit'); + expect(declaration.length).toBe(3); + expect(declaration.borderLeft).toBe('inherit'); expect(declaration.borderLeftColor).toBe('inherit'); expect(declaration.borderLeftWidth).toBe('inherit'); expect(declaration.borderLeftStyle).toBe('inherit'); + element.setAttribute('style', 'border-left: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.borderLeft).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-left: green solid 2px'); expect(declaration.border).toBe(''); @@ -304,11 +340,19 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'border-width: inherit'); + expect(declaration.length).toBe(4); + expect(declaration.borderTopWidth).toBe('inherit'); expect(declaration.borderRightWidth).toBe('inherit'); expect(declaration.borderBottomWidth).toBe('inherit'); expect(declaration.borderLeftWidth).toBe('inherit'); + element.setAttribute('style', 'border-width: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.borderWidth).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-width: 1px 2px 3px 4px'); expect(declaration.borderTopWidth).toBe('1px'); @@ -331,11 +375,19 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'border-style: inherit'); + expect(declaration.length).toBe(4); + expect(declaration.borderTopStyle).toBe('inherit'); expect(declaration.borderRightStyle).toBe('inherit'); expect(declaration.borderBottomStyle).toBe('inherit'); expect(declaration.borderLeftStyle).toBe('inherit'); + element.setAttribute('style', 'border-style: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.borderStyle).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-style: none hidden dotted dashed'); expect(declaration.borderTopStyle).toBe('none'); @@ -358,11 +410,19 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'border-color: inherit'); + expect(declaration.length).toBe(4); + expect(declaration.borderTopColor).toBe('inherit'); expect(declaration.borderRightColor).toBe('inherit'); expect(declaration.borderBottomColor).toBe('inherit'); expect(declaration.borderLeftColor).toBe('inherit'); + element.setAttribute('style', 'border-color: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.borderColor).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-color: #000 #ffffff rgba(135,200,150,0.5) blue'); expect(declaration.borderTopColor).toBe('#000'); @@ -384,6 +444,9 @@ describe('CSSStyleDeclaration', () => { const declaration = new CSSStyleDeclaration(element); element.setAttribute('style', 'border-image: inherit'); + + expect(declaration.length).toBe(5); + expect(declaration.borderImage).toBe('inherit'); expect(declaration.borderImageSource).toBe('inherit'); expect(declaration.borderImageOutset).toBe('inherit'); @@ -391,6 +454,12 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderImageSlice).toBe('inherit'); expect(declaration.borderImageWidth).toBe('inherit'); + element.setAttribute('style', 'border-image: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.borderImage).toBe('var(--test-variable)'); + element.setAttribute( 'style', 'border-image: repeating-linear-gradient(30deg, #4d9f0c, #9198e5, #4d9f0c 20px) 60' @@ -443,6 +512,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderImageSource).toBe('inherit'); + element.setAttribute('style', `border-image-source: var(--test-variable)`); + + expect(declaration.borderImageSource).toBe('var(--test-variable)'); + element.setAttribute( 'style', `border-image-source: url('/media/examples/border-diamonds.png')` @@ -473,6 +546,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderImageSlice).toBe('inherit'); + element.setAttribute('style', 'border-image-slice: var(--test-variable)'); + + expect(declaration.borderImageSlice).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-image-slice: 30'); expect(declaration.borderImageSlice).toBe('30'); @@ -498,6 +575,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderImageWidth).toBe('inherit'); + element.setAttribute('style', 'border-image-width: var(--test-variable)'); + + expect(declaration.borderImageWidth).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-image-width: auto'); expect(declaration.borderImageWidth).toBe('auto'); @@ -520,6 +601,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderImageOutset).toBe('inherit'); + element.setAttribute('style', 'border-image-outset: var(--test-variable)'); + + expect(declaration.borderImageOutset).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-image-outset: 1rem'); expect(declaration.borderImageOutset).toBe('1rem'); @@ -542,6 +627,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderImageRepeat).toBe('inherit'); + element.setAttribute('style', 'border-image-repeat: var(--test-variable)'); + + expect(declaration.borderImageRepeat).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-image-repeat: stretch'); expect(declaration.borderImageRepeat).toBe('stretch'); @@ -564,6 +653,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderTopWidth).toBe('inherit'); + element.setAttribute('style', 'border-top-width: var(--test-variable)'); + + expect(declaration.borderTopWidth).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-top-width: thick'); expect(declaration.borderTopWidth).toBe('thick'); @@ -582,6 +675,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderRightWidth).toBe('inherit'); + element.setAttribute('style', 'border-right-width: var(--test-variable)'); + + expect(declaration.borderRightWidth).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-right-width: thick'); expect(declaration.borderRightWidth).toBe('thick'); @@ -600,6 +697,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderBottomWidth).toBe('inherit'); + element.setAttribute('style', 'border-bottom-width: var(--test-variable)'); + + expect(declaration.borderBottomWidth).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-bottom-width: thick'); expect(declaration.borderBottomWidth).toBe('thick'); @@ -618,6 +719,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderLeftWidth).toBe('inherit'); + element.setAttribute('style', 'border-left-width: var(--test-variable)'); + + expect(declaration.borderLeftWidth).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-left-width: thick'); expect(declaration.borderLeftWidth).toBe('thick'); @@ -636,6 +741,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderTopColor).toBe('inherit'); + element.setAttribute('style', 'border-top-color: var(--test-variable)'); + + expect(declaration.borderTopColor).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-top-color: red'); expect(declaration.borderTopColor).toBe('red'); @@ -654,6 +763,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderRightColor).toBe('inherit'); + element.setAttribute('style', 'border-right-color: var(--test-variable)'); + + expect(declaration.borderRightColor).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-right-color: red'); expect(declaration.borderRightColor).toBe('red'); @@ -672,6 +785,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderBottomColor).toBe('inherit'); + element.setAttribute('style', 'border-bottom-color: var(--test-variable)'); + + expect(declaration.borderBottomColor).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-bottom-color: red'); expect(declaration.borderBottomColor).toBe('red'); @@ -690,6 +807,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderLeftColor).toBe('inherit'); + element.setAttribute('style', 'border-left-color: var(--test-variable)'); + + expect(declaration.borderLeftColor).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-left-color: red'); expect(declaration.borderLeftColor).toBe('red'); @@ -708,6 +829,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderTopStyle).toBe('inherit'); + element.setAttribute('style', 'border-top-style: var(--test-variable)'); + + expect(declaration.borderTopStyle).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-top-style: dotted'); expect(declaration.borderTopStyle).toBe('dotted'); @@ -726,6 +851,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderRightStyle).toBe('inherit'); + element.setAttribute('style', 'border-right-style: var(--test-variable)'); + + expect(declaration.borderRightStyle).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-right-style: dotted'); expect(declaration.borderRightStyle).toBe('dotted'); @@ -744,6 +873,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderBottomStyle).toBe('inherit'); + element.setAttribute('style', 'border-bottom-style: var(--test-variable)'); + + expect(declaration.borderBottomStyle).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-bottom-style: dotted'); expect(declaration.borderBottomStyle).toBe('dotted'); @@ -762,6 +895,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderLeftStyle).toBe('inherit'); + element.setAttribute('style', 'border-left-style: var(--test-variable)'); + + expect(declaration.borderLeftStyle).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-left-style: dotted'); expect(declaration.borderLeftStyle).toBe('dotted'); @@ -778,12 +915,20 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'border-radius: inherit'); + expect(declaration.length).toBe(4); + expect(declaration.borderRadius).toBe('inherit'); expect(declaration.borderTopLeftRadius).toBe('inherit'); expect(declaration.borderTopRightRadius).toBe('inherit'); expect(declaration.borderBottomRightRadius).toBe('inherit'); expect(declaration.borderBottomLeftRadius).toBe('inherit'); + element.setAttribute('style', 'border-radius: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.borderRadius).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-radius: 1px 2px 3px 4px'); expect(declaration.borderRadius).toBe('1px 2px 3px 4px'); @@ -814,6 +959,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderTopLeftRadius).toBe('inherit'); + element.setAttribute('style', 'border-top-left-radius: var(--test-variable)'); + + expect(declaration.borderTopLeftRadius).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-top-left-radius: 1rem'); expect(declaration.borderTopLeftRadius).toBe('1rem'); @@ -828,6 +977,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderTopRightRadius).toBe('inherit'); + element.setAttribute('style', 'border-top-right-radius: var(--test-variable)'); + + expect(declaration.borderTopRightRadius).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-top-right-radius: 1rem'); expect(declaration.borderTopRightRadius).toBe('1rem'); @@ -842,6 +995,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderBottomRightRadius).toBe('inherit'); + element.setAttribute('style', 'border-bottom-right-radius: var(--test-variable)'); + + expect(declaration.borderBottomRightRadius).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-bottom-right-radius: 1rem'); expect(declaration.borderBottomRightRadius).toBe('1rem'); @@ -856,6 +1013,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.borderBottomLeftRadius).toBe('inherit'); + element.setAttribute('style', 'border-bottom-left-radius: var(--test-variable)'); + + expect(declaration.borderBottomLeftRadius).toBe('var(--test-variable)'); + element.setAttribute('style', 'border-bottom-left-radius: 1rem'); expect(declaration.borderBottomLeftRadius).toBe('1rem'); @@ -866,7 +1027,7 @@ describe('CSSStyleDeclaration', () => { it('Returns style property.', () => { const declaration = new CSSStyleDeclaration(element); - for (const value of ['collapse', 'separate', 'inherit']) { + for (const value of ['collapse', 'separate', 'inherit', 'var(--test-variable)']) { element.setAttribute('style', `border-collapse: ${value}`); expect(declaration.borderCollapse).toBe(value); @@ -874,11 +1035,172 @@ describe('CSSStyleDeclaration', () => { }); }); + describe('get outline()', () => { + it('Returns style property.', () => { + const declaration = new CSSStyleDeclaration(element); + + element.setAttribute('style', 'outline: inherit'); + + expect(declaration.length).toBe(3); + + expect(declaration.outline).toBe('inherit'); + expect(declaration.outlineColor).toBe('inherit'); + expect(declaration.outlineWidth).toBe('inherit'); + expect(declaration.outlineStyle).toBe('inherit'); + + element.setAttribute('style', 'outline: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.outline).toBe('var(--test-variable)'); + + element.setAttribute('style', 'outline: green 2px solid'); + + expect(declaration.border).toBe(''); + + expect(declaration.outlineColor).toBe('green'); + expect(declaration.outlineWidth).toBe('2px'); + expect(declaration.outlineStyle).toBe('solid'); + + element.setAttribute('style', 'outline: thick double #32a1ce'); + + expect(declaration.outlineColor).toBe('#32a1ce'); + expect(declaration.outlineWidth).toBe('thick'); + expect(declaration.outlineStyle).toBe('double'); + }); + }); + + describe('get outlineColor()', () => { + it('Returns style property.', () => { + const declaration = new CSSStyleDeclaration(element); + + element.setAttribute('style', 'outline-color: inherit'); + + expect(declaration.outlineColor).toBe('inherit'); + + element.setAttribute('style', 'outline-color: var(--test-variable)'); + + expect(declaration.outlineColor).toBe('var(--test-variable)'); + + element.setAttribute('style', 'outline-color: #32a1ce'); + + expect(declaration.outlineColor).toBe('#32a1ce'); + }); + }); + + describe('get outlineWidth()', () => { + it('Returns style property.', () => { + const declaration = new CSSStyleDeclaration(element); + + element.setAttribute('style', 'outline-width: inherit'); + + expect(declaration.outlineWidth).toBe('inherit'); + + element.setAttribute('style', 'outline-width: var(--test-variable)'); + + expect(declaration.outlineWidth).toBe('var(--test-variable)'); + + element.setAttribute('style', 'outline-width: 2px'); + + expect(declaration.outlineWidth).toBe('2px'); + }); + }); + + describe('get outlineStyle()', () => { + it('Returns style property.', () => { + const declaration = new CSSStyleDeclaration(element); + + element.setAttribute('style', 'outline-style: inherit'); + + expect(declaration.outlineStyle).toBe('inherit'); + + element.setAttribute('style', 'outline-style: var(--test-variable)'); + + expect(declaration.outlineStyle).toBe('var(--test-variable)'); + + element.setAttribute('style', 'outline-style: solid'); + + expect(declaration.outlineStyle).toBe('solid'); + }); + }); + + describe('get outlineOffset()', () => { + it('Returns style property.', () => { + const declaration = new CSSStyleDeclaration(element); + + element.setAttribute('style', 'outline-offset: inherit'); + + expect(declaration.outlineOffset).toBe('inherit'); + + element.setAttribute('style', 'outline-offset: var(--test-variable)'); + + expect(declaration.outlineOffset).toBe('var(--test-variable)'); + + element.setAttribute('style', 'outline-offset: 2px'); + + expect(declaration.outlineOffset).toBe('2px'); + }); + }); + + describe('get letterSpacing()', () => { + it('Returns style property.', () => { + const declaration = new CSSStyleDeclaration(element); + + element.setAttribute('style', 'letter-spacing: inherit'); + + expect(declaration.letterSpacing).toBe('inherit'); + + element.setAttribute('style', 'letter-spacing: var(--test-variable)'); + + expect(declaration.letterSpacing).toBe('var(--test-variable)'); + + element.setAttribute('style', 'letter-spacing: 2px'); + + expect(declaration.letterSpacing).toBe('2px'); + }); + }); + + describe('get wordSpacing()', () => { + it('Returns style property.', () => { + const declaration = new CSSStyleDeclaration(element); + + element.setAttribute('style', 'word-spacing: inherit'); + + expect(declaration.wordSpacing).toBe('inherit'); + + element.setAttribute('style', 'word-spacing: var(--test-variable)'); + + expect(declaration.wordSpacing).toBe('var(--test-variable)'); + + element.setAttribute('style', 'word-spacing: 2px'); + + expect(declaration.wordSpacing).toBe('2px'); + }); + }); + + describe('get textIndent()', () => { + it('Returns style property.', () => { + const declaration = new CSSStyleDeclaration(element); + + element.setAttribute('style', 'text-indent: inherit'); + + expect(declaration.textIndent).toBe('inherit'); + + element.setAttribute('style', 'text-indent: var(--test-variable)'); + + expect(declaration.textIndent).toBe('var(--test-variable)'); + + element.setAttribute('style', 'text-indent: 2px'); + + expect(declaration.textIndent).toBe('2px'); + }); + }); + describe('get clear()', () => { it('Returns style property.', () => { const declaration = new CSSStyleDeclaration(element); - for (const value of ['inherit', 'none', 'left', 'right', 'both']) { + for (const value of ['inherit', 'var(--test-variable)', 'none', 'left', 'right', 'both']) { element.setAttribute('style', `clear: ${value}`); expect(declaration.clear).toBe(value); @@ -894,6 +1216,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.clip).toBe('inherit'); + element.setAttribute('style', 'clip: var(--test-variable)'); + + expect(declaration.clip).toBe('var(--test-variable)'); + element.setAttribute('style', 'clip: auto'); expect(declaration.clip).toBe('auto'); @@ -908,7 +1234,15 @@ describe('CSSStyleDeclaration', () => { it('Returns style property.', () => { const declaration = new CSSStyleDeclaration(element); - for (const value of ['inherit', 'none', 'left', 'right', 'inline-start', 'inline-end']) { + for (const value of [ + 'inherit', + 'var(--test-variable)', + 'none', + 'left', + 'right', + 'inline-start', + 'inline-end' + ]) { element.setAttribute('style', `css-float: ${value}`); expect(declaration.cssFloat).toBe(value); @@ -920,7 +1254,15 @@ describe('CSSStyleDeclaration', () => { it('Returns style property.', () => { const declaration = new CSSStyleDeclaration(element); - for (const value of ['inherit', 'none', 'left', 'right', 'inline-start', 'inline-end']) { + for (const value of [ + 'inherit', + 'var(--test-variable)', + 'none', + 'left', + 'right', + 'inline-start', + 'inline-end' + ]) { element.setAttribute('style', `float: ${value}`); expect(declaration.float).toBe(value); @@ -971,7 +1313,15 @@ describe('CSSStyleDeclaration', () => { it('Returns style property.', () => { const declaration = new CSSStyleDeclaration(element); - for (const value of ['inherit', 'initial', 'revert', 'unset', 'ltr', 'rtl']) { + for (const value of [ + 'inherit', + 'initial', + 'revert', + 'unset', + 'var(--test-variable)', + 'ltr', + 'rtl' + ]) { element.setAttribute('style', `direction: ${value}`); expect(declaration.direction).toBe(value); @@ -985,11 +1335,19 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'flex: inherit'); + expect(declaration.length).toBe(3); + expect(declaration.flex).toBe('inherit'); expect(declaration.flexGrow).toBe('inherit'); expect(declaration.flexShrink).toBe('inherit'); expect(declaration.flexBasis).toBe('inherit'); + element.setAttribute('style', 'flex: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.flex).toBe('var(--test-variable)'); + element.setAttribute('style', 'flex: none'); expect(declaration.flex).toBe('0 0 auto'); @@ -1049,6 +1407,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.flexShrink).toBe('inherit'); + element.setAttribute('style', 'flex-shrink: var(--test-variable)'); + + expect(declaration.flexShrink).toBe('var(--test-variable)'); + element.setAttribute('style', 'flex-shrink: 2'); expect(declaration.flexShrink).toBe('2'); @@ -1067,6 +1429,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.flexGrow).toBe('inherit'); + element.setAttribute('style', 'flex-grow: var(--test-variable)'); + + expect(declaration.flexGrow).toBe('var(--test-variable)'); + element.setAttribute('style', 'flex-grow: 2'); expect(declaration.flexGrow).toBe('2'); @@ -1094,6 +1460,7 @@ describe('CSSStyleDeclaration', () => { 'initial', 'revert', 'unset', + 'var(--test-variable)', 'auto', 'fill', 'content', @@ -1114,23 +1481,51 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'padding: inherit'); + expect(declaration.length).toBe(4); + expect(declaration.padding).toBe('inherit'); + expect(declaration.paddingTop).toBe('inherit'); + expect(declaration.paddingRight).toBe('inherit'); + expect(declaration.paddingBottom).toBe('inherit'); + expect(declaration.paddingLeft).toBe('inherit'); + + element.setAttribute('style', 'padding: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.padding).toBe('var(--test-variable)'); element.setAttribute('style', 'padding: 1px 2px 3px 4px'); expect(declaration.padding).toBe('1px 2px 3px 4px'); + expect(declaration.paddingTop).toBe('1px'); + expect(declaration.paddingRight).toBe('2px'); + expect(declaration.paddingBottom).toBe('3px'); + expect(declaration.paddingLeft).toBe('4px'); element.setAttribute('style', 'padding: 1px 2px 3px'); expect(declaration.padding).toBe('1px 2px 3px'); + expect(declaration.paddingTop).toBe('1px'); + expect(declaration.paddingRight).toBe('2px'); + expect(declaration.paddingBottom).toBe('3px'); + expect(declaration.paddingLeft).toBe('2px'); element.setAttribute('style', 'padding: 1px 2px'); expect(declaration.padding).toBe('1px 2px'); + expect(declaration.paddingTop).toBe('1px'); + expect(declaration.paddingRight).toBe('2px'); + expect(declaration.paddingBottom).toBe('1px'); + expect(declaration.paddingLeft).toBe('2px'); element.setAttribute('style', 'padding: 1px'); expect(declaration.padding).toBe('1px'); + expect(declaration.paddingTop).toBe('1px'); + expect(declaration.paddingRight).toBe('1px'); + expect(declaration.paddingBottom).toBe('1px'); + expect(declaration.paddingLeft).toBe('1px'); element.setAttribute('style', 'padding: auto'); @@ -1146,6 +1541,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.paddingTop).toBe('inherit'); + element.setAttribute('style', 'padding-top: var(--test-variable)'); + + expect(declaration.paddingTop).toBe('var(--test-variable)'); + element.setAttribute('style', 'padding-top: 1px'); expect(declaration.paddingTop).toBe('1px'); @@ -1164,6 +1563,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.paddingRight).toBe('inherit'); + element.setAttribute('style', 'padding-right: var(--test-variable)'); + + expect(declaration.paddingRight).toBe('var(--test-variable)'); + element.setAttribute('style', 'padding-right: 1px'); expect(declaration.paddingRight).toBe('1px'); @@ -1182,6 +1585,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.paddingBottom).toBe('inherit'); + element.setAttribute('style', 'padding-bottom: var(--test-variable)'); + + expect(declaration.paddingBottom).toBe('var(--test-variable)'); + element.setAttribute('style', 'padding-bottom: 1px'); expect(declaration.paddingBottom).toBe('1px'); @@ -1200,6 +1607,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.paddingLeft).toBe('inherit'); + element.setAttribute('style', 'padding-left: var(--test-variable)'); + + expect(declaration.paddingLeft).toBe('var(--test-variable)'); + element.setAttribute('style', 'padding-left: 1px'); expect(declaration.paddingLeft).toBe('1px'); @@ -1216,27 +1627,59 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'margin: inherit'); + expect(declaration.length).toBe(4); + expect(declaration.margin).toBe('inherit'); + expect(declaration.marginTop).toBe('inherit'); + expect(declaration.marginRight).toBe('inherit'); + expect(declaration.marginBottom).toBe('inherit'); + expect(declaration.marginLeft).toBe('inherit'); + + element.setAttribute('style', 'margin: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.margin).toBe('var(--test-variable)'); element.setAttribute('style', 'margin: 1px 2px 3px 4px'); expect(declaration.margin).toBe('1px 2px 3px 4px'); + expect(declaration.marginTop).toBe('1px'); + expect(declaration.marginRight).toBe('2px'); + expect(declaration.marginBottom).toBe('3px'); + expect(declaration.marginLeft).toBe('4px'); element.setAttribute('style', 'margin: 1px 2px 3px'); expect(declaration.margin).toBe('1px 2px 3px'); + expect(declaration.marginTop).toBe('1px'); + expect(declaration.marginRight).toBe('2px'); + expect(declaration.marginBottom).toBe('3px'); + expect(declaration.marginLeft).toBe('2px'); element.setAttribute('style', 'margin: 1px 2px'); expect(declaration.margin).toBe('1px 2px'); + expect(declaration.marginTop).toBe('1px'); + expect(declaration.marginRight).toBe('2px'); + expect(declaration.marginBottom).toBe('1px'); + expect(declaration.marginLeft).toBe('2px'); element.setAttribute('style', 'margin: 1px'); expect(declaration.margin).toBe('1px'); + expect(declaration.marginTop).toBe('1px'); + expect(declaration.marginRight).toBe('1px'); + expect(declaration.marginBottom).toBe('1px'); + expect(declaration.marginLeft).toBe('1px'); element.setAttribute('style', 'margin: auto'); expect(declaration.margin).toBe('auto'); + expect(declaration.marginTop).toBe('auto'); + expect(declaration.marginRight).toBe('auto'); + expect(declaration.marginBottom).toBe('auto'); + expect(declaration.marginLeft).toBe('auto'); }); }); @@ -1248,6 +1691,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.marginTop).toBe('inherit'); + element.setAttribute('style', 'margin-top: var(--test-variable)'); + + expect(declaration.marginTop).toBe('var(--test-variable)'); + element.setAttribute('style', 'margin-top: 1px'); expect(declaration.marginTop).toBe('1px'); @@ -1270,6 +1717,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.marginRight).toBe('inherit'); + element.setAttribute('style', 'margin-right: var(--test-variable)'); + + expect(declaration.marginRight).toBe('var(--test-variable)'); + element.setAttribute('style', 'margin-right: 1px'); expect(declaration.marginRight).toBe('1px'); @@ -1292,6 +1743,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.marginBottom).toBe('inherit'); + element.setAttribute('style', 'margin-bottom: var(--test-variable)'); + + expect(declaration.marginBottom).toBe('var(--test-variable)'); + element.setAttribute('style', 'margin-bottom: 1px'); expect(declaration.marginBottom).toBe('1px'); @@ -1314,6 +1769,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.marginLeft).toBe('inherit'); + element.setAttribute('style', 'margin-left: var(--test-variable)'); + + expect(declaration.marginLeft).toBe('var(--test-variable)'); + element.setAttribute('style', 'margin-left: 1px'); expect(declaration.marginLeft).toBe('1px'); @@ -1334,18 +1793,37 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'background: inherit'); + expect(declaration.length).toBe(9); + expect(declaration.background).toBe('inherit'); expect(declaration.backgroundAttachment).toBe('inherit'); expect(declaration.backgroundClip).toBe('inherit'); expect(declaration.backgroundColor).toBe('inherit'); expect(declaration.backgroundImage).toBe('inherit'); - expect(declaration.backgroundPosition).toBe('inherit'); + expect(declaration.backgroundOrigin).toBe('inherit'); + expect(declaration.backgroundPositionX).toBe('inherit'); + expect(declaration.backgroundPositionY).toBe('inherit'); expect(declaration.backgroundRepeat).toBe('inherit'); expect(declaration.backgroundSize).toBe('inherit'); + element.setAttribute('style', 'background: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.background).toBe('var(--test-variable)'); + element.setAttribute('style', 'background: green'); expect(declaration.background).toBe('green'); + expect(declaration.backgroundAttachment).toBe('initial'); + expect(declaration.backgroundClip).toBe('initial'); + expect(declaration.backgroundColor).toBe('green'); + expect(declaration.backgroundImage).toBe('initial'); + expect(declaration.backgroundOrigin).toBe('initial'); + expect(declaration.backgroundPositionX).toBe('initial'); + expect(declaration.backgroundPositionY).toBe('initial'); + expect(declaration.backgroundRepeat).toBe('initial'); + expect(declaration.backgroundSize).toBe('initial'); element.setAttribute('style', 'background: rgb(255, 255, 255)'); @@ -1362,6 +1840,15 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'background: no-repeat center/80% url("../img/image.png")'); expect(declaration.background).toBe('url("../img/image.png") center center / 80% no-repeat'); + expect(declaration.backgroundAttachment).toBe('initial'); + expect(declaration.backgroundClip).toBe('initial'); + expect(declaration.backgroundColor).toBe('initial'); + expect(declaration.backgroundImage).toBe('url("../img/image.png")'); + expect(declaration.backgroundOrigin).toBe('initial'); + expect(declaration.backgroundPositionX).toBe('center'); + expect(declaration.backgroundPositionY).toBe('center'); + expect(declaration.backgroundRepeat).toBe('no-repeat'); + expect(declaration.backgroundSize).toBe('80%'); element.setAttribute( 'style', @@ -1382,6 +1869,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.backgroundImage).toBe('inherit'); + element.setAttribute('style', 'background-image: var(--test-variable)'); + + expect(declaration.backgroundImage).toBe('var(--test-variable)'); + element.setAttribute('style', 'background-image: url("test.jpg")'); expect(declaration.backgroundImage).toBe('url("test.jpg")'); @@ -1404,6 +1895,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.backgroundColor).toBe('inherit'); + element.setAttribute('style', 'background-color: var(--test-variable)'); + + expect(declaration.backgroundColor).toBe('var(--test-variable)'); + element.setAttribute('style', 'background-color: red'); expect(declaration.backgroundColor).toBe('red'); @@ -1423,6 +1918,7 @@ describe('CSSStyleDeclaration', () => { 'initial', 'revert', 'unset', + 'var(--test-variable)', 'repeat', 'repeat-x', 'repeat-y', @@ -1438,7 +1934,15 @@ describe('CSSStyleDeclaration', () => { it('Returns style property.', () => { const declaration = new CSSStyleDeclaration(element); - for (const repeat of ['inherit', 'initial', 'revert', 'unset', 'scroll', 'fixed']) { + for (const repeat of [ + 'inherit', + 'initial', + 'revert', + 'unset', + 'var(--test-variable)', + 'scroll', + 'fixed' + ]) { element.setAttribute('style', `background-attachment: ${repeat}`); expect(declaration.backgroundAttachment).toBe(repeat); } @@ -1451,7 +1955,17 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'background-position: inherit'); + expect(declaration.length).toBe(2); + expect(declaration.backgroundPosition).toBe('inherit'); + expect(declaration.backgroundPositionX).toBe('inherit'); + expect(declaration.backgroundPositionY).toBe('inherit'); + + element.setAttribute('style', 'background-position: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.backgroundPosition).toBe('var(--test-variable)'); element.setAttribute('style', 'background-position: top'); @@ -1559,6 +2073,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.width).toBe('inherit'); + element.setAttribute('style', 'width: var(--test-variable)'); + + expect(declaration.width).toBe('var(--test-variable)'); + element.setAttribute('style', 'width: 75%'); expect(declaration.width).toBe('75%'); @@ -1570,6 +2088,40 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'width: fit-content(20em)'); expect(declaration.width).toBe('fit-content(20em)'); + + element.setAttribute('style', 'width: 0'); + + expect(declaration.width).toBe('0px'); + }); + }); + + describe('get height()', () => { + it('Returns style property.', () => { + const declaration = new CSSStyleDeclaration(element); + + element.setAttribute('style', 'height: inherit'); + + expect(declaration.height).toBe('inherit'); + + element.setAttribute('style', 'height: var(--test-variable)'); + + expect(declaration.height).toBe('var(--test-variable)'); + + element.setAttribute('style', 'height: 75%'); + + expect(declaration.height).toBe('75%'); + + element.setAttribute('style', 'height: 75px'); + + expect(declaration.height).toBe('75px'); + + element.setAttribute('style', 'height: fit-content(20em)'); + + expect(declaration.height).toBe('fit-content(20em)'); + + element.setAttribute('style', 'height: 0'); + + expect(declaration.height).toBe('0px'); }); }); @@ -1581,6 +2133,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.top).toBe('inherit'); + element.setAttribute('style', 'top: var(--test-variable)'); + + expect(declaration.top).toBe('var(--test-variable)'); + element.setAttribute('style', 'top: 75%'); expect(declaration.top).toBe('75%'); @@ -1607,6 +2163,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.right).toBe('inherit'); + element.setAttribute('style', 'right: var(--test-variable)'); + + expect(declaration.right).toBe('var(--test-variable)'); + element.setAttribute('style', 'right: 75%'); expect(declaration.right).toBe('75%'); @@ -1629,6 +2189,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.bottom).toBe('inherit'); + element.setAttribute('style', 'bottom: var(--test-variable)'); + + expect(declaration.bottom).toBe('var(--test-variable)'); + element.setAttribute('style', 'bottom: 75%'); expect(declaration.bottom).toBe('75%'); @@ -1651,6 +2215,10 @@ describe('CSSStyleDeclaration', () => { expect(declaration.left).toBe('inherit'); + element.setAttribute('style', 'left: var(--test-variable)'); + + expect(declaration.left).toBe('var(--test-variable)'); + element.setAttribute('style', 'left: 75%'); expect(declaration.left).toBe('75%'); @@ -1671,7 +2239,22 @@ describe('CSSStyleDeclaration', () => { element.setAttribute('style', 'font: inherit'); + expect(declaration.length).toBe(7); + expect(declaration.font).toBe('inherit'); + expect(declaration.fontFamily).toBe('inherit'); + expect(declaration.fontSize).toBe('inherit'); + expect(declaration.fontStretch).toBe('inherit'); + expect(declaration.fontStyle).toBe('inherit'); + expect(declaration.fontVariant).toBe('inherit'); + expect(declaration.fontWeight).toBe('inherit'); + expect(declaration.lineHeight).toBe('inherit'); + + element.setAttribute('style', 'font: var(--test-variable)'); + + expect(declaration.length).toBe(1); + + expect(declaration.font).toBe('var(--test-variable)'); element.setAttribute('style', 'font: 1.2em "Fira Sans", sans-serif'); @@ -1712,7 +2295,14 @@ describe('CSSStyleDeclaration', () => { it('Returns style property.', () => { const declaration = new CSSStyleDeclaration(element); - for (const property of ['inherit', 'normal', 'italic', 'oblique', 'oblique 10deg']) { + for (const property of [ + 'inherit', + 'var(--test-variable)', + 'normal', + 'italic', + 'oblique', + 'oblique 10deg' + ]) { element.setAttribute('style', `font-style: ${property}`); expect(declaration.fontStyle).toBe(property); } @@ -1723,7 +2313,7 @@ describe('CSSStyleDeclaration', () => { it('Returns style property.', () => { const declaration = new CSSStyleDeclaration(element); - for (const property of ['inherit', 'normal', 'small-caps']) { + for (const property of ['inherit', 'var(--test-variable)', 'normal', 'small-caps']) { element.setAttribute('style', `font-variant: ${property}`); expect(declaration.fontVariant).toBe(property); } @@ -1736,6 +2326,7 @@ describe('CSSStyleDeclaration', () => { for (const property of [ 'inherit', + 'var(--test-variable)', 'normal', 'bold', 'bolder', @@ -1762,6 +2353,7 @@ describe('CSSStyleDeclaration', () => { for (const property of [ 'inherit', + 'var(--test-variable)', 'normal', 'ultra-condensed', 'extra-condensed', @@ -1784,6 +2376,7 @@ describe('CSSStyleDeclaration', () => { for (const property of [ 'inherit', + 'var(--test-variable)', 'medium', 'xx-small', 'x-small', @@ -1807,7 +2400,15 @@ describe('CSSStyleDeclaration', () => { it('Returns style property.', () => { const declaration = new CSSStyleDeclaration(element); - for (const property of ['inherit', 'normal', '10px', '10em', '10%', '10']) { + for (const property of [ + 'inherit', + 'var(--test-variable)', + 'normal', + '10px', + '10em', + '10%', + '10' + ]) { element.setAttribute('style', `line-height: ${property}`); expect(declaration.lineHeight).toBe(property); } @@ -1820,6 +2421,7 @@ describe('CSSStyleDeclaration', () => { for (const property of [ 'inherit', + 'var(--test-variable)', 'serif', 'sans-serif', 'cursive', @@ -1848,7 +2450,13 @@ describe('CSSStyleDeclaration', () => { it('Returns style property.', () => { const declaration = new CSSStyleDeclaration(element); - for (const property of ['inherit', 'red', 'rgb(255, 0, 0)', '#ff0000']) { + for (const property of [ + 'inherit', + 'var(--test-variable)', + 'red', + 'rgb(255, 0, 0)', + '#ff0000' + ]) { element.setAttribute('style', `color: ${property}`); expect(declaration.color).toBe(property); } @@ -1859,7 +2467,13 @@ describe('CSSStyleDeclaration', () => { it('Returns style property.', () => { const declaration = new CSSStyleDeclaration(element); - for (const property of ['inherit', 'red', 'rgb(255, 0, 0)', '#ff0000']) { + for (const property of [ + 'inherit', + 'var(--test-variable)', + 'red', + 'rgb(255, 0, 0)', + '#ff0000' + ]) { element.setAttribute('style', `flood-color: ${property}`); expect(declaration.floodColor).toBe(property); } diff --git a/packages/happy-dom/test/match-media/MediaQueryList.test.ts b/packages/happy-dom/test/match-media/MediaQueryList.test.ts index c9b6cb3bf..893356f31 100644 --- a/packages/happy-dom/test/match-media/MediaQueryList.test.ts +++ b/packages/happy-dom/test/match-media/MediaQueryList.test.ts @@ -7,35 +7,483 @@ describe('MediaQueryList', () => { let window: IWindow; beforeEach(() => { - window = new Window({ innerWidth: 1024, innerHeight: 1024 }); + window = new Window({ innerWidth: 1024, innerHeight: 768 }); + }); + + describe('get media()', () => { + it('Returns media string.', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(min-width: 1023px)' }).media).toBe( + '(min-width: 1023px)' + ); + expect( + new MediaQueryList({ ownerWindow: window, media: 'PRINT and (MAX-width: 1024px)' }).media + ).toBe('print and (max-width: 1024px)'); + expect( + new MediaQueryList({ ownerWindow: window, media: 'NOT all AND (prefers-COLOR-scheme)' }) + .media + ).toBe('not all and (prefers-color-scheme)'); + expect(new MediaQueryList({ ownerWindow: window, media: 'all and (hover: none' }).media).toBe( + 'all and (hover: none)' + ); + expect( + new MediaQueryList({ + ownerWindow: window, + media: 'all and (400px <= height <= 2000px) and (400px <= width <= 2000px)' + }).media + ).toBe('all and (400px <= height <= 2000px) and (400px <= width <= 2000px)'); + expect( + new MediaQueryList({ + ownerWindow: window, + media: + 'all and (400px <= height <= 2000px) and (400px <= width <= 2000px) and (min-width: 400px)' + }).media + ).toBe( + 'all and (400px <= height <= 2000px) and (400px <= width <= 2000px) and (min-width: 400px)' + ); + expect(new MediaQueryList({ ownerWindow: window, media: 'prefers-color-scheme' }).media).toBe( + '' + ); + expect( + new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme' }).media + ).toBe('not all'); + expect( + new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme)' }).media + ).toBe('(prefers-color-scheme)'); + }); }); describe('get matches()', () => { + it('Handles media type with name "all".', () => { + expect( + new MediaQueryList({ ownerWindow: window, media: 'all and (min-width: 1024px)' }).matches + ).toBe(true); + }); + + it('Handles media type with name "print".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: 'print' }).matches).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: 'print and (min-width: 1024px)' }).matches + ).toBe(false); + + window.happyDOM.settings.device.mediaType = 'print'; + + expect(new MediaQueryList({ ownerWindow: window, media: 'print' }).matches).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: 'print and (min-width: 1024px)' }).matches + ).toBe(true); + }); + + it('Handles media type with name "screen".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: 'screen' }).matches).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: 'screen and (min-width: 1024px)' }).matches + ).toBe(true); + }); + + it('Handles "not" keyword.', () => { + expect(new MediaQueryList({ ownerWindow: window, media: 'not all' }).matches).toBe(false); + expect(new MediaQueryList({ ownerWindow: window, media: 'not print' }).matches).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: 'not (min-width: 1025px)' }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: 'not (min-width: 1024px)' }).matches + ).toBe(false); + }); + + it('Handles "only" keyword.', () => { + expect(new MediaQueryList({ ownerWindow: window, media: 'only all' }).matches).toBe(true); + expect(new MediaQueryList({ ownerWindow: window, media: 'only print' }).matches).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: 'only screen and (min-width: 1024px)' }) + .matches + ).toBe(true); + }); + it('Handles "min-width".', () => { - expect(new MediaQueryList(window, '(min-width: 1025px)').matches).toBe(false); - expect(new MediaQueryList(window, '(min-width: 1024px)').matches).toBe(true); + expect(new MediaQueryList({ ownerWindow: window, media: '(min-width)' }).matches).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(min-width: 1025px)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(min-width: 1024px)' }).matches + ).toBe(true); + + expect( + new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1025 / 16}rem)` }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1024 / 16}rem)` }).matches + ).toBe(true); + + expect( + new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1025 / 16}em)` }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1024 / 16}em)` }).matches + ).toBe(true); + + expect(new MediaQueryList({ ownerWindow: window, media: '(min-width: 101vw)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(min-width: 100vw)' }).matches).toBe( + true + ); + + // Percentages should never match + expect(new MediaQueryList({ ownerWindow: window, media: '(min-width: 0%)' }).matches).toBe( + false + ); + + window.document.documentElement.style.fontSize = '10px'; + + expect( + new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1025 / 10}rem)` }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1024 / 10}rem)` }).matches + ).toBe(true); + + expect( + new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1025 / 10}em)` }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: `(min-width: ${1024 / 10}em)` }).matches + ).toBe(true); }); it('Handles "max-width".', () => { - expect(new MediaQueryList(window, '(max-width: 1023px)').matches).toBe(false); - expect(new MediaQueryList(window, '(max-width: 1024px)').matches).toBe(true); + expect(new MediaQueryList({ ownerWindow: window, media: '(max-width)' }).matches).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(max-width: 1023px)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(max-width: 1024px)' }).matches + ).toBe(true); + + expect( + new MediaQueryList({ ownerWindow: window, media: `(max-width: ${1023 / 16}rem)` }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: `(max-width: ${1024 / 16}rem)` }).matches + ).toBe(true); }); it('Handles "min-height".', () => { - expect(new MediaQueryList(window, '(min-height: 1025px)').matches).toBe(false); - expect(new MediaQueryList(window, '(min-height: 1024px)').matches).toBe(true); + expect(new MediaQueryList({ ownerWindow: window, media: '(min-height)' }).matches).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(min-height: 769px)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(min-height: 768px)' }).matches + ).toBe(true); + + expect( + new MediaQueryList({ ownerWindow: window, media: '(min-height: 101vh)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(min-height: 100vh)' }).matches + ).toBe(true); + + // Percentages should never match + expect(new MediaQueryList({ ownerWindow: window, media: '(min-height: 0%)' }).matches).toBe( + false + ); + + expect( + new MediaQueryList({ ownerWindow: window, media: `(min-height: ${769 / 16}rem)` }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: `(min-height: ${768 / 16}rem)` }).matches + ).toBe(true); }); it('Handles "max-height".', () => { - expect(new MediaQueryList(window, '(max-height: 1023px)').matches).toBe(false); - expect(new MediaQueryList(window, '(max-height: 1024px)').matches).toBe(true); + expect(new MediaQueryList({ ownerWindow: window, media: '(max-height)' }).matches).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(max-height: 767px)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(max-height: 768px)' }).matches + ).toBe(true); + + expect( + new MediaQueryList({ ownerWindow: window, media: `(max-height: ${767 / 16}rem)` }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: `(max-height: ${768 / 16}rem)` }).matches + ).toBe(true); }); - }); - describe('get media()', () => { - it('Returns media string.', () => { - const media = '(min-width: 1023px)'; - expect(new MediaQueryList(window, media).media).toBe(media); + it('Handles "width".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(width)' }).matches).toBe(true); + expect(new MediaQueryList({ ownerWindow: window, media: '(width: 1023px)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(width: 1024px)' }).matches).toBe( + true + ); + }); + + it('Handles "height".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(height)' }).matches).toBe(true); + expect(new MediaQueryList({ ownerWindow: window, media: '(height: 767px)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(height: 768px)' }).matches).toBe( + true + ); + }); + + it('Handles "orientation".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(orientation)' }).matches).toBe( + true + ); + expect( + new MediaQueryList({ ownerWindow: window, media: '(orientation: portrait)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(orientation: landscape)' }).matches + ).toBe(true); + + window.happyDOM.setInnerWidth(500); + window.happyDOM.setInnerHeight(1000); + + expect( + new MediaQueryList({ ownerWindow: window, media: '(orientation: portrait)' }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(orientation: landscape)' }).matches + ).toBe(false); + }); + + it('Handles "prefers-color-scheme".', () => { + expect( + new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme)' }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme: dark)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme: light)' }).matches + ).toBe(true); + + window.happyDOM.settings.device.prefersColorScheme = 'dark'; + + expect( + new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme: dark)' }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme: light)' }).matches + ).toBe(false); + }); + + it('Handles "hover".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(hover)' }).matches).toBe(true); + expect(new MediaQueryList({ ownerWindow: window, media: '(hover: invalid)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(hover: none)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(hover: hover)' }).matches).toBe( + true + ); + }); + + it('Handles "pointer".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(pointer)' }).matches).toBe(true); + expect(new MediaQueryList({ ownerWindow: window, media: '(pointer: invalid)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(pointer: none)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(pointer: coarse)' }).matches).toBe( + false + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(pointer: fine)' }).matches).toBe( + true + ); + }); + + it('Handles "any-pointer".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(any-pointer)' }).matches).toBe( + true + ); + expect( + new MediaQueryList({ ownerWindow: window, media: '(any-pointer: invalid)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(any-pointer: none)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(any-pointer: coarse)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(any-pointer: fine)' }).matches + ).toBe(true); + }); + + it('Handles "display-mode".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(display-mode)' }).matches).toBe( + true + ); + expect( + new MediaQueryList({ ownerWindow: window, media: '(display-mode: invalid)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(display-mode: browser)' }).matches + ).toBe(true); + }); + + it('Handles "min-aspect-ratio".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(min-aspect-ratio)' }).matches).toBe( + true + ); + expect( + new MediaQueryList({ ownerWindow: window, media: '(min-aspect-ratio: 1024/770)' }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(min-aspect-ratio: 1024/760)' }).matches + ).toBe(false); + }); + + it('Handles "max-aspect-ratio".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(max-aspect-ratio)' }).matches).toBe( + true + ); + expect( + new MediaQueryList({ ownerWindow: window, media: '(max-aspect-ratio: 1024/760)' }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(max-aspect-ratio: 1024/770)' }).matches + ).toBe(false); + }); + + it('Handles "aspect-ratio".', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(aspect-ratio)' }).matches).toBe( + true + ); + expect( + new MediaQueryList({ ownerWindow: window, media: '(aspect-ratio: 1024/768)' }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(aspect-ratio: 1024/769)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(aspect-ratio: 1024/767)' }).matches + ).toBe(false); + }); + + it('Handles defining a resolution range using the range syntax.', () => { + expect(new MediaQueryList({ ownerWindow: window, media: '(400px <= width)' }).matches).toBe( + true + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(400px < width)' }).matches).toBe( + true + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(2000px < width)' }).matches).toBe( + false + ); + expect( + new MediaQueryList({ ownerWindow: window, media: '(400px <= width <= 2000px)' }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(400px <= width <= 1023px)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(400px <= width <= 1024px)' }).matches + ).toBe(true); + expect(new MediaQueryList({ ownerWindow: window, media: '(2000px => width)' }).matches).toBe( + true + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(2000px > width)' }).matches).toBe( + true + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(700px > width)' }).matches).toBe( + false + ); + expect( + new MediaQueryList({ ownerWindow: window, media: `(${1024 / 16}rem <= width)` }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: `(${1024 / 16}em <= width)` }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: `(${1024 / 16}rem < width)` }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: `(${1024 / 16}em < width)` }).matches + ).toBe(false); + + expect(new MediaQueryList({ ownerWindow: window, media: '(400px <= height)' }).matches).toBe( + true + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(400px < height)' }).matches).toBe( + true + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(2000px < height)' }).matches).toBe( + false + ); + expect( + new MediaQueryList({ ownerWindow: window, media: '(400px <= height <= 2000px)' }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: '(400px <= height <= 767px)' }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: '(400px <= height <= 768px)' }).matches + ).toBe(true); + expect(new MediaQueryList({ ownerWindow: window, media: '(2000px => height)' }).matches).toBe( + true + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(2000px > height)' }).matches).toBe( + true + ); + expect(new MediaQueryList({ ownerWindow: window, media: '(700px > height)' }).matches).toBe( + false + ); + expect( + new MediaQueryList({ ownerWindow: window, media: `(${768 / 16}rem <= height)` }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: `(${768 / 16}em <= height)` }).matches + ).toBe(true); + expect( + new MediaQueryList({ ownerWindow: window, media: `(${768 / 16}rem < height)` }).matches + ).toBe(false); + expect( + new MediaQueryList({ ownerWindow: window, media: `(${768 / 16}em < height)` }).matches + ).toBe(false); + + expect( + new MediaQueryList({ + ownerWindow: window, + media: '(400px <= height <= 2000px) and (400px <= width <= 2000px)' + }).matches + ).toBe(true); + }); + + it('Handles multiple rules.', () => { + expect( + new MediaQueryList({ + ownerWindow: window, + media: '(min-width: 1024px) and (max-width: 2000px)' + }).matches + ).toBe(true); + expect( + new MediaQueryList({ + ownerWindow: window, + media: '(min-width: 768px) and (max-width: 1023px)' + }).matches + ).toBe(false); + expect( + new MediaQueryList({ + ownerWindow: window, + media: 'screen and (min-width: 1024px) and (max-width: 2000px)' + }).matches + ).toBe(true); }); }); @@ -43,7 +491,7 @@ describe('MediaQueryList', () => { it('Listens for window "resize" event when sending in a "change" event.', () => { let triggeredEvent = null; const media = '(min-width: 1025px)'; - const mediaQueryList = new MediaQueryList(window, media); + const mediaQueryList = new MediaQueryList({ ownerWindow: window, media: media }); mediaQueryList.addEventListener('change', (event: MediaQueryListEvent): void => { triggeredEvent = event; @@ -61,7 +509,10 @@ describe('MediaQueryList', () => { describe('removeEventListener()', () => { it('Removes listener for window "resize" event when sending in a "change" event.', () => { let triggeredEvent = null; - const mediaQueryList = new MediaQueryList(window, '(min-width: 1025px)'); + const mediaQueryList = new MediaQueryList({ + ownerWindow: window, + media: '(min-width: 1025px)' + }); const listener = (event: MediaQueryListEvent): void => { triggeredEvent = event; }; diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index cb38db48a..0bb8f8e64 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -90,17 +90,37 @@ describe('Window', () => { const windowWithOptions = new Window({ innerWidth: 1920, innerHeight: 1080, - url: 'http://localhost:8080' + url: 'http://localhost:8080', + settings: { + disableJavaScriptEvaluation: true, + device: { + prefersColorScheme: 'dark' + } + } }); const windowWithoutOptions = new Window(); expect(windowWithOptions.innerWidth).toBe(1920); expect(windowWithOptions.innerHeight).toBe(1080); expect(windowWithOptions.location.href).toBe('http://localhost:8080/'); + expect(windowWithOptions.happyDOM.settings.disableJavaScriptEvaluation).toBe(true); + expect(windowWithOptions.happyDOM.settings.disableJavaScriptFileLoading).toBe(false); + expect(windowWithOptions.happyDOM.settings.disableCSSFileLoading).toBe(false); + expect(windowWithOptions.happyDOM.settings.disableIframePageLoading).toBe(false); + expect(windowWithOptions.happyDOM.settings.enableFileSystemHttpRequests).toBe(false); + expect(windowWithOptions.happyDOM.settings.device.prefersColorScheme).toBe('dark'); + expect(windowWithOptions.happyDOM.settings.device.mediaType).toBe('screen'); expect(windowWithoutOptions.innerWidth).toBe(1024); expect(windowWithoutOptions.innerHeight).toBe(768); expect(windowWithoutOptions.location.href).toBe('about:blank'); + expect(windowWithoutOptions.happyDOM.settings.disableJavaScriptEvaluation).toBe(false); + expect(windowWithoutOptions.happyDOM.settings.disableJavaScriptFileLoading).toBe(false); + expect(windowWithoutOptions.happyDOM.settings.disableCSSFileLoading).toBe(false); + expect(windowWithoutOptions.happyDOM.settings.disableIframePageLoading).toBe(false); + expect(windowWithoutOptions.happyDOM.settings.enableFileSystemHttpRequests).toBe(false); + expect(windowWithoutOptions.happyDOM.settings.device.prefersColorScheme).toBe('light'); + expect(windowWithoutOptions.happyDOM.settings.device.mediaType).toBe('screen'); }); }); @@ -278,7 +298,7 @@ describe('Window', () => { } } - @media (max-width: 768px) { + @media (max-width: ${768 / 16}rem) { div { font-size: 20px; } @@ -330,7 +350,8 @@ describe('Window', () => { customElement.shadowRoot.querySelector('span') ); - expect(elementComputedStyle.font).toBe(''); + // Default value on HTML is "16px Times New Roman" + expect(elementComputedStyle.font).toBe('16px "Times New Roman"'); expect(elementComputedStyle.color).toBe('green'); expect(customElementComputedStyle.color).toBe('yellow'); @@ -349,17 +370,22 @@ describe('Window', () => { window.happyDOM.setInnerWidth(1024); parentStyle.innerHTML = ` + html { + font: 14px "Times New Roman"; + } + div { --color-variable: #000; - --valid-variable: 1px solid var(--color-variable); - --invalid-variable: invalid; + --border-variable: 1px solid var(--color-variable); + --font-variable: 1rem "Tahoma"; } `; elementStyle.innerHTML = ` span { - border: var(--valid-variable); - font: var(--invalid-variable); + border: var(--border-variable); + font: var(--font-variable); + color: var(--invalid-variable); } `; @@ -370,8 +396,101 @@ describe('Window', () => { document.body.appendChild(parent); expect(computedStyle.border).toBe('1px solid #000'); - expect(computedStyle.font).toBe(''); + expect(computedStyle.font).toBe('14px "Tahoma"'); + expect(computedStyle.color).toBe(''); + }); + + it('Returns a CSSStyleDeclaration object with computed styles containing "rem" and "em" measurement values converted to pixels.', () => { + const parent = document.createElement('div'); + const element = document.createElement('span'); + const computedStyle = window.getComputedStyle(element); + const parentStyle = document.createElement('style'); + const elementStyle = document.createElement('style'); + + window.happyDOM.setInnerWidth(1024); + + parentStyle.innerHTML = ` + html { + font-size: 10px; + } + + div { + font-size: 1.5rem; + } + `; + + elementStyle.innerHTML = ` + span { + width: 10rem; + height: 10em; + } + `; + + parent.appendChild(elementStyle); + parent.appendChild(element); + + document.body.appendChild(parentStyle); + document.body.appendChild(parent); + + expect(computedStyle.width).toBe('100px'); + expect(computedStyle.height).toBe('150px'); + }); + + it('Returns a CSSStyleDeclaration object with computed styles containing "%" measurement values converted to pixels.', () => { + const parent = document.createElement('div'); + const element = document.createElement('span'); + const computedStyle = window.getComputedStyle(element); + const parentStyle = document.createElement('style'); + const elementStyle = document.createElement('style'); + + window.happyDOM.setInnerWidth(1024); + + parentStyle.innerHTML = ` + html { + font-size: 62.5%; + } + + div { + font-size: 1.5rem; + } + `; + + elementStyle.innerHTML = ` + span { + width: 100%; + height: 10em; + } + `; + + parent.appendChild(elementStyle); + parent.appendChild(element); + + document.body.appendChild(parentStyle); + document.body.appendChild(parent); + + expect(computedStyle.width).toBe('0px'); + expect(computedStyle.height).toBe('150px'); }); + + for (const measurement of [ + { value: '100vw', result: '1024px' }, + { value: '100vh', result: '768px' }, + { value: '100vmin', result: '768px' }, + { value: '100vmax', result: '1024px' }, + { value: '1cm', result: '37.7812px' }, + { value: '1mm', result: '3.7781px' }, + { value: '1in', result: '96px' }, + { value: '1pt', result: '1.3281px' }, + { value: '1pc', result: '16px' }, + { value: '1Q', result: '0.945px' } + ]) { + it(`Returns a CSSStyleDeclaration object with computed styles for a "${measurement.value}" measurement value converted to pixels.`, () => { + const element = document.createElement('div'); + element.style.width = measurement.value; + document.body.appendChild(element); + expect(window.getComputedStyle(element).width).toBe(measurement.result); + }); + } }); describe('eval()', () => {