diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index 739a381e9..19895a36e 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -86,6 +86,10 @@ export default abstract class AbstractCSSStyleDeclaration { this._ownerElement['_attributes']['style'].name = 'style'; } + if (this._ownerElement.isConnected) { + this._ownerElement.ownerDocument['_cacheID']++; + } + this._ownerElement['_attributes']['style'].value = style.toString(); } } else { @@ -137,6 +141,10 @@ export default abstract class AbstractCSSStyleDeclaration { const style = this._elementStyle.getElementStyle(); style.set(name, value, !!priority); + if (this._ownerElement.isConnected) { + this._ownerElement.ownerDocument['_cacheID']++; + } + this._ownerElement['_attributes']['style'].value = style.toString(); } else { this._style.set(name, value, !!priority); @@ -163,6 +171,10 @@ export default abstract class AbstractCSSStyleDeclaration { style.remove(name); const newCSSText = style.toString(); if (newCSSText) { + if (this._ownerElement.isConnected) { + this._ownerElement.ownerDocument['_cacheID']++; + } + this._ownerElement['_attributes']['style'].value = newCSSText; } else { delete this._ownerElement['_attributes']['style']; diff --git a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts index 686ed4659..de3749d17 100644 --- a/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/utilities/CSSStyleDeclarationElementStyle.ts @@ -12,14 +12,29 @@ import CSSStyleRule from '../../rules/CSSStyleRule'; import CSSStyleDeclarationElementDefaultCSS from './CSSStyleDeclarationElementDefaultCSS'; import CSSStyleDeclarationElementInheritedProperties from './CSSStyleDeclarationElementInheritedProperties'; import CSSStyleDeclarationCSSParser from './CSSStyleDeclarationCSSParser'; +import QuerySelector from '../../../query-selector/QuerySelector'; const CSS_VARIABLE_REGEXP = /var\( *(--[^) ]+)\)/g; +type IStyleAndElement = { + element: IElement | IShadowRoot | IDocument; + cssTexts: Array<{ cssText: string; priorityWeight: number }>; +}; + /** * CSS Style Declaration utility */ export default class CSSStyleDeclarationElementStyle { - private cache: { [k: string]: CSSStyleDeclarationPropertyManager } = {}; + private cache: { + propertyManager: CSSStyleDeclarationPropertyManager; + cssText: string; + documentCacheID: number; + } = { + propertyManager: null, + cssText: null, + documentCacheID: null + }; + private element: IElement; private computed: boolean; @@ -47,11 +62,12 @@ export default class CSSStyleDeclarationElementStyle { const cssText = this.element['_attributes']['style']?.value; if (cssText) { - if (this.cache[cssText]) { - return this.cache[cssText]; + if (this.cache.propertyManager && this.cache.cssText === cssText) { + return this.cache.propertyManager; } - this.cache[cssText] = new CSSStyleDeclarationPropertyManager({ cssText }); - return this.cache[cssText]; + this.cache.cssText = cssText; + this.cache.propertyManager = new CSSStyleDeclarationPropertyManager({ cssText }); + return this.cache.propertyManager; } return new CSSStyleDeclarationPropertyManager(); @@ -64,28 +80,37 @@ export default class CSSStyleDeclarationElementStyle { * @returns Style sheets. */ private getComputedElementStyle(): CSSStyleDeclarationPropertyManager { - const documentElements: Array<{ element: IElement; cssText: string }> = []; - const parentElements: Array<{ element: IElement; cssText: string }> = []; - let styleAndElement = { + const documentElements: Array = []; + const parentElements: Array = []; + let styleAndElement: IStyleAndElement = { element: this.element, - cssText: '' + cssTexts: [] }; - let shadowRootElements: Array<{ element: IElement; cssText: string }> = []; + let shadowRootElements: Array = []; if (!this.element.isConnected) { return new CSSStyleDeclarationPropertyManager(); } + if ( + this.cache.propertyManager && + this.cache.documentCacheID === this.element.ownerDocument['_cacheID'] + ) { + return this.cache.propertyManager; + } + + this.cache.documentCacheID = this.element.ownerDocument['_cacheID']; + // Walks through all parent elements and stores them in an array with element and matching CSS text. while (styleAndElement.element) { if (styleAndElement.element.nodeType === NodeTypeEnum.elementNode) { const rootNode = styleAndElement.element.getRootNode(); if (rootNode.nodeType === NodeTypeEnum.documentNode) { - documentElements.unshift(<{ element: IElement; cssText: string }>styleAndElement); + documentElements.unshift(styleAndElement); } else { - shadowRootElements.unshift(<{ element: IElement; cssText: string }>styleAndElement); + shadowRootElements.unshift(styleAndElement); } - parentElements.unshift(<{ element: IElement; cssText: string }>styleAndElement); + parentElements.unshift(styleAndElement); } if (styleAndElement.element === this.element.ownerDocument) { @@ -103,7 +128,7 @@ export default class CSSStyleDeclarationElementStyle { } } - styleAndElement = { element: null, cssText: '' }; + styleAndElement = { element: null, cssTexts: [] }; } else if ((styleAndElement.element).host) { const styleSheets = >( (styleAndElement.element).querySelectorAll('style,link[rel="stylesheet"]') @@ -111,7 +136,7 @@ export default class CSSStyleDeclarationElementStyle { styleAndElement = { element: (styleAndElement.element).host, - cssText: '' + cssTexts: [] }; for (const styleSheet of styleSheets) { @@ -120,39 +145,58 @@ export default class CSSStyleDeclarationElementStyle { this.parseCSSRules({ elements: shadowRootElements, cssRules: sheet.cssRules, - hostElement: <{ element: IElement; cssText: string }>styleAndElement + hostElement: styleAndElement }); } } shadowRootElements = []; } else { - styleAndElement = { element: styleAndElement.element.parentNode, cssText: '' }; + styleAndElement = { element: styleAndElement.element.parentNode, cssTexts: [] }; } } // Concatenates all parent element CSS to one string. const targetElement = parentElements[parentElements.length - 1]; - let inheritedCSSText = CSSStyleDeclarationElementDefaultCSS.default; + let inheritedCSSText = ''; for (const parentElement of parentElements) { if (parentElement !== targetElement) { - inheritedCSSText += - (CSSStyleDeclarationElementDefaultCSS[parentElement.element.tagName] || '') + - parentElement.cssText + - (parentElement.element['_attributes']['style']?.value || ''); + parentElement.cssTexts.sort((a, b) => a.priorityWeight - b.priorityWeight); + + if (CSSStyleDeclarationElementDefaultCSS[(parentElement.element).tagName]) { + inheritedCSSText += + CSSStyleDeclarationElementDefaultCSS[(parentElement.element).tagName]; + } + + for (const cssText of parentElement.cssTexts) { + inheritedCSSText += cssText.cssText; + } + + if (parentElement.element['_attributes']['style']?.value) { + inheritedCSSText += parentElement.element['_attributes']['style'].value; + } } } const cssVariables: { [k: string]: string } = {}; const properties = {}; - const targetCSSText = - (CSSStyleDeclarationElementDefaultCSS[targetElement.element.tagName] || '') + - targetElement.cssText + - (targetElement.element['_attributes']['style']?.value || ''); + 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[combinedCSSText]) { - return this.cache[combinedCSSText]; + if (this.cache.propertyManager && this.cache.cssText === combinedCSSText) { + return this.cache.propertyManager; } // Parses the parent element CSS and stores CSS variables and inherited properties. @@ -204,7 +248,8 @@ export default class CSSStyleDeclarationElementStyle { propertyManager.set(name, properties[name].value, properties[name].important); } - this.cache[combinedCSSText] = propertyManager; + this.cache.cssText = combinedCSSText; + this.cache.propertyManager = propertyManager; return propertyManager; } @@ -216,13 +261,11 @@ export default class CSSStyleDeclarationElementStyle { * @param options.elements Elements. * @param options.cssRules CSS rules. * @param [options.hostElement] Host element. - * @param [options.hostElement.element] Element. - * @param [options.hostElement.cssText] CSS text. */ private parseCSSRules(options: { cssRules: CSSRule[]; - elements: Array<{ element: IElement; cssText: string }>; - hostElement?: { element: IElement; cssText: string }; + elements: Array; + hostElement?: IStyleAndElement; }): void { if (!options.elements.length) { return; @@ -236,12 +279,19 @@ export default class CSSStyleDeclarationElementStyle { if (selectorText) { if (selectorText.startsWith(':host')) { if (options.hostElement) { - options.hostElement.cssText += (rule)._cssText; + options.hostElement.cssTexts.push({ + cssText: (rule)._cssText, + priorityWeight: 0 + }); } } else { for (const element of options.elements) { - if (element.element.matches(selectorText)) { - element.cssText += (rule)._cssText; + const matchResult = QuerySelector.match(element.element, selectorText); + if (matchResult.matches) { + element.cssTexts.push({ + cssText: (rule)._cssText, + priorityWeight: matchResult.priorityWeight + }); } } } diff --git a/packages/happy-dom/src/nodes/character-data/CharacterData.ts b/packages/happy-dom/src/nodes/character-data/CharacterData.ts index 0a3875e90..5d039ca94 100644 --- a/packages/happy-dom/src/nodes/character-data/CharacterData.ts +++ b/packages/happy-dom/src/nodes/character-data/CharacterData.ts @@ -56,6 +56,10 @@ export default abstract class CharacterData extends Node implements ICharacterDa const oldValue = this._data; this._data = data; + if (this.isConnected) { + this.ownerDocument['_cacheID']++; + } + // MutationObserver if (this._observers.length > 0) { for (const observer of this._observers) { diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index bcf51a28b..1e1493a2d 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -60,6 +60,10 @@ export default class Document extends Node implements IDocument { public readonly defaultView: IWindow; public readonly _readyStateManager: DocumentReadyStateManager; public _activeElement: IHTMLElement = null; + + // Used as an unique identifier which is updated whenever the DOM gets modified. + public _cacheID = 0; + protected _isFirstWrite = true; protected _isFirstWriteAfterOpen = false; private _cookie = ''; diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 0cf926b6f..a90f3a487 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -5,7 +5,6 @@ import DOMRect from './DOMRect'; import DOMTokenList from '../../dom-token-list/DOMTokenList'; import IDOMTokenList from '../../dom-token-list/IDOMTokenList'; import QuerySelector from '../../query-selector/QuerySelector'; -import SelectorItem from '../../query-selector/SelectorItem'; import MutationRecord from '../../mutation-observer/MutationRecord'; import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum'; import NamespaceURI from '../../config/NamespaceURI'; @@ -744,12 +743,7 @@ export default class Element extends Node implements IElement { * @returns "true" if matching. */ public matches(selector: string): boolean { - for (const part of selector.split(',')) { - if (new SelectorItem(part.trim()).match(this)) { - return true; - } - } - return false; + return QuerySelector.match(this, selector).matches; } /** @@ -852,6 +846,10 @@ export default class Element extends Node implements IElement { (attribute.ownerElement) = this; (attribute.ownerDocument) = this.ownerDocument; + if (this.isConnected) { + this.ownerDocument['_cacheID']++; + } + this._attributes[name] = attribute; this._updateDomListIndices(); @@ -937,6 +935,10 @@ export default class Element extends Node implements IElement { public removeAttributeNode(attribute: IAttr): void { delete this._attributes[attribute.name]; + if (this.isConnected) { + this.ownerDocument['_cacheID']++; + } + this._updateDomListIndices(); if ( diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 35799a2d0..ae2cc05a5 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -308,6 +308,10 @@ export default class Node extends EventTarget implements INode { } } + if (this.isConnected) { + (this.ownerDocument || this)['_cacheID']++; + } + this.childNodes.push(node); (node)._connectToNode(this); @@ -345,6 +349,10 @@ export default class Node extends EventTarget implements INode { throw new DOMException('Failed to remove node. Node is not child of parent.'); } + if (this.isConnected) { + (this.ownerDocument || this)['_cacheID']++; + } + this.childNodes.splice(index, 1); (node)._connectToNode(null); @@ -404,6 +412,10 @@ export default class Node extends EventTarget implements INode { ); } + if (this.isConnected) { + (this.ownerDocument || this)['_cacheID']++; + } + if (newNode.parentNode) { const index = newNode.parentNode.childNodes.indexOf(newNode); if (index !== -1) { diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index ceab058e7..31bf010bc 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -8,9 +8,6 @@ import NodeListFactory from '../nodes/node/NodeListFactory'; const SELECTOR_PART_REGEXP = /(\[[^\]]+\]|[a-zA-Z0-9-_.#"*:()\]]+)|([ ,>]+)/g; -// The above one seem to work fine and is faster, but this one can be useful if more rules need to be added as it is more "correct". -// Const SELECTOR_PART_REGEXP = /([a-zA-Z0-9-$.]+|\[[a-zA-Z0-9-]+\]|\[[a-zA-Z0-9$-~|^$*]+[ ]*=[ ]*"[^"]+"\])|([ ,]+)/g; - /** * Utility for query selection in an HTML element. * @@ -57,6 +54,70 @@ export default class QuerySelector { return null; } + /** + * Checks if a node matches a selector and returns priority weight. + * + * @param node Node to search in. + * @param selector Selector. + * @returns Result. + */ + public static match(node: INode, selector: string): { priorityWeight: number; matches: boolean } { + for (const parts of this.getSelectorParts(selector)) { + const result = this.matchesSelector(node, node, parts.reverse()); + + if (result.matches) { + return result; + } + } + + return { priorityWeight: 0, matches: false }; + } + + /** + * Checks if a node matches a selector. + * + * @param targetNode Target node. + * @param currentNode Current node. + * @param selectorParts Selector parts. + * @param [priorityWeight] Priority weight. + * @returns Result. + */ + private static matchesSelector( + targetNode: INode, + currentNode: INode, + selectorParts: string[], + priorityWeight = 0 + ): { + priorityWeight: number; + matches: boolean; + } { + const isDirectChild = selectorParts[0] === '>'; + if (isDirectChild) { + selectorParts = selectorParts.slice(1); + if (selectorParts.length === 0) { + return { priorityWeight: 0, matches: false }; + } + } + + if (selectorParts.length === 0) { + return { priorityWeight, matches: true }; + } + + const selector = new SelectorItem(selectorParts[0]); + const result = selector.match(currentNode); + + if (targetNode === currentNode && !result.matches) { + return { priorityWeight: 0, matches: false }; + } + + return this.matchesSelector( + isDirectChild ? currentNode.parentNode : targetNode, + currentNode.parentNode, + selectorParts.slice(1), + priorityWeight + result.priorityWeight + ); + } + /** * Finds elements based on a query selector for a part of a list of selectors separated with comma. * @@ -81,7 +142,7 @@ export default class QuerySelector { for (const node of nodes) { if (node.nodeType === Node.ELEMENT_NODE) { - if (selector.match(node)) { + if (selector.match(node).matches) { if (selectorParts.length === 1) { if (rootNode !== node) { matched.push(node); @@ -125,7 +186,7 @@ export default class QuerySelector { const selector = selectorItem || new SelectorItem(selectorParts[0]); for (const node of nodes) { - if (node.nodeType === Node.ELEMENT_NODE && selector.match(node)) { + if (node.nodeType === Node.ELEMENT_NODE && selector.match(node).matches) { if (selectorParts.length === 1) { if (rootNode !== node) { return node; diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts index dc0b2389b..5c24fda9d 100644 --- a/packages/happy-dom/src/query-selector/SelectorItem.ts +++ b/packages/happy-dom/src/query-selector/SelectorItem.ts @@ -1,4 +1,5 @@ import DOMException from '../exception/DOMException'; +import IElement from '../nodes/element/IElement'; import Element from '../nodes/element/Element'; const ATTRIBUTE_REGEXP = @@ -13,12 +14,12 @@ const ID_REGEXP = /#[A-Za-z][-A-Za-z0-9_]*/g; * Selector item. */ export default class SelectorItem { - public isAll: boolean; - public isID: boolean; - public isAttribute: boolean; - public isPseudo: boolean; - public isClass: boolean; - public isTagName: boolean; + private isAll: boolean; + private isID: boolean; + private isAttribute: boolean; + private isPseudo: boolean; + private isClass: boolean; + private isTagName: boolean; private tagName = null; private selector: string; private id: string; @@ -42,6 +43,7 @@ export default class SelectorItem { this.isTagName = this.tagName !== null; this.selector = selector; this.id = null; + if (!this.isAll && this.isID) { const idMatches = baseSelector.match(ID_REGEXP); if (idMatches) { @@ -54,46 +56,60 @@ export default class SelectorItem { * Matches a selector against an element. * * @param element HTML element. - * @returns TRUE if matching. + * @returns Result. */ - public match(element: Element): boolean { + public match(element: IElement): { priorityWeight: number; matches: boolean } { const selector = this.selector; + let priorityWeight = 0; + // Is all (*) if (this.isAll) { - return true; + return { priorityWeight: 0, matches: true }; } // ID Match if (this.isID) { + priorityWeight += 100; + if (this.id !== element.id) { - return false; + return { priorityWeight: 0, matches: false }; } } // Tag name match if (this.isTagName) { + priorityWeight += 1; + if (this.tagName !== element.tagName) { - return false; + return { priorityWeight: 0, matches: false }; } } // Class match - if (this.isClass && !this.matchesClass(element, selector)) { - return false; + if (this.isClass) { + const result = this.matchesClass(element, selector); + priorityWeight += result.priorityWeight; + if (!result.matches) { + return { priorityWeight: 0, matches: false }; + } } // Pseudo match if (this.isPseudo && !this.matchesPsuedo(element, selector)) { - return false; + return { priorityWeight: 0, matches: false }; } // Attribute match - if (this.isAttribute && !this.matchesAttribute(element, selector)) { - return false; + if (this.isAttribute) { + const result = this.matchesAttribute(element, selector); + priorityWeight += result.priorityWeight; + if (!result.matches) { + return { priorityWeight: 0, matches: false }; + } } - return true; + return { priorityWeight, matches: true }; } /** @@ -103,7 +119,7 @@ export default class SelectorItem { * @param selector Selector. * @returns True if it is a match. */ - private matchesPsuedo(element: Element, selector: string): boolean { + private matchesPsuedo(element: IElement, selector: string): boolean { const regexp = new RegExp(PSUEDO_REGEXP, 'g'); let match: RegExpMatchArray; @@ -113,8 +129,8 @@ export default class SelectorItem { return false; } else if ( match[3] && - ((isNotClass && this.matchesClass(element, match[3])) || - (!isNotClass && this.matchesAttribute(element, match[3]))) + ((isNotClass && this.matchesClass(element, match[3]).matches) || + (!isNotClass && this.matchesAttribute(element, match[3])).matches) ) { return false; } else if (match[4] && !this.matchesPsuedoExpression(element, match[4])) { @@ -133,8 +149,8 @@ export default class SelectorItem { * @param place Place. * @returns True if it is a match. */ - private matchesNthChild(element: Element, psuedo: string, place: string): boolean { - let children = element.parentNode ? (element.parentNode).children : []; + private matchesNthChild(element: IElement, psuedo: string, place: string): boolean { + let children = element.parentNode ? (element.parentNode).children : []; switch (psuedo.toLowerCase()) { case 'nth-of-type': @@ -188,8 +204,8 @@ export default class SelectorItem { * @param psuedo Psuedo name. * @returns True if it is a match. */ - private matchesPsuedoExpression(element: Element, psuedo: string): boolean { - const parent = element.parentNode; + private matchesPsuedoExpression(element: IElement, psuedo: string): boolean { + const parent = element.parentNode; if (!parent) { return false; @@ -240,24 +256,31 @@ export default class SelectorItem { * * @param element Element. * @param selector Selector. - * @returns True if it is a match. + * @returns Result. */ - private matchesAttribute(element: Element, selector: string): boolean { + private matchesAttribute( + element: IElement, + selector: string + ): { priorityWeight: number; matches: boolean } { const regexp = new RegExp(ATTRIBUTE_REGEXP, 'g'); let match: RegExpMatchArray; + let priorityWeight = 0; while ((match = regexp.exec(selector))) { const isPsuedo = match.index > 0 && selector[match.index - 1] === '('; + + priorityWeight += 10; + if ( !isPsuedo && ((match[1] && !this.matchesAttributeName(element, match[1])) || (match[2] && !this.matchesAttributeNameAndValue(element, match[2], match[4], match[3]))) ) { - return false; + return { priorityWeight: 0, matches: false }; } } - return true; + return { priorityWeight, matches: true }; } /** @@ -265,20 +288,26 @@ export default class SelectorItem { * * @param element Element. * @param selector Selector. - * @returns True if it is a match. + * @returns Result. */ - private matchesClass(element: Element, selector: string): boolean { + private matchesClass( + element: IElement, + selector: string + ): { priorityWeight: number; matches: boolean } { const regexp = new RegExp(CLASS_REGEXP, 'g'); const classList = element.className.split(' '); + const classSelector = selector.split(':')[0]; + let priorityWeight = 0; let match: RegExpMatchArray; - while ((match = regexp.exec(selector.split(':')[0]))) { + while ((match = regexp.exec(classSelector))) { + priorityWeight += 10; if (!classList.includes(match[1])) { - return false; + return { priorityWeight: 0, matches: false }; } } - return true; + return { priorityWeight, matches: true }; } /** @@ -288,12 +317,12 @@ export default class SelectorItem { * @param attributeName Attribute name. * @returns True if it is a match. */ - private matchesAttributeName(element: Element, attributeName: string): boolean { + private matchesAttributeName(element: IElement, attributeName: string): boolean { if (ATTRIBUTE_NAME_REGEXP.test(attributeName)) { throw new DOMException(`The selector "${this.selector}" is not valid.`); } - return !!element._attributes[attributeName.toLowerCase()]; + return !!(element)._attributes[attributeName.toLowerCase()]; } /** . @@ -314,12 +343,12 @@ export default class SelectorItem { * @param matchType */ private matchesAttributeNameAndValue( - element: Element, + element: IElement, attributeName: string, attributeValue: string, matchType: string = null ): boolean { - const attribute = element._attributes[attributeName.toLowerCase()]; + const attribute = (element)._attributes[attributeName.toLowerCase()]; const value = attributeValue; if (ATTRIBUTE_NAME_REGEXP.test(attributeName)) { diff --git a/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts b/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts index 749b46402..5db6b0f43 100644 --- a/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts +++ b/packages/happy-dom/test/css/declaration/CSSStyleDeclaration.test.ts @@ -2217,6 +2217,30 @@ describe('CSSStyleDeclaration', () => { expect(declaration.getPropertyValue('text-transform')).toBe('uppercase'); expect(declaration.getPropertyPriority('text-transform')).toBe('important'); }); + + it('Is using a cache.', () => { + const declaration = new CSSStyleDeclaration(element); + + document.body.appendChild(element); + element.setAttribute('style', `border: 2px solid green;border-radius: 2px;font-size: 12px;`); + + declaration.getPropertyValue('border'); + const elementStyle = declaration['_elementStyle'].getElementStyle(); + declaration.getPropertyValue('border'); + expect(elementStyle).toBe(declaration['_elementStyle'].getElementStyle()); + + const computedDeclaration = new CSSStyleDeclaration(element, true); + + computedDeclaration.getPropertyValue('border'); + const computedElementStyle = declaration['_elementStyle'].getElementStyle(); + computedDeclaration.getPropertyValue('border'); + expect(computedElementStyle).toBe(declaration['_elementStyle'].getElementStyle()); + + element.setAttribute('style', `border: 2px solid green;`); + + expect(elementStyle).not.toBe(declaration['_elementStyle'].getElementStyle()); + expect(computedElementStyle).not.toBe(declaration['_elementStyle'].getElementStyle()); + }); }); describe('getPropertyPriority()', () => { diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index fceebab10..8c8a92837 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -249,8 +249,14 @@ describe('Window', () => { cursor: pointer; } - span { + div span { border-radius: 1px !important; + direction: ltr; + } + + .mySpan { + /* Should have higher priority because of the specifity of the rule */ + direction: rtl; } @media (min-width: 1024px) { @@ -266,12 +272,14 @@ describe('Window', () => { } `; + element.className = 'mySpan'; elementStyle.innerHTML = ` span { border: 1px solid #000; border-radius: 2px; color: green; cursor: default; + direction: ltr; } `; @@ -286,6 +294,7 @@ describe('Window', () => { expect(computedStyle.borderRadius).toBe('1px'); expect(computedStyle.color).toBe('red'); expect(computedStyle.cursor).toBe('default'); + expect(computedStyle.direction).toBe('rtl'); }); it('Returns a CSSStyleDeclaration object with computed styles from style sheets for elements in a HTMLShadowRoot.', () => {