Skip to content

Commit

Permalink
Merge pull request #1208 from capricorn86/task/1207-fails-to-create-a…
Browse files Browse the repository at this point in the history
…n-element-while-creating-another-element

#1207@patch: Fixes problem related to exception thrown when creating …
  • Loading branch information
capricorn86 committed Jan 13, 2024
2 parents 5e061cf + 689f935 commit b9d5b65
Show file tree
Hide file tree
Showing 14 changed files with 133 additions and 108 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import DocumentType from '../nodes/document-type/DocumentType.js';
import * as PropertySymbol from '../PropertySymbol.js';
import IDocument from '../nodes/document/IDocument.js';
import NodeCreationOwnerDocument from '../nodes/document/NodeCreationOwnerDocument.js';
import NodeFactory from '../nodes/NodeFactory.js';

/**
* The DOMImplementation interface represents an object providing methods which are not dependent on any particular document. Such an object is returned by the.
Expand Down Expand Up @@ -46,9 +46,10 @@ export default class DOMImplementation {
publicId: string,
systemId: string
): DocumentType {
NodeCreationOwnerDocument.ownerDocument = this.#document;
const documentType = new this.#document[PropertySymbol.defaultView].DocumentType();
NodeCreationOwnerDocument.ownerDocument = null;
const documentType = NodeFactory.createNode<DocumentType>(
this.#document,
this.#document[PropertySymbol.defaultView].DocumentType
);
documentType.name = qualifiedName;
documentType.publicId = publicId;
documentType.systemId = systemId;
Expand Down
34 changes: 34 additions & 0 deletions packages/happy-dom/src/nodes/NodeFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import IDocument from '../nodes/document/IDocument.js';

/**
* Node factory used for setting the owner document to nodes.
*/
export default class NodeFactory {
public static ownerDocuments: IDocument[] = [];

/**
* Creates a node instance with the given owner document.
*
* @param ownerDocument Owner document.
* @param nodeClass Node class.
* @param [args] Node arguments.
* @returns Node instance.
*/
public static createNode<T>(
ownerDocument: IDocument,
nodeClass: new (...args) => T,
...args: any[]
): T {
this.ownerDocuments.push(ownerDocument);
return new nodeClass(...args);
}

/**
* Pulls an owner document from the queue.
*
* @returns Document.
*/
public static pullOwnerDocument(): IDocument {
return this.ownerDocuments.pop();
}
}
34 changes: 13 additions & 21 deletions packages/happy-dom/src/nodes/document/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import VisibilityStateEnum from './VisibilityStateEnum.js';
import NodeTypeEnum from '../node/NodeTypeEnum.js';
import CookieStringUtility from '../../cookie/urilities/CookieStringUtility.js';
import IBrowserFrame from '../../browser/types/IBrowserFrame.js';
import NodeCreationOwnerDocument from './NodeCreationOwnerDocument.js';
import NodeFactory from '../NodeFactory.js';

const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/;

