From 2df1cc47f46695602f1b21a6d868fa9ee8a2277d Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 27 Dec 2024 13:45:11 +0100 Subject: [PATCH] fix: [#1647] Fixes problem with children of created documents not being considered as connected to the DOM that was introduced in v16 (#1648) * fix: [#1647] Fixes problem with children of created documents not being considered as connected to the DOM that was introduced in v16 * chore: [#1647] Fixes attributeChangedCallback --- .../happy-dom/src/css/rules/CSSStyleRule.ts | 2 +- .../CustomElementReactionStack.ts | 5 ++ .../happy-dom/src/nodes/document/Document.ts | 2 +- .../src/nodes/html-element/HTMLElement.ts | 26 ++++--- .../happy-dom/src/window/BrowserWindow.ts | 11 --- .../test/dom-parser/DOMParser.test.ts | 8 ++- .../HTMLStyleElement.test.ts | 70 +++++++++++++++++++ 7 files changed, 102 insertions(+), 22 deletions(-) diff --git a/packages/happy-dom/src/css/rules/CSSStyleRule.ts b/packages/happy-dom/src/css/rules/CSSStyleRule.ts index a02c1e6e..3bafc3d7 100644 --- a/packages/happy-dom/src/css/rules/CSSStyleRule.ts +++ b/packages/happy-dom/src/css/rules/CSSStyleRule.ts @@ -7,8 +7,8 @@ import CSSStyleDeclaration from '../declaration/CSSStyleDeclaration.js'; */ export default class CSSStyleRule extends CSSRule { public readonly type = CSSRule.STYLE_RULE; - public readonly selectorText = ''; public readonly styleMap = new Map(); + public selectorText = ''; public [PropertySymbol.cssText] = ''; #style: CSSStyleDeclaration | null = null; diff --git a/packages/happy-dom/src/custom-element/CustomElementReactionStack.ts b/packages/happy-dom/src/custom-element/CustomElementReactionStack.ts index 31ad292f..f720d4dc 100644 --- a/packages/happy-dom/src/custom-element/CustomElementReactionStack.ts +++ b/packages/happy-dom/src/custom-element/CustomElementReactionStack.ts @@ -37,6 +37,11 @@ export default class CustomElementReactionStack { return; } + // If the element is not connected to the main document, we should not invoke the callback. + if (element[PropertySymbol.ownerDocument] !== this.window.document) { + return; + } + // According to the spec, we should use a queue for each element and then invoke the reactions in the order they were enqueued asynchronously. // However, the browser seem to always invoke the reactions synchronously. // TODO: Can we find an example where the reactions are invoked asynchronously? In that case we should use a queue for those cases. diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 3ec60cde..b5afb670 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -65,7 +65,7 @@ export default class Document extends Node { public [PropertySymbol.isFirstWrite] = true; public [PropertySymbol.isFirstWriteAfterOpen] = false; public [PropertySymbol.nodeType] = NodeTypeEnum.documentNode; - public [PropertySymbol.isConnected] = false; + public [PropertySymbol.isConnected] = true; public [PropertySymbol.adoptedStyleSheets]: CSSStyleSheet[] = []; public [PropertySymbol.implementation] = new DOMImplementation(this); public [PropertySymbol.readyState] = DocumentReadyStateEnum.interactive; diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 629eeb08..0ab83546 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -613,10 +613,15 @@ export default class HTMLElement extends Element { public override [PropertySymbol.connectedToDocument](): void { super[PropertySymbol.connectedToDocument](); - this[PropertySymbol.window][PropertySymbol.customElementReactionStack].enqueueReaction( - this, - 'connectedCallback' - ); + if ( + this[PropertySymbol.ownerDocument][PropertySymbol.window].document === + this[PropertySymbol.ownerDocument] + ) { + this[PropertySymbol.window][PropertySymbol.customElementReactionStack].enqueueReaction( + this, + 'connectedCallback' + ); + } } /** @@ -625,10 +630,15 @@ export default class HTMLElement extends Element { public override [PropertySymbol.disconnectedFromDocument](): void { super[PropertySymbol.disconnectedFromDocument](); - this[PropertySymbol.window][PropertySymbol.customElementReactionStack].enqueueReaction( - this, - 'disconnectedCallback' - ); + if ( + this[PropertySymbol.ownerDocument][PropertySymbol.window].document === + this[PropertySymbol.ownerDocument] + ) { + this[PropertySymbol.window][PropertySymbol.customElementReactionStack].enqueueReaction( + this, + 'disconnectedCallback' + ); + } } /** diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index e682056d..ccdc8fe5 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -835,17 +835,6 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal // Document this.document = new this.HTMLDocument(); this.document[PropertySymbol.defaultView] = this; - this.document[PropertySymbol.isConnected] = true; - - this.document[PropertySymbol.nodeArray][0][PropertySymbol.isConnected] = true; - - this.document[PropertySymbol.nodeArray][0][PropertySymbol.nodeArray][0][ - PropertySymbol.isConnected - ] = true; - - this.document[PropertySymbol.nodeArray][0][PropertySymbol.nodeArray][1][ - PropertySymbol.isConnected - ] = true; // Ready state manager this[PropertySymbol.readyStateManager].waitUntilComplete().then(() => { diff --git a/packages/happy-dom/test/dom-parser/DOMParser.test.ts b/packages/happy-dom/test/dom-parser/DOMParser.test.ts index 0d81af6f..937f81b4 100644 --- a/packages/happy-dom/test/dom-parser/DOMParser.test.ts +++ b/packages/happy-dom/test/dom-parser/DOMParser.test.ts @@ -235,6 +235,7 @@ describe('DOMParser', () => { class CustomElement extends HTMLElement { public connectedCount = 0; public disconnectedCount = 0; + public changedAttributes = 0; constructor() { super(); @@ -248,6 +249,10 @@ describe('DOMParser', () => { public disconnectedCallback(): void { this.disconnectedCount++; } + + public attributeChangedCallback(): void { + this.changedAttributes++; + } } window.customElements.define('custom-element', CustomElement); @@ -259,13 +264,14 @@ describe('DOMParser', () => { 'text/html' ); - expect(newDocument.isConnected).toBe(false); + expect(newDocument.isConnected).toBe(true); expect(newDocument.defaultView).toBe(window); const customElement = newDocument.querySelector('custom-element'); expect(customElement.connectedCount).toBe(0); expect(customElement.disconnectedCount).toBe(0); + expect(customElement.changedAttributes).toBe(0); document.body.appendChild(customElement); 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 27299618..fdcbf51b 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 @@ -3,6 +3,8 @@ import Document from '../../../src/nodes/document/Document.js'; import HTMLStyleElement from '../../../src/nodes/html-style-element/HTMLStyleElement.js'; import { beforeEach, describe, it, expect } from 'vitest'; import HTMLElement from '../../../src/nodes/html-element/HTMLElement.js'; +import DOMImplementation from '../../../src/dom-implementation/DOMImplementation.js'; +import CSSStyleRule from '../../../src/css/rules/CSSStyleRule.js'; describe('HTMLStyleElement', () => { let window: Window; @@ -185,5 +187,73 @@ describe('HTMLStyleElement', () => { expect(documentElementComputedStyle.backgroundColor).toBe('blue'); }); + + it('Returns an CSSStyleSheet instance with its text content as style rules for #1647.', () => { + const StyleTagRegexp = /]*>(?.*?)<\/style>/gis; + + function getScopedCssRules(html: string, scope: string, dom?: DOMImplementation): string { + const css = Array.from(html.matchAll(StyleTagRegexp)) + .map(({ groups }) => groups?.content ?? '') + .join('\n'); + + if (!css) { + return ''; + } + + // Use the browser's CSSOM to parse CSS + const doc = (dom ?? document.implementation).createHTMLDocument(); + const styleElement = doc.createElement('style'); + styleElement.textContent = css; + doc.body.appendChild(styleElement); + + return Array.from(styleElement.sheet?.cssRules ?? []) + .map((rule) => { + if (rule instanceof CSSStyleRule) { + rule.selectorText = rule.selectorText + .split(',') + .map((selector) => `${scope} ${selector}`) + .join(','); + } + + return rule.cssText; + }) + .join('\n'); + } + + function scopeCssSelectors(html: string, scope: string, dom?: DOMImplementation): string { + const scopedCss = getScopedCssRules(html, scope, dom); + + return `${scopedCss ? `` : ''}${html.replace( + StyleTagRegexp, + '' + )}`; + } + + const html = ` + + +

I love this

+ + + `; + + expect(scopeCssSelectors(html, '.scope')) + .toEqual(` + + +

I love this

+ + + `); + }); }); });