From 3bce5470474998e39a17f5321ab6188f6545fa64 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 28 May 2024 14:22:08 +0200 Subject: [PATCH] fix: [#1445] Adds fix to update CSS rules in HTMLStyleElement sheet when editing the data of a child Text node --- packages/happy-dom/src/PropertySymbol.ts | 2 + .../html-style-element/HTMLStyleElement.ts | 23 +++-- packages/happy-dom/src/nodes/node/Node.ts | 11 ++- packages/happy-dom/src/nodes/text/Text.ts | 4 + .../HTMLStyleElement.test.ts | 86 +++++++++++++++++++ 5 files changed, 116 insertions(+), 10 deletions(-) diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index 81e22cf21..5aca67fc0 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -164,3 +164,5 @@ export const appendChild = Symbol('appendChild'); export const removeChild = Symbol('removeChild'); export const insertBefore = Symbol('insertBefore'); export const replaceChild = Symbol('replaceChild'); +export const styleNode = Symbol('styleNode'); +export const updateSheet = Symbol('updateSheet'); diff --git a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts index 2d0ec33e0..677e4d7dd 100644 --- a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts +++ b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts @@ -11,6 +11,7 @@ import Node from '../node/Node.js'; */ export default class HTMLStyleElement extends HTMLElement { private [PropertySymbol.sheet]: CSSStyleSheet | null = null; + public [PropertySymbol.styleNode] = this; /** * Returns CSS style sheet. @@ -84,9 +85,7 @@ export default class HTMLStyleElement extends HTMLElement { */ public override [PropertySymbol.appendChild](node: Node): Node { const returnValue = super[PropertySymbol.appendChild](node); - if (this[PropertySymbol.sheet]) { - this[PropertySymbol.sheet].replaceSync(this.textContent); - } + this[PropertySymbol.updateSheet](); return returnValue; } @@ -95,9 +94,7 @@ export default class HTMLStyleElement extends HTMLElement { */ public override [PropertySymbol.removeChild](node: Node): Node { const returnValue = super[PropertySymbol.removeChild](node); - if (this[PropertySymbol.sheet]) { - this[PropertySymbol.sheet].replaceSync(this.textContent); - } + this[PropertySymbol.updateSheet](); return returnValue; } @@ -106,9 +103,7 @@ export default class HTMLStyleElement extends HTMLElement { */ public override [PropertySymbol.insertBefore](newNode: Node, referenceNode: Node | null): Node { const returnValue = super[PropertySymbol.insertBefore](newNode, referenceNode); - if (this[PropertySymbol.sheet]) { - this[PropertySymbol.sheet].replaceSync(this.textContent); - } + this[PropertySymbol.updateSheet](); return returnValue; } @@ -125,4 +120,14 @@ export default class HTMLStyleElement extends HTMLElement { this[PropertySymbol.sheet] = null; } } + + /** + * Updates the CSSStyleSheet with the text content. + */ + public [PropertySymbol.updateSheet](): void { + if (this[PropertySymbol.sheet]) { + this[PropertySymbol.ownerDocument][PropertySymbol.cacheID]++; + this[PropertySymbol.sheet].replaceSync(this.textContent); + } + } } diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 2321df6e1..865c92f7a 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -61,6 +61,7 @@ export default class Node extends EventTarget { public [PropertySymbol.formNode]: Node = null; public [PropertySymbol.selectNode]: Node = null; public [PropertySymbol.textAreaNode]: Node = null; + public [PropertySymbol.styleNode]: Node = null; public [PropertySymbol.observers]: MutationListener[] = []; public [PropertySymbol.childNodes]: NodeList = new NodeList(); @@ -516,6 +517,7 @@ export default class Node extends EventTarget { const formNode = (this)[PropertySymbol.formNode]; const selectNode = (this)[PropertySymbol.selectNode]; const textAreaNode = (this)[PropertySymbol.textAreaNode]; + const styleNode = (this)[PropertySymbol.styleNode]; if (this[PropertySymbol.nodeType] !== NodeTypeEnum.documentFragmentNode) { this[PropertySymbol.parentNode] = parentNode; @@ -539,6 +541,12 @@ export default class Node extends EventTarget { ? (parentNode)[PropertySymbol.textAreaNode] : null; } + + if (this['tagName'] !== 'STYLE') { + (this)[PropertySymbol.styleNode] = parentNode + ? (parentNode)[PropertySymbol.styleNode] + : null; + } } if (this[PropertySymbol.isConnected] !== isConnected) { @@ -584,7 +592,8 @@ export default class Node extends EventTarget { } else if ( formNode !== this[PropertySymbol.formNode] || selectNode !== this[PropertySymbol.selectNode] || - textAreaNode !== this[PropertySymbol.textAreaNode] + textAreaNode !== this[PropertySymbol.textAreaNode] || + styleNode !== this[PropertySymbol.styleNode] ) { for (const child of this[PropertySymbol.childNodes]) { (child)[PropertySymbol.connectToNode](this); diff --git a/packages/happy-dom/src/nodes/text/Text.ts b/packages/happy-dom/src/nodes/text/Text.ts index eb29afc3d..da9db4e7c 100644 --- a/packages/happy-dom/src/nodes/text/Text.ts +++ b/packages/happy-dom/src/nodes/text/Text.ts @@ -38,6 +38,10 @@ export default class Text extends CharacterData { if (this[PropertySymbol.textAreaNode]) { (this[PropertySymbol.textAreaNode])[PropertySymbol.resetSelection](); } + + if (this[PropertySymbol.styleNode]) { + this[PropertySymbol.styleNode][PropertySymbol.updateSheet](); + } } /** diff --git a/packages/happy-dom/test/nodes/html-style-element/HTMLStyleElement.test.ts b/packages/happy-dom/test/nodes/html-style-element/HTMLStyleElement.test.ts index e5fd8e0a3..930296f95 100644 --- a/packages/happy-dom/test/nodes/html-style-element/HTMLStyleElement.test.ts +++ b/packages/happy-dom/test/nodes/html-style-element/HTMLStyleElement.test.ts @@ -75,5 +75,91 @@ describe('HTMLStyleElement', () => { expect(element.sheet.cssRules[1].cssText).toBe('body { background-color: red; }'); expect(element.sheet.cssRules[2].cssText).toBe('div { background-color: green; }'); }); + + it('Updates rules when appending a text node.', () => { + document.head.appendChild(element); + + expect(element.sheet.cssRules.length).toBe(0); + + const textNode = document.createTextNode( + 'body { background-color: red }\ndiv { background-color: green }' + ); + + element.appendChild(textNode); + + expect(element.sheet.cssRules[0].cssText).toBe('body { background-color: red; }'); + expect(element.sheet.cssRules[1].cssText).toBe('div { background-color: green; }'); + }); + + it('Updates rules when removing a text node.', () => { + document.head.appendChild(element); + + const textNode = document.createTextNode( + 'body { background-color: red }\ndiv { background-color: green }' + ); + + element.appendChild(textNode); + + expect(element.sheet.cssRules.length).toBe(2); + + expect(element.sheet.cssRules[0].cssText).toBe('body { background-color: red; }'); + expect(element.sheet.cssRules[1].cssText).toBe('div { background-color: green; }'); + + element.removeChild(textNode); + + expect(element.sheet.cssRules.length).toBe(0); + }); + + it('Updates rules when inserting a text node.', () => { + document.head.appendChild(element); + + const textNode = document.createTextNode( + 'body { background-color: red }\ndiv { background-color: green }' + ); + + element.appendChild(textNode); + + expect(element.sheet.cssRules.length).toBe(2); + + expect(element.sheet.cssRules[0].cssText).toBe('body { background-color: red; }'); + expect(element.sheet.cssRules[1].cssText).toBe('div { background-color: green; }'); + + const textNode2 = document.createTextNode('html { background-color: blue }'); + + element.insertBefore(textNode2, textNode); + + expect(element.sheet.cssRules.length).toBe(3); + + expect(element.sheet.cssRules[0].cssText).toBe('html { background-color: blue; }'); + expect(element.sheet.cssRules[1].cssText).toBe('body { background-color: red; }'); + expect(element.sheet.cssRules[2].cssText).toBe('div { background-color: green; }'); + }); + + it('Updates rules editing data of a child Text node.', () => { + document.head.appendChild(element); + + expect(element.sheet.cssRules.length).toBe(0); + + const textNode = document.createTextNode( + 'body { background-color: red }\ndiv { background-color: green }' + ); + + const documentElementComputedStyle = window.getComputedStyle(document.documentElement); + + element.appendChild(textNode); + + expect(element.sheet.cssRules.length).toBe(2); + expect(element.sheet.cssRules[0].cssText).toBe('body { background-color: red; }'); + expect(element.sheet.cssRules[1].cssText).toBe('div { background-color: green; }'); + + expect(documentElementComputedStyle.backgroundColor).toBe(''); + + textNode.data = 'html { background-color: blue }'; + + expect(element.sheet.cssRules.length).toBe(1); + expect(element.sheet.cssRules[0].cssText).toBe('html { background-color: blue; }'); + + expect(documentElementComputedStyle.backgroundColor).toBe('blue'); + }); }); });