Expand Down Expand Up @@ -835,10 +835,8 @@ export default class Document extends Node implements IDocument {
this[PropertySymbol.defaultView][ElementTag[tagName]] ||
HTMLUnknownElement;

NodeCreationOwnerDocument.ownerDocument = this;
const element = new elementClass();
NodeCreationOwnerDocument.ownerDocument = null;
element.tagName = tagName;
const element = NodeFactory.createNode<IElement>(this, elementClass);
(<string>element.tagName) = tagName;

(<string>element.namespaceURI) = namespaceURI;
if (element instanceof Element && options && options.is) {
Expand All @@ -857,10 +855,7 @@ export default class Document extends Node implements IDocument {
* @returns Text node.
*/
public createTextNode(data?: string): IText {
NodeCreationOwnerDocument.ownerDocument = this;
const node = new this[PropertySymbol.defaultView].Text(data);
NodeCreationOwnerDocument.ownerDocument = null;
return node;
return NodeFactory.createNode<IText>(this, this[PropertySymbol.defaultView].Text, data);
}

/**
Expand All @@ -870,10 +865,7 @@ export default class Document extends Node implements IDocument {
* @returns Text node.
*/
public createComment(data?: string): IComment {
NodeCreationOwnerDocument.ownerDocument = this;
const node = new this[PropertySymbol.defaultView].Comment(data);
NodeCreationOwnerDocument.ownerDocument = null;
return node;
return NodeFactory.createNode<IComment>(this, this[PropertySymbol.defaultView].Comment, data);
}

/**
Expand Down Expand Up @@ -943,9 +935,7 @@ export default class Document extends Node implements IDocument {
* @returns Element.
*/
public createAttributeNS(namespaceURI: string, qualifiedName: string): IAttr {
NodeCreationOwnerDocument.ownerDocument = this;
const attribute = new this[PropertySymbol.defaultView].Attr();
NodeCreationOwnerDocument.ownerDocument = null;
const attribute = NodeFactory.createNode<IAttr>(this, this[PropertySymbol.defaultView].Attr);
attribute.namespaceURI = namespaceURI;
attribute.name = qualifiedName;
return <IAttr>attribute;
Expand Down Expand Up @@ -1016,9 +1006,9 @@ export default class Document extends Node implements IDocument {
/**
* Creates a Processing Instruction node.
*
* @param target Target.
* @param data Data.
* @returns IProcessingInstruction.
* @param target
* @param data
*/
public createProcessingInstruction(target: string, data: string): IProcessingInstruction {
if (!target || !PROCESSING_INSTRUCTION_TARGET_REGEXP.test(target)) {
Expand All @@ -1031,9 +1021,11 @@ export default class Document extends Node implements IDocument {
`Failed to execute 'createProcessingInstruction' on 'Document': The data provided ('?>') contains '?>'`
);
}
NodeCreationOwnerDocument.ownerDocument = this;
const processingInstruction = new this[PropertySymbol.defaultView].ProcessingInstruction(data);
NodeCreationOwnerDocument.ownerDocument = null;
const processingInstruction = NodeFactory.createNode<IProcessingInstruction>(
this,
this[PropertySymbol.defaultView].ProcessingInstruction,
data
);
processingInstruction.target = target;
return processingInstruction;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/happy-dom/src/nodes/document/IDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,9 @@ export default interface IDocument extends IParentNode {
/**
* Creates a Processing Instruction node.
*
* @param target Target.
* @param data Data.
* @returns IProcessingInstruction.
* @param target
* @param data
*/
createProcessingInstruction(target: string, data: string): IProcessingInstruction;
}
14 changes: 0 additions & 14 deletions packages/happy-dom/src/nodes/document/NodeCreationOwnerDocument.ts

This file was deleted.

26 changes: 14 additions & 12 deletions packages/happy-dom/src/nodes/element/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import ElementNamedNodeMap from './ElementNamedNodeMap.js';
import WindowErrorUtility from '../../window/WindowErrorUtility.js';
import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js';
import BrowserErrorCaptureEnum from '../../browser/enums/BrowserErrorCaptureEnum.js';
import NodeCreationOwnerDocument from '../document/NodeCreationOwnerDocument.js';
import NodeFactory from '../NodeFactory.js';

/**
* Element.
Expand Down Expand Up @@ -688,17 +688,19 @@ export default class Element extends Node implements IElement {
throw new DOMException('Shadow root has already been attached.');
}

NodeCreationOwnerDocument.ownerDocument = this.ownerDocument;
(<IShadowRoot>this[PropertySymbol.shadowRoot]) = new this.ownerDocument[
PropertySymbol.defaultView
].ShadowRoot();
NodeCreationOwnerDocument.ownerDocument = null;
(<Element>this[PropertySymbol.shadowRoot].host) = this;
(<string>this[PropertySymbol.shadowRoot].mode) = init.mode;
(<ShadowRoot>this[PropertySymbol.shadowRoot])[PropertySymbol.connectToNode](this);

if (this[PropertySymbol.shadowRoot].mode === 'open') {
(<IShadowRoot>this.shadowRoot) = this[PropertySymbol.shadowRoot];
const shadowRoot = NodeFactory.createNode<IShadowRoot>(
this.ownerDocument,
this.ownerDocument[PropertySymbol.defaultView].ShadowRoot
);

(<IShadowRoot>this[PropertySymbol.shadowRoot]) = shadowRoot;

(<Element>shadowRoot.host) = this;
(<string>shadowRoot.mode) = init.mode;
(<ShadowRoot>shadowRoot)[PropertySymbol.connectToNode](this);

if (shadowRoot.mode === 'open') {
(<IShadowRoot>this.shadowRoot) = shadowRoot;
}

return this[PropertySymbol.shadowRoot];
Expand Down
25 changes: 13 additions & 12 deletions packages/happy-dom/src/nodes/node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import NodeUtility from './NodeUtility.js';
import IAttr from '../attr/IAttr.js';
import NodeList from './NodeList.js';
import INodeList from './INodeList.js';
import NodeCreationOwnerDocument from '../document/NodeCreationOwnerDocument.js';
import NodeFactory from '../NodeFactory.js';

/**
* Node.
*/
export default class Node extends EventTarget implements INode {
// Can be set before the Node is created.
public static [PropertySymbol.ownerDocument]: IDocument | null = null;
public static [PropertySymbol.ownerDocument]: IDocument | null;

// Public properties
public static readonly ELEMENT_NODE = NodeTypeEnum.elementNode;
Expand Down Expand Up @@ -71,13 +71,16 @@ export default class Node extends EventTarget implements INode {
*/
constructor() {
super();
if (
NodeCreationOwnerDocument.ownerDocument ||
(<typeof Node>this.constructor)[PropertySymbol.ownerDocument]
) {
this.ownerDocument =
NodeCreationOwnerDocument.ownerDocument ||
(<typeof Node>this.constructor)[PropertySymbol.ownerDocument];
if ((<typeof Node>this.constructor)[PropertySymbol.ownerDocument] !== undefined) {
this.ownerDocument = (<typeof Node>this.constructor)[PropertySymbol.ownerDocument];
} else {
const ownerDocument = NodeFactory.pullOwnerDocument();
if (!ownerDocument) {
throw new Error(
'Failed to construct "Node": No owner document in queue. Please use "NodeFactory" to create instances of a Node.'
);
}
this.ownerDocument = ownerDocument;
}
}

Expand Down Expand Up @@ -279,9 +282,7 @@ export default class Node extends EventTarget implements INode {
* @returns Cloned node.
*/
public cloneNode(deep = false): INode {
NodeCreationOwnerDocument.ownerDocument = this.ownerDocument;
const clone = new (<typeof Node>this.constructor)();
NodeCreationOwnerDocument.ownerDocument = null;
const clone = NodeFactory.createNode<Node>(this.ownerDocument, <typeof Node>this.constructor);

// Document has childNodes directly when it is created
if (clone[PropertySymbol.childNodes].length) {
Expand Down
6 changes: 6 additions & 0 deletions packages/happy-dom/src/window/BrowserWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,12 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow
this.XMLDocument = XMLDocument;
this.SVGDocument = SVGDocument;

// Override owner document
this.Document[PropertySymbol.ownerDocument] = null;
this.HTMLDocument[PropertySymbol.ownerDocument] = null;
this.XMLDocument[PropertySymbol.ownerDocument] = null;
this.SVGDocument[PropertySymbol.ownerDocument] = null;

// Document
this.document = new HTMLDocument();
(<IBrowserWindow>this.document.defaultView) = this;
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-dom/test/CustomElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export default class CustomElement extends HTMLElement {
constructor() {
super();
this.internalShadowRoot = this.attachShadow({ mode: CustomElement.shadowRootMode });

// Test to create a node while constructing this node.
this.ownerDocument.createElement('div');
}

/**
Expand Down
7 changes: 5 additions & 2 deletions packages/happy-dom/test/event/events/SubmitEvent.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import Event from '../../../src/event/Event.js';
import SubmitEvent from '../../../src/event/events/SubmitEvent.js';
import HTMLButtonElement from '../../../src/nodes/html-button-element/HTMLButtonElement.js';
import IHTMLButtonElement from '../../../src/nodes/html-button-element/IHTMLButtonElement.js';
import Window from '../../../src/window/Window.js';
import { describe, it, expect } from 'vitest';

describe('SubmitEvent', () => {
describe('constructor', () => {
it('Creates a submit event.', () => {
const submitter = new HTMLButtonElement();
const window = new Window();
const document = window.document;
const submitter = document.createElement('button');
const event = new SubmitEvent('submit', { bubbles: true, submitter });
expect(event).toBeInstanceOf(Event);
expect(event.bubbles).toBe(true);
Expand Down

0 comments on commit b9d5b65

Please sign in to comment.