From b091da6106049f027c6feb9b14d251c1ae56be8b Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 19 Sep 2023 19:58:47 +0200 Subject: [PATCH 01/63] #466@trivial: Starts on implementation. --- .../src/browser-context/BrowserContext.ts | 108 ++++++++++ .../browser-context/IBrowserContextOptions.ts | 30 +++ .../IBrowserContextSettings.ts | 20 ++ .../html-iframe-element/HTMLIFrameElement.ts | 6 +- .../html-iframe-element/HTMLIFrameUtility.ts | 15 +- .../html-iframe-element/IHTMLIFrameElement.ts | 4 +- .../src/window/BrowserContextLoader.ts | 196 ++++++++++++++++++ .../CrossOriginWindow.ts} | 13 +- .../src/window/ICrossOriginWindow.ts | 23 ++ .../happy-dom/src/window/IHappyDOMOptions.ts | 27 +-- .../happy-dom/src/window/IHappyDOMSettings.ts | 22 +- packages/happy-dom/src/window/IWindow.ts | 19 +- packages/happy-dom/src/window/Window.ts | 39 +++- 13 files changed, 463 insertions(+), 59 deletions(-) create mode 100644 packages/happy-dom/src/browser-context/BrowserContext.ts create mode 100644 packages/happy-dom/src/browser-context/IBrowserContextOptions.ts create mode 100644 packages/happy-dom/src/browser-context/IBrowserContextSettings.ts create mode 100644 packages/happy-dom/src/window/BrowserContextLoader.ts rename packages/happy-dom/src/{nodes/html-iframe-element/IFrameCrossOriginWindow.ts => window/CrossOriginWindow.ts} (77%) create mode 100644 packages/happy-dom/src/window/ICrossOriginWindow.ts diff --git a/packages/happy-dom/src/browser-context/BrowserContext.ts b/packages/happy-dom/src/browser-context/BrowserContext.ts new file mode 100644 index 000000000..0b3bb6a09 --- /dev/null +++ b/packages/happy-dom/src/browser-context/BrowserContext.ts @@ -0,0 +1,108 @@ +import IDocument from '../nodes/document/IDocument.js'; +import IWindow from '../window/IWindow.js'; +import Event from '../event/Event.js'; +import Window from '../window/Window.js'; +import IBrowserContextOptions from './IBrowserContextOptions.js'; +import IBrowserContextSettings from './IBrowserContextSettings.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; + +/** + * Browser context. + */ +export default class BrowserContext { + public window: IWindow; + public document: IDocument; + public settings: IBrowserContextSettings; + public consolePrinter: VirtualConsolePrinter | null; + private _asyncTaskManager = new AsyncTaskManager(); + + /** + * Constructor. + * + * @param [options] Options. + */ + constructor(options?: IBrowserContextOptions) { + this.window = new Window(options); + this.document = this.window.document; + + if (options?.html) { + this.document.write(options.html); + } + + if (options?.ownerBrowserContext) { + (this.window.top) = options.ownerBrowserContext.window; + (this.window.parent) = options.ownerBrowserContext.window; + (this.window.opener) = options.ownerBrowserContext.window; + } + } + + /** + * Aborts asynchronous tasks and destroys the context. + * + * @returns Promise. + */ + public async destroy(): Promise { + await this.abortAsyncTasks(); + this.window = null; + this.document = null; + } + + /** + * Returns a promise that is resolved when all async tasks are complete. + * + * @returns Promise. + */ + public async whenAsyncTasksComplete(): Promise { + await this._asyncTaskManager.whenComplete(); + } + + /** + * Aborts all async tasks. + * + * @returns Promise. + */ + public async abortAsyncTasks(): Promise { + this._asyncTaskManager.cancelAll(); + } + + /** + * Sets the window size and triggers a resize event. + * + * @param options Options. + * @param options.width Width. + * @param options.height Height. + */ + public resizeWindow(options: { width?: number; height?: number }): void { + if ( + (options.width !== undefined && this.window.innerWidth !== options.width) || + (options.height !== undefined && this.window.innerHeight !== options.height) + ) { + if (options.width !== undefined && this.window.innerWidth !== options.width) { + (this.window.innerWidth) = options.width; + (this.window.outerWidth) = options.width; + } + + if (options.height !== undefined && this.window.innerHeight !== options.height) { + (this.window.innerHeight) = options.height; + (this.window.outerHeight) = options.height; + } + + this.window.dispatchEvent(new Event('resize')); + } + } + + /** + * Go to a page. + * + * @param url URL. + */ + public async goto(url: string): Promise { + this.window.location.href = url; + + const response = await this.window.fetch(url); + const responseText = await response.text(); + + this.document.write(responseText); + } +} diff --git a/packages/happy-dom/src/browser-context/IBrowserContextOptions.ts b/packages/happy-dom/src/browser-context/IBrowserContextOptions.ts new file mode 100644 index 000000000..47bfe9bc1 --- /dev/null +++ b/packages/happy-dom/src/browser-context/IBrowserContextOptions.ts @@ -0,0 +1,30 @@ +import BrowserContext from './BrowserContext.js'; + +/** + * Browser context options. + */ +export default interface IBrowserContextOptions { + width?: number; + height?: number; + url?: string; + html?: string; + console?: Console; + ownerBrowserContext?: BrowserContext; + settings?: { + disableJavaScriptEvaluation?: boolean; + disableJavaScriptFileLoading?: boolean; + disableCSSFileLoading?: boolean; + disableIframePageLoading?: boolean; + disableWindowOpenPageLoading?: boolean; + disableComputedStyleRendering?: boolean; + disableErrorCapturing?: boolean; + enableFileSystemHttpRequests?: boolean; + navigator?: { + userAgent?: string; + }; + device?: { + prefersColorScheme?: string; + mediaType?: string; + }; + }; +} diff --git a/packages/happy-dom/src/browser-context/IBrowserContextSettings.ts b/packages/happy-dom/src/browser-context/IBrowserContextSettings.ts new file mode 100644 index 000000000..ea2d9d472 --- /dev/null +++ b/packages/happy-dom/src/browser-context/IBrowserContextSettings.ts @@ -0,0 +1,20 @@ +/** + * Browser context settings. + */ +export default interface IBrowserContextSettings { + disableJavaScriptEvaluation: boolean; + disableJavaScriptFileLoading: boolean; + disableCSSFileLoading: boolean; + disableIframePageLoading: boolean; + disableWindowOpenPageLoading: boolean; + disableComputedStyleRendering: boolean; + disableErrorCapturing: boolean; + enableFileSystemHttpRequests: boolean; + navigator: { + userAgent: string; + }; + device: { + prefersColorScheme: string; + mediaType: string; + }; +} diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index 93f4dbba0..d9723b9b6 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -3,11 +3,11 @@ import IWindow from '../../window/IWindow.js'; import IDocument from '../document/IDocument.js'; import HTMLElement from '../html-element/HTMLElement.js'; import INode from '../node/INode.js'; -import IFrameCrossOriginWindow from './IFrameCrossOriginWindow.js'; import IHTMLIFrameElement from './IHTMLIFrameElement.js'; import HTMLIFrameUtility from './HTMLIFrameUtility.js'; import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import HTMLIFrameElementNamedNodeMap from './HTMLIFrameElementNamedNodeMap.js'; +import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; /** * HTML Iframe Element. @@ -23,7 +23,7 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram public onerror: (event: Event) => void | null = null; // Internal properties - public _contentWindow: IWindow | IFrameCrossOriginWindow | null = null; + public _contentWindow: IWindow | ICrossOriginWindow | null = null; /** * Returns source. @@ -165,7 +165,7 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram * * @returns Content window. */ - public get contentWindow(): IWindow | IFrameCrossOriginWindow | null { + public get contentWindow(): IWindow | ICrossOriginWindow | null { return this._contentWindow; } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts index 2a430b6da..6ee00c636 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts @@ -2,8 +2,9 @@ import { URL } from 'url'; import Event from '../../event/Event.js'; import ErrorEvent from '../../event/events/ErrorEvent.js'; import IWindow from '../../window/IWindow.js'; -import IFrameCrossOriginWindow from './IFrameCrossOriginWindow.js'; +import CrossOriginWindow from '../../window/CrossOriginWindow.js'; import HTMLIFrameElement from './HTMLIFrameElement.js'; +import WindowErrorUtility from '../../window/WindowErrorUtility.js'; /** * HTML Iframe Utility. @@ -40,7 +41,15 @@ export default class HTMLIFrameUtility { if (src.startsWith('javascript:')) { element._contentWindow = contentWindow; - element._contentWindow.eval(src.replace('javascript:', '')); + if (!element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptEvaluation) { + if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + (element._contentWindow).eval(src.replace('javascript:', '')); + } else { + WindowErrorUtility.captureError(element, () => + (element._contentWindow).eval(src.replace('javascript:', '')) + ); + } + } return; } @@ -76,7 +85,7 @@ export default class HTMLIFrameUtility { } element._contentWindow = isCORS - ? new IFrameCrossOriginWindow(element.ownerDocument.defaultView, contentWindow) + ? new CrossOriginWindow(element.ownerDocument.defaultView, contentWindow) : contentWindow; contentWindow.document.write(responseText); element.dispatchEvent(new Event('load')); diff --git a/packages/happy-dom/src/nodes/html-iframe-element/IHTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/IHTMLIFrameElement.ts index f1c50eedb..dc441ab88 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/IHTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/IHTMLIFrameElement.ts @@ -2,7 +2,7 @@ import Event from '../../event/Event.js'; import IWindow from '../../window/IWindow.js'; import IDocument from '../document/IDocument.js'; import IHTMLElement from '../html-element/IHTMLElement.js'; -import IFrameCrossOriginWindow from './IFrameCrossOriginWindow.js'; +import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; /** * HTML Iframe Element. @@ -19,7 +19,7 @@ export default interface IHTMLIFrameElement extends IHTMLElement { sandbox: string | null; srcdoc: string | null; readonly contentDocument: IDocument | null; - readonly contentWindow: IWindow | IFrameCrossOriginWindow | null; + readonly contentWindow: IWindow | ICrossOriginWindow | null; // Events onload: (event: Event) => void | null; diff --git a/packages/happy-dom/src/window/BrowserContextLoader.ts b/packages/happy-dom/src/window/BrowserContextLoader.ts new file mode 100644 index 000000000..2d4aa18e6 --- /dev/null +++ b/packages/happy-dom/src/window/BrowserContextLoader.ts @@ -0,0 +1,196 @@ +import { URL } from 'url'; +import Document from '../nodes/document/Document.js'; +import IWindow from './IWindow.js'; +import CrossOriginWindow from './CrossOriginWindow.js'; +import WindowErrorUtility from './WindowErrorUtility.js'; +import ICrossOriginWindow from './ICrossOriginWindow.js'; +import IHTMLElement from '../nodes/html-element/IHTMLElement.js'; +import Window from './Window.js'; + +/** + * Browser context. + */ +export default class BrowserContextLoader { + /** + * Creates a new browser context for an iframe or a new window using Window.open(). + * + * @param ownerWindow Owner window. + * @param [options] Options. + * @param [options.url] URL. + * @param [options.target] Target. + * @param [options.features] Window features. + * @param [options.ownerIframeElement] Owner iframe element. + */ + public static getBrowserContext( + ownerWindow: IWindow, + options?: { + url?: string; + target?: string; + features?: string; + ownerIframeElement?: IHTMLElement; + } + ): IWindow | ICrossOriginWindow | null { + const features = this.getWindowFeatures(options?.features || ''); + const contentWindow = new (ownerWindow.constructor)({ + url: options?.url ? new URL(options.url, ownerWindow.location.href).href : null, + console: ownerWindow.console, + width: features.width || undefined, + height: features.height || undefined, + settings: { + ...ownerWindow.happyDOM.settings + } + }); + const url = options?.url || 'about:blank'; + const target = options?.target !== undefined ? String(options.target) : null; + + contentWindow.happyDOM.virtualConsolePrinter = ownerWindow.happyDOM.virtualConsolePrinter; + + // TODO: This can perhaps be improved, so that it is possible to watch async tasks on each child window. + contentWindow.happyDOM.asyncTaskManager = ownerWindow.happyDOM.asyncTaskManager; + contentWindow.happyDOM.whenAsyncComplete = ownerWindow.happyDOM.whenAsyncComplete; + contentWindow.happyDOM.cancelAsync = ownerWindow.happyDOM.cancelAsync; + + (contentWindow.document.referrer) = !features.noreferrer + ? ownerWindow.location.href + : ''; + if (!features.noopener) { + (contentWindow.opener) = ownerWindow; + (contentWindow.parent) = ownerWindow; + (contentWindow.top) = ownerWindow; + } + + if (target) { + (contentWindow.name) = target; + } + + if (features?.left) { + (contentWindow.screenLeft) = features.left; + (contentWindow.screenX) = features.left; + } + + if (features?.top) { + (contentWindow.screenTop) = features.top; + (contentWindow.screenY) = features.top; + } + + if (url === 'about:blank') { + return features.noopener ? null : contentWindow; + } + + if (url.startsWith('javascript:')) { + if (!ownerWindow.happyDOM.settings.disableJavaScriptEvaluation) { + if (ownerWindow.happyDOM.settings.disableErrorCapturing) { + contentWindow.eval(url.replace('javascript:', '')); + } else { + WindowErrorUtility.captureError(ownerWindow, () => + contentWindow.eval(url.replace('javascript:', '')) + ); + } + } + return features.noopener ? null : contentWindow; + } + + const originURL = ownerWindow.location; + const targetURL = new URL(url, originURL); + const isCORS = + (originURL.hostname !== targetURL.hostname && + !originURL.hostname.endsWith(targetURL.hostname)) || + originURL.protocol !== targetURL.protocol; + + (contentWindow.document)._readyStateManager.startTask(); + + ownerWindow + .fetch(url, { + referrer: features.noreferrer ? 'no-referrer' : undefined + }) + .then((response) => response.text()) + .then((responseText) => { + contentWindow.document.write(responseText); + (contentWindow.document)._readyStateManager.endTask(); + }) + .catch((error) => { + WindowErrorUtility.dispatchError( + options?.ownerIframeElement ? options.ownerIframeElement : ownerWindow, + error + ); + (contentWindow.document)._readyStateManager.endTask(); + if (!ownerWindow.happyDOM.settings.disableErrorCapturing) { + throw error; + } + }); + + if (features.noopener) { + return null; + } + + return isCORS ? new CrossOriginWindow(ownerWindow, contentWindow) : contentWindow; + } + + /** + * Returns window features. + * + * @param features Window features string. + * @returns Window features. + */ + private static getWindowFeatures(features: string): { + popup: boolean; + width: number; + height: number; + left: number; + top: number; + noopener: boolean; + noreferrer: boolean; + } { + const parts = features.split(','); + const result: { + popup: boolean; + width: number; + height: number; + left: number; + top: number; + noopener: boolean; + noreferrer: boolean; + } = { + popup: false, + width: 0, + height: 0, + left: 0, + top: 0, + noopener: false, + noreferrer: false + }; + + for (const part of parts) { + const [key, value] = part.split('='); + switch (key) { + case 'popup': + result.popup = value === 'yes' || value === '1' || value === 'true'; + break; + case 'width': + case 'innerWidth': + result.width = parseInt(value, 10); + break; + case 'height': + case 'innerHeight': + result.height = parseInt(value, 10); + break; + case 'left': + case 'screenX': + result.left = parseInt(value, 10); + break; + case 'top': + case 'screenY': + result.top = parseInt(value, 10); + break; + case 'noopener': + result.noopener = true; + break; + case 'noreferrer': + result.noreferrer = true; + break; + } + } + + return result; + } +} diff --git a/packages/happy-dom/src/nodes/html-iframe-element/IFrameCrossOriginWindow.ts b/packages/happy-dom/src/window/CrossOriginWindow.ts similarity index 77% rename from packages/happy-dom/src/nodes/html-iframe-element/IFrameCrossOriginWindow.ts rename to packages/happy-dom/src/window/CrossOriginWindow.ts index 450703260..576f9b6d8 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/IFrameCrossOriginWindow.ts +++ b/packages/happy-dom/src/window/CrossOriginWindow.ts @@ -1,13 +1,14 @@ -import EventTarget from '../../event/EventTarget.js'; -import IWindow from '../../window/IWindow.js'; -import DOMException from '../../exception/DOMException.js'; -import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; -import Location from '../../location/Location.js'; +import EventTarget from '../event/EventTarget.js'; +import IWindow from './IWindow.js'; +import DOMException from '../exception/DOMException.js'; +import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; +import Location from '../location/Location.js'; +import ICrossOriginWindow from './ICrossOriginWindow.js'; /** * Browser window with limited access due to CORS restrictions in iframes. */ -export default class IFrameCrossOriginWindow extends EventTarget { +export default class CrossOriginWindow extends EventTarget implements ICrossOriginWindow { public readonly self = this; public readonly window = this; public readonly parent: IWindow; diff --git a/packages/happy-dom/src/window/ICrossOriginWindow.ts b/packages/happy-dom/src/window/ICrossOriginWindow.ts new file mode 100644 index 000000000..acfcdf2ad --- /dev/null +++ b/packages/happy-dom/src/window/ICrossOriginWindow.ts @@ -0,0 +1,23 @@ +import IWindow from './IWindow.js'; +import Location from '../location/Location.js'; +import IEventTarget from '../event/IEventTarget.js'; + +/** + * Browser window with limited access due to CORS restrictions in iframes. + */ +export default interface ICrossOriginWindow extends IEventTarget { + readonly self: ICrossOriginWindow; + readonly window: ICrossOriginWindow; + readonly parent: IWindow; + readonly top: IWindow; + readonly location: Location; + + /** + * Safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it. + * + * @param message Message. + * @param [targetOrigin=*] Target origin. + * @param transfer Transfer. Not implemented. + */ + postMessage(message: unknown, targetOrigin?: string, transfer?: unknown[]): void; +} diff --git a/packages/happy-dom/src/window/IHappyDOMOptions.ts b/packages/happy-dom/src/window/IHappyDOMOptions.ts index 92ebb25bd..03669c04f 100644 --- a/packages/happy-dom/src/window/IHappyDOMOptions.ts +++ b/packages/happy-dom/src/window/IHappyDOMOptions.ts @@ -1,28 +1,11 @@ +import IBrowserContextOptions from '../browser-context/IBrowserContextOptions.js'; + /** * Happy DOM options. + * + * @deprecated */ -export default interface IHappyDOMOptions { - width?: number; - height?: number; - url?: string; - console?: Console; - settings?: { - disableJavaScriptEvaluation?: boolean; - disableJavaScriptFileLoading?: boolean; - disableCSSFileLoading?: boolean; - disableIframePageLoading?: boolean; - disableComputedStyleRendering?: boolean; - disableErrorCapturing?: boolean; - enableFileSystemHttpRequests?: boolean; - navigator?: { - userAgent?: string; - }; - device?: { - prefersColorScheme?: string; - mediaType?: string; - }; - }; - +export default interface IHappyDOMOptions extends IBrowserContextOptions { /** * @deprecated */ diff --git a/packages/happy-dom/src/window/IHappyDOMSettings.ts b/packages/happy-dom/src/window/IHappyDOMSettings.ts index 1b2afeb00..48c7dda17 100644 --- a/packages/happy-dom/src/window/IHappyDOMSettings.ts +++ b/packages/happy-dom/src/window/IHappyDOMSettings.ts @@ -1,19 +1,9 @@ +import IBrowserContextSettings from '../browser-context/IBrowserContextSettings.js'; + /** * Happy DOM settings. + * + * @deprecated */ -export default interface IHappyDOMSettings { - disableJavaScriptEvaluation: boolean; - disableJavaScriptFileLoading: boolean; - disableCSSFileLoading: boolean; - disableIframePageLoading: boolean; - disableComputedStyleRendering: boolean; - disableErrorCapturing: boolean; - enableFileSystemHttpRequests: boolean; - navigator: { - userAgent: string; - }; - device: { - prefersColorScheme: string; - mediaType: string; - }; -} +type IHappyDOMSettings = IBrowserContextSettings; +export default IHappyDOMSettings; diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index eb786e0e3..33768b803 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -1,5 +1,6 @@ import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; import Document from '../nodes/document/Document.js'; +import IDocument from '../nodes/document/IDocument.js'; import HTMLDocument from '../nodes/html-document/HTMLDocument.js'; import XMLDocument from '../nodes/xml-document/XMLDocument.js'; import SVGDocument from '../nodes/svg-document/SVGDocument.js'; @@ -125,6 +126,7 @@ import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; import ValidityState from '../validity-state/ValidityState.js'; import INodeJSGlobal from './INodeJSGlobal.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import ICrossOriginWindow from './ICrossOriginWindow.js'; /** * Window without dependencies to server side specific packages. @@ -273,7 +275,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { onerror: (event: ErrorEvent) => void; // Public Properties - readonly document: Document; + readonly document: IDocument; readonly customElements: CustomElementRegistry; readonly location: Location; readonly history: History; @@ -281,14 +283,20 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly console: Console; readonly self: IWindow; readonly top: IWindow; + readonly opener: IWindow | null; readonly parent: IWindow; readonly window: IWindow; readonly globalThis: IWindow; + readonly name: string; readonly screen: Screen; readonly innerWidth: number; readonly innerHeight: number; readonly outerWidth: number; readonly outerHeight: number; + readonly screenLeft: number; + readonly screenTop: number; + readonly screenX: number; + readonly screenY: number; readonly sessionStorage: Storage; readonly localStorage: Storage; readonly performance: Performance; @@ -329,6 +337,15 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { */ scrollTo(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void; + /** + * Loads a specified resource into a new or existing browsing context (that is, a tab, a window, or an iframe) under a specified name. + * + * @param [url] URL. + * @param [target] Target. + * @param [windowFeatures] Window features. + */ + open(url?: string, target?: string, windowFeatures?: string): IWindow | ICrossOriginWindow | null; + /** * Returns a new MediaQueryList object that can then be used to determine if the document matches the media query string. * diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 67562cb04..4142b6020 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -141,6 +141,8 @@ import VirtualConsole from '../console/VirtualConsole.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; import IHappyDOMSettings from './IHappyDOMSettings.js'; import PackageVersion from '../version.js'; +import ICrossOriginWindow from './ICrossOriginWindow.js'; +import BrowserContext from './BrowserContextLoader.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -209,6 +211,7 @@ export default class Window extends EventTarget implements IWindow { disableJavaScriptFileLoading: false, disableCSSFileLoading: false, disableIframePageLoading: false, + disableWindowOpenPageLoading: false, disableComputedStyleRendering: false, disableErrorCapturing: false, enableFileSystemHttpRequests: false, @@ -352,17 +355,19 @@ export default class Window extends EventTarget implements IWindow { public onerror: (event: ErrorEvent) => void = null; // Public Properties - public readonly document: Document; + public readonly document: IDocument; public readonly customElements: CustomElementRegistry; public readonly location: Location; public readonly history: History; public readonly navigator: Navigator; public readonly console: Console; - public readonly self = this; - public readonly top = this; - public readonly parent = this; - public readonly window = this; - public readonly globalThis = this; + public readonly opener: IWindow | null = null; + public readonly self: IWindow = this; + public readonly top: IWindow = this; + public readonly parent: IWindow = this; + public readonly window: IWindow = this; + public readonly globalThis: IWindow = this; + public readonly name: string = ''; public readonly screen: Screen; public readonly devicePixelRatio = 1; public readonly sessionStorage: Storage; @@ -372,6 +377,10 @@ export default class Window extends EventTarget implements IWindow { public readonly innerHeight: number = 768; public readonly outerWidth: number = 1024; public readonly outerHeight: number = 768; + public readonly screenLeft: number = 0; + public readonly screenTop: number = 0; + public readonly screenX: number = 0; + public readonly screenY: number = 0; public readonly crypto = webcrypto; // Node.js Globals @@ -716,6 +725,24 @@ export default class Window extends EventTarget implements IWindow { this.scroll(x, y); } + /** + * Loads a specified resource into a new or existing browsing context (that is, a tab, a window, or an iframe) under a specified name. + * + * @param [url] URL. + * @param [target] Target. + * @param [features] Window features. + */ + public open( + url?: string, + target?: string, + features?: string + ): IWindow | ICrossOriginWindow | null { + if (this.happyDOM.settings.disableWindowOpenPageLoading) { + return null; + } + return BrowserContext.createContext(this, { url, target, features }); + } + /** * Returns a new MediaQueryList object that can then be used to determine if the document matches the media query string. * From efdf26b91e326f3a14c4d28fc12779cf29f03690 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 20 Sep 2023 01:24:34 +0200 Subject: [PATCH 02/63] #466@trivial: Starts on implementation. --- packages/happy-dom/src/browser/Browser.ts | 125 ++++++++++++++++++ .../BrowserContext.ts | 45 +++---- .../happy-dom/src/browser/BrowserFrame.ts | 101 ++++++++++++++ packages/happy-dom/src/browser/BrowserPage.ts | 98 ++++++++++++++ .../IBrowserContextOptions.ts | 0 .../src/browser/IBrowserPageViewport.ts | 6 + .../IBrowserSettings.ts} | 4 +- .../src/browser/IOptionalBrowserSettings.ts | 20 +++ 8 files changed, 369 insertions(+), 30 deletions(-) create mode 100644 packages/happy-dom/src/browser/Browser.ts rename packages/happy-dom/src/{browser-context => browser}/BrowserContext.ts (62%) create mode 100644 packages/happy-dom/src/browser/BrowserFrame.ts create mode 100644 packages/happy-dom/src/browser/BrowserPage.ts rename packages/happy-dom/src/{browser-context => browser}/IBrowserContextOptions.ts (100%) create mode 100644 packages/happy-dom/src/browser/IBrowserPageViewport.ts rename packages/happy-dom/src/{browser-context/IBrowserContextSettings.ts => browser/IBrowserSettings.ts} (83%) create mode 100644 packages/happy-dom/src/browser/IOptionalBrowserSettings.ts diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts new file mode 100644 index 000000000..f22bab326 --- /dev/null +++ b/packages/happy-dom/src/browser/Browser.ts @@ -0,0 +1,125 @@ +import IDocument from '../nodes/document/IDocument.js'; +import IWindow from '../window/IWindow.js'; +import Event from '../event/Event.js'; +import Window from '../window/Window.js'; +import IBrowserSettings from './IBrowserSettings.js'; +import BrowserContext from './BrowserContext.js'; +import PackageVersion from '../version.js'; +import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; + +/** + * Browser context. + */ +export default class Browser { + public browserContexts: BrowserContext[]; + public defaultBrowserContext: BrowserContext; + public settings: IBrowserSettings = { + disableJavaScriptEvaluation: false, + disableJavaScriptFileLoading: false, + disableCSSFileLoading: false, + disableIframePageLoading: false, + disableWindowOpenPageLoading: false, + disableComputedStyleRendering: false, + disableErrorCapturing: false, + enableFileSystemHttpRequests: false, + navigator: { + userAgent: `Mozilla/5.0 (X11; ${ + process.platform.charAt(0).toUpperCase() + process.platform.slice(1) + ' ' + process.arch + }) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}` + }, + device: { + prefersColorScheme: 'light', + mediaType: 'screen' + } + }; + + /** + * Constructor. + * + * @param [options] Options. + * @param [options.settings] Browser settings. + */ + constructor(options?: { settings?: IOptionalBrowserSettings }) { + if (options.settings) { + this.settings = { + ...this.settings, + ...options.settings, + navigator: { + ...this.settings.navigator, + ...options.settings.navigator + }, + device: { + ...this.settings.device, + ...options.settings.device + } + }; + } + } + + /** + * Aborts asynchronous tasks and destroys the context. + * + * @returns Promise. + */ + public async close(): Promise { + await Promise.all(this.browserContexts.map((browserContext) => browserContext.close())); + } + + /** + * Returns a promise that is resolved when all async tasks are complete. + * + * @returns Promise. + */ + public async whenAsyncTasksComplete(): Promise { + await this._asyncTaskManager.whenComplete(); + } + + /** + * Aborts all async tasks. + * + * @returns Promise. + */ + public async abortAsyncTasks(): Promise { + this._asyncTaskManager.cancelAll(); + } + + /** + * Sets the window size and triggers a resize event. + * + * @param options Options. + * @param options.width Width. + * @param options.height Height. + */ + public resizeWindow(options: { width?: number; height?: number }): void { + if ( + (options.width !== undefined && this.window.innerWidth !== options.width) || + (options.height !== undefined && this.window.innerHeight !== options.height) + ) { + if (options.width !== undefined && this.window.innerWidth !== options.width) { + (this.window.innerWidth) = options.width; + (this.window.outerWidth) = options.width; + } + + if (options.height !== undefined && this.window.innerHeight !== options.height) { + (this.window.innerHeight) = options.height; + (this.window.outerHeight) = options.height; + } + + this.window.dispatchEvent(new Event('resize')); + } + } + + /** + * Go to a page. + * + * @param url URL. + */ + public async goto(url: string): Promise { + this.window.location.href = url; + + const response = await this.window.fetch(url); + const responseText = await response.text(); + + this.document.write(responseText); + } +} diff --git a/packages/happy-dom/src/browser-context/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts similarity index 62% rename from packages/happy-dom/src/browser-context/BrowserContext.ts rename to packages/happy-dom/src/browser/BrowserContext.ts index 0b3bb6a09..ac584b3f1 100644 --- a/packages/happy-dom/src/browser-context/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -3,37 +3,28 @@ import IWindow from '../window/IWindow.js'; import Event from '../event/Event.js'; import Window from '../window/Window.js'; import IBrowserContextOptions from './IBrowserContextOptions.js'; -import IBrowserContextSettings from './IBrowserContextSettings.js'; +import IBrowserContextSettings from './IBrowserSettings.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import BrowserPage from './BrowserPage.js'; +import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; /** * Browser context. */ export default class BrowserContext { - public window: IWindow; - public document: IDocument; - public settings: IBrowserContextSettings; - public consolePrinter: VirtualConsolePrinter | null; - private _asyncTaskManager = new AsyncTaskManager(); + public pages: BrowserPage[] = []; + public default = false; /** * Constructor. * * @param [options] Options. + * @param [options.default] Default. */ - constructor(options?: IBrowserContextOptions) { - this.window = new Window(options); - this.document = this.window.document; - - if (options?.html) { - this.document.write(options.html); - } - - if (options?.ownerBrowserContext) { - (this.window.top) = options.ownerBrowserContext.window; - (this.window.parent) = options.ownerBrowserContext.window; - (this.window.opener) = options.ownerBrowserContext.window; + constructor(options?: { default?: boolean }) { + if (options?.default) { + this.default = true; } } @@ -42,28 +33,26 @@ export default class BrowserContext { * * @returns Promise. */ - public async destroy(): Promise { - await this.abortAsyncTasks(); - this.window = null; - this.document = null; + public async close(): Promise { + await Promise.all(this.pages.map((page) => page.close())); } /** - * Returns a promise that is resolved when all async tasks are complete. + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. * * @returns Promise. */ - public async whenAsyncTasksComplete(): Promise { - await this._asyncTaskManager.whenComplete(); + public async whenComplete(): Promise { + await Promise.all(this.pages.map((page) => page.whenComplete())); } /** - * Aborts all async tasks. + * Aborts all ongoing operations. * * @returns Promise. */ - public async abortAsyncTasks(): Promise { - this._asyncTaskManager.cancelAll(); + public async abort(): Promise { + await Promise.all(this.pages.map((page) => page.abort())); } /** diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts new file mode 100644 index 000000000..335c1806c --- /dev/null +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -0,0 +1,101 @@ +import IWindow from '../window/IWindow.js'; +import Event from '../event/Event.js'; +import IBrowserContextSettings from './IBrowserSettings.js'; +import IViewport from './IBrowserPageViewport.js'; +import BrowserPage from './BrowserPage.js'; +import { AsyncTaskManager } from '../index.js'; + +/** + * Browser frame. + */ +export default class BrowserFrame { + public childFrames: BrowserFrame[] = []; + public detached = false; + public page: BrowserPage | null = null; + public window: IWindow | null = null; + public settings: IBrowserContextSettings; + private _asyncTaskManager = new AsyncTaskManager(); + + /** + * Constructor. + * + * @param page Page. + */ + constructor(page: BrowserPage) { + this.page = page; + } + + /** + * Returns the viewport. + */ + public get content(): string { + return this.window.document.documentElement.outerHTML; + } + + /** + * Returns a promise that is resolved when all async tasks are complete. + * + * @returns Promise. + */ + public async whenComplete(): Promise { + await this._asyncTaskManager.whenComplete(); + } + + /** + * Aborts asynchronous tasks and destroys the context. + * + * @returns Promise. + */ + public async abort(): Promise { + await this._asyncTaskManager.cancelAll(); + this.window = null; + } + + /** + * Aborts asynchronous tasks and destroys the context. + * + * @returns Promise. + */ + public async destroy(): Promise { + await this.abort(); + this.window = null; + } + + /** + * Sets the viewport. + * + * @param viewport Viewport. + */ + public setViewport(viewport: IViewport): void { + if ( + (viewport.width !== undefined && this.window.innerWidth !== viewport.width) || + (viewport.height !== undefined && this.window.innerHeight !== viewport.height) + ) { + if (viewport.width !== undefined && this.window.innerWidth !== viewport.width) { + (this.window.innerWidth) = viewport.width; + (this.window.outerWidth) = viewport.width; + } + + if (viewport.height !== undefined && this.window.innerHeight !== viewport.height) { + (this.window.innerHeight) = viewport.height; + (this.window.outerHeight) = viewport.height; + } + + this.window.dispatchEvent(new Event('resize')); + } + } + + /** + * Go to a page. + * + * @param url URL. + */ + public async goto(url: string): Promise { + this.window.location.href = url; + + const response = await this.window.fetch(url); + const responseText = await response.text(); + + this.window.document.write(responseText); + } +} diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts new file mode 100644 index 000000000..6ac0febae --- /dev/null +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -0,0 +1,98 @@ +import Event from '../event/Event.js'; +import IBrowserContextSettings from './IBrowserSettings.js'; +import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import IBrowserPageViewport from './IBrowserPageViewport.js'; +import BrowserFrame from './BrowserFrame.js'; +import BrowserContext from './BrowserContext.js'; + +/** + * Browser page. + */ +export default class BrowserPage { + public settings: IBrowserContextSettings; + public consolePrinter: VirtualConsolePrinter | null; + public mainFrame: BrowserFrame | null = null; + public frames: BrowserFrame[]; + public context: BrowserContext; + + /** + * Constructor. + * + * @param context Browser context. + */ + constructor(context: BrowserContext) { + this.context = context; + } + + /** + * Returns the viewport. + */ + public get content(): string { + return this.mainFrame.window.document.documentElement.outerHTML; + } + + /** + * Aborts asynchronous tasks and destroys the context. + * + * @returns Promise. + */ + public async close(): Promise { + await Promise.all(this.frames.map((frame) => frame.destroy())); + } + + /** + * Returns a promise that is resolved when all async tasks are complete. + * + * @returns Promise. + */ + public async whenComplete(): Promise { + await Promise.all(this.frames.map((frame) => frame.whenComplete())); + } + + /** + * Aborts all async tasks. + * + * @returns Promise. + */ + public async abort(): Promise { + await Promise.all(this.frames.map((frame) => frame.abort())); + } + + /** + * Sets the viewport. + * + * @param viewport Viewport. + */ + public setViewport(viewport: IBrowserPageViewport): void { + if ( + (viewport.width !== undefined && this.window.innerWidth !== viewport.width) || + (viewport.height !== undefined && this.window.innerHeight !== viewport.height) + ) { + if (viewport.width !== undefined && this.window.innerWidth !== viewport.width) { + (this.window.innerWidth) = viewport.width; + (this.window.outerWidth) = viewport.width; + } + + if (viewport.height !== undefined && this.window.innerHeight !== viewport.height) { + (this.window.innerHeight) = viewport.height; + (this.window.outerHeight) = viewport.height; + } + + this.window.dispatchEvent(new Event('resize')); + } + } + + /** + * Go to a page. + * + * @param url URL. + */ + public async goto(url: string): Promise { + this.window.location.href = url; + + const response = await this.window.fetch(url); + const responseText = await response.text(); + + this.document.write(responseText); + } +} diff --git a/packages/happy-dom/src/browser-context/IBrowserContextOptions.ts b/packages/happy-dom/src/browser/IBrowserContextOptions.ts similarity index 100% rename from packages/happy-dom/src/browser-context/IBrowserContextOptions.ts rename to packages/happy-dom/src/browser/IBrowserContextOptions.ts diff --git a/packages/happy-dom/src/browser/IBrowserPageViewport.ts b/packages/happy-dom/src/browser/IBrowserPageViewport.ts new file mode 100644 index 000000000..950bb0e57 --- /dev/null +++ b/packages/happy-dom/src/browser/IBrowserPageViewport.ts @@ -0,0 +1,6 @@ +export default interface IBrowserPageViewport { + width: number; + height: number; + deviceScaleFactor?: number; + hasTouch?: boolean; +} diff --git a/packages/happy-dom/src/browser-context/IBrowserContextSettings.ts b/packages/happy-dom/src/browser/IBrowserSettings.ts similarity index 83% rename from packages/happy-dom/src/browser-context/IBrowserContextSettings.ts rename to packages/happy-dom/src/browser/IBrowserSettings.ts index ea2d9d472..4339bdbaa 100644 --- a/packages/happy-dom/src/browser-context/IBrowserContextSettings.ts +++ b/packages/happy-dom/src/browser/IBrowserSettings.ts @@ -1,7 +1,7 @@ /** - * Browser context settings. + * Browser settings. */ -export default interface IBrowserContextSettings { +export default interface IBrowserSettings { disableJavaScriptEvaluation: boolean; disableJavaScriptFileLoading: boolean; disableCSSFileLoading: boolean; diff --git a/packages/happy-dom/src/browser/IOptionalBrowserSettings.ts b/packages/happy-dom/src/browser/IOptionalBrowserSettings.ts new file mode 100644 index 000000000..5832e17b2 --- /dev/null +++ b/packages/happy-dom/src/browser/IOptionalBrowserSettings.ts @@ -0,0 +1,20 @@ +/** + * Browser settings. + */ +export default interface IOptionalBrowserSettings { + disableJavaScriptEvaluation?: boolean; + disableJavaScriptFileLoading?: boolean; + disableCSSFileLoading?: boolean; + disableIframePageLoading?: boolean; + disableWindowOpenPageLoading?: boolean; + disableComputedStyleRendering?: boolean; + disableErrorCapturing?: boolean; + enableFileSystemHttpRequests?: boolean; + navigator?: { + userAgent?: string; + }; + device?: { + prefersColorScheme?: string; + mediaType?: string; + }; +} From 17cfde79ff6480dbfa023d92854433ac0d2c0504 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 21 Sep 2023 00:20:09 +0200 Subject: [PATCH 03/63] #466@trivial: Starts on implementation. --- packages/happy-dom/src/browser/Browser.ts | 62 +++---------------- .../happy-dom/src/browser/BrowserContext.ts | 62 ++----------------- .../happy-dom/src/browser/BrowserFrame.ts | 42 +++---------- packages/happy-dom/src/browser/BrowserPage.ts | 59 +++++++++++------- .../src/browser/IBrowserContextOptions.ts | 3 - 5 files changed, 61 insertions(+), 167 deletions(-) diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index f22bab326..582245802 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -1,7 +1,3 @@ -import IDocument from '../nodes/document/IDocument.js'; -import IWindow from '../window/IWindow.js'; -import Event from '../event/Event.js'; -import Window from '../window/Window.js'; import IBrowserSettings from './IBrowserSettings.js'; import BrowserContext from './BrowserContext.js'; import PackageVersion from '../version.js'; @@ -11,7 +7,7 @@ import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; * Browser context. */ export default class Browser { - public browserContexts: BrowserContext[]; + public contexts: BrowserContext[]; public defaultBrowserContext: BrowserContext; public settings: IBrowserSettings = { disableJavaScriptEvaluation: false, @@ -57,69 +53,29 @@ export default class Browser { } /** - * Aborts asynchronous tasks and destroys the context. + * Aborts all ongoing operations and destroys the browser. * * @returns Promise. */ public async close(): Promise { - await Promise.all(this.browserContexts.map((browserContext) => browserContext.close())); + await Promise.all(this.contexts.map((context) => context.close())); } /** - * Returns a promise that is resolved when all async tasks are complete. + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. * * @returns Promise. */ - public async whenAsyncTasksComplete(): Promise { - await this._asyncTaskManager.whenComplete(); + public async whenComplete(): Promise { + await Promise.all(this.contexts.map((page) => page.whenComplete())); } /** - * Aborts all async tasks. + * Aborts all ongoing operations. * * @returns Promise. */ - public async abortAsyncTasks(): Promise { - this._asyncTaskManager.cancelAll(); - } - - /** - * Sets the window size and triggers a resize event. - * - * @param options Options. - * @param options.width Width. - * @param options.height Height. - */ - public resizeWindow(options: { width?: number; height?: number }): void { - if ( - (options.width !== undefined && this.window.innerWidth !== options.width) || - (options.height !== undefined && this.window.innerHeight !== options.height) - ) { - if (options.width !== undefined && this.window.innerWidth !== options.width) { - (this.window.innerWidth) = options.width; - (this.window.outerWidth) = options.width; - } - - if (options.height !== undefined && this.window.innerHeight !== options.height) { - (this.window.innerHeight) = options.height; - (this.window.outerHeight) = options.height; - } - - this.window.dispatchEvent(new Event('resize')); - } - } - - /** - * Go to a page. - * - * @param url URL. - */ - public async goto(url: string): Promise { - this.window.location.href = url; - - const response = await this.window.fetch(url); - const responseText = await response.text(); - - this.document.write(responseText); + public async abort(): Promise { + await Promise.all(this.contexts.map((page) => page.abort())); } } diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index ac584b3f1..63bb5e88e 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -1,35 +1,25 @@ -import IDocument from '../nodes/document/IDocument.js'; -import IWindow from '../window/IWindow.js'; import Event from '../event/Event.js'; -import Window from '../window/Window.js'; -import IBrowserContextOptions from './IBrowserContextOptions.js'; -import IBrowserContextSettings from './IBrowserSettings.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; -import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import Browser from './Browser.js'; import BrowserPage from './BrowserPage.js'; -import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; /** * Browser context. */ export default class BrowserContext { public pages: BrowserPage[] = []; - public default = false; + public browser: Browser; /** * Constructor. * - * @param [options] Options. - * @param [options.default] Default. + * @param browser */ - constructor(options?: { default?: boolean }) { - if (options?.default) { - this.default = true; - } + constructor(browser: Browser) { + this.browser = browser; } /** - * Aborts asynchronous tasks and destroys the context. + * Aborts all ongoing operations and destroys the context. * * @returns Promise. */ @@ -54,44 +44,4 @@ export default class BrowserContext { public async abort(): Promise { await Promise.all(this.pages.map((page) => page.abort())); } - - /** - * Sets the window size and triggers a resize event. - * - * @param options Options. - * @param options.width Width. - * @param options.height Height. - */ - public resizeWindow(options: { width?: number; height?: number }): void { - if ( - (options.width !== undefined && this.window.innerWidth !== options.width) || - (options.height !== undefined && this.window.innerHeight !== options.height) - ) { - if (options.width !== undefined && this.window.innerWidth !== options.width) { - (this.window.innerWidth) = options.width; - (this.window.outerWidth) = options.width; - } - - if (options.height !== undefined && this.window.innerHeight !== options.height) { - (this.window.innerHeight) = options.height; - (this.window.outerHeight) = options.height; - } - - this.window.dispatchEvent(new Event('resize')); - } - } - - /** - * Go to a page. - * - * @param url URL. - */ - public async goto(url: string): Promise { - this.window.location.href = url; - - const response = await this.window.fetch(url); - const responseText = await response.text(); - - this.document.write(responseText); - } } diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 335c1806c..47dbe1274 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -1,9 +1,7 @@ import IWindow from '../window/IWindow.js'; -import Event from '../event/Event.js'; import IBrowserContextSettings from './IBrowserSettings.js'; -import IViewport from './IBrowserPageViewport.js'; import BrowserPage from './BrowserPage.js'; -import { AsyncTaskManager } from '../index.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; /** * Browser frame. @@ -13,7 +11,6 @@ export default class BrowserFrame { public detached = false; public page: BrowserPage | null = null; public window: IWindow | null = null; - public settings: IBrowserContextSettings; private _asyncTaskManager = new AsyncTaskManager(); /** @@ -42,55 +39,34 @@ export default class BrowserFrame { } /** - * Aborts asynchronous tasks and destroys the context. + * Aborts all ongoing operations. * * @returns Promise. */ public async abort(): Promise { - await this._asyncTaskManager.cancelAll(); - this.window = null; + await Promise.all(this.childFrames.map((frame) => frame.abort())); + this._asyncTaskManager.cancelAll(); } /** - * Aborts asynchronous tasks and destroys the context. + * Aborts all ongoing operations and destroys the frame. * * @returns Promise. */ - public async destroy(): Promise { + public async close(): Promise { await this.abort(); + this.page = null; this.window = null; } - /** - * Sets the viewport. - * - * @param viewport Viewport. - */ - public setViewport(viewport: IViewport): void { - if ( - (viewport.width !== undefined && this.window.innerWidth !== viewport.width) || - (viewport.height !== undefined && this.window.innerHeight !== viewport.height) - ) { - if (viewport.width !== undefined && this.window.innerWidth !== viewport.width) { - (this.window.innerWidth) = viewport.width; - (this.window.outerWidth) = viewport.width; - } - - if (viewport.height !== undefined && this.window.innerHeight !== viewport.height) { - (this.window.innerHeight) = viewport.height; - (this.window.outerHeight) = viewport.height; - } - - this.window.dispatchEvent(new Event('resize')); - } - } - /** * Go to a page. * * @param url URL. */ public async goto(url: string): Promise { + await this.abort(); + this.window.location.href = url; const response = await this.window.fetch(url); diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index 6ac0febae..289fd72dd 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -1,5 +1,4 @@ import Event from '../event/Event.js'; -import IBrowserContextSettings from './IBrowserSettings.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; import IBrowserPageViewport from './IBrowserPageViewport.js'; import BrowserFrame from './BrowserFrame.js'; @@ -9,10 +8,8 @@ import BrowserContext from './BrowserContext.js'; * Browser page. */ export default class BrowserPage { - public settings: IBrowserContextSettings; public consolePrinter: VirtualConsolePrinter | null; public mainFrame: BrowserFrame | null = null; - public frames: BrowserFrame[]; public context: BrowserContext; /** @@ -24,6 +21,13 @@ export default class BrowserPage { this.context = context; } + /** + * Returns frames. + */ + public get frames(): BrowserFrame[] { + return this._getFrames(this.mainFrame); + } + /** * Returns the viewport. */ @@ -32,12 +36,15 @@ export default class BrowserPage { } /** - * Aborts asynchronous tasks and destroys the context. + * Aborts all ongoing operations and destroys the page. * * @returns Promise. */ public async close(): Promise { - await Promise.all(this.frames.map((frame) => frame.destroy())); + await this.mainFrame.close(); + this.consolePrinter = null; + this.mainFrame = null; + this.context = null; } /** @@ -46,16 +53,16 @@ export default class BrowserPage { * @returns Promise. */ public async whenComplete(): Promise { - await Promise.all(this.frames.map((frame) => frame.whenComplete())); + await this.mainFrame.whenComplete(); } /** - * Aborts all async tasks. + * Aborts all ongoing operations. * * @returns Promise. */ public async abort(): Promise { - await Promise.all(this.frames.map((frame) => frame.abort())); + await this.mainFrame.abort(); } /** @@ -65,20 +72,20 @@ export default class BrowserPage { */ public setViewport(viewport: IBrowserPageViewport): void { if ( - (viewport.width !== undefined && this.window.innerWidth !== viewport.width) || - (viewport.height !== undefined && this.window.innerHeight !== viewport.height) + (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) || + (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) ) { - if (viewport.width !== undefined && this.window.innerWidth !== viewport.width) { - (this.window.innerWidth) = viewport.width; - (this.window.outerWidth) = viewport.width; + if (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) { + (this.mainFrame.window.innerWidth) = viewport.width; + (this.mainFrame.window.outerWidth) = viewport.width; } - if (viewport.height !== undefined && this.window.innerHeight !== viewport.height) { - (this.window.innerHeight) = viewport.height; - (this.window.outerHeight) = viewport.height; + if (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) { + (this.mainFrame.window.innerHeight) = viewport.height; + (this.mainFrame.window.outerHeight) = viewport.height; } - this.window.dispatchEvent(new Event('resize')); + this.mainFrame.window.dispatchEvent(new Event('resize')); } } @@ -88,11 +95,19 @@ export default class BrowserPage { * @param url URL. */ public async goto(url: string): Promise { - this.window.location.href = url; - - const response = await this.window.fetch(url); - const responseText = await response.text(); + this.mainFrame.goto(url); + } - this.document.write(responseText); + /** + * Returns frames. + * + * @param parent Parent frame. + */ + public _getFrames(parent: BrowserFrame): BrowserFrame[] { + let frames = [parent]; + for (const frame of parent.childFrames) { + frames = frames.concat(this._getFrames(frame)); + } + return frames; } } diff --git a/packages/happy-dom/src/browser/IBrowserContextOptions.ts b/packages/happy-dom/src/browser/IBrowserContextOptions.ts index 47bfe9bc1..2e4cc0f49 100644 --- a/packages/happy-dom/src/browser/IBrowserContextOptions.ts +++ b/packages/happy-dom/src/browser/IBrowserContextOptions.ts @@ -1,5 +1,3 @@ -import BrowserContext from './BrowserContext.js'; - /** * Browser context options. */ @@ -9,7 +7,6 @@ export default interface IBrowserContextOptions { url?: string; html?: string; console?: Console; - ownerBrowserContext?: BrowserContext; settings?: { disableJavaScriptEvaluation?: boolean; disableJavaScriptFileLoading?: boolean; From 87e51e8fa44f9ce50d007ee7fc9ee1a52d43f76b Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 5 Oct 2023 23:46:59 +0200 Subject: [PATCH 04/63] #466@trivial: Continue on implementation. --- packages/happy-dom/src/browser/Browser.ts | 37 ++----------------- .../happy-dom/src/browser/BrowserContext.ts | 1 - .../src/browser/BrowserSettingsFactory.ts | 29 +++++++++++++++ .../src/browser/DefaultBrowserSettings.ts | 21 +++++++++++ packages/happy-dom/src/window/Window.ts | 12 +++++- 5 files changed, 64 insertions(+), 36 deletions(-) create mode 100644 packages/happy-dom/src/browser/BrowserSettingsFactory.ts create mode 100644 packages/happy-dom/src/browser/DefaultBrowserSettings.ts diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index 582245802..5bd6e95c2 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -1,7 +1,7 @@ import IBrowserSettings from './IBrowserSettings.js'; import BrowserContext from './BrowserContext.js'; -import PackageVersion from '../version.js'; import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; +import BrowserSettingsFactory from './BrowserSettingsFactory.js'; /** * Browser context. @@ -9,25 +9,7 @@ import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; export default class Browser { public contexts: BrowserContext[]; public defaultBrowserContext: BrowserContext; - public settings: IBrowserSettings = { - disableJavaScriptEvaluation: false, - disableJavaScriptFileLoading: false, - disableCSSFileLoading: false, - disableIframePageLoading: false, - disableWindowOpenPageLoading: false, - disableComputedStyleRendering: false, - disableErrorCapturing: false, - enableFileSystemHttpRequests: false, - navigator: { - userAgent: `Mozilla/5.0 (X11; ${ - process.platform.charAt(0).toUpperCase() + process.platform.slice(1) + ' ' + process.arch - }) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}` - }, - device: { - prefersColorScheme: 'light', - mediaType: 'screen' - } - }; + public settings: IBrowserSettings; /** * Constructor. @@ -36,20 +18,7 @@ export default class Browser { * @param [options.settings] Browser settings. */ constructor(options?: { settings?: IOptionalBrowserSettings }) { - if (options.settings) { - this.settings = { - ...this.settings, - ...options.settings, - navigator: { - ...this.settings.navigator, - ...options.settings.navigator - }, - device: { - ...this.settings.device, - ...options.settings.device - } - }; - } + this.settings = BrowserSettingsFactory.getSettings(options?.settings); } /** diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index 63bb5e88e..a80545932 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -1,4 +1,3 @@ -import Event from '../event/Event.js'; import Browser from './Browser.js'; import BrowserPage from './BrowserPage.js'; diff --git a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts new file mode 100644 index 000000000..b8a5e32b5 --- /dev/null +++ b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts @@ -0,0 +1,29 @@ +import IBrowserSettings from './IBrowserSettings.js'; +import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; +import DefaultBrowserSettings from './DefaultBrowserSettings.js'; + +/** + * Browser settings utility. + */ +export default class BrowserSettingsFactory { + /** + * Returns browser settings. + * + * @param [settings] Browser settings. + * @returns Settings. + */ + public static getSettings(settings?: IOptionalBrowserSettings): IBrowserSettings { + return { + ...DefaultBrowserSettings, + ...settings, + navigator: { + ...DefaultBrowserSettings.navigator, + ...settings.navigator + }, + device: { + ...DefaultBrowserSettings.device, + ...settings.device + } + }; + } +} diff --git a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts new file mode 100644 index 000000000..d976f43d6 --- /dev/null +++ b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts @@ -0,0 +1,21 @@ +import PackageVersion from '../version.js'; + +export default { + disableJavaScriptEvaluation: false, + disableJavaScriptFileLoading: false, + disableCSSFileLoading: false, + disableIframePageLoading: false, + disableWindowOpenPageLoading: false, + disableComputedStyleRendering: false, + disableErrorCapturing: false, + enableFileSystemHttpRequests: false, + navigator: { + userAgent: `Mozilla/5.0 (X11; ${ + process.platform.charAt(0).toUpperCase() + process.platform.slice(1) + ' ' + process.arch + }) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}` + }, + device: { + prefersColorScheme: 'light', + mediaType: 'screen' + } +}; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 4142b6020..bb2cc76d6 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -143,6 +143,7 @@ import IHappyDOMSettings from './IHappyDOMSettings.js'; import PackageVersion from '../version.js'; import ICrossOriginWindow from './ICrossOriginWindow.js'; import BrowserContext from './BrowserContextLoader.js'; +import IBrowserSettings from '../browser/IBrowserSettings.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -466,9 +467,18 @@ export default class Window extends EventTarget implements IWindow { * @param [options.innerWidth] Inner width. Deprecated. Defaults to "1024". * @param [options.innerHeight] Inner height. Deprecated. Defaults to "768". * @param [options.url] URL. + * @param [options.console] Console. * @param [options.settings] Settings. */ - constructor(options?: IHappyDOMOptions) { + constructor(options?: { + width?: number; + height?: number; + innerWidth?: number; + innerHeight?: number; + url?: string; + console?: Console; + settings?: IBrowserSettings; + }) { super(); this.customElements = new CustomElementRegistry(); From 0e8b0ed0f461e02dd6aa899c8fd349e42f2b7ad0 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 6 Oct 2023 01:00:52 +0200 Subject: [PATCH 05/63] #466@trivial: Continues on implementation. --- .../happy-dom/.eslintrc.cjs => .eslintrc.cjs | 0 .huskyrc.json | 2 +- .lintignore | 5 + package.json | 5 +- packages/global-registrator/.eslintrc.cjs | 1 - packages/global-registrator/package.json | 10 -- packages/happy-dom/package.json | 11 -- .../happy-dom/src/browser/BrowserFrame.ts | 1 - packages/happy-dom/src/fetch/Fetch.ts | 6 +- .../src/window/BrowserContextLoader.ts | 8 +- .../happy-dom/src/window/DetachedWindowAPI.ts | 106 +++++++++++++++++ .../happy-dom/src/window/IHappyDOMOptions.ts | 18 --- .../happy-dom/src/window/IHappyDOMSettings.ts | 9 -- packages/happy-dom/src/window/IWindow.ts | 30 +---- packages/happy-dom/src/window/Window.ts | 112 +++--------------- .../src/xml-http-request/XMLHttpRequest.ts | 32 +++-- packages/integration-test/.eslintrc.cjs | 1 - packages/integration-test/package.json | 10 -- packages/jest-environment/.eslintrc.cjs | 1 - packages/jest-environment/package.json | 11 -- .../uncaught-exception-observer/.eslintrc.cjs | 1 - .../uncaught-exception-observer/package.json | 10 -- turbo.json | 6 - 23 files changed, 163 insertions(+), 233 deletions(-) rename packages/happy-dom/.eslintrc.cjs => .eslintrc.cjs (100%) create mode 100755 .lintignore delete mode 100644 packages/global-registrator/.eslintrc.cjs create mode 100644 packages/happy-dom/src/window/DetachedWindowAPI.ts delete mode 100644 packages/happy-dom/src/window/IHappyDOMOptions.ts delete mode 100644 packages/happy-dom/src/window/IHappyDOMSettings.ts delete mode 100644 packages/integration-test/.eslintrc.cjs delete mode 100644 packages/jest-environment/.eslintrc.cjs delete mode 100644 packages/uncaught-exception-observer/.eslintrc.cjs diff --git a/packages/happy-dom/.eslintrc.cjs b/.eslintrc.cjs similarity index 100% rename from packages/happy-dom/.eslintrc.cjs rename to .eslintrc.cjs diff --git a/.huskyrc.json b/.huskyrc.json index 4375e705c..6aff645db 100644 --- a/.huskyrc.json +++ b/.huskyrc.json @@ -1,6 +1,6 @@ { "hooks": { - "pre-commit": "npm run lint:fix", + "pre-commit": "npm run lint:changed", "commit-msg": "node ./bin/husky-commit-message.js --filepath=$HUSKY_GIT_PARAMS%HUSKY_GIT_PARAMS%" } } diff --git a/.lintignore b/.lintignore new file mode 100755 index 000000000..181fc97c4 --- /dev/null +++ b/.lintignore @@ -0,0 +1,5 @@ +/node_modules +/tmp/ +/lib/ +/cjs/ +/tmp/ \ No newline at end of file diff --git a/package.json b/package.json index 7da917e64..db1c88459 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ "compile": "turbo run compile --cache-dir=.turbo", "watch": "turbo run watch --parallel", "clean": "git clean -Xdfq", - "lint": "turbo run lint --cache-dir=.turbo", - "lint:fix": "turbo run lint:fix", + "lint": "eslint --ignore-path .lintignore --max-warnings 0 .", + "lint:fix": "eslint --ignore-path .lintignore --max-warnings 0 --fix .", + "lint:changed": "eslint --ignore-path .lintignore --max-warnings 0 --fix $(git diff --diff-filter=d --name-only HEAD | grep -E '^[a-zA-Z0-9_].*\\.(cjs|mjs|js|jsx|ts|tsx|json)$' | xargs)", "test": "turbo run test --cache-dir=.turbo", "test:watch": "turbo run test:watch --parallel" }, diff --git a/packages/global-registrator/.eslintrc.cjs b/packages/global-registrator/.eslintrc.cjs deleted file mode 100644 index f4088a566..000000000 --- a/packages/global-registrator/.eslintrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('happy-dom/.eslintrc.cjs'); diff --git a/packages/global-registrator/package.json b/packages/global-registrator/package.json index aaa1d0240..5c2cb86d4 100644 --- a/packages/global-registrator/package.json +++ b/packages/global-registrator/package.json @@ -68,8 +68,6 @@ "compile": "tsc && tsc --moduleResolution Node --module CommonJS --outDir cjs && npm run change-cjs-file-extension", "change-cjs-file-extension": "node ../happy-dom/bin/change-file-extension.cjs --dir=./cjs --fromExt=.js --toExt=.cjs", "watch": "npm run compile && tsc -w --preserveWatchOutput", - "lint": "eslint --ignore-path .gitignore --max-warnings 0 .", - "lint:fix": "eslint --ignore-path .gitignore --max-warnings 0 --fix .", "test": "tsc --project ./test && node ../happy-dom/bin/change-file-extension.cjs --dir=./tmp --fromExt=.js --toExt=.cjs && node ./tmp/react/React.test.cjs", "test:debug": "tsc --project ./test && node ../happy-dom/bin/change-file-extension.cjs --dir=./tmp --fromExt=.js --toExt=.cjs && node --inspect-brk ./tmp/react/React.test.cjs" }, @@ -80,14 +78,6 @@ "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "@types/node": "^16.11.7", - "eslint": "^8.11.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-filenames": "^1.3.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^38.0.6", - "eslint-plugin-json": "^3.1.0", - "eslint-plugin-turbo": "^0.0.7", "prettier": "^2.6.0", "typescript": "^5.0.4", "react": "^18.2.0", diff --git a/packages/happy-dom/package.json b/packages/happy-dom/package.json index f1d3eef84..0bb37621b 100644 --- a/packages/happy-dom/package.json +++ b/packages/happy-dom/package.json @@ -69,9 +69,6 @@ "change-cjs-file-extension": "node ./bin/change-file-extension.cjs --dir=./cjs --fromExt=.js --toExt=.cjs", "build-version-file": "node ./bin/build-version-file.cjs", "watch": "tsc -w --preserveWatchOutput", - "lint": "eslint --ignore-path .gitignore --max-warnings 0 .", - "lint:fix": "eslint --ignore-path .gitignore --max-warnings 0 --fix .", - "test": "vitest run --singleThread", "test:ui": "vitest --ui", "test:watch": "vitest --singleThread", "test:debug": "vitest run --inspect-brk --threads=false" @@ -92,14 +89,6 @@ "@typescript-eslint/parser": "^5.16.0", "@vitest/ui": "^0.33.0", "@webref/css": "6.6.2", - "eslint": "^8.11.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-filenames": "^1.3.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^38.0.6", - "eslint-plugin-json": "^3.1.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-turbo": "^0.0.7", "prettier": "^2.6.0", "typescript": "^5.0.4", "vitest": "^0.32.4" diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 47dbe1274..50bf0836f 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -1,5 +1,4 @@ import IWindow from '../window/IWindow.js'; -import IBrowserContextSettings from './IBrowserSettings.js'; import BrowserPage from './BrowserPage.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index 817bff430..46e4aa841 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -1,5 +1,6 @@ import IRequestInit from './types/IRequestInit.js'; import IDocument from '../nodes/document/IDocument.js'; +import Document from '../nodes/document/Document.js'; import IResponse from './types/IResponse.js'; import Request from './Request.js'; import IRequestInfo from './types/IRequestInfo.js'; @@ -16,7 +17,6 @@ import { Socket } from 'net'; import Stream from 'stream'; import DataURIParser from './data-uri/DataURIParser.js'; import FetchCORSUtility from './utilities/FetchCORSUtility.js'; -import CookieJar from '../cookie/CookieJar.js'; const SUPPORTED_SCHEMAS = ['data:', 'http:', 'https:']; const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; @@ -559,7 +559,7 @@ export default class Fetch { this.request.credentials === 'include' || (this.request.credentials === 'same-origin' && !isCORS) ) { - const cookie = document.defaultView.document._cookie.getCookieString( + const cookie = (document.defaultView.document)._cookie.getCookieString( this.ownerDocument.defaultView.location, false ); @@ -619,7 +619,7 @@ export default class Fetch { // Handles setting cookie headers to the document. // "set-cookie" and "set-cookie2" are not allowed in response headers according to spec. if (lowerKey === 'set-cookie' || lowerKey === 'set-cookie2') { - (this.ownerDocument['_cookie']).addCookieString(this.request._url, header); + (this.ownerDocument)._cookie.addCookieString(this.request._url, header); } else { headers.append(key, header); } diff --git a/packages/happy-dom/src/window/BrowserContextLoader.ts b/packages/happy-dom/src/window/BrowserContextLoader.ts index 2d4aa18e6..6c49aab76 100644 --- a/packages/happy-dom/src/window/BrowserContextLoader.ts +++ b/packages/happy-dom/src/window/BrowserContextLoader.ts @@ -6,6 +6,7 @@ import WindowErrorUtility from './WindowErrorUtility.js'; import ICrossOriginWindow from './ICrossOriginWindow.js'; import IHTMLElement from '../nodes/html-element/IHTMLElement.js'; import Window from './Window.js'; +import DetachedWindowAPI from './DetachedWindowAPI.js'; /** * Browser context. @@ -43,12 +44,7 @@ export default class BrowserContextLoader { const url = options?.url || 'about:blank'; const target = options?.target !== undefined ? String(options.target) : null; - contentWindow.happyDOM.virtualConsolePrinter = ownerWindow.happyDOM.virtualConsolePrinter; - - // TODO: This can perhaps be improved, so that it is possible to watch async tasks on each child window. - contentWindow.happyDOM.asyncTaskManager = ownerWindow.happyDOM.asyncTaskManager; - contentWindow.happyDOM.whenAsyncComplete = ownerWindow.happyDOM.whenAsyncComplete; - contentWindow.happyDOM.cancelAsync = ownerWindow.happyDOM.cancelAsync; + (contentWindow.happyDOM) = contentWindow.happyDOM; (contentWindow.document.referrer) = !features.noreferrer ? ownerWindow.location.href diff --git a/packages/happy-dom/src/window/DetachedWindowAPI.ts b/packages/happy-dom/src/window/DetachedWindowAPI.ts new file mode 100644 index 000000000..14e484902 --- /dev/null +++ b/packages/happy-dom/src/window/DetachedWindowAPI.ts @@ -0,0 +1,106 @@ +import BrowserSettingsFactory from '../browser/BrowserSettingsFactory.js'; +import IBrowserSettings from '../browser/IBrowserSettings.js'; +import IOptionalBrowserSettings from '../browser/IOptionalBrowserSettings.js'; +import IVirtualConsolePrinter from '../console/types/IVirtualConsolePrinter.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import IWindow from './IWindow.js'; +import Event from '../event/Event.js'; + +/** + * API for detached windows to be able to access features of the owner window. + */ +export default class DetachedWindowAPI { + public readonly asyncTaskManager = new AsyncTaskManager(); + public readonly settings: IBrowserSettings; + public readonly virtualConsolePrinter: IVirtualConsolePrinter | null; + #ownerWindow: IWindow; + + /** + * Constructor. + * + * @param options Options. + * @param options.ownerWindow Owner window. + * @param [options.settings] Browser settings. + * @param [options.virtualConsolePrinter] Virtual console printer. + */ + constructor(options: { + ownerWindow: IWindow; + settings?: IOptionalBrowserSettings; + virtualConsolePrinter?: IVirtualConsolePrinter; + }) { + this.#ownerWindow = options.ownerWindow; + this.settings = BrowserSettingsFactory.getSettings(options.settings); + this.virtualConsolePrinter = options.virtualConsolePrinter || null; + } + + /** + * Waits for all async tasks to complete. + * + * @returns Promise. + */ + public async whenAsyncComplete(): Promise { + return await this.asyncTaskManager.whenComplete(); + } + + /** + * Aborts all async tasks. + */ + public cancelAsync(): void { + this.asyncTaskManager.cancelAll(); + } + + /** + * Sets the URL. + * + * @param url URL. + */ + public setURL(url: string): void { + this.#ownerWindow.location.href = url; + } + + /** + * Sets the window size. + * + * @param options Options. + * @param options.width Width. + * @param options.height Height. + */ + public setWindowSize(options: { width?: number; height?: number }): void { + if ( + (options.width !== undefined && this.#ownerWindow.innerWidth !== options.width) || + (options.height !== undefined && this.#ownerWindow.innerHeight !== options.height) + ) { + if (options.width !== undefined && this.#ownerWindow.innerWidth !== options.width) { + (this.#ownerWindow.innerWidth) = options.width; + (this.#ownerWindow.outerWidth) = options.width; + } + + if (options.height !== undefined && this.#ownerWindow.innerHeight !== options.height) { + (this.#ownerWindow.innerHeight) = options.height; + (this.#ownerWindow.outerHeight) = options.height; + } + + this.#ownerWindow.dispatchEvent(new Event('resize')); + } + } + + /** + * Sets the window width. + * + * @deprecated Use setWindowSize() instead. + * @param width Width. + */ + public setInnerWidth(width: number): void { + this.setWindowSize({ width }); + } + + /** + * Sets the window height. + * + * @deprecated Use setWindowSize() instead. + * @param height Height. + */ + public setInnerHeight(height: number): void { + this.setWindowSize({ height }); + } +} diff --git a/packages/happy-dom/src/window/IHappyDOMOptions.ts b/packages/happy-dom/src/window/IHappyDOMOptions.ts deleted file mode 100644 index 03669c04f..000000000 --- a/packages/happy-dom/src/window/IHappyDOMOptions.ts +++ /dev/null @@ -1,18 +0,0 @@ -import IBrowserContextOptions from '../browser-context/IBrowserContextOptions.js'; - -/** - * Happy DOM options. - * - * @deprecated - */ -export default interface IHappyDOMOptions extends IBrowserContextOptions { - /** - * @deprecated - */ - innerWidth?: number; - - /** - * @deprecated - */ - innerHeight?: number; -} diff --git a/packages/happy-dom/src/window/IHappyDOMSettings.ts b/packages/happy-dom/src/window/IHappyDOMSettings.ts deleted file mode 100644 index 48c7dda17..000000000 --- a/packages/happy-dom/src/window/IHappyDOMSettings.ts +++ /dev/null @@ -1,9 +0,0 @@ -import IBrowserContextSettings from '../browser-context/IBrowserContextSettings.js'; - -/** - * Happy DOM settings. - * - * @deprecated - */ -type IHappyDOMSettings = IBrowserContextSettings; -export default IHappyDOMSettings; diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index db5fc5a00..718f20d38 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -83,7 +83,6 @@ import SubmitEvent from '../event/events/SubmitEvent.js'; import MessageEvent from '../event/events/MessageEvent.js'; import MessagePort from '../event/MessagePort.js'; import Screen from '../screen/Screen.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import Storage from '../storage/Storage.js'; import NodeFilter from '../tree-walker/NodeFilter.js'; import HTMLCollection from '../nodes/element/HTMLCollection.js'; @@ -113,7 +112,6 @@ import NamedNodeMap from '../named-node-map/NamedNodeMap.js'; import { Performance } from 'perf_hooks'; import IElement from '../nodes/element/IElement.js'; import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction.js'; -import IHappyDOMSettings from './IHappyDOMSettings.js'; import RequestInfo from '../fetch/types/IRequestInfo.js'; import FileList from '../nodes/html-input-element/FileList.js'; import Stream from 'stream'; @@ -127,38 +125,20 @@ import IHeadersInit from '../fetch/types/IHeadersInit.js'; import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; import ValidityState from '../validity-state/ValidityState.js'; import INodeJSGlobal from './INodeJSGlobal.js'; -import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; import ICrossOriginWindow from './ICrossOriginWindow.js'; import Permissions from '../permissions/Permissions.js'; import PermissionStatus from '../permissions/PermissionStatus.js'; import Clipboard from '../clipboard/Clipboard.js'; import ClipboardItem from '../clipboard/ClipboardItem.js'; import ClipboardEvent from '../event/events/ClipboardEvent.js'; +import DetachedWindowAPI from './DetachedWindowAPI.js'; /** * Window without dependencies to server side specific packages. */ export default interface IWindow extends IEventTarget, INodeJSGlobal { - // Happy DOM property. - readonly happyDOM: { - whenAsyncComplete: () => Promise; - cancelAsync: () => void; - asyncTaskManager: AsyncTaskManager; - setWindowSize: (options: { width?: number; height?: number }) => void; - setURL: (url: string) => void; - virtualConsolePrinter: VirtualConsolePrinter | null; - settings: IHappyDOMSettings; - - /** - * @deprecated - */ - setInnerWidth: (width: number) => void; - - /** - * @deprecated - */ - setInnerHeight: (height: number) => void; - }; + // Detached Window API. + readonly happyDOM: DetachedWindowAPI; // Nodes readonly Node: typeof Node; @@ -388,8 +368,8 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly MutationRecord: typeof MutationRecord; // Events - onload: (event: Event) => void; - onerror: (event: ErrorEvent) => void; + onload: ((event: Event) => void) | null; + onerror: ((event: ErrorEvent) => void) | null; // Public Properties readonly document: IDocument; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 989f406b2..da21703e7 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -85,7 +85,6 @@ import ErrorEvent from '../event/events/ErrorEvent.js'; import StorageEvent from '../event/events/StorageEvent.js'; import SubmitEvent from '../event/events/SubmitEvent.js'; import Screen from '../screen/Screen.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import IResponse from '../fetch/types/IResponse.js'; import IResponseInit from '../fetch/types/IResponseInit.js'; import IRequest from '../fetch/types/IRequest.js'; @@ -138,16 +137,15 @@ import ValidityState from '../validity-state/ValidityState.js'; import WindowErrorUtility from './WindowErrorUtility.js'; import VirtualConsole from '../console/VirtualConsole.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; -import IHappyDOMSettings from './IHappyDOMSettings.js'; -import PackageVersion from '../version.js'; import ICrossOriginWindow from './ICrossOriginWindow.js'; -import BrowserContext from './BrowserContextLoader.js'; import IBrowserSettings from '../browser/IBrowserSettings.js'; import Permissions from '../permissions/Permissions.js'; import PermissionStatus from '../permissions/PermissionStatus.js'; import Clipboard from '../clipboard/Clipboard.js'; import ClipboardItem from '../clipboard/ClipboardItem.js'; import ClipboardEvent from '../event/events/ClipboardEvent.js'; +import DetachedWindowAPI from './DetachedWindowAPI.js'; +import IVirtualConsolePrinter from '../console/types/IVirtualConsolePrinter.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -162,77 +160,8 @@ const ORIGINAL_QUEUE_MICROTASK = queueMicrotask; * https://developer.mozilla.org/en-US/docs/Web/API/Window. */ export default class Window extends EventTarget implements IWindow { - // Happy DOM property. - public readonly happyDOM: { - whenAsyncComplete: () => Promise; - cancelAsync: () => void; - asyncTaskManager: AsyncTaskManager; - setWindowSize: (options: { width?: number; height?: number }) => void; - setURL: (url: string) => void; - virtualConsolePrinter: VirtualConsolePrinter | null; - settings: IHappyDOMSettings; - - /** - * @deprecated - */ - setInnerWidth: (width: number) => void; - - /** - * @deprecated - */ - setInnerHeight: (height: number) => void; - } = { - whenAsyncComplete: async (): Promise => { - return await this.happyDOM.asyncTaskManager.whenComplete(); - }, - cancelAsync: (): void => { - this.happyDOM.asyncTaskManager.cancelAll(); - }, - asyncTaskManager: new AsyncTaskManager(), - setWindowSize: (options: { width?: number; height?: number }): void => { - if ( - (options.width !== undefined && this.innerWidth !== options.width) || - (options.height !== undefined && this.innerHeight !== options.height) - ) { - if (options.width !== undefined && this.innerWidth !== options.width) { - (this.innerWidth) = options.width; - (this.outerWidth) = options.width; - } - - if (options.height !== undefined && this.innerHeight !== options.height) { - (this.innerHeight) = options.height; - (this.outerHeight) = options.height; - } - - this.dispatchEvent(new Event('resize')); - } - }, - virtualConsolePrinter: null, - setURL: (url: string) => { - this.location.href = url; - }, - settings: { - disableJavaScriptEvaluation: false, - disableJavaScriptFileLoading: false, - disableCSSFileLoading: false, - disableIframePageLoading: false, - disableWindowOpenPageLoading: false, - disableComputedStyleRendering: false, - disableErrorCapturing: false, - enableFileSystemHttpRequests: false, - navigator: { - userAgent: `Mozilla/5.0 (X11; ${ - process.platform.charAt(0).toUpperCase() + process.platform.slice(1) + ' ' + process.arch - }) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}` - }, - device: { - prefersColorScheme: 'light', - mediaType: 'screen' - } - }, - setInnerWidth: (width: number): void => this.happyDOM.setWindowSize({ width }), - setInnerHeight: (height: number): void => this.happyDOM.setWindowSize({ height }) - }; + // Detached Window API. + public readonly happyDOM: DetachedWindowAPI; // Nodes public readonly Node = Node; @@ -461,8 +390,8 @@ export default class Window extends EventTarget implements IWindow { public readonly Audio; // Events - public onload: (event: Event) => void = null; - public onerror: (event: ErrorEvent) => void = null; + public onload: ((event: Event) => void) | null = null; + public onerror: ((event: ErrorEvent) => void) | null = null; // Public properties. public readonly document: Document; @@ -618,28 +547,23 @@ export default class Window extends EventTarget implements IWindow { if (options.url !== undefined) { this.location.href = options.url; } - - if (options.settings) { - this.happyDOM.settings = { - ...this.happyDOM.settings, - ...options.settings, - navigator: { - ...this.happyDOM.settings.navigator, - ...options.settings.navigator - }, - device: { - ...this.happyDOM.settings.device, - ...options.settings.device - } - }; - } } if (options && options.console) { this.console = options.console; + this.happyDOM = new DetachedWindowAPI({ + ownerWindow: this, + settings: options?.settings + }); } else { - this.happyDOM.virtualConsolePrinter = new VirtualConsolePrinter(); - this.console = new VirtualConsole(this.happyDOM.virtualConsolePrinter); + this.happyDOM = new DetachedWindowAPI({ + ownerWindow: this, + settings: options?.settings, + virtualConsolePrinter: new VirtualConsolePrinter() + }); + this.console = new VirtualConsole( + this.happyDOM.virtualConsolePrinter + ); } this._setTimeout = ORIGINAL_SET_TIMEOUT; diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index c3685e7f0..e617b8fad 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -19,6 +19,7 @@ import XMLHttpRequestCertificate from './XMLHttpRequestCertificate.js'; import XMLHttpRequestSyncRequestScriptBuilder from './utilities/XMLHttpRequestSyncRequestScriptBuilder.js'; import IconvLite from 'iconv-lite'; import ErrorEvent from '../event/events/ErrorEvent.js'; +import Document from '../nodes/document/Document.js'; // These headers are not user setable. // The following are allowed but banned in the spec: @@ -576,7 +577,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { accept: '*/*', referer: location.href, 'user-agent': navigator.userAgent, - cookie: document._cookie.getCookieString(location, false) + cookie: (document)._cookie.getCookieString(location, false) }; } @@ -967,17 +968,15 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } case XMLHttpResponseTypeEnum.document: const window = this._ownerDocument.defaultView; - const happyDOMSettings = window.happyDOM.settings; let response: IDocument; // Temporary disables unsecure features. - window.happyDOM.settings = { - ...happyDOMSettings, - enableFileSystemHttpRequests: false, - disableJavaScriptEvaluation: true, - disableCSSFileLoading: true, - disableJavaScriptFileLoading: true - }; + const originalSettings = Object.assign({}, window.happyDOM.settings); + + window.happyDOM.settings.enableFileSystemHttpRequests = false; + window.happyDOM.settings.disableJavaScriptEvaluation = true; + window.happyDOM.settings.disableCSSFileLoading = true; + window.happyDOM.settings.disableJavaScriptFileLoading = true; const domParser = new window.DOMParser(); @@ -988,7 +987,13 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } // Restores unsecure features. - window.happyDOM.settings = happyDOMSettings; + window.happyDOM.settings.enableFileSystemHttpRequests = + originalSettings.enableFileSystemHttpRequests; + window.happyDOM.settings.disableJavaScriptEvaluation = + originalSettings.disableJavaScriptEvaluation; + window.happyDOM.settings.disableCSSFileLoading = originalSettings.disableCSSFileLoading; + window.happyDOM.settings.disableJavaScriptFileLoading = + originalSettings.disableJavaScriptFileLoading; return { response, responseText: null, responseXML: response }; case XMLHttpResponseTypeEnum.json: @@ -1025,10 +1030,13 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { for (const header of ['set-cookie', 'set-cookie2']) { if (Array.isArray(headers[header])) { for (const cookie of headers[header]) { - this._ownerDocument.defaultView.document._cookie.addCookieString(originURL, cookie); + (this._ownerDocument.defaultView.document)._cookie.addCookieString( + originURL, + cookie + ); } } else if (headers[header]) { - this._ownerDocument.defaultView.document._cookie.addCookieString( + (this._ownerDocument.defaultView.document)._cookie.addCookieString( originURL, headers[header] ); diff --git a/packages/integration-test/.eslintrc.cjs b/packages/integration-test/.eslintrc.cjs deleted file mode 100644 index f4088a566..000000000 --- a/packages/integration-test/.eslintrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('happy-dom/.eslintrc.cjs'); diff --git a/packages/integration-test/package.json b/packages/integration-test/package.json index 5504f6953..03abbb7c2 100644 --- a/packages/integration-test/package.json +++ b/packages/integration-test/package.json @@ -10,8 +10,6 @@ "access": "restricted" }, "scripts": { - "lint": "eslint --ignore-path .gitignore --max-warnings 0 .", - "lint:fix": "eslint --ignore-path .gitignore --max-warnings 0 --fix .", "test": "node ./test/index.js", "test:debug": "node --inspect-brk ./test/index.js" }, @@ -22,14 +20,6 @@ "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "@types/node": "^16.11.7", - "eslint": "^8.11.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-filenames": "^1.3.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^38.0.6", - "eslint-plugin-json": "^3.1.0", - "eslint-plugin-turbo": "^0.0.7", "prettier": "^2.6.0", "express": "^4.18.2" } diff --git a/packages/jest-environment/.eslintrc.cjs b/packages/jest-environment/.eslintrc.cjs deleted file mode 100644 index f4088a566..000000000 --- a/packages/jest-environment/.eslintrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('happy-dom/.eslintrc.cjs'); diff --git a/packages/jest-environment/package.json b/packages/jest-environment/package.json index 313613b34..fe31d09ae 100644 --- a/packages/jest-environment/package.json +++ b/packages/jest-environment/package.json @@ -27,8 +27,6 @@ "scripts": { "compile": "node ./bin/build-lit-ts-config && tsc --project tmp/tsconfig.lit-reactive-element.json && tsc --project tmp/tsconfig.lit-element.json && tsc --project tmp/tsconfig.lit-html.json && tsc --project tmp/tsconfig.lit.json && node ./bin/transform-lit-require.js && node ./bin/copy-tsdef-for-lit.js && node ./bin/copy-package-json-for-lit.js && tsc", "watch": "npm run compile && tsc -w --preserveWatchOutput", - "lint": "eslint --ignore-path .gitignore --max-warnings 0 .", - "lint:fix": "eslint --ignore-path .gitignore --max-warnings 0 --fix .", "test": "jest", "test:watch": "jest --watch", "test:debug": "node --inspect-brk ../../node_modules/.bin/jest --runInBand --testTimeout 60000" @@ -65,15 +63,6 @@ "@testing-library/user-event": "^14.5.1", "@types/react-dom": "^18.2.0", "@types/jest": "29.5.2", - "eslint": "^8.11.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-filenames": "^1.3.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jest": "^26.1.2", - "eslint-plugin-jsdoc": "^38.0.6", - "eslint-plugin-json": "^3.1.0", - "eslint-plugin-turbo": "^0.0.7", "prettier": "^2.6.0", "typescript": "^5.0.4", "jest": "^29.4.0", diff --git a/packages/uncaught-exception-observer/.eslintrc.cjs b/packages/uncaught-exception-observer/.eslintrc.cjs deleted file mode 100644 index f4088a566..000000000 --- a/packages/uncaught-exception-observer/.eslintrc.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('happy-dom/.eslintrc.cjs'); diff --git a/packages/uncaught-exception-observer/package.json b/packages/uncaught-exception-observer/package.json index b52a99e00..fab419450 100644 --- a/packages/uncaught-exception-observer/package.json +++ b/packages/uncaught-exception-observer/package.json @@ -69,8 +69,6 @@ "compile": "tsc && tsc --moduleResolution Node --module CommonJS --outDir cjs && npm run change-cjs-file-extension", "change-cjs-file-extension": "node ../happy-dom/bin/change-file-extension.cjs --dir=./cjs --fromExt=.js --toExt=.cjs", "watch": "npm run compile && tsc -w --preserveWatchOutput", - "lint": "eslint --ignore-path .gitignore --max-warnings 0 .", - "lint:fix": "eslint --ignore-path .gitignore --max-warnings 0 --fix .", "test": "tsc --project ./test && node ./tmp/UncaughtExceptionObserver.test.js", "test:debug": "tsc --project ./test && node --inspect-brk ./tmp/UncaughtExceptionObserver.test.js" }, @@ -81,14 +79,6 @@ "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "@types/node": "^16.11.7", - "eslint": "^8.11.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-filenames": "^1.3.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^38.0.6", - "eslint-plugin-json": "^3.1.0", - "eslint-plugin-turbo": "^0.0.7", "prettier": "^2.6.0", "typescript": "^5.0.4", "happy-dom": "^0.0.0" diff --git a/turbo.json b/turbo.json index a085097f4..47e8ba1b1 100644 --- a/turbo.json +++ b/turbo.json @@ -35,12 +35,6 @@ "dependsOn": ["happy-dom#compile", "uncaught-exception-observer#compile"], "outputs": ["tmp/**"] }, - "lint": { - "outputs": [] - }, - "lint:fix": { - "outputs": [] - }, "test": { "inputs": ["test/**"] } From 531d18b8fb52a7995be0d98dd10f68b53ef676db Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 12 Oct 2023 16:51:48 +0200 Subject: [PATCH 06/63] #466@trivial: Continues on implementation. --- packages/happy-dom/src/browser/Browser.ts | 16 +++++++- .../happy-dom/src/browser/BrowserContext.ts | 11 ++++++ .../happy-dom/src/browser/BrowserFrame.ts | 16 +++++--- .../happy-dom/src/dom-parser/DOMParser.ts | 12 +----- packages/happy-dom/src/fetch/Request.ts | 7 +--- packages/happy-dom/src/fetch/Response.ts | 5 +-- packages/happy-dom/src/file/FileReader.ts | 14 ++----- .../happy-dom/src/nodes/document/Document.ts | 15 ++++++-- packages/happy-dom/src/range/Range.ts | 6 +-- packages/happy-dom/src/window/Window.ts | 37 ++++++++++--------- .../src/xml-http-request/XMLHttpRequest.ts | 13 +------ 11 files changed, 79 insertions(+), 73 deletions(-) diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index 5bd6e95c2..5e5c42e15 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -2,13 +2,14 @@ import IBrowserSettings from './IBrowserSettings.js'; import BrowserContext from './BrowserContext.js'; import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; import BrowserSettingsFactory from './BrowserSettingsFactory.js'; +import BrowserPage from './BrowserPage.js'; /** * Browser context. */ export default class Browser { - public contexts: BrowserContext[]; - public defaultBrowserContext: BrowserContext; + public defaultContext: BrowserContext | null = new BrowserContext(this); + public contexts: BrowserContext[] = [this.defaultContext]; public settings: IBrowserSettings; /** @@ -28,6 +29,8 @@ export default class Browser { */ public async close(): Promise { await Promise.all(this.contexts.map((context) => context.close())); + this.contexts = []; + this.defaultContext = null; } /** @@ -47,4 +50,13 @@ export default class Browser { public async abort(): Promise { await Promise.all(this.contexts.map((page) => page.abort())); } + + /** + * Creates a new page. + * + * @returns Page. + */ + public newPage(): BrowserPage { + return this.defaultContext.newPage(); + } } diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index a80545932..6f757bc32 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -43,4 +43,15 @@ export default class BrowserContext { public async abort(): Promise { await Promise.all(this.pages.map((page) => page.abort())); } + + /** + * Creates a new page. + * + * @returns Page. + */ + public newPage(): BrowserPage { + const page = new BrowserPage(this); + this.pages.push(page); + return page; + } } diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 50bf0836f..a74f92e81 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -10,15 +10,20 @@ export default class BrowserFrame { public detached = false; public page: BrowserPage | null = null; public window: IWindow | null = null; - private _asyncTaskManager = new AsyncTaskManager(); + public _asyncTaskManager = new AsyncTaskManager(); /** * Constructor. * + * @param options * @param page Page. + * @param [window] Window. + * @param options.page + * @param options.window */ - constructor(page: BrowserPage) { - this.page = page; + constructor(options: { page?: BrowserPage; window: IWindow }) { + this.page = options.page ?? null; + this.window = options.window; } /** @@ -44,7 +49,7 @@ export default class BrowserFrame { */ public async abort(): Promise { await Promise.all(this.childFrames.map((frame) => frame.abort())); - this._asyncTaskManager.cancelAll(); + await this._asyncTaskManager.cancelAll(); } /** @@ -64,7 +69,8 @@ export default class BrowserFrame { * @param url URL. */ public async goto(url: string): Promise { - await this.abort(); + await Promise.all(this.childFrames.map((frame) => frame.close())); + this._asyncTaskManager.cancelAll(); this.window.location.href = url; diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index a85c1d407..2333d7d6d 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -16,16 +16,8 @@ import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; * https://developer.mozilla.org/en-US/docs/Web/API/DOMParser. */ export default class DOMParser { - // Owner document is set by a sub-class in the Window constructor - public static _ownerDocument: IDocument = null; - public readonly _ownerDocument: IDocument = null; - - /** - * Constructor. - */ - constructor() { - this._ownerDocument = (this.constructor)._ownerDocument; - } + // Will be populated by a sub-class in Window. + public readonly _ownerDocument: IDocument; /** * Parses HTML and returns a root element. diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts index fbefbe0b2..3ac8a3f29 100644 --- a/packages/happy-dom/src/fetch/Request.ts +++ b/packages/happy-dom/src/fetch/Request.ts @@ -30,9 +30,8 @@ import MultipartFormDataParser from './multipart/MultipartFormDataParser.js'; * @see https://fetch.spec.whatwg.org/#request-class */ export default class Request implements IRequest { - // Owner document is set by a sub-class in the Window constructor - public static _ownerDocument: IDocument = null; - public readonly _ownerDocument: IDocument = null; + // Will be populated by a sub-class in Window. + public readonly _ownerDocument: IDocument; // Public properties public readonly method: string; @@ -58,8 +57,6 @@ export default class Request implements IRequest { * @param [init] Init. */ constructor(input: IRequestInfo, init?: IRequestInit) { - this._ownerDocument = (this.constructor)._ownerDocument; - if (!input) { throw new TypeError(`Failed to contruct 'Request': 1 argument required, only 0 present.`); } diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts index 72b19d2ae..59f374819 100644 --- a/packages/happy-dom/src/fetch/Response.ts +++ b/packages/happy-dom/src/fetch/Response.ts @@ -27,8 +27,7 @@ const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; * @see https://developer.mozilla.org/en-US/docs/Web/API/Response/Response */ export default class Response implements IResponse { - // Owner document is set by a sub-class in the Window constructor - public static _ownerDocument: IDocument = null; + // Will be populated by a sub-class in Window. public readonly _ownerDocument: IDocument = null; // Public properties @@ -51,8 +50,6 @@ export default class Response implements IResponse { * @param [init] Init. */ constructor(body?: IResponseBody, init?: IResponseInit) { - this._ownerDocument = (this.constructor)._ownerDocument; - this.status = init?.status !== undefined ? init.status : 200; this.statusText = init?.statusText || ''; this.ok = this.status >= 200 && this.status < 300; diff --git a/packages/happy-dom/src/file/FileReader.ts b/packages/happy-dom/src/file/FileReader.ts index 3eaa32cb5..ca348ad99 100644 --- a/packages/happy-dom/src/file/FileReader.ts +++ b/packages/happy-dom/src/file/FileReader.ts @@ -18,8 +18,9 @@ import FileReaderEventTypeEnum from './FileReaderEventTypeEnum.js'; * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/file-api/FileReader-impl.js (MIT licensed). */ export default class FileReader extends EventTarget { - // Owner document is set by a sub-class in the Window constructor - public static _ownerDocument: IDocument = null; + // Will be populated by a sub-class in Window. + public readonly _ownerDocument: IDocument; + public readonly error: Error = null; public readonly result: Buffer | ArrayBuffer | string = null; public readonly readyState: number = FileReaderReadyStateEnum.empty; @@ -29,19 +30,10 @@ export default class FileReader extends EventTarget { public readonly onloadstart: (event: ProgressEvent) => void = null; public readonly onloadend: (event: ProgressEvent) => void = null; public readonly onprogress: (event: ProgressEvent) => void = null; - public readonly _ownerDocument: IDocument = null; private _isTerminated = false; private _loadTimeout: NodeJS.Timeout = null; private _parseTimeout: NodeJS.Timeout = null; - /** - * Constructor. - */ - constructor() { - super(); - this._ownerDocument = (this.constructor)._ownerDocument; - } - /** * Reads as ArrayBuffer. * diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 1000dccff..5548245f5 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -839,6 +839,8 @@ export default class Document extends Node implements IDocument { element._isValue = String(options.is); } + elementClass._ownerDocument = null; + return element; } @@ -852,7 +854,9 @@ export default class Document extends Node implements IDocument { */ public createTextNode(data?: string): IText { Text._ownerDocument = this; - return new Text(data); + const text = new Text(data); + Text._ownerDocument = null; + return text; } /** @@ -863,7 +867,9 @@ export default class Document extends Node implements IDocument { */ public createComment(data?: string): IComment { Comment._ownerDocument = this; - return new Comment(data); + const comment = new Comment(data); + Comment._ownerDocument = null; + return comment; } /** @@ -873,7 +879,9 @@ export default class Document extends Node implements IDocument { */ public createDocumentFragment(): IDocumentFragment { DocumentFragment._ownerDocument = this; - return new DocumentFragment(); + const fragment = new DocumentFragment(); + DocumentFragment._ownerDocument = null; + return fragment; } /** @@ -1034,6 +1042,7 @@ export default class Document extends Node implements IDocument { ProcessingInstruction._ownerDocument = this; const processingInstruction = new ProcessingInstruction(data); processingInstruction.target = target; + ProcessingInstruction._ownerDocument = null; return processingInstruction; } } diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts index 9bdb9ddde..4ca0657d2 100644 --- a/packages/happy-dom/src/range/Range.ts +++ b/packages/happy-dom/src/range/Range.ts @@ -26,8 +26,8 @@ import IRangeBoundaryPoint from './IRangeBoundaryPoint.js'; * https://developer.mozilla.org/en-US/docs/Web/API/Range. */ export default class Range { - // Owner document is set by a sub-class in the Window constructor - public static _ownerDocument: IDocument = null; + // Will be populated by a sub-class in Window. + public readonly _ownerDocument: IDocument; public static readonly END_TO_END: number = RangeHowEnum.endToEnd; public static readonly END_TO_START: number = RangeHowEnum.endToStart; public static readonly START_TO_END: number = RangeHowEnum.startToEnd; @@ -36,7 +36,6 @@ export default class Range { public readonly END_TO_START: number = RangeHowEnum.endToStart; public readonly START_TO_END: number = RangeHowEnum.startToEnd; public readonly START_TO_START: number = RangeHowEnum.startToStart; - public readonly _ownerDocument: IDocument = null; public _start: IRangeBoundaryPoint = null; public _end: IRangeBoundaryPoint = null; @@ -44,7 +43,6 @@ export default class Range { * Constructor. */ constructor() { - this._ownerDocument = (this.constructor)._ownerDocument; this._start = { node: this._ownerDocument, offset: 0 }; this._end = { node: this._ownerDocument, offset: 0 }; } diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index da21703e7..4a3f58f85 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -146,6 +146,7 @@ import ClipboardItem from '../clipboard/ClipboardItem.js'; import ClipboardEvent from '../event/events/ClipboardEvent.js'; import DetachedWindowAPI from './DetachedWindowAPI.js'; import IVirtualConsolePrinter from '../console/types/IVirtualConsolePrinter.js'; +import BrowserFrame from '../browser/BrowserFrame.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -495,6 +496,7 @@ export default class Window extends EventTarget implements IWindow { private _setInterval: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; private _clearInterval: (id: NodeJS.Timeout) => void; private _queueMicrotask: (callback: Function) => void; + #browserFrame: BrowserFrame; /** * Constructor. @@ -507,6 +509,7 @@ export default class Window extends EventTarget implements IWindow { * @param [options.url] URL. * @param [options.console] Console. * @param [options.settings] Settings. + * @param [options.browserFrame] Browser frame. */ constructor(options?: { width?: number; @@ -516,6 +519,7 @@ export default class Window extends EventTarget implements IWindow { url?: string; console?: Console; settings?: IBrowserSettings; + browserFrame?: BrowserFrame; }) { super(); @@ -527,6 +531,12 @@ export default class Window extends EventTarget implements IWindow { this.sessionStorage = new Storage(); this.localStorage = new Storage(); + if (options?.browserFrame) { + this.#browserFrame = options.browserFrame; + } else { + this.#browserFrame = new BrowserFrame(); + } + if (options) { if (options.width !== undefined) { this.innerWidth = options.width; @@ -596,42 +606,33 @@ export default class Window extends EventTarget implements IWindow { // We need to set the correct owner document when the class is constructed. // To achieve this we will extend the original implementation with a class that sets the owner document. - ResponseImplementation._ownerDocument = document; - RequestImplementation._ownerDocument = document; - ImageImplementation._ownerDocument = document; - DocumentFragmentImplementation._ownerDocument = document; - FileReaderImplementation._ownerDocument = document; - DOMParserImplementation._ownerDocument = document; - RangeImplementation._ownerDocument = document; - XMLHttpRequestImplementation._ownerDocument = document; - /* eslint-disable jsdoc/require-jsdoc */ class Response extends ResponseImplementation { - public static _ownerDocument: IDocument = document; + public readonly _ownerDocument: IDocument = document; } class Request extends RequestImplementation { - public static _ownerDocument: IDocument = document; + public readonly _ownerDocument: IDocument = document; } class Image extends ImageImplementation { - public static _ownerDocument: IDocument = document; + public readonly ownerDocument: IDocument = document; } class DocumentFragment extends DocumentFragmentImplementation { - public static _ownerDocument: IDocument = document; + public readonly ownerDocument: IDocument = document; } class FileReader extends FileReaderImplementation { - public static _ownerDocument: IDocument = document; + public readonly _ownerDocument: IDocument = document; } class DOMParser extends DOMParserImplementation { - public static _ownerDocument: IDocument = document; + public readonly _ownerDocument: IDocument = document; } class XMLHttpRequest extends XMLHttpRequestImplementation { - public static _ownerDocument: IDocument = document; + public readonly ownerDocument: IDocument = document; } class Range extends RangeImplementation { - public static _ownerDocument: IDocument = document; + public readonly ownerDocument: IDocument = document; } class Audio extends AudioImplementation { - public static _ownerDocument: IDocument = document; + public readonly ownerDocument: IDocument = document; } /* eslint-enable jsdoc/require-jsdoc */ diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index e617b8fad..1694963fd 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -59,8 +59,8 @@ const CONTENT_TYPE_ENCODING_REGEXP = /charset=([^;]*)/i; * https://github.com/mjwwit/node-XMLHttpRequest/blob/master/lib/XMLHttpRequest.js */ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { - // Owner document is set by a sub-class in the Window constructor - public static _ownerDocument: IDocument = null; + // Will be populated by a sub-class in Window. + public readonly _ownerDocument: IDocument; // Constants public static UNSENT = XMLHttpRequestReadyStateEnum.unsent; @@ -73,7 +73,6 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { public upload: XMLHttpRequestUpload = new XMLHttpRequestUpload(); // Private properties - private readonly _ownerDocument: IDocument = null; private _state: { incommingMessage: | HTTP.IncomingMessage @@ -124,14 +123,6 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { password: null }; - /** - * Constructor. - */ - constructor() { - super(); - this._ownerDocument = XMLHttpRequest._ownerDocument; - } - /** * Returns the status. * From fc8497843ec00f08ea8b1fa710070212a23c341b Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 18 Oct 2023 10:05:48 +0200 Subject: [PATCH 07/63] #466@trivial: Continues on implementation. --- .../async-task-manager/AsyncTaskManager.ts | 17 +- packages/happy-dom/src/browser/Browser.ts | 17 +- .../happy-dom/src/browser/BrowserFrame.ts | 38 +- packages/happy-dom/src/browser/BrowserPage.ts | 6 +- .../src/browser/BrowserSettingsFactory.ts | 23 + .../src/browser/DetachedBrowserFrame.ts | 86 ++++ .../happy-dom/src/browser/IBrowserFrame.ts | 39 ++ .../src/browser/IReadOnlyBrowserSettings.ts | 20 + packages/happy-dom/src/fetch/Fetch.ts | 33 +- packages/happy-dom/src/fetch/Request.ts | 34 +- packages/happy-dom/src/fetch/Response.ts | 38 +- packages/happy-dom/src/index.ts | 2 - .../happy-dom/src/nodes/element/Element.ts | 1 + .../HTMLLinkElementUtility.ts | 13 +- .../HTMLScriptElementUtility.ts | 13 +- .../src/window/BrowserContextLoader.ts | 2 +- .../happy-dom/src/window/DetachedWindowAPI.ts | 106 ---- .../happy-dom/src/window/HappyDOMWindowAPI.ts | 147 ++++++ packages/happy-dom/src/window/IWindow.ts | 19 +- packages/happy-dom/src/window/Window.ts | 118 +++-- .../src/xml-http-request/XMLHttpRequest.ts | 455 ++++++++++-------- 21 files changed, 746 insertions(+), 481 deletions(-) create mode 100644 packages/happy-dom/src/browser/DetachedBrowserFrame.ts create mode 100644 packages/happy-dom/src/browser/IBrowserFrame.ts create mode 100644 packages/happy-dom/src/browser/IReadOnlyBrowserSettings.ts delete mode 100644 packages/happy-dom/src/window/DetachedWindowAPI.ts create mode 100644 packages/happy-dom/src/window/HappyDOMWindowAPI.ts diff --git a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts index ae906b455..10b594edc 100644 --- a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts +++ b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts @@ -3,7 +3,7 @@ */ export default class AsyncTaskManager { private static taskID = 0; - private runningTasks: { [k: string]: () => void } = {}; + private runningTasks: { [k: string]: (destroy: boolean) => void } = {}; private runningTaskCount = 0; private runningTimers: NodeJS.Timeout[] = []; private runningImmediates: NodeJS.Immediate[] = []; @@ -23,9 +23,11 @@ export default class AsyncTaskManager { } /** - * Cancels all tasks. + * Aborts all tasks. + * + * @param destroy Destroy. */ - public cancelAll(): void { + public abortAll(destroy = false): void { const runningTimers = this.runningTimers; const runningImmediates = this.runningImmediates; const runningTasks = this.runningTasks; @@ -49,12 +51,19 @@ export default class AsyncTaskManager { } for (const key of Object.keys(runningTasks)) { - runningTasks[key](); + runningTasks[key](destroy); } this.resolveWhenComplete(); } + /** + * Destroys the manager. + */ + public destroy(): void { + this.abortAll(); + } + /** * Starts a timer. * diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index 5e5c42e15..552015a10 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -8,17 +8,22 @@ import BrowserPage from './BrowserPage.js'; * Browser context. */ export default class Browser { - public defaultContext: BrowserContext | null = new BrowserContext(this); - public contexts: BrowserContext[] = [this.defaultContext]; - public settings: IBrowserSettings; + public readonly defaultContext: BrowserContext; + public readonly contexts: BrowserContext[]; + public readonly settings: IBrowserSettings; + public readonly console: Console | null; /** * Constructor. * * @param [options] Options. * @param [options.settings] Browser settings. + * @param [options.console] Console. */ - constructor(options?: { settings?: IOptionalBrowserSettings }) { + constructor(options?: { settings?: IOptionalBrowserSettings; console?: Console }) { + this.console = options?.console || null; + this.defaultContext = new BrowserContext(this); + this.contexts = [this.defaultContext]; this.settings = BrowserSettingsFactory.getSettings(options?.settings); } @@ -29,8 +34,8 @@ export default class Browser { */ public async close(): Promise { await Promise.all(this.contexts.map((context) => context.close())); - this.contexts = []; - this.defaultContext = null; + (this.contexts) = []; + (this.defaultContext) = null; } /** diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index a74f92e81..03c579ce7 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -1,29 +1,30 @@ import IWindow from '../window/IWindow.js'; import BrowserPage from './BrowserPage.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import IBrowserFrame from './IBrowserFrame.js'; +import Window from '../window/Window.js'; /** * Browser frame. */ -export default class BrowserFrame { - public childFrames: BrowserFrame[] = []; +export default class BrowserFrame implements IBrowserFrame { + public readonly childFrames: BrowserFrame[] = []; public detached = false; - public page: BrowserPage | null = null; - public window: IWindow | null = null; + public readonly page: BrowserPage; + public readonly window: IWindow; public _asyncTaskManager = new AsyncTaskManager(); /** * Constructor. * - * @param options * @param page Page. - * @param [window] Window. - * @param options.page - * @param options.window */ - constructor(options: { page?: BrowserPage; window: IWindow }) { - this.page = options.page ?? null; - this.window = options.window; + constructor(page: BrowserPage) { + this.page = page; + this.window = new Window({ + browserFrame: this, + console: page.console + }); } /** @@ -49,7 +50,7 @@ export default class BrowserFrame { */ public async abort(): Promise { await Promise.all(this.childFrames.map((frame) => frame.abort())); - await this._asyncTaskManager.cancelAll(); + await this._asyncTaskManager.abortAll(); } /** @@ -57,10 +58,11 @@ export default class BrowserFrame { * * @returns Promise. */ - public async close(): Promise { - await this.abort(); - this.page = null; - this.window = null; + public async destroy(): Promise { + await Promise.all(this.childFrames.map((frame) => frame.destroy())); + await this._asyncTaskManager.destroy(); + (this.page) = null; + (this.window) = null; } /** @@ -69,8 +71,8 @@ export default class BrowserFrame { * @param url URL. */ public async goto(url: string): Promise { - await Promise.all(this.childFrames.map((frame) => frame.close())); - this._asyncTaskManager.cancelAll(); + await Promise.all(this.childFrames.map((frame) => frame.destroy())); + this._asyncTaskManager.abortAll(); this.window.location.href = url; diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index 289fd72dd..8b9a93e97 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -3,6 +3,7 @@ import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; import IBrowserPageViewport from './IBrowserPageViewport.js'; import BrowserFrame from './BrowserFrame.js'; import BrowserContext from './BrowserContext.js'; +import VirtualConsole from '../console/VirtualConsole.js'; /** * Browser page. @@ -11,6 +12,8 @@ export default class BrowserPage { public consolePrinter: VirtualConsolePrinter | null; public mainFrame: BrowserFrame | null = null; public context: BrowserContext; + public readonly console: Console; + public readonly virtualConsolePrinter = new VirtualConsolePrinter(); /** * Constructor. @@ -19,6 +22,7 @@ export default class BrowserPage { */ constructor(context: BrowserContext) { this.context = context; + this.console = context.browser.console ?? new VirtualConsole(this.virtualConsolePrinter); } /** @@ -41,7 +45,7 @@ export default class BrowserPage { * @returns Promise. */ public async close(): Promise { - await this.mainFrame.close(); + await this.mainFrame.destroy(); this.consolePrinter = null; this.mainFrame = null; this.context = null; diff --git a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts index b8a5e32b5..06e1e7abe 100644 --- a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts +++ b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts @@ -1,6 +1,7 @@ import IBrowserSettings from './IBrowserSettings.js'; import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; import DefaultBrowserSettings from './DefaultBrowserSettings.js'; +import IReadOnlyBrowserSettings from './IReadOnlyBrowserSettings.js'; /** * Browser settings utility. @@ -10,6 +11,7 @@ export default class BrowserSettingsFactory { * Returns browser settings. * * @param [settings] Browser settings. + * @param [freezeObject] "true" to freeze the object. * @returns Settings. */ public static getSettings(settings?: IOptionalBrowserSettings): IBrowserSettings { @@ -26,4 +28,25 @@ export default class BrowserSettingsFactory { } }; } + /** + * Returns readonly browser settings. + * + * @param [settings] Browser settings. + * @param [freezeObject] "true" to freeze the object. + * @returns Settings. + */ + public static getReadOnlySettings(settings?: IOptionalBrowserSettings): IReadOnlyBrowserSettings { + return Object.freeze({ + ...DefaultBrowserSettings, + ...settings, + navigator: Object.freeze({ + ...DefaultBrowserSettings.navigator, + ...settings.navigator + }), + device: Object.freeze({ + ...DefaultBrowserSettings.device, + ...settings.device + }) + }); + } } diff --git a/packages/happy-dom/src/browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/DetachedBrowserFrame.ts new file mode 100644 index 000000000..06d8e348d --- /dev/null +++ b/packages/happy-dom/src/browser/DetachedBrowserFrame.ts @@ -0,0 +1,86 @@ +import IWindow from '../window/IWindow.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import IBrowserFrame from './IBrowserFrame.js'; +import Window from '../window/Window.js'; +import IBrowserSettings from './IBrowserSettings.js'; +import { VirtualConsolePrinter } from '../index.js'; +import BrowserSettingsFactory from './BrowserSettingsFactory.js'; + +/** + * Browser frame. + */ +export default class DetachedBrowserFrame implements IBrowserFrame { + public readonly childFrames: DetachedBrowserFrame[] = []; + public detached = false; + public readonly window: IWindow; + public _asyncTaskManager = new AsyncTaskManager(); + public readonly virtualConsolePrinter = new VirtualConsolePrinter(); + public readonly settings: IBrowserSettings; + public readonly console: Console; + + /** + * Constructor. + * + * @param options Options. + * @param options.window Window. + * @param [options.settings] Browser settings. + */ + constructor(options: { window: Window; settings?: IBrowserSettings }) { + this.window = options.window; + this.settings = BrowserSettingsFactory.getSettings(options.settings); + } + + /** + * Returns the viewport. + */ + public get content(): string { + return this.window.document.documentElement.outerHTML; + } + + /** + * Returns a promise that is resolved when all async tasks are complete. + * + * @returns Promise. + */ + public async whenComplete(): Promise { + await this._asyncTaskManager.whenComplete(); + } + + /** + * Aborts all ongoing operations. + * + * @returns Promise. + */ + public async abort(): Promise { + await Promise.all(this.childFrames.map((frame) => frame.abort())); + await this._asyncTaskManager.abortAll(); + } + + /** + * Aborts all ongoing operations and destroys the frame. + * + * @returns Promise. + */ + public async destroy(): Promise { + await Promise.all(this.childFrames.map((frame) => frame.destroy())); + await this._asyncTaskManager.destroy(); + (this.window) = null; + } + + /** + * Go to a page. + * + * @param url URL. + */ + public async goto(url: string): Promise { + await Promise.all(this.childFrames.map((frame) => frame.destroy())); + this._asyncTaskManager.abortAll(); + + this.window.location.href = url; + + const response = await this.window.fetch(url); + const responseText = await response.text(); + + this.window.document.write(responseText); + } +} diff --git a/packages/happy-dom/src/browser/IBrowserFrame.ts b/packages/happy-dom/src/browser/IBrowserFrame.ts new file mode 100644 index 000000000..d5f138f8c --- /dev/null +++ b/packages/happy-dom/src/browser/IBrowserFrame.ts @@ -0,0 +1,39 @@ +import IWindow from '../window/IWindow.js'; + +/** + * Browser frame. + */ +export default interface IBrowserFrame { + readonly childFrames: IBrowserFrame[]; + detached: boolean; + readonly window: IWindow; + readonly content: string; + + /** + * Returns a promise that is resolved when all async tasks are complete. + * + * @returns Promise. + */ + whenComplete(): Promise; + + /** + * Aborts all ongoing operations. + * + * @returns Promise. + */ + abort(): Promise; + + /** + * Aborts all ongoing operations and destroys the frame. + * + * @returns Promise. + */ + destroy(): Promise; + + /** + * Go to a page. + * + * @param url URL. + */ + goto(url: string): Promise; +} diff --git a/packages/happy-dom/src/browser/IReadOnlyBrowserSettings.ts b/packages/happy-dom/src/browser/IReadOnlyBrowserSettings.ts new file mode 100644 index 000000000..0235fac19 --- /dev/null +++ b/packages/happy-dom/src/browser/IReadOnlyBrowserSettings.ts @@ -0,0 +1,20 @@ +/** + * Browser settings. + */ +export default interface IReadOnlyBrowserSettings { + readonly disableJavaScriptEvaluation: boolean; + readonly disableJavaScriptFileLoading: boolean; + readonly disableCSSFileLoading: boolean; + readonly disableIframePageLoading: boolean; + readonly disableWindowOpenPageLoading: boolean; + readonly disableComputedStyleRendering: boolean; + readonly disableErrorCapturing: boolean; + readonly enableFileSystemHttpRequests: boolean; + readonly navigator: { + readonly userAgent: string; + }; + readonly device: { + readonly prefersColorScheme: string; + readonly mediaType: string; + }; +} diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index 46e4aa841..6081be487 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -2,13 +2,11 @@ import IRequestInit from './types/IRequestInit.js'; import IDocument from '../nodes/document/IDocument.js'; import Document from '../nodes/document/Document.js'; import IResponse from './types/IResponse.js'; -import Request from './Request.js'; import IRequestInfo from './types/IRequestInfo.js'; import Headers from './Headers.js'; import FetchRequestReferrerUtility from './utilities/FetchRequestReferrerUtility.js'; import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; -import Response from './Response.js'; import HTTP, { IncomingMessage } from 'http'; import HTTPS from 'https'; import Zlib from 'zlib'; @@ -17,6 +15,9 @@ import { Socket } from 'net'; import Stream from 'stream'; import DataURIParser from './data-uri/DataURIParser.js'; import FetchCORSUtility from './utilities/FetchCORSUtility.js'; +import Request from './Request.js'; +import Response from './Response.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; const SUPPORTED_SCHEMAS = ['data:', 'http:', 'https:']; const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; @@ -43,6 +44,7 @@ export default class Fetch { private nodeRequest: HTTP.ClientRequest | null = null; private response: Response | null = null; private ownerDocument: IDocument; + private asyncTaskManager: AsyncTaskManager; private request: Request; private redirectCount = 0; @@ -51,6 +53,7 @@ export default class Fetch { * * @param options Options. * @param options.document + * @param options.asyncTaskManager Async task manager. * @param options.url URL. * @param [options.init] Init. * @param [options.ownerDocument] Owner document. @@ -59,6 +62,7 @@ export default class Fetch { */ constructor(options: { ownerDocument: IDocument; + asyncTaskManager: AsyncTaskManager; url: IRequestInfo; init?: IRequestInit; redirectCount?: number; @@ -67,9 +71,10 @@ export default class Fetch { const url = options.url; this.ownerDocument = options.ownerDocument; + this.asyncTaskManager = options.asyncTaskManager; this.request = typeof options.url === 'string' || options.url instanceof URL - ? new Request(options.url, options.init) + ? new options.ownerDocument.defaultView.Request(options.url, options.init) : url; if (options.contentType) { (this.request._contentType) = options.contentType; @@ -84,19 +89,18 @@ export default class Fetch { */ public send(): Promise { return new Promise((resolve, reject) => { - const taskManager = this.ownerDocument.defaultView.happyDOM.asyncTaskManager; - const taskID = taskManager.startTask(() => this.abort()); + const taskID = this.asyncTaskManager.startTask(() => this.abort()); if (this.resolve) { throw new Error('Fetch already sent.'); } this.resolve = (response: IResponse | Promise): void => { - taskManager.endTask(taskID); + this.asyncTaskManager.endTask(taskID); resolve(response); }; this.reject = (error: Error): void => { - taskManager.endTask(taskID); + this.asyncTaskManager.endTask(taskID); reject(error); }; @@ -105,7 +109,7 @@ export default class Fetch { if (this.request._url.protocol === 'data:') { const result = DataURIParser.parse(this.request.url); - this.response = new Response(result.buffer, { + this.response = new this.ownerDocument.defaultView.Response(result.buffer, { headers: { 'Content-Type': result.type } }); resolve(this.response); @@ -247,7 +251,7 @@ export default class Fetch { nodeResponse.statusCode === 204 || nodeResponse.statusCode === 304 ) { - this.response = new Response(body, responseOptions); + this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -269,7 +273,7 @@ export default class Fetch { // Ignore error as it is forwarded to the response body. } }); - this.response = new Response(body, responseOptions); + this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -301,7 +305,7 @@ export default class Fetch { }); } - this.response = new Response(body, responseOptions); + this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -309,7 +313,7 @@ export default class Fetch { raw.on('end', () => { // Some old IIS servers return zero-length OK deflate responses, so 'data' is never emitted. if (!this.response) { - this.response = new Response(body, responseOptions); + this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -325,7 +329,7 @@ export default class Fetch { // Ignore error as it is forwarded to the response body. } }); - this.response = new Response(body, responseOptions); + this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -333,7 +337,7 @@ export default class Fetch { } // Otherwise, use response as is - this.response = new Response(body, responseOptions); + this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -471,6 +475,7 @@ export default class Fetch { const fetch = new (this.constructor)({ ownerDocument: this.ownerDocument, + asyncTaskManager: this.asyncTaskManager, url: locationURL, init: requestInit, redirectCount: this.redirectCount + 1, diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts index 3ac8a3f29..2c7d568ff 100644 --- a/packages/happy-dom/src/fetch/Request.ts +++ b/packages/happy-dom/src/fetch/Request.ts @@ -20,6 +20,7 @@ import FetchRequestHeaderUtility from './utilities/FetchRequestHeaderUtility.js' import IRequestCredentials from './types/IRequestCredentials.js'; import FormData from '../form-data/FormData.js'; import MultipartFormDataParser from './multipart/MultipartFormDataParser.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; /** * Fetch request. @@ -30,8 +31,9 @@ import MultipartFormDataParser from './multipart/MultipartFormDataParser.js'; * @see https://fetch.spec.whatwg.org/#request-class */ export default class Request implements IRequest { - // Will be populated by a sub-class in Window. - public readonly _ownerDocument: IDocument; + // Needs to be injected by a sub-class. + protected readonly _asyncTaskManager: AsyncTaskManager; + protected readonly _ownerDocument: IDocument; // Public properties public readonly method: string; @@ -182,18 +184,17 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; - const taskID = taskManager.startTask(() => this.signal._abort()); + const taskID = this._asyncTaskManager.startTask(() => this.signal._abort()); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); throw error; } - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); } @@ -225,18 +226,17 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; - const taskID = taskManager.startTask(() => this.signal._abort()); + const taskID = this._asyncTaskManager.startTask(() => this.signal._abort()); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); throw error; } - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); return buffer; } @@ -256,18 +256,17 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; - const taskID = taskManager.startTask(() => this.signal._abort()); + const taskID = this._asyncTaskManager.startTask(() => this.signal._abort()); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); throw error; } - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); return new TextDecoder().decode(buffer); } @@ -297,19 +296,18 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; - const taskID = taskManager.startTask(() => this.signal._abort()); + const taskID = this._asyncTaskManager.startTask(() => this.signal._abort()); let formData: FormData; try { const type = this._contentType; formData = await MultipartFormDataParser.streamToFormData(this.body, type); } catch (error) { - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); throw error; } - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); return formData; } diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts index 59f374819..7d9ac9328 100644 --- a/packages/happy-dom/src/fetch/Response.ts +++ b/packages/happy-dom/src/fetch/Response.ts @@ -1,6 +1,5 @@ import IResponse from './types/IResponse.js'; import IBlob from '../file/IBlob.js'; -import IDocument from '../nodes/document/IDocument.js'; import IResponseInit from './types/IResponseInit.js'; import IResponseBody from './types/IResponseBody.js'; import Headers from './Headers.js'; @@ -15,6 +14,7 @@ import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import { TextDecoder } from 'util'; import MultipartFormDataParser from './multipart/MultipartFormDataParser.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; @@ -27,8 +27,8 @@ const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; * @see https://developer.mozilla.org/en-US/docs/Web/API/Response/Response */ export default class Response implements IResponse { - // Will be populated by a sub-class in Window. - public readonly _ownerDocument: IDocument = null; + // Needs to be injected by a sub-class. + protected readonly _asyncTaskManager: AsyncTaskManager; // Public properties public readonly body: Stream.Readable | null = null; @@ -89,18 +89,17 @@ export default class Response implements IResponse { (this.bodyUsed) = true; - const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; - const taskID = taskManager.startTask(); + const taskID = this._asyncTaskManager.startTask(); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); throw error; } - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); } @@ -132,18 +131,17 @@ export default class Response implements IResponse { (this.bodyUsed) = true; - const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; - const taskID = taskManager.startTask(); + const taskID = this._asyncTaskManager.startTask(); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); throw error; } - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); return buffer; } @@ -163,18 +161,17 @@ export default class Response implements IResponse { (this.bodyUsed) = true; - const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; - const taskID = taskManager.startTask(); + const taskID = this._asyncTaskManager.startTask(); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); throw error; } - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); return new TextDecoder().decode(buffer); } @@ -196,8 +193,7 @@ export default class Response implements IResponse { */ public async formData(): Promise { const contentType = this.headers.get('content-type'); - const taskManager = this._ownerDocument.defaultView.happyDOM.asyncTaskManager; - const taskID = taskManager.startTask(); + const taskID = this._asyncTaskManager.startTask(); if (contentType.startsWith('application/x-www-form-urlencoded')) { const formData = new FormData(); @@ -206,7 +202,7 @@ export default class Response implements IResponse { try { text = await this.text(); } catch (error) { - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); throw error; } @@ -216,7 +212,7 @@ export default class Response implements IResponse { formData.append(name, value); } - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); return formData; } @@ -226,11 +222,11 @@ export default class Response implements IResponse { try { formData = await MultipartFormDataParser.streamToFormData(this.body, contentType); } catch (error) { - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); throw error; } - taskManager.endTask(taskID); + this._asyncTaskManager.endTask(taskID); return formData; } diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index a7615c9d1..c926ed59f 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -17,7 +17,6 @@ import DOMException from './exception/DOMException.js'; import History from './history/History.js'; import CSSStyleDeclaration from './css/declaration/CSSStyleDeclaration.js'; import Screen from './screen/Screen.js'; -import AsyncTaskManager from './async-task-manager/AsyncTaskManager.js'; import NodeFilter from './tree-walker/NodeFilter.js'; import Event from './event/Event.js'; import EventTarget from './event/EventTarget.js'; @@ -179,7 +178,6 @@ export { History, CSSStyleDeclaration, Screen, - AsyncTaskManager, NodeFilter, Event, EventTarget, diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 35d47852a..d887938c2 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -927,6 +927,7 @@ export default class Element extends Node implements IElement { const returnValue = super.dispatchEvent(event); if ( + !this.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptEvaluation && (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && !event._immediatePropagationStopped diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts index 687635ab7..c726a179a 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts @@ -24,14 +24,13 @@ export default class HTMLLinkElementUtility { if (href !== null && rel && rel.toLowerCase() === 'stylesheet' && element.isConnected) { if (element.ownerDocument.defaultView.happyDOM.settings.disableCSSFileLoading) { - const error = new DOMException( - `Failed to load external stylesheet "${href}". CSS file loading is disabled.`, - DOMExceptionNameEnum.notSupportedError + WindowErrorUtility.dispatchError( + element, + new DOMException( + `Failed to load external stylesheet "${href}". CSS file loading is disabled.`, + DOMExceptionNameEnum.notSupportedError + ) ); - WindowErrorUtility.dispatchError(element, error); - if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { - throw error; - } return; } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts index 93ea3d3d9..2d1a5551d 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts @@ -25,14 +25,13 @@ export default class HTMLScriptElementUtility { element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptFileLoading || element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptEvaluation ) { - const error = new DOMException( - `Failed to load external script "${src}". JavaScript file loading is disabled.`, - DOMExceptionNameEnum.notSupportedError + WindowErrorUtility.dispatchError( + element, + new DOMException( + `Failed to load external script "${src}". JavaScript file loading is disabled.`, + DOMExceptionNameEnum.notSupportedError + ) ); - WindowErrorUtility.dispatchError(element, error); - if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { - throw error; - } return; } diff --git a/packages/happy-dom/src/window/BrowserContextLoader.ts b/packages/happy-dom/src/window/BrowserContextLoader.ts index 6c49aab76..20c9ccf6f 100644 --- a/packages/happy-dom/src/window/BrowserContextLoader.ts +++ b/packages/happy-dom/src/window/BrowserContextLoader.ts @@ -6,7 +6,7 @@ import WindowErrorUtility from './WindowErrorUtility.js'; import ICrossOriginWindow from './ICrossOriginWindow.js'; import IHTMLElement from '../nodes/html-element/IHTMLElement.js'; import Window from './Window.js'; -import DetachedWindowAPI from './DetachedWindowAPI.js'; +import DetachedWindowAPI from './HappyDOMWindowAPI.js'; /** * Browser context. diff --git a/packages/happy-dom/src/window/DetachedWindowAPI.ts b/packages/happy-dom/src/window/DetachedWindowAPI.ts deleted file mode 100644 index 14e484902..000000000 --- a/packages/happy-dom/src/window/DetachedWindowAPI.ts +++ /dev/null @@ -1,106 +0,0 @@ -import BrowserSettingsFactory from '../browser/BrowserSettingsFactory.js'; -import IBrowserSettings from '../browser/IBrowserSettings.js'; -import IOptionalBrowserSettings from '../browser/IOptionalBrowserSettings.js'; -import IVirtualConsolePrinter from '../console/types/IVirtualConsolePrinter.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; -import IWindow from './IWindow.js'; -import Event from '../event/Event.js'; - -/** - * API for detached windows to be able to access features of the owner window. - */ -export default class DetachedWindowAPI { - public readonly asyncTaskManager = new AsyncTaskManager(); - public readonly settings: IBrowserSettings; - public readonly virtualConsolePrinter: IVirtualConsolePrinter | null; - #ownerWindow: IWindow; - - /** - * Constructor. - * - * @param options Options. - * @param options.ownerWindow Owner window. - * @param [options.settings] Browser settings. - * @param [options.virtualConsolePrinter] Virtual console printer. - */ - constructor(options: { - ownerWindow: IWindow; - settings?: IOptionalBrowserSettings; - virtualConsolePrinter?: IVirtualConsolePrinter; - }) { - this.#ownerWindow = options.ownerWindow; - this.settings = BrowserSettingsFactory.getSettings(options.settings); - this.virtualConsolePrinter = options.virtualConsolePrinter || null; - } - - /** - * Waits for all async tasks to complete. - * - * @returns Promise. - */ - public async whenAsyncComplete(): Promise { - return await this.asyncTaskManager.whenComplete(); - } - - /** - * Aborts all async tasks. - */ - public cancelAsync(): void { - this.asyncTaskManager.cancelAll(); - } - - /** - * Sets the URL. - * - * @param url URL. - */ - public setURL(url: string): void { - this.#ownerWindow.location.href = url; - } - - /** - * Sets the window size. - * - * @param options Options. - * @param options.width Width. - * @param options.height Height. - */ - public setWindowSize(options: { width?: number; height?: number }): void { - if ( - (options.width !== undefined && this.#ownerWindow.innerWidth !== options.width) || - (options.height !== undefined && this.#ownerWindow.innerHeight !== options.height) - ) { - if (options.width !== undefined && this.#ownerWindow.innerWidth !== options.width) { - (this.#ownerWindow.innerWidth) = options.width; - (this.#ownerWindow.outerWidth) = options.width; - } - - if (options.height !== undefined && this.#ownerWindow.innerHeight !== options.height) { - (this.#ownerWindow.innerHeight) = options.height; - (this.#ownerWindow.outerHeight) = options.height; - } - - this.#ownerWindow.dispatchEvent(new Event('resize')); - } - } - - /** - * Sets the window width. - * - * @deprecated Use setWindowSize() instead. - * @param width Width. - */ - public setInnerWidth(width: number): void { - this.setWindowSize({ width }); - } - - /** - * Sets the window height. - * - * @deprecated Use setWindowSize() instead. - * @param height Height. - */ - public setInnerHeight(height: number): void { - this.setWindowSize({ height }); - } -} diff --git a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts new file mode 100644 index 000000000..29edaf2a5 --- /dev/null +++ b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts @@ -0,0 +1,147 @@ +import IBrowserSettings from '../browser/IBrowserSettings.js'; +import IWindow from './IWindow.js'; +import Event from '../event/Event.js'; +import BrowserFrame from '../browser/BrowserFrame.js'; +import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; +import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import BrowserSettingsFactory from '../browser/BrowserSettingsFactory.js'; +import IReadOnlyBrowserSettings from '../browser/IReadOnlyBrowserSettings.js'; + +/** + * API for detached windows to be able to access features of the owner window. + */ +export default class HappyDOMWindowAPI { + #window: IWindow; + #browserFrame?: BrowserFrame | DetachedBrowserFrame; + #settings: IBrowserSettings | null = null; + + /** + * Constructor. + * + * @param options Options. + * @param options.window Owner window. + * @param options.browserFrame Browser frame. + */ + constructor(options: { window: IWindow; browserFrame: BrowserFrame | DetachedBrowserFrame }) { + this.#window = options.window; + this.#browserFrame = options.browserFrame; + } + + /** + * Returns settings. + * + * @returns Settings. + */ + public get settings(): IReadOnlyBrowserSettings { + if (!this.#settings) { + this.#settings = BrowserSettingsFactory.getReadOnlySettings( + this.#browserFrame instanceof DetachedBrowserFrame + ? this.#browserFrame.settings + : this.#browserFrame.page.context.browser.settings + ); + } + return this.#settings; + } + + /** + * Returns virtual console printer. + * + * @returns Virtual console printer. + */ + public get virtualConsolePrinter(): VirtualConsolePrinter { + if (this.#browserFrame instanceof DetachedBrowserFrame) { + return this.#browserFrame.virtualConsolePrinter; + } + return this.#browserFrame.page.virtualConsolePrinter; + } + + /** + * Waits for all async tasks to complete. + * + * @deprecated Use whenComplete() instead. + * @returns Promise. + */ + public async whenAsyncComplete(): Promise { + return await this.whenComplete(); + } + + /** + * Waits for all async tasks to complete. + * + * @returns Promise. + */ + public async whenComplete(): Promise { + return await this.#browserFrame.whenComplete(); + } + + /** + * Aborts all async tasks. + * + * @deprecated Use abort() instead. + */ + public async cancelAsync(): Promise { + await this.abort(); + } + + /** + * Aborts all async tasks. + */ + public async abort(): Promise { + await this.#browserFrame.abort(); + } + + /** + * Sets the URL. + * + * @param url URL. + */ + public setURL(url: string): void { + this.#window.location.href = url; + } + + /** + * Sets the window size. + * + * @param options Options. + * @param options.width Width. + * @param options.height Height. + */ + public setWindowSize(options: { width?: number; height?: number }): void { + if ( + (options.width !== undefined && this.#window.innerWidth !== options.width) || + (options.height !== undefined && this.#window.innerHeight !== options.height) + ) { + if (options.width !== undefined && this.#window.innerWidth !== options.width) { + (this.#window.innerWidth) = options.width; + (this.#window.outerWidth) = options.width; + } + + if (options.height !== undefined && this.#window.innerHeight !== options.height) { + (this.#window.innerHeight) = options.height; + (this.#window.outerHeight) = options.height; + } + + this.#window.dispatchEvent(new Event('resize')); + } + } + + /** + * Sets the window width. + * + * @deprecated Use setWindowSize() instead. + * @param width Width. + */ + public setInnerWidth(width: number): void { + this.setWindowSize({ width }); + } + + /** + * Sets the window height. + * + * @deprecated Use setWindowSize() instead. + * @param height Height. + */ + public setInnerHeight(height: number): void { + this.setWindowSize({ height }); + } +} diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index 718f20d38..fbb2bab54 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -95,9 +95,6 @@ import MimeType from '../navigator/MimeType.js'; import MimeTypeArray from '../navigator/MimeTypeArray.js'; import Plugin from '../navigator/Plugin.js'; import PluginArray from '../navigator/PluginArray.js'; -import IResponseInit from '../fetch/types/IResponseInit.js'; -import IRequest from '../fetch/types/IRequest.js'; -import IHeaders from '../fetch/types/IHeaders.js'; import IRequestInit from '../fetch/types/IRequestInit.js'; import IResponse from '../fetch/types/IResponse.js'; import Range from '../range/Range.js'; @@ -119,9 +116,6 @@ import { webcrypto } from 'crypto'; import FormData from '../form-data/FormData.js'; import AbortController from '../fetch/AbortController.js'; import AbortSignal from '../fetch/AbortSignal.js'; -import IResponseBody from '../fetch/types/IResponseBody.js'; -import IRequestInfo from '../fetch/types/IRequestInfo.js'; -import IHeadersInit from '../fetch/types/IHeadersInit.js'; import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; import ValidityState from '../validity-state/ValidityState.js'; import INodeJSGlobal from './INodeJSGlobal.js'; @@ -131,7 +125,10 @@ import PermissionStatus from '../permissions/PermissionStatus.js'; import Clipboard from '../clipboard/Clipboard.js'; import ClipboardItem from '../clipboard/ClipboardItem.js'; import ClipboardEvent from '../event/events/ClipboardEvent.js'; -import DetachedWindowAPI from './DetachedWindowAPI.js'; +import DetachedWindowAPI from './HappyDOMWindowAPI.js'; +import Headers from '../fetch/Headers.js'; +import Request from '../fetch/Request.js'; +import Response from '../fetch/Response.js'; /** * Window without dependencies to server side specific packages. @@ -337,11 +334,9 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly MimeTypeArray: typeof MimeTypeArray; readonly Plugin: typeof Plugin; readonly PluginArray: typeof PluginArray; - readonly Headers: { new (init?: IHeadersInit): IHeaders }; - readonly Request: { - new (input: IRequestInfo, init?: IRequestInit): IRequest; - }; - readonly Response: { new (body?: IResponseBody | null, init?: IResponseInit): IResponse }; + readonly Headers: typeof Headers; + readonly Request: typeof Request; + readonly Response: typeof Response; readonly Range: typeof Range; readonly DOMRect: typeof DOMRect; readonly XMLHttpRequest: typeof XMLHttpRequest; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 4a3f58f85..c2539cf01 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -86,12 +86,7 @@ import StorageEvent from '../event/events/StorageEvent.js'; import SubmitEvent from '../event/events/SubmitEvent.js'; import Screen from '../screen/Screen.js'; import IResponse from '../fetch/types/IResponse.js'; -import IResponseInit from '../fetch/types/IResponseInit.js'; -import IRequest from '../fetch/types/IRequest.js'; import IRequestInit from '../fetch/types/IRequestInit.js'; -import IHeaders from '../fetch/types/IHeaders.js'; -import IHeadersInit from '../fetch/types/IHeadersInit.js'; -import Headers from '../fetch/Headers.js'; import RequestImplementation from '../fetch/Request.js'; import ResponseImplementation from '../fetch/Response.js'; import Storage from '../storage/Storage.js'; @@ -129,14 +124,11 @@ import Stream from 'stream'; import FormData from '../form-data/FormData.js'; import AbortController from '../fetch/AbortController.js'; import AbortSignal from '../fetch/AbortSignal.js'; -import IResponseBody from '../fetch/types/IResponseBody.js'; -import IRequestInfo from '../fetch/types/IRequestInfo.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; import ValidityState from '../validity-state/ValidityState.js'; import WindowErrorUtility from './WindowErrorUtility.js'; import VirtualConsole from '../console/VirtualConsole.js'; -import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; import ICrossOriginWindow from './ICrossOriginWindow.js'; import IBrowserSettings from '../browser/IBrowserSettings.js'; import Permissions from '../permissions/Permissions.js'; @@ -144,9 +136,11 @@ import PermissionStatus from '../permissions/PermissionStatus.js'; import Clipboard from '../clipboard/Clipboard.js'; import ClipboardItem from '../clipboard/ClipboardItem.js'; import ClipboardEvent from '../event/events/ClipboardEvent.js'; -import DetachedWindowAPI from './DetachedWindowAPI.js'; -import IVirtualConsolePrinter from '../console/types/IVirtualConsolePrinter.js'; +import HappyDOMWindowAPI from './HappyDOMWindowAPI.js'; import BrowserFrame from '../browser/BrowserFrame.js'; +import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import Headers from '../fetch/Headers.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -162,7 +156,7 @@ const ORIGINAL_QUEUE_MICROTASK = queueMicrotask; */ export default class Window extends EventTarget implements IWindow { // Detached Window API. - public readonly happyDOM: DetachedWindowAPI; + public readonly happyDOM: HappyDOMWindowAPI; // Nodes public readonly Node = Node; @@ -360,16 +354,12 @@ export default class Window extends EventTarget implements IWindow { public readonly Plugin = Plugin; public readonly PluginArray = PluginArray; public readonly FileList = FileList; - public readonly Headers: { new (init?: IHeadersInit): IHeaders } = Headers; public readonly DOMRect: typeof DOMRect; public readonly RadioNodeList: typeof RadioNodeList; public readonly ValidityState: typeof ValidityState; - public readonly Request: { - new (input: IRequestInfo, init?: IRequestInit): IRequest; - }; - public readonly Response: { - new (body?: IResponseBody, init?: IResponseInit): IResponse; - }; + public readonly Headers: typeof Headers; + public readonly Request: typeof RequestImplementation; + public readonly Response: typeof ResponseImplementation; public readonly XMLHttpRequestUpload = XMLHttpRequestUpload; public readonly XMLHttpRequestEventTarget = XMLHttpRequestEventTarget; public readonly ReadableStream = Stream.Readable; @@ -496,7 +486,7 @@ export default class Window extends EventTarget implements IWindow { private _setInterval: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; private _clearInterval: (id: NodeJS.Timeout) => void; private _queueMicrotask: (callback: Function) => void; - #browserFrame: BrowserFrame; + #browserFrame: BrowserFrame | DetachedBrowserFrame; /** * Constructor. @@ -533,10 +523,22 @@ export default class Window extends EventTarget implements IWindow { if (options?.browserFrame) { this.#browserFrame = options.browserFrame; + this.console = + options?.console ?? new VirtualConsole(this.#browserFrame.page.virtualConsolePrinter); } else { - this.#browserFrame = new BrowserFrame(); + this.#browserFrame = new DetachedBrowserFrame({ + window: this, + settings: options?.settings + }); + this.console = + options?.console ?? new VirtualConsole(this.#browserFrame.virtualConsolePrinter); } + this.happyDOM = new HappyDOMWindowAPI({ + window: this, + browserFrame: this.#browserFrame + }); + if (options) { if (options.width !== undefined) { this.innerWidth = options.width; @@ -559,23 +561,6 @@ export default class Window extends EventTarget implements IWindow { } } - if (options && options.console) { - this.console = options.console; - this.happyDOM = new DetachedWindowAPI({ - ownerWindow: this, - settings: options?.settings - }); - } else { - this.happyDOM = new DetachedWindowAPI({ - ownerWindow: this, - settings: options?.settings, - virtualConsolePrinter: new VirtualConsolePrinter() - }); - this.console = new VirtualConsole( - this.happyDOM.virtualConsolePrinter - ); - } - this._setTimeout = ORIGINAL_SET_TIMEOUT; this._clearTimeout = ORIGINAL_CLEAR_TIMEOUT; this._setInterval = ORIGINAL_SET_INTERVAL; @@ -600,6 +585,7 @@ export default class Window extends EventTarget implements IWindow { HTMLDocument._windowClass = Window; const document = new HTMLDocument(); + const browserFrame = this.#browserFrame; this.document = document; @@ -608,10 +594,11 @@ export default class Window extends EventTarget implements IWindow { /* eslint-disable jsdoc/require-jsdoc */ class Response extends ResponseImplementation { - public readonly _ownerDocument: IDocument = document; + protected readonly _asyncTaskManager: AsyncTaskManager = browserFrame._asyncTaskManager; } class Request extends RequestImplementation { - public readonly _ownerDocument: IDocument = document; + protected readonly _ownerDocument: IDocument = document; + protected readonly _asyncTaskManager: AsyncTaskManager = browserFrame._asyncTaskManager; } class Image extends ImageImplementation { public readonly ownerDocument: IDocument = document; @@ -626,7 +613,18 @@ export default class Window extends EventTarget implements IWindow { public readonly _ownerDocument: IDocument = document; } class XMLHttpRequest extends XMLHttpRequestImplementation { - public readonly ownerDocument: IDocument = document; + constructor() { + super({ + ownerDocument: document, + asyncTaskManager: browserFrame._asyncTaskManager, + browserSettings: { + enableFileSystemHttpRequests: + browserFrame instanceof BrowserFrame + ? browserFrame.page.context.browser.settings.enableFileSystemHttpRequests + : browserFrame.settings.enableFileSystemHttpRequests + } + }); + } } class Range extends RangeImplementation { public readonly ownerDocument: IDocument = document; @@ -778,6 +776,19 @@ export default class Window extends EventTarget implements IWindow { return null; } + /** + * Closes the window. + */ + public close(): void { + if (this.#browserFrame instanceof BrowserFrame) { + if (this.#browserFrame.page.mainFrame === this.#browserFrame) { + this.#browserFrame.page.close(); + } + } else { + this.#browserFrame.destroy(); + } + } + /** * Returns a new MediaQueryList object that can then be used to determine if the document matches the media query string. * @@ -803,9 +814,9 @@ export default class Window extends EventTarget implements IWindow { } else { WindowErrorUtility.captureError(this, () => callback(...args)); } - this.happyDOM.asyncTaskManager.endTimer(id); + this.#browserFrame._asyncTaskManager.endTimer(id); }, delay); - this.happyDOM.asyncTaskManager.startTimer(id); + this.#browserFrame._asyncTaskManager.startTimer(id); return id; } @@ -816,7 +827,7 @@ export default class Window extends EventTarget implements IWindow { */ public clearTimeout(id: NodeJS.Timeout): void { this._clearTimeout(id); - this.happyDOM.asyncTaskManager.endTimer(id); + this.#browserFrame._asyncTaskManager.endTimer(id); } /** @@ -839,7 +850,7 @@ export default class Window extends EventTarget implements IWindow { ); } }, delay); - this.happyDOM.asyncTaskManager.startTimer(id); + this.#browserFrame._asyncTaskManager.startTimer(id); return id; } @@ -850,7 +861,7 @@ export default class Window extends EventTarget implements IWindow { */ public clearInterval(id: NodeJS.Timeout): void { this._clearInterval(id); - this.happyDOM.asyncTaskManager.endTimer(id); + this.#browserFrame._asyncTaskManager.endTimer(id); } /** @@ -866,9 +877,9 @@ export default class Window extends EventTarget implements IWindow { } else { WindowErrorUtility.captureError(this, () => callback(this.performance.now())); } - this.happyDOM.asyncTaskManager.endImmediate(id); + this.#browserFrame._asyncTaskManager.endImmediate(id); }); - this.happyDOM.asyncTaskManager.startImmediate(id); + this.#browserFrame._asyncTaskManager.startImmediate(id); return id; } @@ -879,7 +890,7 @@ export default class Window extends EventTarget implements IWindow { */ public cancelAnimationFrame(id: NodeJS.Immediate): void { global.clearImmediate(id); - this.happyDOM.asyncTaskManager.endImmediate(id); + this.#browserFrame._asyncTaskManager.endImmediate(id); } /** @@ -889,7 +900,7 @@ export default class Window extends EventTarget implements IWindow { */ public queueMicrotask(callback: Function): void { let isAborted = false; - const taskId = this.happyDOM.asyncTaskManager.startTask(() => (isAborted = true)); + const taskId = this.#browserFrame._asyncTaskManager.startTask(() => (isAborted = true)); this._queueMicrotask(() => { if (!isAborted) { if (this.happyDOM.settings.disableErrorCapturing) { @@ -897,7 +908,7 @@ export default class Window extends EventTarget implements IWindow { } else { WindowErrorUtility.captureError(this, <() => unknown>callback); } - this.happyDOM.asyncTaskManager.endTask(taskId); + this.#browserFrame._asyncTaskManager.endTask(taskId); } }); } @@ -910,7 +921,12 @@ export default class Window extends EventTarget implements IWindow { * @returns Promise. */ public async fetch(url: RequestInfo, init?: IRequestInit): Promise { - return await new Fetch({ ownerDocument: this.document, url, init }).send(); + return await new Fetch({ + ownerDocument: this.document, + asyncTaskManager: this.#browserFrame._asyncTaskManager, + url, + init + }).send(); } /** diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index 1694963fd..c115a73f0 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -20,6 +20,7 @@ import XMLHttpRequestSyncRequestScriptBuilder from './utilities/XMLHttpRequestSy import IconvLite from 'iconv-lite'; import ErrorEvent from '../event/events/ErrorEvent.js'; import Document from '../nodes/document/Document.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; // These headers are not user setable. // The following are allowed but banned in the spec: @@ -59,9 +60,6 @@ const CONTENT_TYPE_ENCODING_REGEXP = /charset=([^;]*)/i; * https://github.com/mjwwit/node-XMLHttpRequest/blob/master/lib/XMLHttpRequest.js */ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { - // Will be populated by a sub-class in Window. - public readonly _ownerDocument: IDocument; - // Constants public static UNSENT = XMLHttpRequestReadyStateEnum.unsent; public static OPENED = XMLHttpRequestReadyStateEnum.opened; @@ -72,56 +70,89 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Public properties public upload: XMLHttpRequestUpload = new XMLHttpRequestUpload(); + // Will be injected by a sub-class in Window. + readonly #ownerDocument: IDocument; + readonly #browserSettings: { enableFileSystemHttpRequests: boolean } = Object.freeze({ + enableFileSystemHttpRequests: false + }); + readonly #asyncTaskManager: AsyncTaskManager; + // Private properties - private _state: { - incommingMessage: - | HTTP.IncomingMessage - | { headers: { [name: string]: string | string[] }; statusCode: number }; - response: ArrayBuffer | Blob | IDocument | object | string; - responseType: XMLHttpResponseTypeEnum | ''; - responseText: string; - responseXML: IDocument; - responseURL: string; - readyState: XMLHttpRequestReadyStateEnum; - asyncRequest: HTTP.ClientRequest; - asyncTaskID: number; - requestHeaders: object; - status: number; - statusText: string; - send: boolean; - error: boolean; - aborted: boolean; + readonly #internal: { + state: { + incommingMessage: + | HTTP.IncomingMessage + | { headers: { [name: string]: string | string[] }; statusCode: number }; + response: ArrayBuffer | Blob | IDocument | object | string; + responseType: XMLHttpResponseTypeEnum | ''; + responseText: string; + responseXML: IDocument; + responseURL: string; + readyState: XMLHttpRequestReadyStateEnum; + asyncRequest: HTTP.ClientRequest; + asyncTaskID: number; + requestHeaders: object; + status: number; + statusText: string; + send: boolean; + error: boolean; + aborted: boolean; + }; + settings: { + method: string; + url: string; + async: boolean; + user: string; + password: string; + }; } = { - incommingMessage: null, - response: null, - responseType: '', - responseText: '', - responseXML: null, - responseURL: '', - readyState: XMLHttpRequestReadyStateEnum.unsent, - asyncRequest: null, - asyncTaskID: null, - requestHeaders: {}, - status: null, - statusText: null, - send: false, - error: false, - aborted: false + state: { + incommingMessage: null, + response: null, + responseType: '', + responseText: '', + responseXML: null, + responseURL: '', + readyState: XMLHttpRequestReadyStateEnum.unsent, + asyncRequest: null, + asyncTaskID: null, + requestHeaders: {}, + status: null, + statusText: null, + send: false, + error: false, + aborted: false + }, + settings: { + method: null, + url: null, + async: true, + user: null, + password: null + } }; - private _settings: { - method: string; - url: string; - async: boolean; - user: string; - password: string; - } = { - method: null, - url: null, - async: true, - user: null, - password: null - }; + /** + * Constructor. + * + * @param [inject] Properties to inject. + * @param [inject.browserSettings] Browser settings. + * @param [inject.browserSettings.enableFileSystemHttpRequests] Enable file system HTTP requests. + * @param inject.ownerDocument + * @param inject.asyncTaskManager + */ + constructor(inject?: { + ownerDocument: IDocument; + asyncTaskManager: AsyncTaskManager; + browserSettings: { enableFileSystemHttpRequests: boolean }; + }) { + super(); + this.#ownerDocument = inject.ownerDocument; + this.#asyncTaskManager = inject.asyncTaskManager; + this.#browserSettings = Object.freeze({ + enableFileSystemHttpRequests: inject.browserSettings.enableFileSystemHttpRequests + }); + } /** * Returns the status. @@ -129,7 +160,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Status. */ public get status(): number { - return this._state.status; + return this.#internal.state.status; } /** @@ -138,7 +169,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Status text. */ public get statusText(): string { - return this._state.statusText; + return this.#internal.state.statusText; } /** @@ -147,7 +178,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Response. */ public get response(): ArrayBuffer | Blob | IDocument | object | string { - return this._state.response; + return this.#internal.state.response; } /** @@ -156,7 +187,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Response URL. */ public get responseURL(): string { - return this._state.responseURL; + return this.#internal.state.responseURL; } /** @@ -165,7 +196,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Ready state. */ public get readyState(): XMLHttpRequestReadyStateEnum { - return this._state.readyState; + return this.#internal.state.readyState; } /** @@ -176,7 +207,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { */ public get responseText(): string { if (this.responseType === XMLHttpResponseTypeEnum.text || this.responseType === '') { - return this._state.responseText; + return this.#internal.state.responseText; } throw new DOMException( `Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'text' (was '${this.responseType}').`, @@ -192,7 +223,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { */ public get responseXML(): IDocument { if (this.responseType === XMLHttpResponseTypeEnum.document || this.responseType === '') { - return this._state.responseXML; + return this.#internal.state.responseXML; } throw new DOMException( `Failed to read the 'responseXML' property from 'XMLHttpRequest': The value is only accessible if the object's 'responseType' is '' or 'document' (was '${this.responseType}').`, @@ -219,13 +250,13 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ); } // Sync requests can only have empty string or 'text' as response type. - if (!this._settings.async) { + if (!this.#internal.settings.async) { throw new DOMException( `Failed to set the 'responseType' property on 'XMLHttpRequest': The response type cannot be changed for synchronous requests made from a document.`, DOMExceptionNameEnum.invalidStateError ); } - this._state.responseType = type; + this.#internal.state.responseType = type; } /** @@ -234,7 +265,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Response type. */ public get responseType(): XMLHttpResponseTypeEnum | '' { - return this._state.responseType; + return this.#internal.state.responseType; } /** @@ -249,8 +280,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { public open(method: string, url: string, async = true, user?: string, password?: string): void { this.abort(); - this._state.aborted = false; - this._state.error = false; + this.#internal.state.aborted = false; + this.#internal.state.error = false; const upperMethod = method.toUpperCase(); @@ -267,7 +298,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ); } - this._settings = { + this.#internal.settings = { method: upperMethod, url: url, async: async, @@ -299,14 +330,14 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return false; } - if (this._state.send) { + if (this.#internal.state.send) { throw new DOMException( `Failed to execute 'setRequestHeader' on 'XMLHttpRequest': Request is in progress.`, DOMExceptionNameEnum.invalidStateError ); } - this._state.requestHeaders[lowerHeader] = value; + this.#internal.state.requestHeaders[lowerHeader] = value; return true; } @@ -326,12 +357,12 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { header !== 'set-cookie' && header !== 'set-cookie2' && this.readyState > XMLHttpRequestReadyStateEnum.opened && - this._state.incommingMessage.headers[lowerHeader] && - !this._state.error + this.#internal.state.incommingMessage.headers[lowerHeader] && + !this.#internal.state.error ) { - return Array.isArray(this._state.incommingMessage.headers[lowerHeader]) - ? (this._state.incommingMessage.headers[lowerHeader]).join(', ') - : this._state.incommingMessage.headers[lowerHeader]; + return Array.isArray(this.#internal.state.incommingMessage.headers[lowerHeader]) + ? (this.#internal.state.incommingMessage.headers[lowerHeader]).join(', ') + : this.#internal.state.incommingMessage.headers[lowerHeader]; } return null; @@ -343,16 +374,19 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns A string with all response headers separated by CR+LF. */ public getAllResponseHeaders(): string { - if (this.readyState < XMLHttpRequestReadyStateEnum.headersRecieved || this._state.error) { + if ( + this.readyState < XMLHttpRequestReadyStateEnum.headersRecieved || + this.#internal.state.error + ) { return ''; } const result = []; - for (const name of Object.keys(this._state.incommingMessage.headers)) { + for (const name of Object.keys(this.#internal.state.incommingMessage.headers)) { // Cookie headers are excluded for security reasons as per spec. if (name !== 'set-cookie' && name !== 'set-cookie2') { - result.push(`${name}: ${this._state.incommingMessage.headers[name]}`); + result.push(`${name}: ${this.#internal.state.incommingMessage.headers[name]}`); } } @@ -372,16 +406,16 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ); } - if (this._state.send) { + if (this.#internal.state.send) { throw new DOMException( `Failed to execute 'send' on 'XMLHttpRequest': Send has already been called.`, DOMExceptionNameEnum.invalidStateError ); } - const { location } = this._ownerDocument.defaultView; + const { location } = this.#ownerDocument.defaultView; - const url = new URL(this._settings.url, location); + const url = new URL(this.#internal.settings.url, location); // Security check. if (url.protocol === 'http:' && location.protocol === 'https:') { @@ -393,21 +427,21 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Load files off the local filesystem (file://) if (XMLHttpRequestURLUtility.isLocal(url)) { - if (!this._ownerDocument.defaultView.happyDOM.settings.enableFileSystemHttpRequests) { + if (!this.#browserSettings.enableFileSystemHttpRequests) { throw new DOMException( 'File system is disabled by default for security reasons. To enable it, set the "window.happyDOM.settings.enableFileSystemHttpRequests" option to true.', DOMExceptionNameEnum.securityError ); } - if (this._settings.method !== 'GET') { + if (this.#internal.settings.method !== 'GET') { throw new DOMException( 'Failed to send local file system request. Only "GET" method is supported for local file system requests.', DOMExceptionNameEnum.notSupportedError ); } - if (this._settings.async) { + if (this.#internal.settings.async) { this._sendLocalAsyncRequest(url).catch((error) => this._onError(error)); } else { this._sendLocalSyncRequest(url); @@ -427,31 +461,34 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { const uri = url.pathname + (url.search ? url.search : ''); // Set the Host header or the server may reject the request - this._state.requestHeaders['host'] = host; + this.#internal.state.requestHeaders['host'] = host; if (!((ssl && port === 443) || port === 80)) { - this._state.requestHeaders['host'] += ':' + url.port; + this.#internal.state.requestHeaders['host'] += ':' + url.port; } // Set Basic Auth if necessary - if (this._settings.user) { - this._settings.password ??= ''; - const authBuffer = Buffer.from(this._settings.user + ':' + this._settings.password); - this._state.requestHeaders['authorization'] = 'Basic ' + authBuffer.toString('base64'); + if (this.#internal.settings.user) { + this.#internal.settings.password ??= ''; + const authBuffer = Buffer.from( + this.#internal.settings.user + ':' + this.#internal.settings.password + ); + this.#internal.state.requestHeaders['authorization'] = + 'Basic ' + authBuffer.toString('base64'); } // Set the Content-Length header if method is POST - switch (this._settings.method) { + switch (this.#internal.settings.method) { case 'GET': case 'HEAD': data = null; break; case 'POST': - this._state.requestHeaders['content-type'] ??= 'text/plain;charset=UTF-8'; + this.#internal.state.requestHeaders['content-type'] ??= 'text/plain;charset=UTF-8'; if (data) { - this._state.requestHeaders['content-length'] = Buffer.isBuffer(data) + this.#internal.state.requestHeaders['content-length'] = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data); } else { - this._state.requestHeaders['content-length'] = 0; + this.#internal.state.requestHeaders['content-length'] = 0; } break; @@ -463,8 +500,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { host: host, port: port, path: uri, - method: this._settings.method, - headers: { ...this._getDefaultRequestHeaders(), ...this._state.requestHeaders }, + method: this.#internal.settings.method, + headers: { ...this._getDefaultRequestHeaders(), ...this.#internal.state.requestHeaders }, agent: false, rejectUnauthorized: true }; @@ -475,10 +512,10 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } // Reset error flag - this._state.error = false; + this.#internal.state.error = false; // Handle async requests - if (this._settings.async) { + if (this.#internal.settings.async) { this._sendAsyncRequest(options, ssl, data).catch((error) => this._onError(error)); } else { this._sendSyncRequest(options, ssl, data); @@ -489,31 +526,31 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * Aborts a request. */ public abort(): void { - if (this._state.asyncRequest) { - this._state.asyncRequest.destroy(); - this._state.asyncRequest = null; + if (this.#internal.state.asyncRequest) { + this.#internal.state.asyncRequest.destroy(); + this.#internal.state.asyncRequest = null; } - this._state.status = null; - this._state.statusText = null; - this._state.requestHeaders = {}; - this._state.responseText = ''; - this._state.responseXML = null; - this._state.aborted = true; - this._state.error = true; + this.#internal.state.status = null; + this.#internal.state.statusText = null; + this.#internal.state.requestHeaders = {}; + this.#internal.state.responseText = ''; + this.#internal.state.responseXML = null; + this.#internal.state.aborted = true; + this.#internal.state.error = true; if ( this.readyState !== XMLHttpRequestReadyStateEnum.unsent && - (this.readyState !== XMLHttpRequestReadyStateEnum.opened || this._state.send) && + (this.readyState !== XMLHttpRequestReadyStateEnum.opened || this.#internal.state.send) && this.readyState !== XMLHttpRequestReadyStateEnum.done ) { - this._state.send = false; + this.#internal.state.send = false; this._setState(XMLHttpRequestReadyStateEnum.done); } - this._state.readyState = XMLHttpRequestReadyStateEnum.unsent; + this.#internal.state.readyState = XMLHttpRequestReadyStateEnum.unsent; - if (this._state.asyncTaskID !== null) { - this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID); + if (this.#internal.state.asyncTaskID !== null) { + this.#asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } } @@ -525,15 +562,15 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { private _setState(state: XMLHttpRequestReadyStateEnum): void { if ( this.readyState === state || - (this.readyState === XMLHttpRequestReadyStateEnum.unsent && this._state.aborted) + (this.readyState === XMLHttpRequestReadyStateEnum.unsent && this.#internal.state.aborted) ) { return; } - this._state.readyState = state; + this.#internal.state.readyState = state; if ( - this._settings.async || + this.#internal.settings.async || this.readyState < XMLHttpRequestReadyStateEnum.opened || this.readyState === XMLHttpRequestReadyStateEnum.done ) { @@ -543,9 +580,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { if (this.readyState === XMLHttpRequestReadyStateEnum.done) { let fire: Event; - if (this._state.aborted) { + if (this.#internal.state.aborted) { fire = new Event('abort'); - } else if (this._state.error) { + } else if (this.#internal.state.error) { fire = new Event('error'); } else { fire = new Event('load'); @@ -562,7 +599,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Default request headers. */ private _getDefaultRequestHeaders(): { [key: string]: string } { - const { location, navigator, document } = this._ownerDocument.defaultView; + const { location, navigator, document } = this.#ownerDocument.defaultView; return { accept: '*/*', @@ -601,45 +638,50 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } if (response) { - this._state.incommingMessage = { + this.#internal.state.incommingMessage = { statusCode: response.statusCode, headers: response.headers }; - this._state.status = response.statusCode; - this._state.statusText = response.statusMessage; + this.#internal.state.status = response.statusCode; + this.#internal.state.statusText = response.statusMessage; // Although it will immediately be set to loading, // According to the spec, the state should be headersRecieved first. this._setState(XMLHttpRequestReadyStateEnum.headersRecieved); this._setState(XMLHttpRequestReadyStateEnum.loading); - this._state.response = this._decodeResponseText(Buffer.from(response.data, 'base64')); - this._state.responseText = this._state.response; - this._state.responseXML = null; - this._state.responseURL = new URL( - this._settings.url, - this._ownerDocument.defaultView.location + this.#internal.state.response = this._decodeResponseText( + Buffer.from(response.data, 'base64') + ); + this.#internal.state.responseText = this.#internal.state.response; + this.#internal.state.responseXML = null; + this.#internal.state.responseURL = new URL( + this.#internal.settings.url, + this.#ownerDocument.defaultView.location ).href; // Set Cookies. - this._setCookies(this._state.incommingMessage.headers); + this._setCookies(this.#internal.state.incommingMessage.headers); // Redirect. if ( - this._state.incommingMessage.statusCode === 301 || - this._state.incommingMessage.statusCode === 302 || - this._state.incommingMessage.statusCode === 303 || - this._state.incommingMessage.statusCode === 307 + this.#internal.state.incommingMessage.statusCode === 301 || + this.#internal.state.incommingMessage.statusCode === 302 || + this.#internal.state.incommingMessage.statusCode === 303 || + this.#internal.state.incommingMessage.statusCode === 307 ) { const redirectUrl = new URL( - this._state.incommingMessage.headers['location'], - this._ownerDocument.defaultView.location + this.#internal.state.incommingMessage.headers['location'], + this.#ownerDocument.defaultView.location ); ssl = redirectUrl.protocol === 'https:'; - this._settings.url = redirectUrl.href; + this.#internal.settings.url = redirectUrl.href; // Recursive call. this._sendSyncRequest( Object.assign(options, { host: redirectUrl.host, path: redirectUrl.pathname + (redirectUrl.search ?? ''), port: redirectUrl.port || (ssl ? 443 : 80), - method: this._state.incommingMessage.statusCode === 303 ? 'GET' : this._settings.method, + method: + this.#internal.state.incommingMessage.statusCode === 303 + ? 'GET' + : this.#internal.settings.method, headers: Object.assign(options.headers, { referer: redirectUrl.origin, host: redirectUrl.host @@ -668,21 +710,19 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ): Promise { return new Promise((resolve) => { // Starts async task in Happy DOM - this._state.asyncTaskID = this._ownerDocument.defaultView.happyDOM.asyncTaskManager.startTask( - this.abort.bind(this) - ); + this.#internal.state.asyncTaskID = this.#asyncTaskManager.startTask(this.abort.bind(this)); // Use the proper protocol const sendRequest = ssl ? HTTPS.request : HTTP.request; // Request is being sent, set send flag - this._state.send = true; + this.#internal.state.send = true; // As per spec, this is called here for historical reasons. this.dispatchEvent(new Event('readystatechange')); // Create the request - this._state.asyncRequest = sendRequest( + this.#internal.state.asyncRequest = sendRequest( options, async (response: HTTP.IncomingMessage) => { await this._onAsyncResponse(response, options, ssl, data); @@ -690,25 +730,23 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { resolve(); // Ends async task in Happy DOM - this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask( - this._state.asyncTaskID - ); + this.#asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } ); - this._state.asyncRequest.on('error', (error: Error) => { + this.#internal.state.asyncRequest.on('error', (error: Error) => { this._onError(error); resolve(); // Ends async task in Happy DOM - this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID); + this.#asyncTaskManager.endTask(this.#internal.state.asyncTaskID); }); // Node 0.4 and later won't accept empty data. Make sure it's needed. if (data) { - this._state.asyncRequest.write(data); + this.#internal.state.asyncRequest.write(data); } - this._state.asyncRequest.end(); + this.#internal.state.asyncRequest.end(); this.dispatchEvent(new Event('loadstart')); }); @@ -732,25 +770,28 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return new Promise((resolve) => { // Set response var to the response we got back // This is so it remains accessable outside this scope - this._state.incommingMessage = response; + this.#internal.state.incommingMessage = response; // Set Cookies - this._setCookies(this._state.incommingMessage.headers); + this._setCookies(this.#internal.state.incommingMessage.headers); // Check for redirect // @TODO Prevent looped redirects if ( - this._state.incommingMessage.statusCode === 301 || - this._state.incommingMessage.statusCode === 302 || - this._state.incommingMessage.statusCode === 303 || - this._state.incommingMessage.statusCode === 307 + this.#internal.state.incommingMessage.statusCode === 301 || + this.#internal.state.incommingMessage.statusCode === 302 || + this.#internal.state.incommingMessage.statusCode === 303 || + this.#internal.state.incommingMessage.statusCode === 307 ) { // TODO: redirect url protocol change. // Change URL to the redirect location - this._settings.url = this._state.incommingMessage.headers.location; + this.#internal.settings.url = this.#internal.state.incommingMessage.headers.location; // Parse the new URL. - const redirectUrl = new URL(this._settings.url, this._ownerDocument.defaultView.location); - this._settings.url = redirectUrl.href; + const redirectUrl = new URL( + this.#internal.settings.url, + this.#ownerDocument.defaultView.location + ); + this.#internal.settings.url = redirectUrl.href; ssl = redirectUrl.protocol === 'https:'; // Issue the new request this._sendAsyncRequest( @@ -759,7 +800,10 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { host: redirectUrl.hostname, port: redirectUrl.port, path: redirectUrl.pathname + (redirectUrl.search ?? ''), - method: this._state.incommingMessage.statusCode === 303 ? 'GET' : this._settings.method, + method: + this.#internal.state.incommingMessage.statusCode === 303 + ? 'GET' + : this.#internal.settings.method, headers: { ...options.headers, referer: redirectUrl.origin, host: redirectUrl.host } }, ssl, @@ -769,24 +813,26 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return; } - this._state.status = this._state.incommingMessage.statusCode; - this._state.statusText = this._state.incommingMessage.statusMessage; + this.#internal.state.status = this.#internal.state.incommingMessage.statusCode; + this.#internal.state.statusText = this.#internal.state.incommingMessage.statusMessage; this._setState(XMLHttpRequestReadyStateEnum.headersRecieved); // Initialize response. let tempResponse = Buffer.from(new Uint8Array(0)); - this._state.incommingMessage.on('data', (chunk: Uint8Array) => { + this.#internal.state.incommingMessage.on('data', (chunk: Uint8Array) => { // Make sure there's some data if (chunk) { tempResponse = Buffer.concat([tempResponse, Buffer.from(chunk)]); } // Don't emit state changes if the connection has been aborted. - if (this._state.send) { + if (this.#internal.state.send) { this._setState(XMLHttpRequestReadyStateEnum.loading); } - const contentLength = Number(this._state.incommingMessage.headers['content-length']); + const contentLength = Number( + this.#internal.state.incommingMessage.headers['content-length'] + ); this.dispatchEvent( new ProgressEvent('progress', { lengthComputable: !isNaN(contentLength), @@ -796,20 +842,20 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ); }); - this._state.incommingMessage.on('end', () => { - if (this._state.send) { + this.#internal.state.incommingMessage.on('end', () => { + if (this.#internal.state.send) { // The sendFlag needs to be set before setState is called. Otherwise, if we are chaining callbacks // There can be a timing issue (the callback is called and a new call is made before the flag is reset). - this._state.send = false; + this.#internal.state.send = false; // Set response according to responseType. const { response, responseXML, responseText } = this._parseResponseData(tempResponse); - this._state.response = response; - this._state.responseXML = responseXML; - this._state.responseText = responseText; - this._state.responseURL = new URL( - this._settings.url, - this._ownerDocument.defaultView.location + this.#internal.state.response = response; + this.#internal.state.responseXML = responseXML; + this.#internal.state.responseText = responseText; + this.#internal.state.responseURL = new URL( + this.#internal.settings.url, + this.#ownerDocument.defaultView.location ).href; // Discard the 'end' event if the connection has been aborted this._setState(XMLHttpRequestReadyStateEnum.done); @@ -818,7 +864,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { resolve(); }); - this._state.incommingMessage.on('error', (error) => { + this.#internal.state.incommingMessage.on('error', (error) => { this._onError(error); resolve(); }); @@ -832,9 +878,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Promise. */ private async _sendLocalAsyncRequest(url: UrlObject): Promise { - this._state.asyncTaskID = this._ownerDocument.defaultView.happyDOM.asyncTaskManager.startTask( - this.abort.bind(this) - ); + this.#internal.state.asyncTaskID = this.#asyncTaskManager.startTask(this.abort.bind(this)); let data: Buffer; @@ -843,7 +887,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } catch (error) { this._onError(error); // Release async task. - this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID); + this.#asyncTaskManager.endTask(this.#internal.state.asyncTaskID); return; } @@ -864,7 +908,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } this._setState(XMLHttpRequestReadyStateEnum.done); - this._ownerDocument.defaultView.happyDOM.asyncTaskManager.endTask(this._state.asyncTaskID); + this.#asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } /** @@ -899,7 +943,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { */ private _parseLocalRequestData(url: UrlObject, data: Buffer): void { // Manually set the response headers. - this._state.incommingMessage = { + this.#internal.state.incommingMessage = { statusCode: 200, headers: { 'content-length': String(data.length), @@ -908,16 +952,16 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } }; - this._state.status = this._state.incommingMessage.statusCode; - this._state.statusText = 'OK'; + this.#internal.state.status = this.#internal.state.incommingMessage.statusCode; + this.#internal.state.statusText = 'OK'; const { response, responseXML, responseText } = this._parseResponseData(data); - this._state.response = response; - this._state.responseXML = responseXML; - this._state.responseText = responseText; - this._state.responseURL = new URL( - this._settings.url, - this._ownerDocument.defaultView.location + this.#internal.state.response = response; + this.#internal.state.responseXML = responseXML; + this.#internal.state.responseText = responseText; + this.#internal.state.responseURL = new URL( + this.#internal.settings.url, + this.#ownerDocument.defaultView.location ).href; this._setState(XMLHttpRequestReadyStateEnum.done); @@ -948,7 +992,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { case XMLHttpResponseTypeEnum.blob: try { return { - response: new this._ownerDocument.defaultView.Blob([new Uint8Array(data)], { + response: new this.#ownerDocument.defaultView.Blob([new Uint8Array(data)], { type: this.getResponseHeader('content-type') || '' }), responseText: null, @@ -958,18 +1002,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return { response: null, responseText: null, responseXML: null }; } case XMLHttpResponseTypeEnum.document: - const window = this._ownerDocument.defaultView; - let response: IDocument; - - // Temporary disables unsecure features. - const originalSettings = Object.assign({}, window.happyDOM.settings); - - window.happyDOM.settings.enableFileSystemHttpRequests = false; - window.happyDOM.settings.disableJavaScriptEvaluation = true; - window.happyDOM.settings.disableCSSFileLoading = true; - window.happyDOM.settings.disableJavaScriptFileLoading = true; - + const window = this.#ownerDocument.defaultView; const domParser = new window.DOMParser(); + let response: IDocument; try { response = domParser.parseFromString(this._decodeResponseText(data), 'text/xml'); @@ -977,15 +1012,6 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return { response: null, responseText: null, responseXML: null }; } - // Restores unsecure features. - window.happyDOM.settings.enableFileSystemHttpRequests = - originalSettings.enableFileSystemHttpRequests; - window.happyDOM.settings.disableJavaScriptEvaluation = - originalSettings.disableJavaScriptEvaluation; - window.happyDOM.settings.disableCSSFileLoading = originalSettings.disableCSSFileLoading; - window.happyDOM.settings.disableJavaScriptFileLoading = - originalSettings.disableJavaScriptFileLoading; - return { response, responseText: null, responseXML: response }; case XMLHttpResponseTypeEnum.json: try { @@ -1017,17 +1043,20 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { private _setCookies( headers: { [name: string]: string | string[] } | HTTP.IncomingHttpHeaders ): void { - const originURL = new URL(this._settings.url, this._ownerDocument.defaultView.location); + const originURL = new URL( + this.#internal.settings.url, + this.#ownerDocument.defaultView.location + ); for (const header of ['set-cookie', 'set-cookie2']) { if (Array.isArray(headers[header])) { for (const cookie of headers[header]) { - (this._ownerDocument.defaultView.document)._cookie.addCookieString( + (this.#ownerDocument.defaultView.document)._cookie.addCookieString( originURL, cookie ); } } else if (headers[header]) { - (this._ownerDocument.defaultView.document)._cookie.addCookieString( + (this.#ownerDocument.defaultView.document)._cookie.addCookieString( originURL, headers[header] ); @@ -1041,10 +1070,10 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param error Error. */ private _onError(error: Error | string): void { - this._state.status = 0; - this._state.statusText = error.toString(); - this._state.responseText = error instanceof Error ? error.stack : ''; - this._state.error = true; + this.#internal.state.status = 0; + this.#internal.state.statusText = error.toString(); + this.#internal.state.responseText = error instanceof Error ? error.stack : ''; + this.#internal.state.error = true; this._setState(XMLHttpRequestReadyStateEnum.done); const errorObject = error instanceof Error ? error : new Error(error); @@ -1053,9 +1082,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { error: errorObject }); - this._ownerDocument.defaultView.console.error(errorObject); + this.#ownerDocument.defaultView.console.error(errorObject); this.dispatchEvent(event); - this._ownerDocument.defaultView.dispatchEvent(event); + this.#ownerDocument.defaultView.dispatchEvent(event); } /** @@ -1068,7 +1097,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { const contextTypeEncodingRegexp = new RegExp(CONTENT_TYPE_ENCODING_REGEXP, 'gi'); // Compatibility with file:// protocol or unpredictable http request. const contentType = - this.getResponseHeader('content-type') ?? this._state.requestHeaders['content-type']; + this.getResponseHeader('content-type') ?? this.#internal.state.requestHeaders['content-type']; const charset = contextTypeEncodingRegexp.exec(contentType); // Default encoding: utf-8. return IconvLite.decode(data, charset ? charset[1] : 'utf-8'); From 8e23bd96e0a8934635c77af4f98ab8c0ff635218 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 18 Oct 2023 17:03:14 +0200 Subject: [PATCH 08/63] #466@trivial: Continues on implementation. --- .../happy-dom/src/browser/BrowserFrame.ts | 26 ++++ packages/happy-dom/src/browser/BrowserPage.ts | 37 ++--- .../src/browser/DetachedBrowserFrame.ts | 26 ++++ .../happy-dom/src/browser/IBrowserFrame.ts | 8 + .../src/browser/IBrowserPageViewport.ts | 4 +- .../happy-dom/src/window/HappyDOMWindowAPI.ts | 37 +++-- packages/happy-dom/src/window/IWindow.ts | 2 +- packages/happy-dom/src/window/Window.ts | 2 +- .../src/window/WindowClassFactory.ts | 146 ++++++++++++++++++ 9 files changed, 241 insertions(+), 47 deletions(-) create mode 100644 packages/happy-dom/src/window/WindowClassFactory.ts diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 03c579ce7..82c2edfe0 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -3,6 +3,8 @@ import BrowserPage from './BrowserPage.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import IBrowserFrame from './IBrowserFrame.js'; import Window from '../window/Window.js'; +import IBrowserPageViewport from './IBrowserPageViewport.js'; +import Event from '../event/Event.js'; /** * Browser frame. @@ -65,6 +67,30 @@ export default class BrowserFrame implements IBrowserFrame { (this.window) = null; } + /** + * Sets the viewport. + * + * @param viewport Viewport. + */ + public setViewport(viewport: IBrowserPageViewport): void { + if ( + (viewport.width !== undefined && this.window.innerWidth !== viewport.width) || + (viewport.height !== undefined && this.window.innerHeight !== viewport.height) + ) { + if (viewport.width !== undefined && this.window.innerWidth !== viewport.width) { + (this.window.innerWidth) = viewport.width; + (this.window.outerWidth) = viewport.width; + } + + if (viewport.height !== undefined && this.window.innerHeight !== viewport.height) { + (this.window.innerHeight) = viewport.height; + (this.window.outerHeight) = viewport.height; + } + + this.window.dispatchEvent(new Event('resize')); + } + } + /** * Go to a page. * diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index 8b9a93e97..a686bbe08 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -9,11 +9,10 @@ import VirtualConsole from '../console/VirtualConsole.js'; * Browser page. */ export default class BrowserPage { - public consolePrinter: VirtualConsolePrinter | null; - public mainFrame: BrowserFrame | null = null; - public context: BrowserContext; - public readonly console: Console; public readonly virtualConsolePrinter = new VirtualConsolePrinter(); + public readonly mainFrame: BrowserFrame; + public readonly context: BrowserContext; + public readonly console: Console; /** * Constructor. @@ -23,6 +22,7 @@ export default class BrowserPage { constructor(context: BrowserContext) { this.context = context; this.console = context.browser.console ?? new VirtualConsole(this.virtualConsolePrinter); + this.mainFrame = new BrowserFrame(this); } /** @@ -46,9 +46,13 @@ export default class BrowserPage { */ public async close(): Promise { await this.mainFrame.destroy(); - this.consolePrinter = null; - this.mainFrame = null; - this.context = null; + const index = this.context.pages.indexOf(this); + if (index !== -1) { + this.context.pages.splice(index, 1); + } + (this.virtualConsolePrinter) = null; + (this.mainFrame) = null; + (this.context) = null; } /** @@ -75,22 +79,7 @@ export default class BrowserPage { * @param viewport Viewport. */ public setViewport(viewport: IBrowserPageViewport): void { - if ( - (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) || - (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) - ) { - if (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) { - (this.mainFrame.window.innerWidth) = viewport.width; - (this.mainFrame.window.outerWidth) = viewport.width; - } - - if (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) { - (this.mainFrame.window.innerHeight) = viewport.height; - (this.mainFrame.window.outerHeight) = viewport.height; - } - - this.mainFrame.window.dispatchEvent(new Event('resize')); - } + this.mainFrame.setViewport(viewport); } /** @@ -107,7 +96,7 @@ export default class BrowserPage { * * @param parent Parent frame. */ - public _getFrames(parent: BrowserFrame): BrowserFrame[] { + private _getFrames(parent: BrowserFrame): BrowserFrame[] { let frames = [parent]; for (const frame of parent.childFrames) { frames = frames.concat(this._getFrames(frame)); diff --git a/packages/happy-dom/src/browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/DetachedBrowserFrame.ts index 06d8e348d..73849e782 100644 --- a/packages/happy-dom/src/browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/DetachedBrowserFrame.ts @@ -5,6 +5,8 @@ import Window from '../window/Window.js'; import IBrowserSettings from './IBrowserSettings.js'; import { VirtualConsolePrinter } from '../index.js'; import BrowserSettingsFactory from './BrowserSettingsFactory.js'; +import IBrowserPageViewport from './IBrowserPageViewport.js'; +import Event from '../event/Event.js'; /** * Browser frame. @@ -67,6 +69,30 @@ export default class DetachedBrowserFrame implements IBrowserFrame { (this.window) = null; } + /** + * Sets the viewport. + * + * @param viewport Viewport. + */ + public setViewport(viewport: IBrowserPageViewport): void { + if ( + (viewport.width !== undefined && this.window.innerWidth !== viewport.width) || + (viewport.height !== undefined && this.window.innerHeight !== viewport.height) + ) { + if (viewport.width !== undefined && this.window.innerWidth !== viewport.width) { + (this.window.innerWidth) = viewport.width; + (this.window.outerWidth) = viewport.width; + } + + if (viewport.height !== undefined && this.window.innerHeight !== viewport.height) { + (this.window.innerHeight) = viewport.height; + (this.window.outerHeight) = viewport.height; + } + + this.window.dispatchEvent(new Event('resize')); + } + } + /** * Go to a page. * diff --git a/packages/happy-dom/src/browser/IBrowserFrame.ts b/packages/happy-dom/src/browser/IBrowserFrame.ts index d5f138f8c..fe685d88a 100644 --- a/packages/happy-dom/src/browser/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/IBrowserFrame.ts @@ -1,4 +1,5 @@ import IWindow from '../window/IWindow.js'; +import IBrowserPageViewport from './IBrowserPageViewport.js'; /** * Browser frame. @@ -30,6 +31,13 @@ export default interface IBrowserFrame { */ destroy(): Promise; + /** + * Sets the viewport. + * + * @param viewport Viewport. + */ + setViewport(viewport: IBrowserPageViewport): void; + /** * Go to a page. * diff --git a/packages/happy-dom/src/browser/IBrowserPageViewport.ts b/packages/happy-dom/src/browser/IBrowserPageViewport.ts index 950bb0e57..ba5bba338 100644 --- a/packages/happy-dom/src/browser/IBrowserPageViewport.ts +++ b/packages/happy-dom/src/browser/IBrowserPageViewport.ts @@ -1,6 +1,6 @@ export default interface IBrowserPageViewport { - width: number; - height: number; + width?: number; + height?: number; deviceScaleFactor?: number; hasTouch?: boolean; } diff --git a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts index 29edaf2a5..7beca9564 100644 --- a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts +++ b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts @@ -6,6 +6,7 @@ import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; import BrowserSettingsFactory from '../browser/BrowserSettingsFactory.js'; import IReadOnlyBrowserSettings from '../browser/IReadOnlyBrowserSettings.js'; +import IBrowserPageViewport from '../browser/IBrowserPageViewport.js'; /** * API for detached windows to be able to access features of the owner window. @@ -102,46 +103,44 @@ export default class HappyDOMWindowAPI { /** * Sets the window size. * + * @deprecated Use setViewport() instead. * @param options Options. * @param options.width Width. * @param options.height Height. */ public setWindowSize(options: { width?: number; height?: number }): void { - if ( - (options.width !== undefined && this.#window.innerWidth !== options.width) || - (options.height !== undefined && this.#window.innerHeight !== options.height) - ) { - if (options.width !== undefined && this.#window.innerWidth !== options.width) { - (this.#window.innerWidth) = options.width; - (this.#window.outerWidth) = options.width; - } - - if (options.height !== undefined && this.#window.innerHeight !== options.height) { - (this.#window.innerHeight) = options.height; - (this.#window.outerHeight) = options.height; - } + this.setViewport({ + width: options?.width, + height: options?.height + }); + } - this.#window.dispatchEvent(new Event('resize')); - } + /** + * Sets the viewport. + * + * @param viewport Viewport. + */ + public setViewport(viewport: IBrowserPageViewport): void { + this.#browserFrame.setViewport(viewport); } /** * Sets the window width. * - * @deprecated Use setWindowSize() instead. + * @deprecated Use setViewport() instead. * @param width Width. */ public setInnerWidth(width: number): void { - this.setWindowSize({ width }); + this.setViewport({ width }); } /** * Sets the window height. * - * @deprecated Use setWindowSize() instead. + * @deprecated Use setViewport() instead. * @param height Height. */ public setInnerHeight(height: number): void { - this.setWindowSize({ height }); + this.setViewport({ height }); } } diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index fbb2bab54..71e23b99f 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -339,7 +339,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly Response: typeof Response; readonly Range: typeof Range; readonly DOMRect: typeof DOMRect; - readonly XMLHttpRequest: typeof XMLHttpRequest; + readonly XMLHttpRequest: { new (): XMLHttpRequest }; readonly XMLHttpRequestUpload: typeof XMLHttpRequestUpload; readonly XMLHttpRequestEventTarget: typeof XMLHttpRequestEventTarget; readonly FileList: typeof FileList; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index c2539cf01..b696c340d 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -372,7 +372,7 @@ export default class Window extends EventTarget implements IWindow { public readonly PermissionStatus = PermissionStatus; public readonly Clipboard = Clipboard; public readonly ClipboardItem = ClipboardItem; - public readonly XMLHttpRequest; + public readonly XMLHttpRequest: { new (): XMLHttpRequestImplementation }; public readonly DOMParser: typeof DOMParserImplementation; public readonly Range; public readonly FileReader; diff --git a/packages/happy-dom/src/window/WindowClassFactory.ts b/packages/happy-dom/src/window/WindowClassFactory.ts new file mode 100644 index 000000000..7beca9564 --- /dev/null +++ b/packages/happy-dom/src/window/WindowClassFactory.ts @@ -0,0 +1,146 @@ +import IBrowserSettings from '../browser/IBrowserSettings.js'; +import IWindow from './IWindow.js'; +import Event from '../event/Event.js'; +import BrowserFrame from '../browser/BrowserFrame.js'; +import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; +import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import BrowserSettingsFactory from '../browser/BrowserSettingsFactory.js'; +import IReadOnlyBrowserSettings from '../browser/IReadOnlyBrowserSettings.js'; +import IBrowserPageViewport from '../browser/IBrowserPageViewport.js'; + +/** + * API for detached windows to be able to access features of the owner window. + */ +export default class HappyDOMWindowAPI { + #window: IWindow; + #browserFrame?: BrowserFrame | DetachedBrowserFrame; + #settings: IBrowserSettings | null = null; + + /** + * Constructor. + * + * @param options Options. + * @param options.window Owner window. + * @param options.browserFrame Browser frame. + */ + constructor(options: { window: IWindow; browserFrame: BrowserFrame | DetachedBrowserFrame }) { + this.#window = options.window; + this.#browserFrame = options.browserFrame; + } + + /** + * Returns settings. + * + * @returns Settings. + */ + public get settings(): IReadOnlyBrowserSettings { + if (!this.#settings) { + this.#settings = BrowserSettingsFactory.getReadOnlySettings( + this.#browserFrame instanceof DetachedBrowserFrame + ? this.#browserFrame.settings + : this.#browserFrame.page.context.browser.settings + ); + } + return this.#settings; + } + + /** + * Returns virtual console printer. + * + * @returns Virtual console printer. + */ + public get virtualConsolePrinter(): VirtualConsolePrinter { + if (this.#browserFrame instanceof DetachedBrowserFrame) { + return this.#browserFrame.virtualConsolePrinter; + } + return this.#browserFrame.page.virtualConsolePrinter; + } + + /** + * Waits for all async tasks to complete. + * + * @deprecated Use whenComplete() instead. + * @returns Promise. + */ + public async whenAsyncComplete(): Promise { + return await this.whenComplete(); + } + + /** + * Waits for all async tasks to complete. + * + * @returns Promise. + */ + public async whenComplete(): Promise { + return await this.#browserFrame.whenComplete(); + } + + /** + * Aborts all async tasks. + * + * @deprecated Use abort() instead. + */ + public async cancelAsync(): Promise { + await this.abort(); + } + + /** + * Aborts all async tasks. + */ + public async abort(): Promise { + await this.#browserFrame.abort(); + } + + /** + * Sets the URL. + * + * @param url URL. + */ + public setURL(url: string): void { + this.#window.location.href = url; + } + + /** + * Sets the window size. + * + * @deprecated Use setViewport() instead. + * @param options Options. + * @param options.width Width. + * @param options.height Height. + */ + public setWindowSize(options: { width?: number; height?: number }): void { + this.setViewport({ + width: options?.width, + height: options?.height + }); + } + + /** + * Sets the viewport. + * + * @param viewport Viewport. + */ + public setViewport(viewport: IBrowserPageViewport): void { + this.#browserFrame.setViewport(viewport); + } + + /** + * Sets the window width. + * + * @deprecated Use setViewport() instead. + * @param width Width. + */ + public setInnerWidth(width: number): void { + this.setViewport({ width }); + } + + /** + * Sets the window height. + * + * @deprecated Use setViewport() instead. + * @param height Height. + */ + public setInnerHeight(height: number): void { + this.setViewport({ height }); + } +} From e2cc329c675fcdc69f38dbbea290400b4322c2c6 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 19 Oct 2023 22:38:28 +0200 Subject: [PATCH 09/63] #466@trivial: Continues on implementation. --- packages/happy-dom/src/config/ElementTag.ts | 283 ++++++------ .../happy-dom/src/nodes/document/Document.ts | 39 +- packages/happy-dom/src/window/Window.ts | 368 ++++++++------- .../src/window/WindowClassFactory.ts | 436 +++++++++++++----- .../src/xml-http-request/XMLHttpRequest.ts | 90 ++-- 5 files changed, 697 insertions(+), 519 deletions(-) diff --git a/packages/happy-dom/src/config/ElementTag.ts b/packages/happy-dom/src/config/ElementTag.ts index d82fc5f84..dbcb19d98 100644 --- a/packages/happy-dom/src/config/ElementTag.ts +++ b/packages/happy-dom/src/config/ElementTag.ts @@ -1,155 +1,130 @@ -import HTMLElement from '../nodes/html-element/HTMLElement.js'; -import HTMLTemplateElement from '../nodes/html-template-element/HTMLTemplateElement.js'; -import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js'; -import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; -import HTMLTextAreaElement from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; -import SVGSVGElement from '../nodes/svg-element/SVGSVGElement.js'; -import SVGElement from '../nodes/svg-element/SVGElement.js'; -import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js'; -import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; -import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement.js'; -import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement.js'; -import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js'; -import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js'; -import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement.js'; -import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js'; -import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement.js'; -import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; -import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; -import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; -import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; -import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; -import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement.js'; -import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; -import HTMLIFrameElement from '../nodes/html-iframe-element/HTMLIFrameElement.js'; - -export default { - A: HTMLAnchorElement, - ABBR: HTMLElement, - ADDRESS: HTMLElement, - AREA: HTMLElement, - ARTICLE: HTMLElement, - ASIDE: HTMLElement, - AUDIO: HTMLAudioElement, - B: HTMLElement, - BASE: HTMLBaseElement, - BDI: HTMLElement, - BDO: HTMLElement, - BLOCKQUAOTE: HTMLElement, - BODY: HTMLElement, - TEMPLATE: HTMLTemplateElement, - FORM: HTMLFormElement, - INPUT: HTMLInputElement, - TEXTAREA: HTMLTextAreaElement, - SCRIPT: HTMLScriptElement, - IMG: HTMLImageElement, - LINK: HTMLLinkElement, - STYLE: HTMLStyleElement, - LABEL: HTMLLabelElement, - SLOT: HTMLSlotElement, - SVG: SVGSVGElement, - G: SVGElement, - CIRCLE: SVGElement, - ELLIPSE: SVGElement, - LINE: SVGElement, - PATH: SVGElement, - POLYGON: SVGElement, - POLYLINE: SVGElement, - RECT: SVGElement, - STOP: SVGElement, - USE: SVGElement, - META: HTMLMetaElement, - BLOCKQUOTE: HTMLElement, - BR: HTMLElement, - BUTTON: HTMLButtonElement, - CANVAS: HTMLElement, - CAPTION: HTMLElement, - CITE: HTMLElement, - CODE: HTMLElement, - COL: HTMLElement, - COLGROUP: HTMLElement, - DATA: HTMLElement, - DATALIST: HTMLElement, - DD: HTMLElement, - DEL: HTMLElement, - DETAILS: HTMLElement, - DFN: HTMLElement, - DIALOG: HTMLDialogElement, - DIV: HTMLElement, - DL: HTMLElement, - DT: HTMLElement, - EM: HTMLElement, - EMBED: HTMLElement, - FIELDSET: HTMLElement, - FIGCAPTION: HTMLElement, - FIGURE: HTMLElement, - FOOTER: HTMLElement, - H1: HTMLElement, - H2: HTMLElement, - H3: HTMLElement, - H4: HTMLElement, - H5: HTMLElement, - H6: HTMLElement, - HEAD: HTMLElement, - HEADER: HTMLElement, - HGROUP: HTMLElement, - HR: HTMLElement, - HTML: HTMLElement, - I: HTMLElement, - IFRAME: HTMLIFrameElement, - INS: HTMLElement, - KBD: HTMLElement, - LEGEND: HTMLElement, - LI: HTMLElement, - MAIN: HTMLElement, - MAP: HTMLElement, - MARK: HTMLElement, - MATH: HTMLElement, - MENU: HTMLElement, - MENUITEM: HTMLElement, - METER: HTMLElement, - NAV: HTMLElement, - NOSCRIPT: HTMLElement, - OBJECT: HTMLElement, - OL: HTMLElement, - OPTGROUP: HTMLOptGroupElement, - OPTION: HTMLOptionElement, - OUTPUT: HTMLElement, - P: HTMLElement, - PARAM: HTMLElement, - PICTURE: HTMLElement, - PRE: HTMLElement, - PROGRESS: HTMLElement, - Q: HTMLElement, - RB: HTMLElement, - RP: HTMLElement, - RT: HTMLElement, - RTC: HTMLElement, - RUBY: HTMLElement, - S: HTMLElement, - SAMP: HTMLElement, - SECTION: HTMLElement, - SELECT: HTMLSelectElement, - SMALL: HTMLElement, - SOURCE: HTMLElement, - SPAN: HTMLElement, - STRONG: HTMLElement, - SUB: HTMLElement, - SUMMARY: HTMLElement, - SUP: HTMLElement, - TABLE: HTMLElement, - TBODY: HTMLElement, - TD: HTMLElement, - TFOOT: HTMLElement, - TH: HTMLElement, - THEAD: HTMLElement, - TIME: HTMLElement, - TITLE: HTMLElement, - TR: HTMLElement, - TRACK: HTMLElement, - U: HTMLElement, - UL: HTMLElement, - VAR: HTMLElement, - VIDEO: HTMLVideoElement, - WBR: HTMLElement +export default <{ [key: string]: string }>{ + A: 'HTMLAnchorElement', + ABBR: 'HTMLElement', + ADDRESS: 'HTMLElement', + AREA: 'HTMLElement', + ARTICLE: 'HTMLElement', + ASIDE: 'HTMLElement', + AUDIO: 'HTMLAudioElement', + B: 'HTMLElement', + BASE: 'HTMLBaseElement', + BDI: 'HTMLElement', + BDO: 'HTMLElement', + BLOCKQUAOTE: 'HTMLElement', + BODY: 'HTMLElement', + TEMPLATE: 'HTMLTemplateElement', + FORM: 'HTMLFormElement', + INPUT: 'HTMLInputElement', + TEXTAREA: 'HTMLTextAreaElement', + SCRIPT: 'HTMLScriptElement', + IMG: 'HTMLImageElement', + LINK: 'HTMLLinkElement', + STYLE: 'HTMLStyleElement', + LABEL: 'HTMLLabelElement', + SLOT: 'HTMLSlotElement', + SVG: 'SVGSVGElement', + G: 'SVGElement', + CIRCLE: 'SVGElement', + ELLIPSE: 'SVGElement', + LINE: 'SVGElement', + PATH: 'SVGElement', + POLYGON: 'SVGElement', + POLYLINE: 'SVGElement', + RECT: 'SVGElement', + STOP: 'SVGElement', + USE: 'SVGElement', + META: 'HTMLMetaElement', + BLOCKQUOTE: 'HTMLElement', + BR: 'HTMLElement', + BUTTON: 'HTMLButtonElement', + CANVAS: 'HTMLElement', + CAPTION: 'HTMLElement', + CITE: 'HTMLElement', + CODE: 'HTMLElement', + COL: 'HTMLElement', + COLGROUP: 'HTMLElement', + DATA: 'HTMLElement', + DATALIST: 'HTMLElement', + DD: 'HTMLElement', + DEL: 'HTMLElement', + DETAILS: 'HTMLElement', + DFN: 'HTMLElement', + DIALOG: 'HTMLDialogElement', + DIV: 'HTMLElement', + DL: 'HTMLElement', + DT: 'HTMLElement', + EM: 'HTMLElement', + EMBED: 'HTMLElement', + FIELDSET: 'HTMLElement', + FIGCAPTION: 'HTMLElement', + FIGURE: 'HTMLElement', + FOOTER: 'HTMLElement', + H1: 'HTMLElement', + H2: 'HTMLElement', + H3: 'HTMLElement', + H4: 'HTMLElement', + H5: 'HTMLElement', + H6: 'HTMLElement', + HEAD: 'HTMLElement', + HEADER: 'HTMLElement', + HGROUP: 'HTMLElement', + HR: 'HTMLElement', + HTML: 'HTMLElement', + I: 'HTMLElement', + IFRAME: 'HTMLIFrameElement', + INS: 'HTMLElement', + KBD: 'HTMLElement', + LEGEND: 'HTMLElement', + LI: 'HTMLElement', + MAIN: 'HTMLElement', + MAP: 'HTMLElement', + MARK: 'HTMLElement', + MATH: 'HTMLElement', + MENU: 'HTMLElement', + MENUITEM: 'HTMLElement', + METER: 'HTMLElement', + NAV: 'HTMLElement', + NOSCRIPT: 'HTMLElement', + OBJECT: 'HTMLElement', + OL: 'HTMLElement', + OPTGROUP: 'HTMLOptGroupElement', + OPTION: 'HTMLOptionElement', + OUTPUT: 'HTMLElement', + P: 'HTMLElement', + PARAM: 'HTMLElement', + PICTURE: 'HTMLElement', + PRE: 'HTMLElement', + PROGRESS: 'HTMLElement', + Q: 'HTMLElement', + RB: 'HTMLElement', + RP: 'HTMLElement', + RT: 'HTMLElement', + RTC: 'HTMLElement', + RUBY: 'HTMLElement', + S: 'HTMLElement', + SAMP: 'HTMLElement', + SECTION: 'HTMLElement', + SELECT: 'HTMLSelectElement', + SMALL: 'HTMLElement', + SOURCE: 'HTMLElement', + SPAN: 'HTMLElement', + STRONG: 'HTMLElement', + SUB: 'HTMLElement', + SUMMARY: 'HTMLElement', + SUP: 'HTMLElement', + TABLE: 'HTMLElement', + TBODY: 'HTMLElement', + TD: 'HTMLElement', + TFOOT: 'HTMLElement', + TH: 'HTMLElement', + THEAD: 'HTMLElement', + TIME: 'HTMLElement', + TITLE: 'HTMLElement', + TR: 'HTMLElement', + TRACK: 'HTMLElement', + U: 'HTMLElement', + UL: 'HTMLElement', + VAR: 'HTMLElement', + VIDEO: 'HTMLVideoElement', + WBR: 'HTMLElement' }; diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 5548245f5..1450b773e 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -43,7 +43,6 @@ import Range from '../../range/Range.js'; import IHTMLBaseElement from '../html-base-element/IHTMLBaseElement.js'; import IAttr from '../attr/IAttr.js'; import IProcessingInstruction from '../processing-instruction/IProcessingInstruction.js'; -import ProcessingInstruction from '../processing-instruction/ProcessingInstruction.js'; import ElementUtility from '../element/ElementUtility.js'; import HTMLCollection from '../element/HTMLCollection.js'; import VisibilityStateEnum from './VisibilityStateEnum.js'; @@ -55,8 +54,6 @@ const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; * Document. */ export default class Document extends Node implements IDocument { - public static _defaultView: IWindow = null; - public static _windowClass: {} | null = null; public nodeType = Node.DOCUMENT_NODE; public adoptedStyleSheets: CSSStyleSheet[] = []; public implementation: DOMImplementation; @@ -64,6 +61,7 @@ export default class Document extends Node implements IDocument { public readonly isConnected: boolean = true; public readonly defaultView: IWindow; public readonly referrer = ''; + public readonly ownerDocument: IDocument | null = null; public readonly _windowClass: {} | null = null; public readonly _readyStateManager: DocumentReadyStateManager; public readonly _children: IHTMLCollection = new HTMLCollection(); @@ -199,10 +197,8 @@ export default class Document extends Node implements IDocument { constructor() { super(); - this.defaultView = (this.constructor)._defaultView; this.implementation = new DOMImplementation(this); - this._windowClass = (this.constructor)._windowClass; this._readyStateManager = new DocumentReadyStateManager(this.defaultView); this._rootNode = this; @@ -633,8 +629,6 @@ export default class Document extends Node implements IDocument { * @returns Cloned node. */ public cloneNode(deep = false): IDocument { - (this.constructor)._defaultView = this.defaultView; - const clone = super.cloneNode(deep); if (deep) { @@ -827,20 +821,19 @@ export default class Document extends Node implements IDocument { } const elementClass: typeof Element = - customElementClass || ElementTag[tagName] || HTMLUnknownElement; - - elementClass._ownerDocument = this; + customElementClass || this.defaultView[ElementTag[tagName]] || HTMLUnknownElement; const element = new elementClass(); element.tagName = tagName; + + // TODO: Should not be necessary as the class should already extend the class created by WindowClassFactory? (element.ownerDocument) = this; + (element.namespaceURI) = namespaceURI; if (element instanceof Element && options && options.is) { element._isValue = String(options.is); } - elementClass._ownerDocument = null; - return element; } @@ -853,10 +846,7 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createTextNode(data?: string): IText { - Text._ownerDocument = this; - const text = new Text(data); - Text._ownerDocument = null; - return text; + return new this.defaultView.Text(data); } /** @@ -866,10 +856,7 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createComment(data?: string): IComment { - Comment._ownerDocument = this; - const comment = new Comment(data); - Comment._ownerDocument = null; - return comment; + return new this.defaultView.Comment(data); } /** @@ -878,10 +865,7 @@ export default class Document extends Node implements IDocument { * @returns Document fragment. */ public createDocumentFragment(): IDocumentFragment { - DocumentFragment._ownerDocument = this; - const fragment = new DocumentFragment(); - DocumentFragment._ownerDocument = null; - return fragment; + return new this.defaultView.DocumentFragment(); } /** @@ -942,8 +926,7 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public createAttributeNS(namespaceURI: string, qualifiedName: string): IAttr { - Attr._ownerDocument = this; - const attribute = new Attr(); + const attribute = new this.defaultView.Attr(); attribute.namespaceURI = namespaceURI; attribute.name = qualifiedName; return attribute; @@ -1039,10 +1022,8 @@ export default class Document extends Node implements IDocument { `Failed to execute 'createProcessingInstruction' on 'Document': The data provided ('?>') contains '?>'` ); } - ProcessingInstruction._ownerDocument = this; - const processingInstruction = new ProcessingInstruction(data); + const processingInstruction = new this.defaultView.ProcessingInstruction(data); processingInstruction.target = target; - ProcessingInstruction._ownerDocument = null; return processingInstruction; } } diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index b696c340d..d9cba2449 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -23,7 +23,6 @@ import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js'; import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement.js'; import HTMLMediaElement from '../nodes/html-media-element/HTMLMediaElement.js'; import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; -import AudioImplementation from '../nodes/html-audio-element/Audio.js'; import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement.js'; import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js'; import HTMLIFrameElement from '../nodes/html-iframe-element/HTMLIFrameElement.js'; @@ -33,8 +32,6 @@ import SVGElement from '../nodes/svg-element/SVGElement.js'; import SVGGraphicsElement from '../nodes/svg-element/SVGGraphicsElement.js'; import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js'; import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; -import ImageImplementation from '../nodes/html-image-element/Image.js'; -import DocumentFragmentImplementation from '../nodes/document-fragment/DocumentFragment.js'; import CharacterData from '../nodes/character-data/CharacterData.js'; import NodeIterator from '../tree-walker/NodeIterator.js'; import TreeWalker from '../tree-walker/TreeWalker.js'; @@ -52,13 +49,11 @@ import URL from '../url/URL.js'; import Location from '../location/Location.js'; import MutationObserver from '../mutation-observer/MutationObserver.js'; import MutationRecord from '../mutation-observer/MutationRecord.js'; -import DOMParserImplementation from '../dom-parser/DOMParser.js'; import XMLSerializer from '../xml-serializer/XMLSerializer.js'; import ResizeObserver from '../resize-observer/ResizeObserver.js'; import Blob from '../file/Blob.js'; import File from '../file/File.js'; import DOMException from '../exception/DOMException.js'; -import FileReaderImplementation from '../file/FileReader.js'; import History from '../history/History.js'; import CSSStyleSheet from '../css/CSSStyleSheet.js'; import CSSStyleDeclaration from '../css/declaration/CSSStyleDeclaration.js'; @@ -85,10 +80,9 @@ import ErrorEvent from '../event/events/ErrorEvent.js'; import StorageEvent from '../event/events/StorageEvent.js'; import SubmitEvent from '../event/events/SubmitEvent.js'; import Screen from '../screen/Screen.js'; +import Response from '../fetch/Response.js'; import IResponse from '../fetch/types/IResponse.js'; import IRequestInit from '../fetch/types/IRequestInit.js'; -import RequestImplementation from '../fetch/Request.js'; -import ResponseImplementation from '../fetch/Response.js'; import Storage from '../storage/Storage.js'; import IWindow from './IWindow.js'; import HTMLCollection from '../nodes/element/HTMLCollection.js'; @@ -102,18 +96,15 @@ import MimeTypeArray from '../navigator/MimeTypeArray.js'; import Plugin from '../navigator/Plugin.js'; import PluginArray from '../navigator/PluginArray.js'; import Fetch from '../fetch/Fetch.js'; -import RangeImplementation from '../range/Range.js'; import DOMRect from '../nodes/element/DOMRect.js'; import VMGlobalPropertyScript from './VMGlobalPropertyScript.js'; import * as PerfHooks from 'perf_hooks'; import VM from 'vm'; import { Buffer } from 'buffer'; import { webcrypto } from 'crypto'; -import XMLHttpRequestImplementation from '../xml-http-request/XMLHttpRequest.js'; import XMLHttpRequestUpload from '../xml-http-request/XMLHttpRequestUpload.js'; import XMLHttpRequestEventTarget from '../xml-http-request/XMLHttpRequestEventTarget.js'; import Base64 from '../base64/Base64.js'; -import IDocument from '../nodes/document/IDocument.js'; import Attr from '../nodes/attr/Attr.js'; import NamedNodeMap from '../named-node-map/NamedNodeMap.js'; import IElement from '../nodes/element/IElement.js'; @@ -139,8 +130,16 @@ import ClipboardEvent from '../event/events/ClipboardEvent.js'; import HappyDOMWindowAPI from './HappyDOMWindowAPI.js'; import BrowserFrame from '../browser/BrowserFrame.js'; import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import Headers from '../fetch/Headers.js'; +import WindowClassFactory from './WindowClassFactory.js'; +import Audio from '../nodes/html-audio-element/Audio.js'; +import Image from '../nodes/html-image-element/Image.js'; +import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; +import DOMParser from '../dom-parser/DOMParser.js'; +import FileReader from '../file/FileReader.js'; +import Request from '../fetch/Request.js'; +import Range from '../range/Range.js'; +import XMLHttpRequest from '../xml-http-request/XMLHttpRequest.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -159,94 +158,94 @@ export default class Window extends EventTarget implements IWindow { public readonly happyDOM: HappyDOMWindowAPI; // Nodes - public readonly Node = Node; - public readonly Attr = Attr; - public readonly SVGSVGElement = SVGSVGElement; - public readonly SVGElement = SVGElement; - public readonly SVGGraphicsElement = SVGGraphicsElement; - public readonly Text = Text; - public readonly Comment = Comment; - public readonly ShadowRoot = ShadowRoot; - public readonly ProcessingInstruction = ProcessingInstruction; - public readonly Element = Element; - public readonly CharacterData = CharacterData; - public readonly Document = Document; - public readonly HTMLDocument = HTMLDocument; - public readonly XMLDocument = XMLDocument; - public readonly SVGDocument = SVGDocument; + public readonly Node: typeof Node; + public readonly Attr: typeof Attr; + public readonly SVGSVGElement: typeof SVGSVGElement; + public readonly SVGElement: typeof SVGElement; + public readonly SVGGraphicsElement: typeof SVGGraphicsElement; + public readonly Text: typeof Text; + public readonly Comment: typeof Comment; + public readonly ShadowRoot: typeof ShadowRoot; + public readonly ProcessingInstruction: typeof ProcessingInstruction; + public readonly Element: typeof Element; + public readonly CharacterData: typeof CharacterData; + public readonly Document: typeof Document; + public readonly HTMLDocument: typeof HTMLDocument; + public readonly XMLDocument: typeof XMLDocument; + public readonly SVGDocument: typeof SVGDocument; // Element classes - public readonly HTMLElement = HTMLElement; - public readonly HTMLUnknownElement = HTMLUnknownElement; - public readonly HTMLTemplateElement = HTMLTemplateElement; - public readonly HTMLFormElement = HTMLFormElement; - public readonly HTMLInputElement = HTMLInputElement; - public readonly HTMLSelectElement = HTMLSelectElement; - public readonly HTMLTextAreaElement = HTMLTextAreaElement; - public readonly HTMLImageElement = HTMLImageElement; - public readonly HTMLScriptElement = HTMLScriptElement; - public readonly HTMLLinkElement = HTMLLinkElement; - public readonly HTMLStyleElement = HTMLStyleElement; - public readonly HTMLLabelElement = HTMLLabelElement; - public readonly HTMLSlotElement = HTMLSlotElement; - public readonly HTMLMetaElement = HTMLMetaElement; - public readonly HTMLMediaElement = HTMLMediaElement; - public readonly HTMLAudioElement = HTMLAudioElement; - public readonly HTMLVideoElement = HTMLVideoElement; - public readonly HTMLBaseElement = HTMLBaseElement; - public readonly HTMLIFrameElement = HTMLIFrameElement; - public readonly HTMLDialogElement = HTMLDialogElement; + public readonly HTMLElement: typeof HTMLElement; + public readonly HTMLUnknownElement: typeof HTMLUnknownElement; + public readonly HTMLTemplateElement: typeof HTMLTemplateElement; + public readonly HTMLFormElement: typeof HTMLFormElement; + public readonly HTMLInputElement: typeof HTMLInputElement; + public readonly HTMLSelectElement: typeof HTMLSelectElement; + public readonly HTMLTextAreaElement: typeof HTMLTextAreaElement; + public readonly HTMLImageElement: typeof HTMLImageElement; + public readonly HTMLScriptElement: typeof HTMLScriptElement; + public readonly HTMLLinkElement: typeof HTMLLinkElement; + public readonly HTMLStyleElement: typeof HTMLStyleElement; + public readonly HTMLLabelElement: typeof HTMLLabelElement; + public readonly HTMLSlotElement: typeof HTMLSlotElement; + public readonly HTMLMetaElement: typeof HTMLMetaElement; + public readonly HTMLMediaElement: typeof HTMLMediaElement; + public readonly HTMLAudioElement: typeof HTMLAudioElement; + public readonly HTMLVideoElement: typeof HTMLVideoElement; + public readonly HTMLBaseElement: typeof HTMLBaseElement; + public readonly HTMLIFrameElement: typeof HTMLIFrameElement; + public readonly HTMLDialogElement: typeof HTMLDialogElement; // Non-implemented element classes - public readonly HTMLHeadElement = HTMLElement; - public readonly HTMLTitleElement = HTMLElement; - public readonly HTMLBodyElement = HTMLElement; - public readonly HTMLHeadingElement = HTMLElement; - public readonly HTMLParagraphElement = HTMLElement; - public readonly HTMLHRElement = HTMLElement; - public readonly HTMLPreElement = HTMLElement; - public readonly HTMLUListElement = HTMLElement; - public readonly HTMLOListElement = HTMLElement; - public readonly HTMLLIElement = HTMLElement; - public readonly HTMLMenuElement = HTMLElement; - public readonly HTMLDListElement = HTMLElement; - public readonly HTMLDivElement = HTMLElement; - public readonly HTMLAnchorElement = HTMLElement; - public readonly HTMLAreaElement = HTMLElement; - public readonly HTMLBRElement = HTMLElement; - public readonly HTMLButtonElement = HTMLElement; - public readonly HTMLCanvasElement = HTMLElement; - public readonly HTMLDataElement = HTMLElement; - public readonly HTMLDataListElement = HTMLElement; - public readonly HTMLDetailsElement = HTMLElement; - public readonly HTMLDirectoryElement = HTMLElement; - public readonly HTMLFieldSetElement = HTMLElement; - public readonly HTMLFontElement = HTMLElement; - public readonly HTMLHtmlElement = HTMLElement; - public readonly HTMLLegendElement = HTMLElement; - public readonly HTMLMapElement = HTMLElement; - public readonly HTMLMarqueeElement = HTMLElement; - public readonly HTMLMeterElement = HTMLElement; - public readonly HTMLModElement = HTMLElement; - public readonly HTMLOutputElement = HTMLElement; - public readonly HTMLPictureElement = HTMLElement; - public readonly HTMLProgressElement = HTMLElement; - public readonly HTMLQuoteElement = HTMLElement; - public readonly HTMLSourceElement = HTMLElement; - public readonly HTMLSpanElement = HTMLElement; - public readonly HTMLTableCaptionElement = HTMLElement; - public readonly HTMLTableCellElement = HTMLElement; - public readonly HTMLTableColElement = HTMLElement; - public readonly HTMLTableElement = HTMLElement; - public readonly HTMLTimeElement = HTMLElement; - public readonly HTMLTableRowElement = HTMLElement; - public readonly HTMLTableSectionElement = HTMLElement; - public readonly HTMLFrameElement = HTMLElement; - public readonly HTMLFrameSetElement = HTMLElement; - public readonly HTMLEmbedElement = HTMLElement; - public readonly HTMLObjectElement = HTMLElement; - public readonly HTMLParamElement = HTMLElement; - public readonly HTMLTrackElement = HTMLElement; + public readonly HTMLHeadElement: typeof HTMLElement; + public readonly HTMLTitleElement: typeof HTMLElement; + public readonly HTMLBodyElement: typeof HTMLElement; + public readonly HTMLHeadingElement: typeof HTMLElement; + public readonly HTMLParagraphElement: typeof HTMLElement; + public readonly HTMLHRElement: typeof HTMLElement; + public readonly HTMLPreElement: typeof HTMLElement; + public readonly HTMLUListElement: typeof HTMLElement; + public readonly HTMLOListElement: typeof HTMLElement; + public readonly HTMLLIElement: typeof HTMLElement; + public readonly HTMLMenuElement: typeof HTMLElement; + public readonly HTMLDListElement: typeof HTMLElement; + public readonly HTMLDivElement: typeof HTMLElement; + public readonly HTMLAnchorElement: typeof HTMLElement; + public readonly HTMLAreaElement: typeof HTMLElement; + public readonly HTMLBRElement: typeof HTMLElement; + public readonly HTMLButtonElement: typeof HTMLElement; + public readonly HTMLCanvasElement: typeof HTMLElement; + public readonly HTMLDataElement: typeof HTMLElement; + public readonly HTMLDataListElement: typeof HTMLElement; + public readonly HTMLDetailsElement: typeof HTMLElement; + public readonly HTMLDirectoryElement: typeof HTMLElement; + public readonly HTMLFieldSetElement: typeof HTMLElement; + public readonly HTMLFontElement: typeof HTMLElement; + public readonly HTMLHtmlElement: typeof HTMLElement; + public readonly HTMLLegendElement: typeof HTMLElement; + public readonly HTMLMapElement: typeof HTMLElement; + public readonly HTMLMarqueeElement: typeof HTMLElement; + public readonly HTMLMeterElement: typeof HTMLElement; + public readonly HTMLModElement: typeof HTMLElement; + public readonly HTMLOutputElement: typeof HTMLElement; + public readonly HTMLPictureElement: typeof HTMLElement; + public readonly HTMLProgressElement: typeof HTMLElement; + public readonly HTMLQuoteElement: typeof HTMLElement; + public readonly HTMLSourceElement: typeof HTMLElement; + public readonly HTMLSpanElement: typeof HTMLElement; + public readonly HTMLTableCaptionElement: typeof HTMLElement; + public readonly HTMLTableCellElement: typeof HTMLElement; + public readonly HTMLTableColElement: typeof HTMLElement; + public readonly HTMLTableElement: typeof HTMLElement; + public readonly HTMLTimeElement: typeof HTMLElement; + public readonly HTMLTableRowElement: typeof HTMLElement; + public readonly HTMLTableSectionElement: typeof HTMLElement; + public readonly HTMLFrameElement: typeof HTMLElement; + public readonly HTMLFrameSetElement: typeof HTMLElement; + public readonly HTMLEmbedElement: typeof HTMLElement; + public readonly HTMLObjectElement: typeof HTMLElement; + public readonly HTMLParamElement: typeof HTMLElement; + public readonly HTMLTrackElement: typeof HTMLElement; // Events classes public readonly Event = Event; @@ -358,8 +357,8 @@ export default class Window extends EventTarget implements IWindow { public readonly RadioNodeList: typeof RadioNodeList; public readonly ValidityState: typeof ValidityState; public readonly Headers: typeof Headers; - public readonly Request: typeof RequestImplementation; - public readonly Response: typeof ResponseImplementation; + public readonly Request: typeof Request; + public readonly Response: typeof Response; public readonly XMLHttpRequestUpload = XMLHttpRequestUpload; public readonly XMLHttpRequestEventTarget = XMLHttpRequestEventTarget; public readonly ReadableStream = Stream.Readable; @@ -372,13 +371,13 @@ export default class Window extends EventTarget implements IWindow { public readonly PermissionStatus = PermissionStatus; public readonly Clipboard = Clipboard; public readonly ClipboardItem = ClipboardItem; - public readonly XMLHttpRequest: { new (): XMLHttpRequestImplementation }; - public readonly DOMParser: typeof DOMParserImplementation; - public readonly Range; - public readonly FileReader; - public readonly Image; - public readonly DocumentFragment; - public readonly Audio; + public readonly XMLHttpRequest: { new (): XMLHttpRequest }; + public readonly DOMParser: typeof DOMParser; + public readonly Range: typeof Range; + public readonly FileReader: typeof FileReader; + public readonly Image: typeof Image; + public readonly DocumentFragment: typeof DocumentFragment; + public readonly Audio: typeof Audio; // Events public onload: ((event: Event) => void) | null = null; @@ -581,68 +580,113 @@ export default class Window extends EventTarget implements IWindow { } } - HTMLDocument._defaultView = this; - HTMLDocument._windowClass = Window; + const classes = WindowClassFactory.getClasses({ + window: this, + browserFrame: this.#browserFrame + }); - const document = new HTMLDocument(); - const browserFrame = this.#browserFrame; + // Classes that require the window to be injected + this.Response = classes.Response; + this.Request = classes.Request; + this.Image = classes.Image; + this.DocumentFragment = classes.DocumentFragment; + this.FileReader = classes.FileReader; + this.DOMParser = classes.DOMParser; + this.XMLHttpRequest = classes.XMLHttpRequest; + this.Range = classes.Range; + this.Audio = classes.Audio; - this.document = document; + // Nodes + this.Node = classes.Node; + this.Attr = classes.Attr; + this.SVGSVGElement = classes.SVGSVGElement; + this.SVGElement = classes.SVGElement; + this.SVGGraphicsElement = classes.SVGGraphicsElement; + this.Text = classes.Text; + this.Comment = classes.Comment; + this.ShadowRoot = classes.ShadowRoot; + this.ProcessingInstruction = classes.ProcessingInstruction; + this.Element = classes.Element; + this.CharacterData = classes.CharacterData; + this.Document = classes.Document; + this.HTMLDocument = classes.HTMLDocument; + this.XMLDocument = classes.XMLDocument; + this.SVGDocument = classes.SVGDocument; - // We need to set the correct owner document when the class is constructed. - // To achieve this we will extend the original implementation with a class that sets the owner document. + // HTML Element classes + this.HTMLElement = classes.HTMLElement; + this.HTMLUnknownElement = classes.HTMLUnknownElement; + this.HTMLTemplateElement = classes.HTMLTemplateElement; + this.HTMLFormElement = classes.HTMLFormElement; + this.HTMLInputElement = classes.HTMLInputElement; + this.HTMLSelectElement = classes.HTMLSelectElement; + this.HTMLTextAreaElement = classes.HTMLTextAreaElement; + this.HTMLImageElement = classes.HTMLImageElement; + this.HTMLScriptElement = classes.HTMLScriptElement; + this.HTMLLinkElement = classes.HTMLLinkElement; + this.HTMLStyleElement = classes.HTMLStyleElement; + this.HTMLLabelElement = classes.HTMLLabelElement; + this.HTMLSlotElement = classes.HTMLSlotElement; + this.HTMLMetaElement = classes.HTMLMetaElement; + this.HTMLMediaElement = classes.HTMLMediaElement; + this.HTMLAudioElement = classes.HTMLAudioElement; + this.HTMLVideoElement = classes.HTMLVideoElement; + this.HTMLBaseElement = classes.HTMLBaseElement; + this.HTMLIFrameElement = classes.HTMLIFrameElement; + this.HTMLDialogElement = classes.HTMLDialogElement; - /* eslint-disable jsdoc/require-jsdoc */ - class Response extends ResponseImplementation { - protected readonly _asyncTaskManager: AsyncTaskManager = browserFrame._asyncTaskManager; - } - class Request extends RequestImplementation { - protected readonly _ownerDocument: IDocument = document; - protected readonly _asyncTaskManager: AsyncTaskManager = browserFrame._asyncTaskManager; - } - class Image extends ImageImplementation { - public readonly ownerDocument: IDocument = document; - } - class DocumentFragment extends DocumentFragmentImplementation { - public readonly ownerDocument: IDocument = document; - } - class FileReader extends FileReaderImplementation { - public readonly _ownerDocument: IDocument = document; - } - class DOMParser extends DOMParserImplementation { - public readonly _ownerDocument: IDocument = document; - } - class XMLHttpRequest extends XMLHttpRequestImplementation { - constructor() { - super({ - ownerDocument: document, - asyncTaskManager: browserFrame._asyncTaskManager, - browserSettings: { - enableFileSystemHttpRequests: - browserFrame instanceof BrowserFrame - ? browserFrame.page.context.browser.settings.enableFileSystemHttpRequests - : browserFrame.settings.enableFileSystemHttpRequests - } - }); - } - } - class Range extends RangeImplementation { - public readonly ownerDocument: IDocument = document; - } - class Audio extends AudioImplementation { - public readonly ownerDocument: IDocument = document; - } - /* eslint-enable jsdoc/require-jsdoc */ - - this.Response = Response; - this.Request = Request; - this.Image = Image; - this.DocumentFragment = DocumentFragment; - this.FileReader = FileReader; - this.DOMParser = DOMParser; - this.XMLHttpRequest = XMLHttpRequest; - this.Range = Range; - this.Audio = Audio; + // Non-implemented HTML element classes + this.HTMLHeadElement = classes.HTMLElement; + this.HTMLTitleElement = classes.HTMLElement; + this.HTMLBodyElement = classes.HTMLElement; + this.HTMLHeadingElement = classes.HTMLElement; + this.HTMLParagraphElement = classes.HTMLElement; + this.HTMLHRElement = classes.HTMLElement; + this.HTMLPreElement = classes.HTMLElement; + this.HTMLUListElement = classes.HTMLElement; + this.HTMLOListElement = classes.HTMLElement; + this.HTMLLIElement = classes.HTMLElement; + this.HTMLMenuElement = classes.HTMLElement; + this.HTMLDListElement = classes.HTMLElement; + this.HTMLDivElement = classes.HTMLElement; + this.HTMLAnchorElement = classes.HTMLElement; + this.HTMLAreaElement = classes.HTMLElement; + this.HTMLBRElement = classes.HTMLElement; + this.HTMLButtonElement = classes.HTMLElement; + this.HTMLCanvasElement = classes.HTMLElement; + this.HTMLDataElement = classes.HTMLElement; + this.HTMLDataListElement = classes.HTMLElement; + this.HTMLDetailsElement = classes.HTMLElement; + this.HTMLDirectoryElement = classes.HTMLElement; + this.HTMLFieldSetElement = classes.HTMLElement; + this.HTMLFontElement = classes.HTMLElement; + this.HTMLHtmlElement = classes.HTMLElement; + this.HTMLLegendElement = classes.HTMLElement; + this.HTMLMapElement = classes.HTMLElement; + this.HTMLMarqueeElement = classes.HTMLElement; + this.HTMLMeterElement = classes.HTMLElement; + this.HTMLModElement = classes.HTMLElement; + this.HTMLOutputElement = classes.HTMLElement; + this.HTMLPictureElement = classes.HTMLElement; + this.HTMLProgressElement = classes.HTMLElement; + this.HTMLQuoteElement = classes.HTMLElement; + this.HTMLSourceElement = classes.HTMLElement; + this.HTMLSpanElement = classes.HTMLElement; + this.HTMLTableCaptionElement = classes.HTMLElement; + this.HTMLTableCellElement = classes.HTMLElement; + this.HTMLTableColElement = classes.HTMLElement; + this.HTMLTableElement = classes.HTMLElement; + this.HTMLTimeElement = classes.HTMLElement; + this.HTMLTableRowElement = classes.HTMLElement; + this.HTMLTableSectionElement = classes.HTMLElement; + this.HTMLFrameElement = classes.HTMLElement; + this.HTMLFrameSetElement = classes.HTMLElement; + this.HTMLEmbedElement = classes.HTMLElement; + this.HTMLObjectElement = classes.HTMLElement; + this.HTMLParamElement = classes.HTMLElement; + this.HTMLTrackElement = classes.HTMLElement; + + this.document = new this.HTMLDocument(); this._setupVMContext(); diff --git a/packages/happy-dom/src/window/WindowClassFactory.ts b/packages/happy-dom/src/window/WindowClassFactory.ts index 7beca9564..1d6a23dc2 100644 --- a/packages/happy-dom/src/window/WindowClassFactory.ts +++ b/packages/happy-dom/src/window/WindowClassFactory.ts @@ -1,146 +1,332 @@ -import IBrowserSettings from '../browser/IBrowserSettings.js'; -import IWindow from './IWindow.js'; -import Event from '../event/Event.js'; import BrowserFrame from '../browser/BrowserFrame.js'; import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; -import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; -import BrowserSettingsFactory from '../browser/BrowserSettingsFactory.js'; -import IReadOnlyBrowserSettings from '../browser/IReadOnlyBrowserSettings.js'; -import IBrowserPageViewport from '../browser/IBrowserPageViewport.js'; +import AudioImplementation from '../nodes/html-audio-element/Audio.js'; +import ImageImplementation from '../nodes/html-image-element/Image.js'; +import DocumentFragmentImplementation from '../nodes/document-fragment/DocumentFragment.js'; +import DOMParserImplementation from '../dom-parser/DOMParser.js'; +import FileReaderImplementation from '../file/FileReader.js'; +import RequestImplementation from '../fetch/Request.js'; +import ResponseImplementation from '../fetch/Response.js'; +import RangeImplementation from '../range/Range.js'; +import XMLHttpRequestImplementation from '../xml-http-request/XMLHttpRequest.js'; +import IWindow from './IWindow.js'; +import IDocument from '../nodes/document/IDocument.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import HTMLElementImplementation from '../nodes/html-element/HTMLElement.js'; +import HTMLUnknownElementImplementation from '../nodes/html-unknown-element/HTMLUnknownElement.js'; +import HTMLTemplateElementImplementation from '../nodes/html-template-element/HTMLTemplateElement.js'; +import HTMLFormElementImplementation from '../nodes/html-form-element/HTMLFormElement.js'; +import HTMLInputElementImplementation from '../nodes/html-input-element/HTMLInputElement.js'; +import HTMLSelectElementImplementation from '../nodes/html-select-element/HTMLSelectElement.js'; +import HTMLTextAreaElementImplementation from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; +import HTMLImageElementImplementation from '../nodes/html-image-element/HTMLImageElement.js'; +import HTMLScriptElementImplementation from '../nodes/html-script-element/HTMLScriptElement.js'; +import HTMLLinkElementImplementation from '../nodes/html-link-element/HTMLLinkElement.js'; +import HTMLStyleElementImplementation from '../nodes/html-style-element/HTMLStyleElement.js'; +import HTMLLabelElementImplementation from '../nodes/html-label-element/HTMLLabelElement.js'; +import HTMLSlotElementImplementation from '../nodes/html-slot-element/HTMLSlotElement.js'; +import HTMLMetaElementImplementation from '../nodes/html-meta-element/HTMLMetaElement.js'; +import HTMLMediaElementImplementation from '../nodes/html-media-element/HTMLMediaElement.js'; +import HTMLAudioElementImplementation from '../nodes/html-audio-element/HTMLAudioElement.js'; +import HTMLVideoElementImplementation from '../nodes/html-video-element/HTMLVideoElement.js'; +import HTMLBaseElementImplementation from '../nodes/html-base-element/HTMLBaseElement.js'; +import HTMLIFrameElementImplementation from '../nodes/html-iframe-element/HTMLIFrameElement.js'; +import HTMLDialogElementImplementation from '../nodes/html-dialog-element/HTMLDialogElement.js'; +import NodeImplementation from '../nodes/node/Node.js'; +import AttrImplementation from '../nodes/attr/Attr.js'; +import SVGSVGElementImplementation from '../nodes/svg-element/SVGSVGElement.js'; +import SVGElementImplementation from '../nodes/svg-element/SVGElement.js'; +import SVGGraphicsElementImplementation from '../nodes/svg-element/SVGGraphicsElement.js'; +import TextImplementation from '../nodes/text/Text.js'; +import CommentImplementation from '../nodes/comment/Comment.js'; +import ShadowRootImplementation from '../nodes/shadow-root/ShadowRoot.js'; +import ProcessingInstructionImplementation from '../nodes/processing-instruction/ProcessingInstruction.js'; +import ElementImplementation from '../nodes/element/Element.js'; +import CharacterDataImplementation from '../nodes/character-data/CharacterData.js'; +import DocumentImplementation from '../nodes/document/Document.js'; +import HTMLDocumentImplementation from '../nodes/html-document/HTMLDocument.js'; +import XMLDocumentImplementation from '../nodes/xml-document/XMLDocument.js'; +import SVGDocumentImplementation from '../nodes/svg-document/SVGDocument.js'; /** - * API for detached windows to be able to access features of the owner window. + * Some classes need to get access to the window object without having a reference to the window in the constructor. + * This factory will extend classes with a class that has a reference to the window. */ -export default class HappyDOMWindowAPI { - #window: IWindow; - #browserFrame?: BrowserFrame | DetachedBrowserFrame; - #settings: IBrowserSettings | null = null; - +export default class WindowClassFactory { /** - * Constructor. + * Returns classes for the given window. * - * @param options Options. - * @param options.window Owner window. - * @param options.browserFrame Browser frame. + * @param properties Properties. + * @param properties.window Window. + * @param properties.browserFrame Browser frame. + * @returns Classes. */ - constructor(options: { window: IWindow; browserFrame: BrowserFrame | DetachedBrowserFrame }) { - this.#window = options.window; - this.#browserFrame = options.browserFrame; - } + public static getClasses(properties: { + window: IWindow; + browserFrame: BrowserFrame | DetachedBrowserFrame; + }): { + // Nodes + Node: typeof NodeImplementation; + Attr: typeof AttrImplementation; + SVGSVGElement: typeof SVGSVGElementImplementation; + SVGElement: typeof SVGElementImplementation; + SVGGraphicsElement: typeof SVGGraphicsElementImplementation; + Text: typeof TextImplementation; + Comment: typeof CommentImplementation; + ShadowRoot: typeof ShadowRootImplementation; + ProcessingInstruction: typeof ProcessingInstructionImplementation; + Element: typeof ElementImplementation; + CharacterData: typeof CharacterDataImplementation; + Document: typeof DocumentImplementation; + HTMLDocument: typeof HTMLDocumentImplementation; + XMLDocument: typeof XMLDocumentImplementation; + SVGDocument: typeof SVGDocumentImplementation; - /** - * Returns settings. - * - * @returns Settings. - */ - public get settings(): IReadOnlyBrowserSettings { - if (!this.#settings) { - this.#settings = BrowserSettingsFactory.getReadOnlySettings( - this.#browserFrame instanceof DetachedBrowserFrame - ? this.#browserFrame.settings - : this.#browserFrame.page.context.browser.settings - ); - } - return this.#settings; - } + // HTML Elements + HTMLElement: typeof HTMLElementImplementation; + HTMLUnknownElement: typeof HTMLUnknownElementImplementation; + HTMLTemplateElement: typeof HTMLTemplateElementImplementation; + HTMLFormElement: typeof HTMLFormElementImplementation; + HTMLInputElement: typeof HTMLInputElementImplementation; + HTMLSelectElement: typeof HTMLSelectElementImplementation; + HTMLTextAreaElement: typeof HTMLTextAreaElementImplementation; + HTMLImageElement: typeof HTMLImageElementImplementation; + HTMLScriptElement: typeof HTMLScriptElementImplementation; + HTMLLinkElement: typeof HTMLLinkElementImplementation; + HTMLStyleElement: typeof HTMLStyleElementImplementation; + HTMLLabelElement: typeof HTMLLabelElementImplementation; + HTMLSlotElement: typeof HTMLSlotElementImplementation; + HTMLMetaElement: typeof HTMLMetaElementImplementation; + HTMLMediaElement: typeof HTMLMediaElementImplementation; + HTMLAudioElement: typeof HTMLAudioElementImplementation; + HTMLVideoElement: typeof HTMLVideoElementImplementation; + HTMLBaseElement: typeof HTMLBaseElementImplementation; + HTMLIFrameElement: typeof HTMLIFrameElementImplementation; + HTMLDialogElement: typeof HTMLDialogElementImplementation; - /** - * Returns virtual console printer. - * - * @returns Virtual console printer. - */ - public get virtualConsolePrinter(): VirtualConsolePrinter { - if (this.#browserFrame instanceof DetachedBrowserFrame) { - return this.#browserFrame.virtualConsolePrinter; - } - return this.#browserFrame.page.virtualConsolePrinter; - } + // Other Classes + Response: typeof ResponseImplementation; + Request: typeof RequestImplementation; + XMLHttpRequest: new () => XMLHttpRequestImplementation; + Image: typeof ImageImplementation; + DocumentFragment: typeof DocumentFragmentImplementation; + FileReader: typeof FileReaderImplementation; + DOMParser: typeof DOMParserImplementation; + Range: typeof RangeImplementation; + Audio: typeof AudioImplementation; + } { + const browserSettings = + properties.browserFrame instanceof DetachedBrowserFrame + ? properties.browserFrame.settings + : properties.browserFrame.page.context.browser.settings; - /** - * Waits for all async tasks to complete. - * - * @deprecated Use whenComplete() instead. - * @returns Promise. - */ - public async whenAsyncComplete(): Promise { - return await this.whenComplete(); - } + /* eslint-disable jsdoc/require-jsdoc */ - /** - * Waits for all async tasks to complete. - * - * @returns Promise. - */ - public async whenComplete(): Promise { - return await this.#browserFrame.whenComplete(); - } + // Nodes + class Node extends NodeImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class Attr extends AttrImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class SVGSVGElement extends SVGSVGElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class SVGElement extends SVGElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class SVGGraphicsElement extends SVGGraphicsElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class Text extends TextImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class Comment extends CommentImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class ShadowRoot extends ShadowRootImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class ProcessingInstruction extends ProcessingInstructionImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class Element extends ElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class CharacterData extends CharacterDataImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class Document extends DocumentImplementation { + public readonly defaultView: IWindow = properties.window; + } + class HTMLDocument extends HTMLDocumentImplementation { + public readonly defaultView: IWindow = properties.window; + } + class XMLDocument extends XMLDocumentImplementation { + public readonly defaultView: IWindow = properties.window; + } + class SVGDocument extends SVGDocumentImplementation { + public readonly defaultView: IWindow = properties.window; + } - /** - * Aborts all async tasks. - * - * @deprecated Use abort() instead. - */ - public async cancelAsync(): Promise { - await this.abort(); - } + // HTML Elements + class Audio extends AudioImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class Image extends ImageImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class DocumentFragment extends DocumentFragmentImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLElement extends HTMLElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLUnknownElement extends HTMLUnknownElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLTemplateElement extends HTMLTemplateElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLFormElement extends HTMLFormElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLInputElement extends HTMLInputElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLSelectElement extends HTMLSelectElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLTextAreaElement extends HTMLTextAreaElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLImageElement extends HTMLImageElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLScriptElement extends HTMLScriptElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLLinkElement extends HTMLLinkElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLStyleElement extends HTMLStyleElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLLabelElement extends HTMLLabelElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLSlotElement extends HTMLSlotElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLMetaElement extends HTMLMetaElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLMediaElement extends HTMLMediaElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLAudioElement extends HTMLAudioElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLVideoElement extends HTMLVideoElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLBaseElement extends HTMLBaseElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLIFrameElement extends HTMLIFrameElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } + class HTMLDialogElement extends HTMLDialogElementImplementation { + public readonly ownerDocument: IDocument = properties.window.document; + } - /** - * Aborts all async tasks. - */ - public async abort(): Promise { - await this.#browserFrame.abort(); - } + // Other Classes + class Request extends RequestImplementation { + protected readonly _asyncTaskManager: AsyncTaskManager = + properties.browserFrame._asyncTaskManager; + protected readonly _ownerDocument: IDocument = properties.window.document; + } - /** - * Sets the URL. - * - * @param url URL. - */ - public setURL(url: string): void { - this.#window.location.href = url; - } + class Response extends ResponseImplementation { + protected readonly _asyncTaskManager: AsyncTaskManager = + properties.browserFrame._asyncTaskManager; + } - /** - * Sets the window size. - * - * @deprecated Use setViewport() instead. - * @param options Options. - * @param options.width Width. - * @param options.height Height. - */ - public setWindowSize(options: { width?: number; height?: number }): void { - this.setViewport({ - width: options?.width, - height: options?.height - }); - } + class XMLHttpRequest extends XMLHttpRequestImplementation { + constructor() { + super( + Object.freeze({ + window: properties.window, + asyncTaskManager: properties.browserFrame._asyncTaskManager, + browserSettings: Object.freeze({ + enableFileSystemHttpRequests: browserSettings.enableFileSystemHttpRequests + }) + }) + ); + } + } + class FileReader extends FileReaderImplementation { + public readonly _ownerDocument: IDocument = properties.window.document; + } + class DOMParser extends DOMParserImplementation { + public readonly _ownerDocument: IDocument = properties.window.document; + } + class Range extends RangeImplementation { + public readonly _ownerDocument: IDocument = properties.window.document; + } - /** - * Sets the viewport. - * - * @param viewport Viewport. - */ - public setViewport(viewport: IBrowserPageViewport): void { - this.#browserFrame.setViewport(viewport); - } + /* eslint-enable jsdoc/require-jsdoc */ - /** - * Sets the window width. - * - * @deprecated Use setViewport() instead. - * @param width Width. - */ - public setInnerWidth(width: number): void { - this.setViewport({ width }); - } + return { + // Nodes + Node, + Attr, + SVGSVGElement, + SVGElement, + SVGGraphicsElement, + Text, + Comment, + ShadowRoot, + ProcessingInstruction, + Element, + CharacterData, + Document, + HTMLDocument, + XMLDocument, + SVGDocument, - /** - * Sets the window height. - * - * @deprecated Use setViewport() instead. - * @param height Height. - */ - public setInnerHeight(height: number): void { - this.setViewport({ height }); + // HTML Elements + HTMLElement, + HTMLUnknownElement, + HTMLTemplateElement, + HTMLFormElement, + HTMLInputElement, + HTMLSelectElement, + HTMLTextAreaElement, + HTMLImageElement, + HTMLScriptElement, + HTMLLinkElement, + HTMLStyleElement, + HTMLLabelElement, + HTMLSlotElement, + HTMLMetaElement, + HTMLMediaElement, + HTMLAudioElement, + HTMLVideoElement, + HTMLBaseElement, + HTMLIFrameElement, + HTMLDialogElement, + + // Other Classes + Response, + Request, + Image, + DocumentFragment, + FileReader, + DOMParser, + XMLHttpRequest, + Range, + Audio + }; } } diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index c115a73f0..9a6815ac4 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -6,6 +6,7 @@ import XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js'; import XMLHttpRequestReadyStateEnum from './XMLHttpRequestReadyStateEnum.js'; import Event from '../event/Event.js'; import IDocument from '../nodes/document/IDocument.js'; +import IWindow from '../window/IWindow.js'; import Blob from '../file/Blob.js'; import XMLHttpRequestUpload from './XMLHttpRequestUpload.js'; import DOMException from '../exception/DOMException.js'; @@ -59,7 +60,7 @@ const CONTENT_TYPE_ENCODING_REGEXP = /charset=([^;]*)/i; * Based on: * https://github.com/mjwwit/node-XMLHttpRequest/blob/master/lib/XMLHttpRequest.js */ -export default class XMLHttpRequest extends XMLHttpRequestEventTarget { +export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { // Constants public static UNSENT = XMLHttpRequestReadyStateEnum.unsent; public static OPENED = XMLHttpRequestReadyStateEnum.opened; @@ -71,11 +72,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { public upload: XMLHttpRequestUpload = new XMLHttpRequestUpload(); // Will be injected by a sub-class in Window. - readonly #ownerDocument: IDocument; - readonly #browserSettings: { enableFileSystemHttpRequests: boolean } = Object.freeze({ - enableFileSystemHttpRequests: false - }); - readonly #asyncTaskManager: AsyncTaskManager; + readonly #injected: { + readonly window: IWindow; + readonly asyncTaskManager: AsyncTaskManager; + readonly browserSettings: { readonly enableFileSystemHttpRequests: boolean }; + }; // Private properties readonly #internal: { @@ -135,23 +136,19 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { /** * Constructor. * - * @param [inject] Properties to inject. - * @param [inject.browserSettings] Browser settings. - * @param [inject.browserSettings.enableFileSystemHttpRequests] Enable file system HTTP requests. - * @param inject.ownerDocument - * @param inject.asyncTaskManager + * @param inject Properties to inject. + * @param inject.window Window. + * @param inject.asyncTaskManager Async task manager. + * @param inject.browserSettings Browser settings. + * @param inject.browserSettings.enableFileSystemHttpRequests Enable file system HTTP requests. */ - constructor(inject?: { - ownerDocument: IDocument; + constructor(inject: { + window: IWindow; asyncTaskManager: AsyncTaskManager; browserSettings: { enableFileSystemHttpRequests: boolean }; }) { super(); - this.#ownerDocument = inject.ownerDocument; - this.#asyncTaskManager = inject.asyncTaskManager; - this.#browserSettings = Object.freeze({ - enableFileSystemHttpRequests: inject.browserSettings.enableFileSystemHttpRequests - }); + this.#injected = inject; } /** @@ -413,7 +410,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ); } - const { location } = this.#ownerDocument.defaultView; + const { location } = this.#injected.window; const url = new URL(this.#internal.settings.url, location); @@ -427,7 +424,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Load files off the local filesystem (file://) if (XMLHttpRequestURLUtility.isLocal(url)) { - if (!this.#browserSettings.enableFileSystemHttpRequests) { + if (!this.#injected.browserSettings.enableFileSystemHttpRequests) { throw new DOMException( 'File system is disabled by default for security reasons. To enable it, set the "window.happyDOM.settings.enableFileSystemHttpRequests" option to true.', DOMExceptionNameEnum.securityError @@ -550,7 +547,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.readyState = XMLHttpRequestReadyStateEnum.unsent; if (this.#internal.state.asyncTaskID !== null) { - this.#asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#injected.asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } } @@ -599,7 +596,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Default request headers. */ private _getDefaultRequestHeaders(): { [key: string]: string } { - const { location, navigator, document } = this.#ownerDocument.defaultView; + const { location, navigator, document } = this.#injected.window; return { accept: '*/*', @@ -655,7 +652,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseXML = null; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this.#ownerDocument.defaultView.location + this.#injected.window.location ).href; // Set Cookies. this._setCookies(this.#internal.state.incommingMessage.headers); @@ -668,7 +665,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ) { const redirectUrl = new URL( this.#internal.state.incommingMessage.headers['location'], - this.#ownerDocument.defaultView.location + this.#injected.window.location ); ssl = redirectUrl.protocol === 'https:'; this.#internal.settings.url = redirectUrl.href; @@ -710,7 +707,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ): Promise { return new Promise((resolve) => { // Starts async task in Happy DOM - this.#internal.state.asyncTaskID = this.#asyncTaskManager.startTask(this.abort.bind(this)); + this.#internal.state.asyncTaskID = this.#injected.asyncTaskManager.startTask( + this.abort.bind(this) + ); // Use the proper protocol const sendRequest = ssl ? HTTPS.request : HTTP.request; @@ -730,7 +729,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { resolve(); // Ends async task in Happy DOM - this.#asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#injected.asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } ); this.#internal.state.asyncRequest.on('error', (error: Error) => { @@ -738,7 +737,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { resolve(); // Ends async task in Happy DOM - this.#asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#injected.asyncTaskManager.endTask(this.#internal.state.asyncTaskID); }); // Node 0.4 and later won't accept empty data. Make sure it's needed. @@ -787,10 +786,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Change URL to the redirect location this.#internal.settings.url = this.#internal.state.incommingMessage.headers.location; // Parse the new URL. - const redirectUrl = new URL( - this.#internal.settings.url, - this.#ownerDocument.defaultView.location - ); + const redirectUrl = new URL(this.#internal.settings.url, this.#injected.window.location); this.#internal.settings.url = redirectUrl.href; ssl = redirectUrl.protocol === 'https:'; // Issue the new request @@ -855,7 +851,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseText = responseText; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this.#ownerDocument.defaultView.location + this.#injected.window.location ).href; // Discard the 'end' event if the connection has been aborted this._setState(XMLHttpRequestReadyStateEnum.done); @@ -878,7 +874,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Promise. */ private async _sendLocalAsyncRequest(url: UrlObject): Promise { - this.#internal.state.asyncTaskID = this.#asyncTaskManager.startTask(this.abort.bind(this)); + this.#internal.state.asyncTaskID = this.#injected.asyncTaskManager.startTask( + this.abort.bind(this) + ); let data: Buffer; @@ -887,7 +885,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } catch (error) { this._onError(error); // Release async task. - this.#asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#injected.asyncTaskManager.endTask(this.#internal.state.asyncTaskID); return; } @@ -908,7 +906,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } this._setState(XMLHttpRequestReadyStateEnum.done); - this.#asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#injected.asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } /** @@ -961,7 +959,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseText = responseText; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this.#ownerDocument.defaultView.location + this.#injected.window.location ).href; this._setState(XMLHttpRequestReadyStateEnum.done); @@ -992,7 +990,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { case XMLHttpResponseTypeEnum.blob: try { return { - response: new this.#ownerDocument.defaultView.Blob([new Uint8Array(data)], { + response: new this.#injected.window.Blob([new Uint8Array(data)], { type: this.getResponseHeader('content-type') || '' }), responseText: null, @@ -1002,7 +1000,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return { response: null, responseText: null, responseXML: null }; } case XMLHttpResponseTypeEnum.document: - const window = this.#ownerDocument.defaultView; + const window = this.#injected.window; const domParser = new window.DOMParser(); let response: IDocument; @@ -1043,20 +1041,14 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { private _setCookies( headers: { [name: string]: string | string[] } | HTTP.IncomingHttpHeaders ): void { - const originURL = new URL( - this.#internal.settings.url, - this.#ownerDocument.defaultView.location - ); + const originURL = new URL(this.#internal.settings.url, this.#injected.window.location); for (const header of ['set-cookie', 'set-cookie2']) { if (Array.isArray(headers[header])) { for (const cookie of headers[header]) { - (this.#ownerDocument.defaultView.document)._cookie.addCookieString( - originURL, - cookie - ); + (this.#injected.window.document)._cookie.addCookieString(originURL, cookie); } } else if (headers[header]) { - (this.#ownerDocument.defaultView.document)._cookie.addCookieString( + (this.#injected.window.document)._cookie.addCookieString( originURL, headers[header] ); @@ -1082,9 +1074,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { error: errorObject }); - this.#ownerDocument.defaultView.console.error(errorObject); + this.#injected.window.console.error(errorObject); this.dispatchEvent(event); - this.#ownerDocument.defaultView.dispatchEvent(event); + this.#injected.window.dispatchEvent(event); } /** From b0141ea63c61c5c21bd5e5a5d2928f894c9dcca8 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 20 Oct 2023 00:23:15 +0200 Subject: [PATCH 10/63] #466@trivial: Continues on implementation. --- .../src/browser/DetachedBrowserFrame.ts | 3 +- .../console/types/IVirtualConsolePrinter.ts | 2 +- .../AbstractCSSStyleDeclaration.ts | 17 +- .../CSSStyleDeclarationElementStyle.ts | 22 ++- packages/happy-dom/src/event/EventTarget.ts | 7 +- packages/happy-dom/src/fetch/Request.ts | 2 +- packages/happy-dom/src/fetch/Response.ts | 8 +- .../src/match-media/MediaQueryItem.ts | 13 +- packages/happy-dom/src/navigator/Navigator.ts | 3 +- .../happy-dom/src/nodes/document/Document.ts | 4 - .../happy-dom/src/nodes/element/Element.ts | 6 +- .../html-iframe-element/HTMLIFrameUtility.ts | 15 +- .../HTMLLinkElementUtility.ts | 8 +- .../html-script-element/HTMLScriptElement.ts | 6 +- .../HTMLScriptElementUtility.ts | 16 +- packages/happy-dom/src/range/Range.ts | 10 +- packages/happy-dom/src/selection/Selection.ts | 12 +- .../happy-dom/src/window/HappyDOMWindowAPI.ts | 2 +- packages/happy-dom/src/window/IWindow.ts | 2 +- packages/happy-dom/src/window/Window.ts | 57 +++++- .../src/window/WindowBrowserSettingsReader.ts | 39 ++++ .../src/window/WindowClassFactory.ts | 27 +-- ...extLoader.ts => __BrowserContextLoader.ts} | 2 +- .../src/xml-http-request/XMLHttpRequest.ts | 90 ++++----- packages/happy-dom/test/fetch/Request.test.ts | 173 ++++++++++-------- .../happy-dom/test/fetch/Response.test.ts | 57 +++--- .../happy-dom/test/file/FileReader.test.ts | 3 +- .../test/match-media/MediaQueryList.test.ts | 22 ++- .../nodes/html-image-element/Image.test.ts | 3 +- .../html-link-element/HTMLLinkElement.test.ts | 9 +- .../HTMLScriptElement.test.ts | 45 +++-- packages/happy-dom/test/window/Window.test.ts | 18 +- .../xml-http-request/XMLHttpRequest.test.ts | 36 +++- .../test/UncaughtExceptionObserver.test.ts | 8 +- 34 files changed, 458 insertions(+), 289 deletions(-) create mode 100644 packages/happy-dom/src/window/WindowBrowserSettingsReader.ts rename packages/happy-dom/src/window/{BrowserContextLoader.ts => __BrowserContextLoader.ts} (99%) diff --git a/packages/happy-dom/src/browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/DetachedBrowserFrame.ts index 73849e782..c49d5e355 100644 --- a/packages/happy-dom/src/browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/DetachedBrowserFrame.ts @@ -7,6 +7,7 @@ import { VirtualConsolePrinter } from '../index.js'; import BrowserSettingsFactory from './BrowserSettingsFactory.js'; import IBrowserPageViewport from './IBrowserPageViewport.js'; import Event from '../event/Event.js'; +import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; /** * Browser frame. @@ -27,7 +28,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @param options.window Window. * @param [options.settings] Browser settings. */ - constructor(options: { window: Window; settings?: IBrowserSettings }) { + constructor(options: { window: Window; settings?: IOptionalBrowserSettings }) { this.window = options.window; this.settings = BrowserSettingsFactory.getSettings(options.settings); } diff --git a/packages/happy-dom/src/console/types/IVirtualConsolePrinter.ts b/packages/happy-dom/src/console/types/IVirtualConsolePrinter.ts index 409a155fe..cdb002892 100644 --- a/packages/happy-dom/src/console/types/IVirtualConsolePrinter.ts +++ b/packages/happy-dom/src/console/types/IVirtualConsolePrinter.ts @@ -54,5 +54,5 @@ export default interface IVirtualConsolePrinter { * @param [logLevel] Log level. * @returns Buffer as a string of concatenated log entries. */ - readAsString(logLevel: VirtualConsoleLogLevelEnum): string; + readAsString(logLevel?: VirtualConsoleLogLevelEnum): string; } diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index 227b0ac8e..07bc406a6 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -20,15 +20,24 @@ export default abstract class AbstractCSSStyleDeclaration { /** * Constructor. * + * @param options Options. * @param [ownerElement] Computed style element. - * @param [computed] Computed. + * @param [options.browserSettings] Browser settings. + * @param [options.browserSettings.disableComputedStyleRendering] Disable computed style rendering. + * @param [options.computed=false] Computed. */ - constructor(ownerElement: IElement = null, computed = false) { + constructor( + ownerElement?: IElement, + options?: { + browserSettings?: { readonly disableComputedStyleRendering: boolean }; + computed?: boolean; + } + ) { this._style = !ownerElement ? new CSSStyleDeclarationPropertyManager() : null; this._ownerElement = ownerElement; - this._computed = ownerElement ? computed : false; + this._computed = ownerElement ? options.computed ?? false : false; this._elementStyle = ownerElement - ? new CSSStyleDeclarationElementStyle(ownerElement, this._computed) + ? new CSSStyleDeclarationElementStyle(ownerElement, options) : null; } diff --git a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 368c64eee..0fce21a1f 100644 --- a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -39,18 +39,30 @@ export default class CSSStyleDeclarationElementStyle { documentCacheID: null }; + readonly #browserSettings: { readonly disableComputedStyleRendering: boolean }; private element: IElement; private computed: boolean; /** * Constructor. * - * @param element Element. - * @param [computed] Computed. + * @param options Options. + * @param options.element Element. + * @param [options.browserSettings] Browser settings. + * @param [options.browserSettings.disableComputedStyleRendering] Disable computed style rendering. + * @param [options.computed=false] Computed. + * @param element */ - constructor(element: IElement, computed = false) { + constructor( + element: IElement, + options?: { + browserSettings?: { readonly disableComputedStyleRendering: boolean }; + computed?: boolean; + } + ) { this.element = element; - this.computed = computed; + this.#browserSettings = options?.browserSettings ?? { disableComputedStyleRendering: false }; + this.computed = options?.computed ?? false; } /** @@ -353,7 +365,7 @@ export default class CSSStyleDeclarationElementStyle { parentFontSize: string | number; parentSize: string | number | null; }): string { - if (this.element.ownerDocument.defaultView.happyDOM.settings.disableComputedStyleRendering) { + if (this.#browserSettings.disableComputedStyleRendering) { return options.value; } diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts index 2ae28d65c..c14402b1b 100644 --- a/packages/happy-dom/src/event/EventTarget.ts +++ b/packages/happy-dom/src/event/EventTarget.ts @@ -7,6 +7,7 @@ import INode from '../nodes/node/INode.js'; import IDocument from '../nodes/document/IDocument.js'; import IWindow from '../window/IWindow.js'; import WindowErrorUtility from '../window/WindowErrorUtility.js'; +import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; /** * Handles events. @@ -140,6 +141,8 @@ export default abstract class EventTarget implements IEventTarget { return !(event.cancelable && event.defaultPrevented); } + const browserSettings = WindowBrowserSettingsReader.getSettings(window); + event._currentTarget = this; if (event.eventPhase !== EventPhaseEnum.capturing) { @@ -150,7 +153,7 @@ export default abstract class EventTarget implements IEventTarget { if ( window && (this !== window || event.type !== 'error') && - !window.happyDOM.settings.disableErrorCapturing + !browserSettings.disableErrorCapturing ) { WindowErrorUtility.captureError(window, this[onEventName].bind(this, event)); } else { @@ -183,7 +186,7 @@ export default abstract class EventTarget implements IEventTarget { if ( window && (this !== window || event.type !== 'error') && - !window.happyDOM.settings.disableErrorCapturing + !browserSettings.disableErrorCapturing ) { if ((listener).handleEvent) { WindowErrorUtility.captureError( diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts index 2c7d568ff..179374deb 100644 --- a/packages/happy-dom/src/fetch/Request.ts +++ b/packages/happy-dom/src/fetch/Request.ts @@ -318,6 +318,6 @@ export default class Request implements IRequest { * @returns Clone. */ public clone(): IRequest { - return new Request(this); + return new (this.constructor)(this); } } diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts index 7d9ac9328..5485d33d2 100644 --- a/packages/happy-dom/src/fetch/Response.ts +++ b/packages/happy-dom/src/fetch/Response.ts @@ -237,7 +237,7 @@ export default class Response implements IResponse { * @returns Clone. */ public clone(): IResponse { - const response = new Response(); + const response = new (this.constructor)(); (response.status) = this.status; (response.statusText) = this.statusText; @@ -266,7 +266,7 @@ export default class Response implements IResponse { ); } - return new Response(null, { + return new (this.constructor)(null, { headers: { location: new URL(url).toString() }, @@ -282,7 +282,7 @@ export default class Response implements IResponse { * @returns Response. */ public static error(): IResponse { - const response = new Response(null, { status: 0, statusText: '' }); + const response = new (this.constructor)(null, { status: 0, statusText: '' }); (response.type) = 'error'; return response; } @@ -307,7 +307,7 @@ export default class Response implements IResponse { headers.set('content-type', 'application/json'); } - return new Response(body, { + return new (this.constructor)(body, { status: 200, ...init, headers diff --git a/packages/happy-dom/src/match-media/MediaQueryItem.ts b/packages/happy-dom/src/match-media/MediaQueryItem.ts index 2ca6eea66..6b0c10ccd 100644 --- a/packages/happy-dom/src/match-media/MediaQueryItem.ts +++ b/packages/happy-dom/src/match-media/MediaQueryItem.ts @@ -1,5 +1,6 @@ import CSSMeasurementConverter from '../css/declaration/measurement-converter/CSSMeasurementConverter.js'; import IWindow from '../window/IWindow.js'; +import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; import IMediaQueryRange from './IMediaQueryRange.js'; import IMediaQueryRule from './IMediaQueryRule.js'; import MediaQueryTypeEnum from './MediaQueryTypeEnum.js'; @@ -114,7 +115,7 @@ export default class MediaQueryItem { if (mediaType === MediaQueryTypeEnum.all) { return true; } - return mediaType === this.ownerWindow.happyDOM.settings.device.mediaType; + return mediaType === WindowBrowserSettingsReader.getSettings(this.ownerWindow).device.mediaType; } /** @@ -246,7 +247,10 @@ export default class MediaQueryItem { ? 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; + return ( + rule.value === + WindowBrowserSettingsReader.getSettings(this.ownerWindow).device.prefersColorScheme + ); case 'any-hover': case 'hover': if (rule.value === 'none') { @@ -313,7 +317,10 @@ export default class MediaQueryItem { * @returns Value in pixels. */ private toPixels(value: string): number | null { - if (!this.ownerWindow.happyDOM.settings.disableComputedStyleRendering && value.endsWith('em')) { + if ( + !WindowBrowserSettingsReader.getSettings(this.ownerWindow).disableComputedStyleRendering && + value.endsWith('em') + ) { this.rootFontSize = this.rootFontSize || parseFloat( diff --git a/packages/happy-dom/src/navigator/Navigator.ts b/packages/happy-dom/src/navigator/Navigator.ts index a9d50af99..75c97f24a 100644 --- a/packages/happy-dom/src/navigator/Navigator.ts +++ b/packages/happy-dom/src/navigator/Navigator.ts @@ -3,6 +3,7 @@ import PluginArray from './PluginArray.js'; import IWindow from '../window/IWindow.js'; import Permissions from '../permissions/Permissions.js'; import Clipboard from '../clipboard/Clipboard.js'; +import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; /** * Browser Navigator API. @@ -151,7 +152,7 @@ export default class Navigator { * "appCodeName/appVersion number (Platform; Security; OS-or-CPU; Localization; rv: revision-version-number) product/productSub Application-Name Application-Name-version". */ public get userAgent(): string { - return this.#ownerWindow.happyDOM.settings.navigator.userAgent; + return WindowBrowserSettingsReader.getSettings(this.#ownerWindow).navigator.userAgent; } /** diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 1450b773e..7dd49f14c 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -1,7 +1,5 @@ import Element from '../element/Element.js'; import HTMLUnknownElement from '../html-unknown-element/HTMLUnknownElement.js'; -import Text from '../text/Text.js'; -import Comment from '../comment/Comment.js'; import IWindow from '../../window/IWindow.js'; import Node from '../node/Node.js'; import NodeIterator from '../../tree-walker/NodeIterator.js'; @@ -12,7 +10,6 @@ import Event from '../../event/Event.js'; import DOMImplementation from '../../dom-implementation/DOMImplementation.js'; import ElementTag from '../../config/ElementTag.js'; import INodeFilter from '../../tree-walker/INodeFilter.js'; -import Attr from '../attr/Attr.js'; import NamespaceURI from '../../config/NamespaceURI.js'; import DocumentType from '../document-type/DocumentType.js'; import ParentNodeUtility from '../parent-node/ParentNodeUtility.js'; @@ -61,7 +58,6 @@ export default class Document extends Node implements IDocument { public readonly isConnected: boolean = true; public readonly defaultView: IWindow; public readonly referrer = ''; - public readonly ownerDocument: IDocument | null = null; public readonly _windowClass: {} | null = null; public readonly _readyStateManager: DocumentReadyStateManager; public readonly _children: IHTMLCollection = new HTMLCollection(); diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index d887938c2..1f1bfae7f 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -32,6 +32,7 @@ import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration.js'; import DocumentFragment from '../document-fragment/DocumentFragment.js'; import ElementNamedNodeMap from './ElementNamedNodeMap.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; +import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; /** * Element. @@ -925,9 +926,10 @@ export default class Element extends Node implements IElement { */ public override dispatchEvent(event: Event): boolean { const returnValue = super.dispatchEvent(event); + const browserSettings = WindowBrowserSettingsReader.getSettings(this.ownerDocument.defaultView); if ( - !this.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptEvaluation && + !browserSettings.disableJavaScriptEvaluation && (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && !event._immediatePropagationStopped @@ -935,7 +937,7 @@ export default class Element extends Node implements IElement { const attribute = this.getAttribute('on' + event.type); if (attribute && !event._immediatePropagationStopped) { - if (this.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + if (browserSettings.disableErrorCapturing) { this.ownerDocument.defaultView.eval(attribute); } else { WindowErrorUtility.captureError(this.ownerDocument.defaultView, () => diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts index 3b92aea61..6ac3013b6 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts @@ -5,6 +5,7 @@ import IWindow from '../../window/IWindow.js'; import CrossOriginWindow from '../../window/CrossOriginWindow.js'; import HTMLIFrameElement from './HTMLIFrameElement.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; +import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; /** * HTML Iframe Utility. @@ -16,10 +17,10 @@ export default class HTMLIFrameUtility { * @param element */ public static async loadPage(element: HTMLIFrameElement): Promise { - if ( - element.isConnected && - !element.ownerDocument.defaultView.happyDOM.settings.disableIframePageLoading - ) { + const browserSettings = WindowBrowserSettingsReader.getSettings( + element.ownerDocument.defaultView + ); + if (element.isConnected && !browserSettings.disableIframePageLoading) { const src = element.src; if (src) { @@ -27,7 +28,7 @@ export default class HTMLIFrameUtility { const contentWindow = new element.ownerDocument['_windowClass']({ url: new URL(src, element.ownerDocument.defaultView.location.href).href, settings: { - ...element.ownerDocument.defaultView.happyDOM.settings + ...browserSettings } }); @@ -41,8 +42,8 @@ export default class HTMLIFrameUtility { if (src.startsWith('javascript:')) { element._contentWindow = contentWindow; - if (!element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptEvaluation) { - if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + if (!browserSettings.disableJavaScriptEvaluation) { + if (browserSettings.disableErrorCapturing) { (element._contentWindow).eval(src.replace('javascript:', '')); } else { WindowErrorUtility.captureError(element, () => diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts index c726a179a..d15eeacce 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts @@ -6,6 +6,7 @@ import CSSStyleSheet from '../../css/CSSStyleSheet.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; +import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; /** * Helper class for getting the URL relative to a Location object. @@ -21,9 +22,12 @@ export default class HTMLLinkElementUtility { public static async loadExternalStylesheet(element: HTMLLinkElement): Promise { const href = element.getAttribute('href'); const rel = element.getAttribute('rel'); + const browserSettings = WindowBrowserSettingsReader.getSettings( + element.ownerDocument.defaultView + ); if (href !== null && rel && rel.toLowerCase() === 'stylesheet' && element.isConnected) { - if (element.ownerDocument.defaultView.happyDOM.settings.disableCSSFileLoading) { + if (browserSettings.disableCSSFileLoading) { WindowErrorUtility.dispatchError( element, new DOMException( @@ -49,7 +53,7 @@ export default class HTMLLinkElementUtility { if (error) { WindowErrorUtility.dispatchError(element, error); - if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + if (browserSettings.disableErrorCapturing) { throw error; } } else { diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index 624fdc772..df1ddc0a8 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -7,6 +7,7 @@ import INode from '../../nodes/node/INode.js'; import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import HTMLScriptElementNamedNodeMap from './HTMLScriptElementNamedNodeMap.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; +import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; /** * HTML Script Element. @@ -171,6 +172,7 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip public override _connectToNode(parentNode: INode = null): void { const isConnected = this.isConnected; const isParentConnected = parentNode ? parentNode.isConnected : false; + const browserSettings = WindowBrowserSettingsReader.getSettings(this.ownerDocument.defaultView); super._connectToNode(parentNode); @@ -179,7 +181,7 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip if (src !== null) { HTMLScriptElementUtility.loadExternalScript(this); - } else if (!this.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptEvaluation) { + } else if (!browserSettings.disableJavaScriptEvaluation) { const textContent = this.textContent; const type = this.getAttribute('type'); if ( @@ -191,7 +193,7 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip ) { this.ownerDocument['_currentScript'] = this; - if (this.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + if (browserSettings.disableErrorCapturing) { this.ownerDocument.defaultView.eval(textContent); } else { WindowErrorUtility.captureError(this.ownerDocument.defaultView, () => diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts index 2d1a5551d..5bae08daa 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts @@ -5,6 +5,7 @@ import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import ResourceFetch from '../../fetch/ResourceFetch.js'; import HTMLScriptElement from './HTMLScriptElement.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; +import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; /** * Helper class for getting the URL relative to a Location object. @@ -20,10 +21,13 @@ export default class HTMLScriptElementUtility { public static async loadExternalScript(element: HTMLScriptElement): Promise { const src = element.getAttribute('src'); const async = element.getAttribute('async') !== null; + const browserSettings = WindowBrowserSettingsReader.getSettings( + element.ownerDocument.defaultView + ); if ( - element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptFileLoading || - element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptEvaluation + browserSettings.disableJavaScriptFileLoading || + browserSettings.disableJavaScriptEvaluation ) { WindowErrorUtility.dispatchError( element, @@ -51,12 +55,12 @@ export default class HTMLScriptElementUtility { if (error) { WindowErrorUtility.dispatchError(element, error); - if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + if (browserSettings.disableErrorCapturing) { throw error; } } else { element.ownerDocument['_currentScript'] = element; - if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + if (browserSettings.disableErrorCapturing) { element.ownerDocument.defaultView.eval(code); } else { WindowErrorUtility.captureError(element.ownerDocument.defaultView, () => @@ -78,12 +82,12 @@ export default class HTMLScriptElementUtility { if (error) { WindowErrorUtility.dispatchError(element, error); - if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + if (browserSettings.disableErrorCapturing) { throw error; } } else { element.ownerDocument['_currentScript'] = element; - if (element.ownerDocument.defaultView.happyDOM.settings.disableErrorCapturing) { + if (browserSettings.disableErrorCapturing) { element.ownerDocument.defaultView.eval(code); } else { WindowErrorUtility.captureError(element.ownerDocument.defaultView, () => diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts index 4ca0657d2..a8584a506 100644 --- a/packages/happy-dom/src/range/Range.ts +++ b/packages/happy-dom/src/range/Range.ts @@ -337,7 +337,7 @@ export default class Range { const clone = firstPartialContainedChild.cloneNode(); fragment.appendChild(clone); - const subRange = new Range(); + const subRange = new (this.constructor)(); subRange._start.node = this._start.node; subRange._start.offset = startOffset; subRange._end.node = firstPartialContainedChild; @@ -366,7 +366,7 @@ export default class Range { const clone = lastPartiallyContainedChild.cloneNode(false); fragment.appendChild(clone); - const subRange = new Range(); + const subRange = new (this.constructor)(); subRange._start.node = lastPartiallyContainedChild; subRange._start.offset = 0; subRange._end.node = this._end.node; @@ -386,7 +386,7 @@ export default class Range { * @returns Range. */ public cloneRange(): Range { - const clone = new Range(); + const clone = new (this.constructor)(); clone._start.node = this._start.node; clone._start.offset = this._start.offset; @@ -620,7 +620,7 @@ export default class Range { const clone = firstPartialContainedChild.cloneNode(false); fragment.appendChild(clone); - const subRange = new Range(); + const subRange = new (this.constructor)(); subRange._start.node = this._start.node; subRange._start.offset = startOffset; subRange._end.node = firstPartialContainedChild; @@ -650,7 +650,7 @@ export default class Range { const clone = lastPartiallyContainedChild.cloneNode(false); fragment.appendChild(clone); - const subRange = new Range(); + const subRange = new (this.constructor)(); subRange._start.node = lastPartiallyContainedChild; subRange._start.offset = 0; subRange._end.node = this._end.node; diff --git a/packages/happy-dom/src/selection/Selection.ts b/packages/happy-dom/src/selection/Selection.ts index 68952f52f..47df6082e 100644 --- a/packages/happy-dom/src/selection/Selection.ts +++ b/packages/happy-dom/src/selection/Selection.ts @@ -249,7 +249,7 @@ export default class Selection { return; } - const newRange = new Range(); + const newRange = new this._ownerDocument.defaultView.Range(); newRange._start.node = node; newRange._start.offset = offset; @@ -285,7 +285,7 @@ export default class Selection { } const { node, offset } = this._range._end; - const newRange = new Range(); + const newRange = new this._ownerDocument.defaultView.Range(); newRange._start.node = node; newRange._start.offset = offset; @@ -309,7 +309,7 @@ export default class Selection { } const { node, offset } = this._range._start; - const newRange = new Range(); + const newRange = new this._ownerDocument.defaultView.Range(); newRange._start.node = node; newRange._start.offset = offset; @@ -379,7 +379,7 @@ export default class Selection { const anchorNode = this.anchorNode; const anchorOffset = this.anchorOffset; - const newRange = new Range(); + const newRange = new this._ownerDocument.defaultView.Range(); newRange._start.node = node; newRange._start.offset = 0; newRange._end.node = node; @@ -435,7 +435,7 @@ export default class Selection { } const length = node.childNodes.length; - const newRange = new Range(); + const newRange = new this._ownerDocument.defaultView.Range(); newRange._start.node = node; newRange._start.offset = 0; @@ -479,7 +479,7 @@ export default class Selection { const anchor = { node: anchorNode, offset: anchorOffset }; const focus = { node: focusNode, offset: focusOffset }; - const newRange = new Range(); + const newRange = new this._ownerDocument.defaultView.Range(); if (RangeUtility.compareBoundaryPointsPosition(anchor, focus) === -1) { newRange._start = anchor; diff --git a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts index 7beca9564..ef237e72f 100644 --- a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts +++ b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts @@ -1,6 +1,5 @@ import IBrowserSettings from '../browser/IBrowserSettings.js'; import IWindow from './IWindow.js'; -import Event from '../event/Event.js'; import BrowserFrame from '../browser/BrowserFrame.js'; import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; @@ -31,6 +30,7 @@ export default class HappyDOMWindowAPI { /** * Returns settings. * + * @deprecated Settings should not be read or written from Window. Use the Browser class instead to access settings. * @returns Settings. */ public get settings(): IReadOnlyBrowserSettings { diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index 71e23b99f..fbb2bab54 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -339,7 +339,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly Response: typeof Response; readonly Range: typeof Range; readonly DOMRect: typeof DOMRect; - readonly XMLHttpRequest: { new (): XMLHttpRequest }; + readonly XMLHttpRequest: typeof XMLHttpRequest; readonly XMLHttpRequestUpload: typeof XMLHttpRequestUpload; readonly XMLHttpRequestEventTarget: typeof XMLHttpRequestEventTarget; readonly FileList: typeof FileList; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index d9cba2449..f5a0b12d9 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -121,7 +121,6 @@ import ValidityState from '../validity-state/ValidityState.js'; import WindowErrorUtility from './WindowErrorUtility.js'; import VirtualConsole from '../console/VirtualConsole.js'; import ICrossOriginWindow from './ICrossOriginWindow.js'; -import IBrowserSettings from '../browser/IBrowserSettings.js'; import Permissions from '../permissions/Permissions.js'; import PermissionStatus from '../permissions/PermissionStatus.js'; import Clipboard from '../clipboard/Clipboard.js'; @@ -140,6 +139,8 @@ import FileReader from '../file/FileReader.js'; import Request from '../fetch/Request.js'; import Range from '../range/Range.js'; import XMLHttpRequest from '../xml-http-request/XMLHttpRequest.js'; +import IOptionalBrowserSettings from '../browser/IOptionalBrowserSettings.js'; +import WindowBrowserSettingsReader from './WindowBrowserSettingsReader.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -371,7 +372,7 @@ export default class Window extends EventTarget implements IWindow { public readonly PermissionStatus = PermissionStatus; public readonly Clipboard = Clipboard; public readonly ClipboardItem = ClipboardItem; - public readonly XMLHttpRequest: { new (): XMLHttpRequest }; + public readonly XMLHttpRequest: typeof XMLHttpRequest; public readonly DOMParser: typeof DOMParser; public readonly Range: typeof Range; public readonly FileReader: typeof FileReader; @@ -507,7 +508,7 @@ export default class Window extends EventTarget implements IWindow { innerHeight?: number; url?: string; console?: Console; - settings?: IBrowserSettings; + settings?: IOptionalBrowserSettings; browserFrame?: BrowserFrame; }) { super(); @@ -533,6 +534,13 @@ export default class Window extends EventTarget implements IWindow { options?.console ?? new VirtualConsole(this.#browserFrame.virtualConsolePrinter); } + WindowBrowserSettingsReader.setSettings( + this, + this.#browserFrame instanceof DetachedBrowserFrame + ? this.#browserFrame.settings + : this.#browserFrame.page.context.browser.settings + ); + this.happyDOM = new HappyDOMWindowAPI({ window: this, browserFrame: this.#browserFrame @@ -745,7 +753,18 @@ export default class Window extends EventTarget implements IWindow { * @returns CSS style declaration. */ public getComputedStyle(element: IElement): CSSStyleDeclaration { - element['_computedStyle'] = element['_computedStyle'] || new CSSStyleDeclaration(element, true); + const browserSettings = + this.#browserFrame instanceof DetachedBrowserFrame + ? this.#browserFrame.settings + : this.#browserFrame.page.context.browser.settings; + element['_computedStyle'] = + element['_computedStyle'] || + new CSSStyleDeclaration(element, { + browserSettings: { + disableComputedStyleRendering: browserSettings.disableComputedStyleRendering + }, + computed: true + }); return element['_computedStyle']; } @@ -814,7 +833,11 @@ export default class Window extends EventTarget implements IWindow { _target?: string, _features?: string ): IWindow | ICrossOriginWindow | null { - if (this.happyDOM.settings.disableWindowOpenPageLoading) { + const browserSettings = + this.#browserFrame instanceof DetachedBrowserFrame + ? this.#browserFrame.settings + : this.#browserFrame.page.context.browser.settings; + if (browserSettings.disableWindowOpenPageLoading) { return null; } return null; @@ -852,8 +875,12 @@ export default class Window extends EventTarget implements IWindow { * @returns Timeout ID. */ public setTimeout(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { + const browserSettings = + this.#browserFrame instanceof DetachedBrowserFrame + ? this.#browserFrame.settings + : this.#browserFrame.page.context.browser.settings; const id = this._setTimeout(() => { - if (this.happyDOM.settings.disableErrorCapturing) { + if (browserSettings.disableErrorCapturing) { callback(...args); } else { WindowErrorUtility.captureError(this, () => callback(...args)); @@ -883,8 +910,12 @@ export default class Window extends EventTarget implements IWindow { * @returns Interval ID. */ public setInterval(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { + const browserSettings = + this.#browserFrame instanceof DetachedBrowserFrame + ? this.#browserFrame.settings + : this.#browserFrame.page.context.browser.settings; const id = this._setInterval(() => { - if (this.happyDOM.settings.disableErrorCapturing) { + if (browserSettings.disableErrorCapturing) { callback(...args); } else { WindowErrorUtility.captureError( @@ -915,8 +946,12 @@ export default class Window extends EventTarget implements IWindow { * @returns ID. */ public requestAnimationFrame(callback: (timestamp: number) => void): NodeJS.Immediate { + const browserSettings = + this.#browserFrame instanceof DetachedBrowserFrame + ? this.#browserFrame.settings + : this.#browserFrame.page.context.browser.settings; const id = global.setImmediate(() => { - if (this.happyDOM.settings.disableErrorCapturing) { + if (browserSettings.disableErrorCapturing) { callback(this.performance.now()); } else { WindowErrorUtility.captureError(this, () => callback(this.performance.now())); @@ -945,9 +980,13 @@ export default class Window extends EventTarget implements IWindow { public queueMicrotask(callback: Function): void { let isAborted = false; const taskId = this.#browserFrame._asyncTaskManager.startTask(() => (isAborted = true)); + const browserSettings = + this.#browserFrame instanceof DetachedBrowserFrame + ? this.#browserFrame.settings + : this.#browserFrame.page.context.browser.settings; this._queueMicrotask(() => { if (!isAborted) { - if (this.happyDOM.settings.disableErrorCapturing) { + if (browserSettings.disableErrorCapturing) { callback(); } else { WindowErrorUtility.captureError(this, <() => unknown>callback); diff --git a/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts b/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts new file mode 100644 index 000000000..df5b36bdc --- /dev/null +++ b/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts @@ -0,0 +1,39 @@ +import IBrowserSettings from '../browser/IBrowserSettings.js'; +import IWindow from './IWindow.js'; + +/** + * Browser settings reader that will allow to read settings more securely as it is not possible to override a settings object to make DOM functionality act on it. + */ +export default class WindowBrowserSettingsReader { + static #settings: IBrowserSettings[] = []; + + /** + * Returns browser settings. + * + * @param window Window. + * @returns Settings. + */ + public static getSettings(window: IWindow): IBrowserSettings | null { + const id = window['__happyDOMSettingsID__']; + + if (id === undefined || !this.#settings[id]) { + return null; + } + + return this.#settings[id]; + } + + /** + * Sets browser settings. + * + * @param window Window. + * @param settings Settings. + */ + public static setSettings(window: IWindow, settings: IBrowserSettings): void { + if (window['__happyDOMSettingsID__'] !== undefined) { + return; + } + window['__happyDOMSettingsID__'] = this.#settings.length; + this.#settings.push(settings); + } +} diff --git a/packages/happy-dom/src/window/WindowClassFactory.ts b/packages/happy-dom/src/window/WindowClassFactory.ts index 1d6a23dc2..f0f3d6286 100644 --- a/packages/happy-dom/src/window/WindowClassFactory.ts +++ b/packages/happy-dom/src/window/WindowClassFactory.ts @@ -107,7 +107,7 @@ export default class WindowClassFactory { // Other Classes Response: typeof ResponseImplementation; Request: typeof RequestImplementation; - XMLHttpRequest: new () => XMLHttpRequestImplementation; + XMLHttpRequest: typeof XMLHttpRequestImplementation; Image: typeof ImageImplementation; DocumentFragment: typeof DocumentFragmentImplementation; FileReader: typeof FileReaderImplementation; @@ -115,11 +115,6 @@ export default class WindowClassFactory { Range: typeof RangeImplementation; Audio: typeof AudioImplementation; } { - const browserSettings = - properties.browserFrame instanceof DetachedBrowserFrame - ? properties.browserFrame.settings - : properties.browserFrame.page.context.browser.settings; - /* eslint-disable jsdoc/require-jsdoc */ // Nodes @@ -157,15 +152,19 @@ export default class WindowClassFactory { public readonly ownerDocument: IDocument = properties.window.document; } class Document extends DocumentImplementation { + public readonly ownerDocument: IDocument | null = null; public readonly defaultView: IWindow = properties.window; } class HTMLDocument extends HTMLDocumentImplementation { + public readonly ownerDocument: IDocument | null = null; public readonly defaultView: IWindow = properties.window; } class XMLDocument extends XMLDocumentImplementation { + public readonly ownerDocument: IDocument | null = null; public readonly defaultView: IWindow = properties.window; } class SVGDocument extends SVGDocumentImplementation { + public readonly ownerDocument: IDocument | null = null; public readonly defaultView: IWindow = properties.window; } @@ -246,24 +245,14 @@ export default class WindowClassFactory { properties.browserFrame._asyncTaskManager; protected readonly _ownerDocument: IDocument = properties.window.document; } - class Response extends ResponseImplementation { protected readonly _asyncTaskManager: AsyncTaskManager = properties.browserFrame._asyncTaskManager; } - class XMLHttpRequest extends XMLHttpRequestImplementation { - constructor() { - super( - Object.freeze({ - window: properties.window, - asyncTaskManager: properties.browserFrame._asyncTaskManager, - browserSettings: Object.freeze({ - enableFileSystemHttpRequests: browserSettings.enableFileSystemHttpRequests - }) - }) - ); - } + protected readonly _asyncTaskManager: AsyncTaskManager = + properties.browserFrame._asyncTaskManager; + protected readonly _ownerDocument: IDocument = properties.window.document; } class FileReader extends FileReaderImplementation { public readonly _ownerDocument: IDocument = properties.window.document; diff --git a/packages/happy-dom/src/window/BrowserContextLoader.ts b/packages/happy-dom/src/window/__BrowserContextLoader.ts similarity index 99% rename from packages/happy-dom/src/window/BrowserContextLoader.ts rename to packages/happy-dom/src/window/__BrowserContextLoader.ts index 20c9ccf6f..98e0d1753 100644 --- a/packages/happy-dom/src/window/BrowserContextLoader.ts +++ b/packages/happy-dom/src/window/__BrowserContextLoader.ts @@ -11,7 +11,7 @@ import DetachedWindowAPI from './HappyDOMWindowAPI.js'; /** * Browser context. */ -export default class BrowserContextLoader { +export default class __BrowserContextLoader { /** * Creates a new browser context for an iframe or a new window using Window.open(). * diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index 9a6815ac4..4ab098094 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -22,6 +22,7 @@ import IconvLite from 'iconv-lite'; import ErrorEvent from '../event/events/ErrorEvent.js'; import Document from '../nodes/document/Document.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; // These headers are not user setable. // The following are allowed but banned in the spec: @@ -60,7 +61,7 @@ const CONTENT_TYPE_ENCODING_REGEXP = /charset=([^;]*)/i; * Based on: * https://github.com/mjwwit/node-XMLHttpRequest/blob/master/lib/XMLHttpRequest.js */ -export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { +export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Constants public static UNSENT = XMLHttpRequestReadyStateEnum.unsent; public static OPENED = XMLHttpRequestReadyStateEnum.opened; @@ -72,11 +73,8 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { public upload: XMLHttpRequestUpload = new XMLHttpRequestUpload(); // Will be injected by a sub-class in Window. - readonly #injected: { - readonly window: IWindow; - readonly asyncTaskManager: AsyncTaskManager; - readonly browserSettings: { readonly enableFileSystemHttpRequests: boolean }; - }; + protected readonly _asyncTaskManager: AsyncTaskManager; + protected readonly _ownerDocument: IDocument; // Private properties readonly #internal: { @@ -133,24 +131,6 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { } }; - /** - * Constructor. - * - * @param inject Properties to inject. - * @param inject.window Window. - * @param inject.asyncTaskManager Async task manager. - * @param inject.browserSettings Browser settings. - * @param inject.browserSettings.enableFileSystemHttpRequests Enable file system HTTP requests. - */ - constructor(inject: { - window: IWindow; - asyncTaskManager: AsyncTaskManager; - browserSettings: { enableFileSystemHttpRequests: boolean }; - }) { - super(); - this.#injected = inject; - } - /** * Returns the status. * @@ -410,7 +390,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { ); } - const { location } = this.#injected.window; + const { location } = this._ownerDocument.defaultView; const url = new URL(this.#internal.settings.url, location); @@ -424,9 +404,12 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { // Load files off the local filesystem (file://) if (XMLHttpRequestURLUtility.isLocal(url)) { - if (!this.#injected.browserSettings.enableFileSystemHttpRequests) { + if ( + !WindowBrowserSettingsReader.getSettings(this._ownerDocument.defaultView) + .enableFileSystemHttpRequests + ) { throw new DOMException( - 'File system is disabled by default for security reasons. To enable it, set the "window.happyDOM.settings.enableFileSystemHttpRequests" option to true.', + 'File system is disabled by default for security reasons. To enable it, set the "enableFileSystemHttpRequests" HappyDOM setting to true.', DOMExceptionNameEnum.securityError ); } @@ -547,7 +530,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.readyState = XMLHttpRequestReadyStateEnum.unsent; if (this.#internal.state.asyncTaskID !== null) { - this.#injected.asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } } @@ -596,7 +579,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Default request headers. */ private _getDefaultRequestHeaders(): { [key: string]: string } { - const { location, navigator, document } = this.#injected.window; + const { location, navigator, document } = this._ownerDocument.defaultView; return { accept: '*/*', @@ -652,7 +635,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseXML = null; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this.#injected.window.location + this._ownerDocument.defaultView.location ).href; // Set Cookies. this._setCookies(this.#internal.state.incommingMessage.headers); @@ -665,7 +648,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { ) { const redirectUrl = new URL( this.#internal.state.incommingMessage.headers['location'], - this.#injected.window.location + this._ownerDocument.defaultView.location ); ssl = redirectUrl.protocol === 'https:'; this.#internal.settings.url = redirectUrl.href; @@ -707,9 +690,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { ): Promise { return new Promise((resolve) => { // Starts async task in Happy DOM - this.#internal.state.asyncTaskID = this.#injected.asyncTaskManager.startTask( - this.abort.bind(this) - ); + this.#internal.state.asyncTaskID = this._asyncTaskManager.startTask(this.abort.bind(this)); // Use the proper protocol const sendRequest = ssl ? HTTPS.request : HTTP.request; @@ -729,7 +710,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { resolve(); // Ends async task in Happy DOM - this.#injected.asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } ); this.#internal.state.asyncRequest.on('error', (error: Error) => { @@ -737,7 +718,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { resolve(); // Ends async task in Happy DOM - this.#injected.asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); }); // Node 0.4 and later won't accept empty data. Make sure it's needed. @@ -786,7 +767,10 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { // Change URL to the redirect location this.#internal.settings.url = this.#internal.state.incommingMessage.headers.location; // Parse the new URL. - const redirectUrl = new URL(this.#internal.settings.url, this.#injected.window.location); + const redirectUrl = new URL( + this.#internal.settings.url, + this._ownerDocument.defaultView.location + ); this.#internal.settings.url = redirectUrl.href; ssl = redirectUrl.protocol === 'https:'; // Issue the new request @@ -851,7 +835,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseText = responseText; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this.#injected.window.location + this._ownerDocument.defaultView.location ).href; // Discard the 'end' event if the connection has been aborted this._setState(XMLHttpRequestReadyStateEnum.done); @@ -874,9 +858,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Promise. */ private async _sendLocalAsyncRequest(url: UrlObject): Promise { - this.#internal.state.asyncTaskID = this.#injected.asyncTaskManager.startTask( - this.abort.bind(this) - ); + this.#internal.state.asyncTaskID = this._asyncTaskManager.startTask(this.abort.bind(this)); let data: Buffer; @@ -885,7 +867,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { } catch (error) { this._onError(error); // Release async task. - this.#injected.asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); return; } @@ -906,7 +888,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { } this._setState(XMLHttpRequestReadyStateEnum.done); - this.#injected.asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } /** @@ -959,7 +941,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseText = responseText; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this.#injected.window.location + this._ownerDocument.defaultView.location ).href; this._setState(XMLHttpRequestReadyStateEnum.done); @@ -990,7 +972,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { case XMLHttpResponseTypeEnum.blob: try { return { - response: new this.#injected.window.Blob([new Uint8Array(data)], { + response: new this._ownerDocument.defaultView.Blob([new Uint8Array(data)], { type: this.getResponseHeader('content-type') || '' }), responseText: null, @@ -1000,7 +982,7 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { return { response: null, responseText: null, responseXML: null }; } case XMLHttpResponseTypeEnum.document: - const window = this.#injected.window; + const window = this._ownerDocument.defaultView; const domParser = new window.DOMParser(); let response: IDocument; @@ -1041,14 +1023,20 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { private _setCookies( headers: { [name: string]: string | string[] } | HTTP.IncomingHttpHeaders ): void { - const originURL = new URL(this.#internal.settings.url, this.#injected.window.location); + const originURL = new URL( + this.#internal.settings.url, + this._ownerDocument.defaultView.location + ); for (const header of ['set-cookie', 'set-cookie2']) { if (Array.isArray(headers[header])) { for (const cookie of headers[header]) { - (this.#injected.window.document)._cookie.addCookieString(originURL, cookie); + (this._ownerDocument.defaultView.document)._cookie.addCookieString( + originURL, + cookie + ); } } else if (headers[header]) { - (this.#injected.window.document)._cookie.addCookieString( + (this._ownerDocument.defaultView.document)._cookie.addCookieString( originURL, headers[header] ); @@ -1074,9 +1062,9 @@ export default abstract class XMLHttpRequest extends XMLHttpRequestEventTarget { error: errorObject }); - this.#injected.window.console.error(errorObject); + this._ownerDocument.defaultView.console.error(errorObject); this.dispatchEvent(event); - this.#injected.window.dispatchEvent(event); + this._ownerDocument.defaultView.dispatchEvent(event); } /** diff --git a/packages/happy-dom/test/fetch/Request.test.ts b/packages/happy-dom/test/fetch/Request.test.ts index d94d96dad..37c3456da 100644 --- a/packages/happy-dom/test/fetch/Request.test.ts +++ b/packages/happy-dom/test/fetch/Request.test.ts @@ -25,7 +25,6 @@ describe('Request', () => { beforeEach(() => { window = new Window(); document = window.document; - Request._ownerDocument = document; }); afterEach(() => { @@ -34,7 +33,7 @@ describe('Request', () => { describe('constructor()', () => { it('Sets default values for properties.', () => { - const request = new Request(TEST_URL); + const request = new window.Request(TEST_URL); let headersLength = 0; for (const _header of request.headers) { @@ -54,35 +53,35 @@ describe('Request', () => { }); it('Supports URL as string from Request object.', () => { - const request = new Request(new Request(TEST_URL)); + const request = new window.Request(new window.Request(TEST_URL)); expect(request.url).toBe(TEST_URL); }); it('Supports URL as URL object from Request object.', () => { - const request = new Request(new Request(new URL(TEST_URL))); + const request = new window.Request(new window.Request(new URL(TEST_URL))); expect(request.url).toBe(TEST_URL); }); it('Supports URL as string from init object.', () => { - const request = new Request(TEST_URL); + const request = new window.Request(TEST_URL); expect(request.url).toBe(TEST_URL); }); it('Supports URL as URL object from init object.', () => { - const request = new Request(new URL(TEST_URL)); + const request = new window.Request(new URL(TEST_URL)); expect(request.url).toBe(TEST_URL); }); it('Supports relative URL.', () => { window.happyDOM.setURL('https://example.com/other/path/'); - const request = new Request('/path/'); + const request = new window.Request('/path/'); expect(request.url).toBe('https://example.com/path/'); }); it('Throws error for invalid URL.', () => { let error: Error | null = null; try { - new Request('/path/'); + new window.Request('/path/'); } catch (e) { error = e; } @@ -96,23 +95,23 @@ describe('Request', () => { }); it('Supports URL from Request object.', () => { - const request = new Request(new Request(TEST_URL)); + const request = new window.Request(new window.Request(TEST_URL)); expect(request.url).toBe(TEST_URL); }); it('Supports method from Request object.', () => { - const request = new Request(new Request(TEST_URL, { method: 'POST' })); + const request = new window.Request(new window.Request(TEST_URL, { method: 'POST' })); expect(request.method).toBe('POST'); }); it('Supports method from init object.', () => { - const request = new Request(TEST_URL, { method: 'POST' }); + const request = new window.Request(TEST_URL, { method: 'POST' }); expect(request.method).toBe('POST'); }); it('Supports body from Request object.', async () => { - const otherRequest = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); - const request = new Request(otherRequest); + const otherRequest = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(otherRequest); const chunks: Buffer[] = []; for await (const chunk of request.body) { @@ -123,7 +122,7 @@ describe('Request', () => { }); it('Supports body from init object.', async () => { - const request = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); const chunks: Buffer[] = []; for await (const chunk of request.body) { @@ -134,12 +133,12 @@ describe('Request', () => { }); it('Supports credentials from Request object.', () => { - const request = new Request(new Request(TEST_URL, { credentials: 'include' })); + const request = new window.Request(new window.Request(TEST_URL, { credentials: 'include' })); expect(request.credentials).toBe('include'); }); it('Supports credentials from init object.', () => { - const request = new Request(TEST_URL, { credentials: 'include' }); + const request = new window.Request(TEST_URL, { credentials: 'include' }); expect(request.credentials).toBe('include'); }); @@ -149,8 +148,8 @@ describe('Request', () => { headers.set('X-Test', 'Hello World'); headers.set('X-Test-2', 'Hello World 2'); - const otherRequest = new Request(TEST_URL, { headers }); - const request = new Request(otherRequest); + const otherRequest = new window.Request(TEST_URL, { headers }); + const request = new window.Request(otherRequest); const headerEntries = {}; for (const [key, value] of request.headers) { @@ -171,7 +170,7 @@ describe('Request', () => { headers.set('X-Test', 'Hello World'); headers.set('X-Test-2', 'Hello World 2'); - const request = new Request(TEST_URL, { headers }); + const request = new window.Request(TEST_URL, { headers }); const headerEntries = {}; for (const [key, value] of request.headers) { @@ -212,7 +211,7 @@ describe('Request', () => { 'safe-header': 'safe' }; - const request = new Request(TEST_URL, { headers }); + const request = new window.Request(TEST_URL, { headers }); const headerEntries = {}; for (const [key, value] of request.headers) { @@ -225,88 +224,98 @@ describe('Request', () => { }); it('Supports content length from Request object.', () => { - const request = new Request(new Request(TEST_URL, { method: 'POST', body: 'Hello World' })); + const request = new window.Request( + new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }) + ); expect(request._contentLength).toBe(11); }); it('Supports content length from init object.', () => { - const request = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); expect(request._contentLength).toBe(11); }); it('Supports content type from Request object.', () => { - const request = new Request(new Request(TEST_URL, { method: 'POST', body: 'Hello World' })); + const request = new window.Request( + new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }) + ); expect(request._contentType).toBe('text/plain;charset=UTF-8'); }); it('Supports content type from init object.', () => { - const request = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); expect(request._contentType).toBe('text/plain;charset=UTF-8'); }); it('Supports content type header from Request object.', () => { - const request = new Request(new Request(TEST_URL, { method: 'POST', body: 'Hello World' })); + const request = new window.Request( + new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }) + ); expect(request.headers.get('Content-Type')).toBe('text/plain;charset=UTF-8'); }); it('Supports content type header from init object.', () => { - const request = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); expect(request.headers.get('Content-Type')).toBe('text/plain;charset=UTF-8'); }); it('Supports redirect from Request object.', () => { - const request = new Request(new Request(TEST_URL, { redirect: 'manual' })); + const request = new window.Request(new window.Request(TEST_URL, { redirect: 'manual' })); expect(request.redirect).toBe('manual'); }); it('Supports redirect from init object.', () => { - const request = new Request(TEST_URL, { redirect: 'manual' }); + const request = new window.Request(TEST_URL, { redirect: 'manual' }); expect(request.redirect).toBe('manual'); }); it('Supports referrer policy from Request object.', () => { - const request = new Request(new Request(TEST_URL, { referrerPolicy: 'no-referrer' })); + const request = new window.Request( + new window.Request(TEST_URL, { referrerPolicy: 'no-referrer' }) + ); expect(request.referrerPolicy).toBe('no-referrer'); }); it('Supports referrer policy from init object.', () => { - const request = new Request(TEST_URL, { referrerPolicy: 'no-referrer' }); + const request = new window.Request(TEST_URL, { referrerPolicy: 'no-referrer' }); expect(request.referrerPolicy).toBe('no-referrer'); }); it('Supports signal from Request object.', () => { const signal = new AbortSignal(); - const request = new Request(new Request(TEST_URL, { signal })); + const request = new window.Request(new window.Request(TEST_URL, { signal })); expect(request.signal).toBe(signal); }); it('Supports signal from init object.', () => { const signal = new AbortSignal(); - const request = new Request(TEST_URL, { signal }); + const request = new window.Request(TEST_URL, { signal }); expect(request.signal).toBe(signal); }); it('Supports referrer from Request object.', () => { - const request1 = new Request(new Request(TEST_URL)); - const request2 = new Request(new Request(TEST_URL, { referrer: '' })); - const request3 = new Request(new Request(TEST_URL, { referrer: 'no-referrer' })); - const request4 = new Request(new Request(TEST_URL, { referrer: 'client' })); - const request5 = new Request( - new Request(TEST_URL, { referrer: 'https://example.com/path/' }) + const request1 = new window.Request(new window.Request(TEST_URL)); + const request2 = new window.Request(new window.Request(TEST_URL, { referrer: '' })); + const request3 = new window.Request( + new window.Request(TEST_URL, { referrer: 'no-referrer' }) + ); + const request4 = new window.Request(new window.Request(TEST_URL, { referrer: 'client' })); + const request5 = new window.Request( + new window.Request(TEST_URL, { referrer: 'https://example.com/path/' }) ); - const request6 = new Request( - new Request(TEST_URL, { referrer: new URL('https://example.com/path/') }) + const request6 = new window.Request( + new window.Request(TEST_URL, { referrer: new URL('https://example.com/path/') }) ); window.happyDOM.setURL('https://example.com/other/path/'); - const request7 = new Request( - new Request(TEST_URL, { referrer: 'https://example.com/path/' }) + const request7 = new window.Request( + new window.Request(TEST_URL, { referrer: 'https://example.com/path/' }) ); - const request8 = new Request( - new Request(TEST_URL, { referrer: new URL('https://example.com/path/') }) + const request8 = new window.Request( + new window.Request(TEST_URL, { referrer: new URL('https://example.com/path/') }) ); - const request9 = new Request(new Request(TEST_URL, { referrer: '/path/' })); + const request9 = new window.Request(new window.Request(TEST_URL, { referrer: '/path/' })); expect(request1.referrer).toBe('about:client'); expect(request2.referrer).toBe(''); @@ -320,18 +329,22 @@ describe('Request', () => { }); it('Supports referrer from init object.', () => { - const request1 = new Request(TEST_URL); - const request2 = new Request(TEST_URL, { referrer: '' }); - const request3 = new Request(TEST_URL, { referrer: 'no-referrer' }); - const request4 = new Request(TEST_URL, { referrer: 'client' }); - const request5 = new Request(TEST_URL, { referrer: 'https://example.com/path/' }); - const request6 = new Request(TEST_URL, { referrer: new URL('https://example.com/path/') }); + const request1 = new window.Request(TEST_URL); + const request2 = new window.Request(TEST_URL, { referrer: '' }); + const request3 = new window.Request(TEST_URL, { referrer: 'no-referrer' }); + const request4 = new window.Request(TEST_URL, { referrer: 'client' }); + const request5 = new window.Request(TEST_URL, { referrer: 'https://example.com/path/' }); + const request6 = new window.Request(TEST_URL, { + referrer: new URL('https://example.com/path/') + }); window.happyDOM.setURL('https://example.com/other/path/'); - const request7 = new Request(TEST_URL, { referrer: 'https://example.com/path/' }); - const request8 = new Request(TEST_URL, { referrer: new URL('https://example.com/path/') }); - const request9 = new Request(TEST_URL, { referrer: '/path/' }); + const request7 = new window.Request(TEST_URL, { referrer: 'https://example.com/path/' }); + const request8 = new window.Request(TEST_URL, { + referrer: new URL('https://example.com/path/') + }); + const request9 = new window.Request(TEST_URL, { referrer: '/path/' }); expect(request1.referrer).toBe('about:client'); expect(request2.referrer).toBe(''); @@ -347,7 +360,7 @@ describe('Request', () => { it('Throws error when combining body with GET method.', () => { let error: Error | null = null; try { - new Request(TEST_URL, { body: 'Hello world' }); + new window.Request(TEST_URL, { body: 'Hello world' }); } catch (e) { error = e; } @@ -363,7 +376,7 @@ describe('Request', () => { it('Throws error when combining body with HEAD method.', () => { let error: Error | null = null; try { - new Request(TEST_URL, { body: 'Hello world', method: 'HEAD' }); + new window.Request(TEST_URL, { body: 'Hello world', method: 'HEAD' }); } catch (e) { error = e; } @@ -379,7 +392,7 @@ describe('Request', () => { it('Throws error using username in URL.', () => { let error: Error | null = null; try { - new Request('https://user@example.com'); + new window.Request('https://user@example.com'); } catch (e) { error = e; } @@ -395,7 +408,7 @@ describe('Request', () => { it('Throws error using password in URL.', () => { let error: Error | null = null; try { - new Request('https://user:pass@example.com'); + new window.Request('https://user:pass@example.com'); } catch (e) { error = e; } @@ -411,7 +424,7 @@ describe('Request', () => { it('Throws error when invalid referrer policy.', () => { let error: Error | null = null; try { - new Request(TEST_URL, { referrerPolicy: 'invalid' }); + new window.Request(TEST_URL, { referrerPolicy: 'invalid' }); } catch (e) { error = e; } @@ -424,7 +437,7 @@ describe('Request', () => { it('Throws error when invalid referrer policy.', () => { let error: Error | null = null; try { - new Request(TEST_URL, { referrerPolicy: 'invalid' }); + new window.Request(TEST_URL, { referrerPolicy: 'invalid' }); } catch (e) { error = e; } @@ -437,7 +450,7 @@ describe('Request', () => { it('Throws error when invalid referrer policy.', () => { let error: Error | null = null; try { - new Request(TEST_URL, { redirect: 'invalid' }); + new window.Request(TEST_URL, { redirect: 'invalid' }); } catch (e) { error = e; } @@ -451,27 +464,27 @@ describe('Request', () => { describe('get referrer()', () => { it('Returns referrer.', () => { window.happyDOM.setURL('https://example.com/other/path/'); - const request = new Request(TEST_URL, { referrer: 'https://example.com/path/' }); + const request = new window.Request(TEST_URL, { referrer: 'https://example.com/path/' }); expect(request.referrer).toBe('https://example.com/path/'); }); }); describe('get url()', () => { it('Returns URL.', () => { - const request = new Request(TEST_URL); + const request = new window.Request(TEST_URL); expect(request.url).toBe(TEST_URL); }); }); describe('get [Symbol.toStringTag]()', () => { it('Returns class name.', () => { - expect(String(new Request(TEST_URL))).toBe('[object Request]'); + expect(String(new window.Request(TEST_URL))).toBe('[object Request]'); }); }); describe('arrayBuffer()', () => { it('Returns ArrayBuffer.', async () => { - const request = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); const arrayBuffer = await request.arrayBuffer(); expect(arrayBuffer).toBeInstanceOf(ArrayBuffer); @@ -480,7 +493,7 @@ describe('Request', () => { it('Supports window.happyDOM.whenAsyncComplete().', async () => { await new Promise((resolve) => { - const request = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; vi.spyOn(FetchBodyUtility, 'consumeBodyStream').mockImplementation( @@ -505,7 +518,7 @@ describe('Request', () => { describe('blob()', () => { it('Returns Blob.', async () => { - const request = new Request(TEST_URL, { + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World', headers: { 'Content-Type': 'text/plain' } @@ -521,7 +534,7 @@ describe('Request', () => { it('Supports window.happyDOM.whenAsyncComplete().', async () => { await new Promise((resolve) => { - const request = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; vi.spyOn(FetchBodyUtility, 'consumeBodyStream').mockImplementation( @@ -546,7 +559,7 @@ describe('Request', () => { describe('buffer()', () => { it('Returns Buffer.', async () => { - const request = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); const buffer = await request.buffer(); expect(buffer).toBeInstanceOf(Buffer); @@ -555,7 +568,7 @@ describe('Request', () => { it('Supports window.happyDOM.whenAsyncComplete().', async () => { await new Promise((resolve) => { - const request = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; vi.spyOn(FetchBodyUtility, 'consumeBodyStream').mockImplementation( @@ -580,7 +593,7 @@ describe('Request', () => { describe('text()', () => { it('Returns text string.', async () => { - const request = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); const text = await request.text(); expect(text).toBe('Hello World'); @@ -588,7 +601,7 @@ describe('Request', () => { it('Supports window.happyDOM.whenAsyncComplete().', async () => { await new Promise((resolve) => { - const request = new Request(TEST_URL, { method: 'POST', body: 'Hello World' }); + const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; vi.spyOn(FetchBodyUtility, 'consumeBodyStream').mockImplementation( @@ -613,7 +626,10 @@ describe('Request', () => { describe('json()', () => { it('Returns JSON.', async () => { - const request = new Request(TEST_URL, { method: 'POST', body: '{ "key1": "value1" }' }); + const request = new window.Request(TEST_URL, { + method: 'POST', + body: '{ "key1": "value1" }' + }); const json = await request.json(); expect(json).toEqual({ key1: 'value1' }); @@ -621,7 +637,10 @@ describe('Request', () => { it('Supports window.happyDOM.whenAsyncComplete().', async () => { await new Promise((resolve) => { - const request = new Request(TEST_URL, { method: 'POST', body: '{ "key1": "value1" }' }); + const request = new window.Request(TEST_URL, { + method: 'POST', + body: '{ "key1": "value1" }' + }); let isAsyncComplete = false; vi.spyOn(FetchBodyUtility, 'consumeBodyStream').mockImplementation( @@ -650,7 +669,7 @@ describe('Request', () => { it('Returns FormData', async () => { const formData = new FormData(); formData.append('some', 'test'); - const request = new Request(TEST_URL, { method: 'POST', body: formData }); + const request = new window.Request(TEST_URL, { method: 'POST', body: formData }); const requestFormData = await request.formData(); expect(requestFormData).toEqual(formData); @@ -660,7 +679,7 @@ describe('Request', () => { await new Promise((resolve) => { const formData = new FormData(); formData.append('some', 'test'); - const request = new Request(TEST_URL, { method: 'POST', body: formData }); + const request = new window.Request(TEST_URL, { method: 'POST', body: formData }); let isAsyncComplete = false; vi.spyOn(MultipartFormDataParser, 'streamToFormData').mockImplementation( @@ -687,7 +706,7 @@ describe('Request', () => { window.happyDOM.setURL('https://example.com/other/path/'); const signal = new AbortSignal(); - const request = new Request(TEST_URL, { + const request = new window.Request(TEST_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: 'Hello world', diff --git a/packages/happy-dom/test/fetch/Response.test.ts b/packages/happy-dom/test/fetch/Response.test.ts index 93121e273..6eecb8e38 100644 --- a/packages/happy-dom/test/fetch/Response.test.ts +++ b/packages/happy-dom/test/fetch/Response.test.ts @@ -23,7 +23,6 @@ describe('Response', () => { beforeEach(() => { window = new Window(); document = window.document; - Response._ownerDocument = document; }); afterEach(() => { @@ -32,7 +31,7 @@ describe('Response', () => { describe('constructor()', () => { it('Sets default values for properties.', () => { - const response = new Response(); + const response = new window.Response(); let headersLength = 0; for (const _header of response.headers) { @@ -49,20 +48,20 @@ describe('Response', () => { }); it('Sets status from init object.', () => { - const response = new Response(null, { status: 404 }); + const response = new window.Response(null, { status: 404 }); expect(response.status).toBe(404); }); it('Sets status text from init object.', () => { - const response = new Response(null, { statusText: 'test' }); + const response = new window.Response(null, { statusText: 'test' }); expect(response.statusText).toBe('test'); }); it('Sets ok state correctly based on status code.', () => { - const response199 = new Response(null, { status: 199 }); - const response200 = new Response(null, { status: 200 }); - const response299 = new Response(null, { status: 299 }); - const response300 = new Response(null, { status: 300 }); + const response199 = new window.Response(null, { status: 199 }); + const response200 = new window.Response(null, { status: 200 }); + const response299 = new window.Response(null, { status: 299 }); + const response300 = new window.Response(null, { status: 300 }); expect(response199.ok).toBe(false); expect(response200.ok).toBe(true); expect(response299.ok).toBe(true); @@ -76,7 +75,7 @@ describe('Response', () => { }; const headers = new Headers(headerValues); - const response = new Response(null, { headers }); + const response = new window.Response(null, { headers }); const headerEntries = {}; @@ -89,7 +88,7 @@ describe('Response', () => { }); it('Sets body from init object.', async () => { - const response = new Response('Hello World'); + const response = new window.Response('Hello World'); const chunks: Buffer[] = []; for await (const chunk of response.body) { @@ -102,13 +101,13 @@ describe('Response', () => { describe('get [Symbol.toStringTag]()', () => { it('Returns class name.', () => { - expect(String(new Response())).toBe('[object Response]'); + expect(String(new window.Response())).toBe('[object Response]'); }); }); describe('arrayBuffer()', () => { it('Returns ArrayBuffer.', async () => { - const response = new Response('Hello World'); + const response = new window.Response('Hello World'); const arrayBuffer = await response.arrayBuffer(); expect(arrayBuffer).toBeInstanceOf(ArrayBuffer); @@ -117,7 +116,7 @@ describe('Response', () => { it('Supports window.happyDOM.whenAsyncComplete().', async () => { await new Promise((resolve) => { - const response = new Response('Hello World'); + const response = new window.Response('Hello World'); let isAsyncComplete = false; vi.spyOn(FetchBodyUtility, 'consumeBodyStream').mockImplementation( @@ -142,7 +141,9 @@ describe('Response', () => { describe('blob()', () => { it('Returns Blob.', async () => { - const response = new Response('Hello World', { headers: { 'Content-Type': 'text/plain' } }); + const response = new window.Response('Hello World', { + headers: { 'Content-Type': 'text/plain' } + }); const blob = await response.blob(); expect(blob).toBeInstanceOf(Blob); @@ -154,7 +155,9 @@ describe('Response', () => { it('Supports window.happyDOM.whenAsyncComplete().', async () => { await new Promise((resolve) => { - const response = new Response('Hello World', { headers: { 'Content-Type': 'text/plain' } }); + const response = new window.Response('Hello World', { + headers: { 'Content-Type': 'text/plain' } + }); let isAsyncComplete = false; vi.spyOn(FetchBodyUtility, 'consumeBodyStream').mockImplementation( @@ -179,7 +182,7 @@ describe('Response', () => { describe('buffer()', () => { it('Returns Buffer.', async () => { - const response = new Response('Hello World'); + const response = new window.Response('Hello World'); const buffer = await response.buffer(); expect(buffer).toBeInstanceOf(Buffer); @@ -188,7 +191,7 @@ describe('Response', () => { it('Supports window.happyDOM.whenAsyncComplete().', async () => { await new Promise((resolve) => { - const response = new Response('Hello World'); + const response = new window.Response('Hello World'); let isAsyncComplete = false; vi.spyOn(FetchBodyUtility, 'consumeBodyStream').mockImplementation( @@ -215,7 +218,7 @@ describe('Response', () => { describe('text()', () => { it('Returns text string.', async () => { - const response = new Response('Hello World'); + const response = new window.Response('Hello World'); const text = await response.text(); expect(text).toBe('Hello World'); @@ -223,7 +226,7 @@ describe('Response', () => { it('Supports window.happyDOM.whenAsyncComplete().', async () => { await new Promise((resolve) => { - const response = new Response('Hello World'); + const response = new window.Response('Hello World'); let isAsyncComplete = false; vi.spyOn(FetchBodyUtility, 'consumeBodyStream').mockImplementation( @@ -248,7 +251,7 @@ describe('Response', () => { describe('json()', () => { it('Returns JSON.', async () => { - const response = new Response('{ "key1": "value1" }'); + const response = new window.Response('{ "key1": "value1" }'); const json = await response.json(); expect(json).toEqual({ key1: 'value1' }); @@ -256,7 +259,7 @@ describe('Response', () => { it('Supports window.happyDOM.whenAsyncComplete().', async () => { await new Promise((resolve) => { - const response = new Response('{ "key1": "value1" }'); + const response = new window.Response('{ "key1": "value1" }'); let isAsyncComplete = false; vi.spyOn(FetchBodyUtility, 'consumeBodyStream').mockImplementation( @@ -289,7 +292,7 @@ describe('Response', () => { urlSearchParams.set('key2', 'value2'); urlSearchParams.set('key3', 'value3'); - const response = new Response(urlSearchParams); + const response = new window.Response(urlSearchParams); const formDataResponse = await response.formData(); let size = 0; @@ -312,7 +315,7 @@ describe('Response', () => { formData.set('key2', 'value2'); formData.set('key3', 'value3'); - const response = new Response(formData); + const response = new window.Response(formData); const formDataResponse = await response.formData(); let size = 0; @@ -339,7 +342,7 @@ describe('Response', () => { formData.set('key2', 'value2'); formData.set('file2', new File([imageBuffer], 'test-image-2.jpg', { type: 'image/jpeg' })); - const response = new Response(formData); + const response = new window.Response(formData); const formDataResponse = await response.formData(); let size = 0; @@ -367,7 +370,7 @@ describe('Response', () => { it('Supports window.happyDOM.whenAsyncComplete() for "application/x-www-form-urlencoded" content.', async () => { await new Promise((resolve) => { - const response = new Response(new URLSearchParams()); + const response = new window.Response(new URLSearchParams()); let isAsyncComplete = false; vi.spyOn(FetchBodyUtility, 'consumeBodyStream').mockImplementation( @@ -391,7 +394,7 @@ describe('Response', () => { it('Supports window.happyDOM.whenAsyncComplete() for multipart content.', async () => { await new Promise((resolve) => { - const response = new Response(new FormData()); + const response = new window.Response(new FormData()); let isAsyncComplete = false; vi.spyOn(MultipartFormDataParser, 'streamToFormData').mockImplementation( @@ -416,7 +419,7 @@ describe('Response', () => { describe('clone()', () => { it('Returns a clone.', async () => { - const response = new Response('Hello World', { + const response = new window.Response('Hello World', { status: 404, statusText: 'Not Found', headers: { 'Content-Type': 'text/plain' } diff --git a/packages/happy-dom/test/file/FileReader.test.ts b/packages/happy-dom/test/file/FileReader.test.ts index e1bb6e49b..0df59c60a 100644 --- a/packages/happy-dom/test/file/FileReader.test.ts +++ b/packages/happy-dom/test/file/FileReader.test.ts @@ -9,8 +9,7 @@ describe('FileReader', () => { beforeEach(() => { window = new Window(); - FileReader._ownerDocument = window.document; - fileReader = new FileReader(); + fileReader = new window.FileReader(); }); describe('readAsDataURL()', () => { diff --git a/packages/happy-dom/test/match-media/MediaQueryList.test.ts b/packages/happy-dom/test/match-media/MediaQueryList.test.ts index 56dca4440..37dabf5dd 100644 --- a/packages/happy-dom/test/match-media/MediaQueryList.test.ts +++ b/packages/happy-dom/test/match-media/MediaQueryList.test.ts @@ -61,13 +61,17 @@ describe('MediaQueryList', () => { }); it('Handles media type with name "print".', () => { + window = new Window({ + width: 1024, + height: 768, + settings: { device: { mediaType: '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 @@ -269,7 +273,11 @@ describe('MediaQueryList', () => { new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme: light)' }).matches ).toBe(true); - window.happyDOM.settings.device.prefersColorScheme = 'dark'; + window = new Window({ + width: 1024, + height: 768, + settings: { device: { prefersColorScheme: 'dark' } } + }); expect( new MediaQueryList({ ownerWindow: window, media: '(prefers-color-scheme: dark)' }).matches @@ -487,7 +495,7 @@ describe('MediaQueryList', () => { ).toBe(true); }); - it('Handles disabling computed style rendering with Window.happyDOM.settings.disableComputedStyleRendering.', () => { + it('Handles disabling computed style rendering with the Happy DOM setting "disableComputedStyleRendering" set to "true".', () => { window.document.documentElement.style.fontSize = '10px'; expect( @@ -497,7 +505,11 @@ describe('MediaQueryList', () => { new MediaQueryList({ ownerWindow: window, media: `(max-width: ${1024 / 10}rem)` }).matches ).toBe(true); - window.happyDOM.settings.disableComputedStyleRendering = true; + window = new Window({ + width: 1024, + height: 768, + settings: { disableComputedStyleRendering: true } + }); expect( new MediaQueryList({ ownerWindow: window, media: `(max-width: ${1023 / 16}rem)` }).matches diff --git a/packages/happy-dom/test/nodes/html-image-element/Image.test.ts b/packages/happy-dom/test/nodes/html-image-element/Image.test.ts index 13a52dfd0..fb9c22bbf 100644 --- a/packages/happy-dom/test/nodes/html-image-element/Image.test.ts +++ b/packages/happy-dom/test/nodes/html-image-element/Image.test.ts @@ -1,5 +1,4 @@ import Window from '../../../src/window/Window.js'; -import Image from '../../../src/nodes/html-image-element/Image.js'; import HTMLImageElement from '../../../src/nodes/html-image-element/HTMLImageElement.js'; import { beforeEach, describe, it, expect } from 'vitest'; @@ -12,7 +11,7 @@ describe('Image', () => { describe('constructor()', () => { it('Create img element without width and height.', () => { - const image = new Image(); + const image = new window.Image(); expect(image.width).toBe(0); expect(image.height).toBe(0); expect(image.tagName).toBe('IMG'); diff --git a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts index 0474c3b71..caf5b4f05 100644 --- a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts +++ b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts @@ -234,7 +234,12 @@ describe('HTMLLinkElement', () => { expect(element.sheet).toBe(null); }); - it('Triggers an error event when "window.happyDOM.settings.disableCSSFileLoading" is set to "true".', async () => { + it('Triggers an error event when the Happy DOM setting "disableCSSFileLoading" is set to "true".', async () => { + window = new Window({ + settings: { disableCSSFileLoading: true } + }); + document = window.document; + const element = document.createElement('link'); let errorEvent: ErrorEvent | null = null; @@ -242,8 +247,6 @@ describe('HTMLLinkElement', () => { element.href = '/test/path/file.css'; element.addEventListener('error', (event) => (errorEvent = event)); - window.happyDOM.settings.disableCSSFileLoading = true; - document.body.appendChild(element); expect(element.sheet).toBe(null); diff --git a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts index b94aee8d1..8867c8348 100644 --- a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts +++ b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts @@ -345,7 +345,12 @@ describe('HTMLScriptElement', () => { expect(window['testContent']).toBe(undefined); }); - it('Triggers an error event when attempting to perform an asynchrounous request and "window.happyDOM.settings.disableJavaScriptFileLoading" is set to "true".', () => { + it('Triggers an error event when attempting to perform an asynchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + window = new Window({ + settings: { disableJavaScriptFileLoading: true } + }); + document = window.document; + let errorEvent: ErrorEvent | null = null; const script = window.document.createElement('script'); @@ -355,8 +360,6 @@ describe('HTMLScriptElement', () => { errorEvent = event; }); - window.happyDOM.settings.disableJavaScriptFileLoading = true; - document.body.appendChild(script); expect(((errorEvent)).message).toBe( @@ -364,7 +367,12 @@ describe('HTMLScriptElement', () => { ); }); - it('Triggers an error event when attempting to perform a synchrounous request and "window.happyDOM.settings.disableJavaScriptFileLoading" is set to "true".', () => { + it('Triggers an error event when attempting to perform a synchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + window = new Window({ + settings: { disableJavaScriptFileLoading: true } + }); + document = window.document; + let errorEvent: ErrorEvent | null = null; const script = window.document.createElement('script'); @@ -373,8 +381,6 @@ describe('HTMLScriptElement', () => { errorEvent = event; }); - window.happyDOM.settings.disableJavaScriptFileLoading = true; - document.body.appendChild(script); expect(((errorEvent)).message).toBe( @@ -382,7 +388,12 @@ describe('HTMLScriptElement', () => { ); }); - it('Triggers an error event when attempting to perform an asynchrounous request and "window.happyDOM.settings.disableJavaScriptEvaluation" is set to "true".', () => { + it('Triggers an error event when attempting to perform an asynchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + window = new Window({ + settings: { disableJavaScriptFileLoading: true } + }); + document = window.document; + let errorEvent: ErrorEvent | null = null; const script = window.document.createElement('script'); @@ -392,8 +403,6 @@ describe('HTMLScriptElement', () => { errorEvent = event; }); - window.happyDOM.settings.disableJavaScriptEvaluation = true; - document.body.appendChild(script); expect(((errorEvent)).message).toBe( @@ -401,7 +410,12 @@ describe('HTMLScriptElement', () => { ); }); - it('Triggers an error event when attempting to perform a synchrounous request and "window.happyDOM.settings.disableJavaScriptEvaluation" is set to "true".', () => { + it('Triggers an error event when attempting to perform a synchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + window = new Window({ + settings: { disableJavaScriptFileLoading: true } + }); + document = window.document; + let errorEvent: ErrorEvent | null = null; const script = window.document.createElement('script'); @@ -410,8 +424,6 @@ describe('HTMLScriptElement', () => { errorEvent = event; }); - window.happyDOM.settings.disableJavaScriptEvaluation = true; - document.body.appendChild(script); expect(((errorEvent)).message).toBe( @@ -491,10 +503,13 @@ describe('HTMLScriptElement', () => { ); }); - it('Throws an exception when appending an element that contains invalid Javascript and Window.happyDOM.settings.disableErrorCapturing is set to true.', () => { - const element = document.createElement('script'); + it('Throws an exception when appending an element that contains invalid Javascript and the Happy DOM setting "disableErrorCapturing" is set to true.', () => { + window = new Window({ + settings: { disableErrorCapturing: true } + }); + document = window.document; - window.happyDOM.settings.disableErrorCapturing = true; + const element = document.createElement('script'); element.text = 'globalThis.test = /;'; diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 3af67d868..2b8052d46 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -784,15 +784,21 @@ describe('Window', () => { expect(computedStyle.height).toBe('150px'); }); - it('Returns a CSSStyleDeclaration object with computed styles containing "rem" and "em" measurement values that has not been converted to pixels if Window.happyDOM.settings.disableComputedStyleRendering is set to "true".', () => { + it('Returns a CSSStyleDeclaration object with computed styles containing "rem" and "em" measurement values that has not been converted to pixels if the Happy DOM setting "disableComputedStyleRendering" is set to "true".', () => { + window = new Window({ + width: 1024, + settings: { + disableComputedStyleRendering: true + } + }); + document = window.document; + 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.setWindowSize({ width: 1024 }); - parentStyle.innerHTML = ` html { font-size: 10px; @@ -816,8 +822,6 @@ describe('Window', () => { document.body.appendChild(parentStyle); document.body.appendChild(parent); - window.happyDOM.settings.disableComputedStyleRendering = true; - expect(computedStyle.width).toBe('10rem'); expect(computedStyle.height).toBe('10em'); }); @@ -1448,7 +1452,7 @@ describe('Window', () => { (window.parent) = parent; - window.addEventListener('message', (event) => (triggeredEvent = event)); + window.addEventListener('message', (event) => (triggeredEvent = event)); window.postMessage(message); expect(triggeredEvent).toBe(null); @@ -1481,7 +1485,7 @@ describe('Window', () => { }; let triggeredEvent: MessageEvent | null = null; - window.addEventListener('message', (event) => (triggeredEvent = event)); + window.addEventListener('message', (event) => (triggeredEvent = event)); window.postMessage(message); expect(triggeredEvent).toBe(null); diff --git a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts index c045604d2..3e7338325 100644 --- a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts +++ b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts @@ -961,24 +961,28 @@ describe('XMLHttpRequest', () => { ); }); - it('Throws an exception when doing a synchronous request towards a local file if "window.happyDOM.settings.enableFileSystemHttpRequests" has not been enabled.', () => { + it('Throws an exception when doing a synchronous request towards a local file if the Happy DOM setting "enableFileSystemHttpRequests" has not been enabled.', () => { request.open('GET', 'file://C:/path/to/file.txt', false); expect(() => request.send()).toThrowError( - 'File system is disabled by default for security reasons. To enable it, set the "window.happyDOM.settings.enableFileSystemHttpRequests" option to true.' + 'File system is disabled by default for security reasons. To enable it, set the Happy DOM setting "enableFileSystemHttpRequests" option to true.' ); }); - it('Throws an exception when doing an asynchronous request towards a local file if "window.happyDOM.settings.enableFileSystemHttpRequests" has not been enabled.', () => { + it('Throws an exception when doing an asynchronous request towards a local file if the Happy DOM setting "enableFileSystemHttpRequests" has not been enabled.', () => { request.open('GET', 'file://C:/path/to/file.txt', true); expect(() => request.send()).toThrowError( - 'File system is disabled by default for security reasons. To enable it, set the "window.happyDOM.settings.enableFileSystemHttpRequests" option to true.' + 'File system is disabled by default for security reasons. To enable it, set the Happy DOM setting "enableFileSystemHttpRequests" option to true.' ); }); it('Throws an exception when doing a synchronous request towards a local file with another method than "GET".', () => { - window.happyDOM.settings.enableFileSystemHttpRequests = true; + window = new Window({ + settings: { + enableFileSystemHttpRequests: false + } + }); request.open('POST', 'file://C:/path/to/file.txt', false); @@ -988,7 +992,11 @@ describe('XMLHttpRequest', () => { }); it('Throws an exception when doing a asynchronous request towards a local file with another method than "GET".', () => { - window.happyDOM.settings.enableFileSystemHttpRequests = true; + window = new Window({ + settings: { + enableFileSystemHttpRequests: true + } + }); request.open('POST', 'file://C:/path/to/file.txt', true); @@ -998,6 +1006,12 @@ describe('XMLHttpRequest', () => { }); it('Performs a synchronous request towards a local file.', () => { + window = new Window({ + settings: { + enableFileSystemHttpRequests: true + } + }); + const filepath = 'C:/path/to/file.txt'; const fileContent = 'test'; @@ -1008,8 +1022,6 @@ describe('XMLHttpRequest', () => { } }); - window.happyDOM.settings.enableFileSystemHttpRequests = true; - request.open('GET', `file://${filepath}`, false); request.send(); @@ -1020,6 +1032,12 @@ describe('XMLHttpRequest', () => { }); it('Performs an asynchronous request towards a local file.', async () => { + window = new Window({ + settings: { + enableFileSystemHttpRequests: true + } + }); + await new Promise((resolve) => { const filepath = 'C:/path/to/file.txt'; const fileContent = 'test'; @@ -1033,8 +1051,6 @@ describe('XMLHttpRequest', () => { } }); - window.happyDOM.settings.enableFileSystemHttpRequests = true; - request.open('GET', `file://${filepath}`, true); let isProgressTriggered = false; diff --git a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts index a3b42305f..de07f955d 100644 --- a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts +++ b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts @@ -46,13 +46,15 @@ async function itObservesUnhandledFetchRejections(): Promise { } async function itObservesUnhandledJavaScriptFetchRejections(): Promise { - const window = new Window(); + const window = new Window({ + settings: { + disableErrorCapturing: true + } + }); const document = window.document; const observer = new UncaughtExceptionObserver(); let errorEvent: ErrorEvent | null = null; - window.happyDOM.settings.disableErrorCapturing = true; - observer.observe(window); window.addEventListener('error', (event) => (errorEvent = event)); From 0c266636f3f030a0c0c4fa27bd3644d5b29c1ed6 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Sun, 22 Oct 2023 12:27:13 +0200 Subject: [PATCH 11/63] #466@trivial: Continues on implementation. --- packages/happy-dom/package.json | 1 + packages/happy-dom/src/browser/BrowserPage.ts | 1 - .../src/browser/BrowserSettingsFactory.ts | 8 ++-- .../happy-dom/src/dom-parser/DOMParser.ts | 17 ++------ packages/happy-dom/src/event/EventTarget.ts | 6 +-- .../happy-dom/src/nodes/document/Document.ts | 43 ++----------------- .../HTMLLinkElementUtility.ts | 9 +++- .../HTMLScriptElementUtility.ts | 10 +++-- packages/happy-dom/src/window/Window.ts | 35 ++++++++++----- .../src/window/WindowClassFactory.ts | 28 ------------ .../src/xml-http-request/XMLHttpRequest.ts | 1 - 11 files changed, 54 insertions(+), 105 deletions(-) diff --git a/packages/happy-dom/package.json b/packages/happy-dom/package.json index 0bb37621b..68f92939b 100644 --- a/packages/happy-dom/package.json +++ b/packages/happy-dom/package.json @@ -69,6 +69,7 @@ "change-cjs-file-extension": "node ./bin/change-file-extension.cjs --dir=./cjs --fromExt=.js --toExt=.cjs", "build-version-file": "node ./bin/build-version-file.cjs", "watch": "tsc -w --preserveWatchOutput", + "test": "vitest run --singleThread", "test:ui": "vitest --ui", "test:watch": "vitest --singleThread", "test:debug": "vitest run --inspect-brk --threads=false" diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index a686bbe08..f29dd020e 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -1,4 +1,3 @@ -import Event from '../event/Event.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; import IBrowserPageViewport from './IBrowserPageViewport.js'; import BrowserFrame from './BrowserFrame.js'; diff --git a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts index 06e1e7abe..e718b5bd5 100644 --- a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts +++ b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts @@ -20,11 +20,11 @@ export default class BrowserSettingsFactory { ...settings, navigator: { ...DefaultBrowserSettings.navigator, - ...settings.navigator + ...settings?.navigator }, device: { ...DefaultBrowserSettings.device, - ...settings.device + ...settings?.device } }; } @@ -41,11 +41,11 @@ export default class BrowserSettingsFactory { ...settings, navigator: Object.freeze({ ...DefaultBrowserSettings.navigator, - ...settings.navigator + ...settings?.navigator }), device: Object.freeze({ ...DefaultBrowserSettings.device, - ...settings.device + ...settings?.device }) }); } diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index 2333d7d6d..dc82bd4db 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -2,11 +2,7 @@ import IDocument from '../nodes/document/IDocument.js'; import XMLParser from '../xml-parser/XMLParser.js'; import Node from '../nodes/node/Node.js'; import DOMException from '../exception/DOMException.js'; -import HTMLDocument from '../nodes/html-document/HTMLDocument.js'; -import XMLDocument from '../nodes/xml-document/XMLDocument.js'; -import SVGDocument from '../nodes/svg-document/SVGDocument.js'; import IWindow from '../window/IWindow.js'; -import Document from '../nodes/document/Document.js'; import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; /** @@ -32,11 +28,9 @@ export default class DOMParser { } const ownerDocument = this._ownerDocument; - const newDocument = this._createDocument(mimeType); + const newDocument = this._createDocument(mimeType); (newDocument.defaultView) = ownerDocument.defaultView; - newDocument._childNodes.length = 0; - newDocument._children.length = 0; const root = XMLParser.parse(newDocument, string, { evaluateScripts: true }); let documentElement = null; @@ -104,16 +98,13 @@ export default class DOMParser { private _createDocument(mimeType: string): IDocument { switch (mimeType) { case 'text/html': - HTMLDocument._defaultView = this._ownerDocument.defaultView; - return new HTMLDocument(); + return new this._ownerDocument.defaultView.HTMLDocument(); case 'image/svg+xml': - SVGDocument._defaultView = this._ownerDocument.defaultView; - return new SVGDocument(); + return new this._ownerDocument.defaultView.SVGDocument(); case 'text/xml': case 'application/xml': case 'application/xhtml+xml': - XMLDocument._defaultView = this._ownerDocument.defaultView; - return new XMLDocument(); + return new this._ownerDocument.defaultView.XMLDocument(); default: throw new DOMException(`Unknown mime type "${mimeType}".`); } diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts index c14402b1b..cd7ba4399 100644 --- a/packages/happy-dom/src/event/EventTarget.ts +++ b/packages/happy-dom/src/event/EventTarget.ts @@ -141,8 +141,6 @@ export default abstract class EventTarget implements IEventTarget { return !(event.cancelable && event.defaultPrevented); } - const browserSettings = WindowBrowserSettingsReader.getSettings(window); - event._currentTarget = this; if (event.eventPhase !== EventPhaseEnum.capturing) { @@ -153,7 +151,7 @@ export default abstract class EventTarget implements IEventTarget { if ( window && (this !== window || event.type !== 'error') && - !browserSettings.disableErrorCapturing + !WindowBrowserSettingsReader.getSettings(window).disableErrorCapturing ) { WindowErrorUtility.captureError(window, this[onEventName].bind(this, event)); } else { @@ -186,7 +184,7 @@ export default abstract class EventTarget implements IEventTarget { if ( window && (this !== window || event.type !== 'error') && - !browserSettings.disableErrorCapturing + !WindowBrowserSettingsReader.getSettings(window).disableErrorCapturing ) { if ((listener).handleEvent) { WindowErrorUtility.captureError( diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 7dd49f14c..e5a741854 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -32,7 +32,6 @@ import IHTMLCollection from '../element/IHTMLCollection.js'; import IHTMLLinkElement from '../html-link-element/IHTMLLinkElement.js'; import IHTMLStyleElement from '../html-style-element/IHTMLStyleElement.js'; import DocumentReadyStateEnum from './DocumentReadyStateEnum.js'; -import DocumentReadyStateManager from './DocumentReadyStateManager.js'; import Location from '../../location/Location.js'; import Selection from '../../selection/Selection.js'; import IShadowRoot from '../shadow-root/IShadowRoot.js'; @@ -53,17 +52,18 @@ const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; export default class Document extends Node implements IDocument { public nodeType = Node.DOCUMENT_NODE; public adoptedStyleSheets: CSSStyleSheet[] = []; - public implementation: DOMImplementation; + public implementation = new DOMImplementation(this); public readonly readyState = DocumentReadyStateEnum.interactive; public readonly isConnected: boolean = true; - public readonly defaultView: IWindow; + public readonly defaultView: IWindow | null = null; public readonly referrer = ''; + public readonly ownerDocument = null; public readonly _windowClass: {} | null = null; - public readonly _readyStateManager: DocumentReadyStateManager; public readonly _children: IHTMLCollection = new HTMLCollection(); public _activeElement: IHTMLElement = null; public _nextActiveElement: IHTMLElement = null; public _currentScript: IHTMLScriptElement = null; + public _rootNode = this; // Used as an unique identifier which is updated whenever the DOM gets modified. public _cacheID = 0; @@ -186,30 +186,6 @@ export default class Document extends Node implements IDocument { public onpaste: (event: Event) => void = null; public onbeforematch: (event: Event) => void = null; - /** - * Creates an instance of Document. - * - */ - constructor() { - super(); - - this.implementation = new DOMImplementation(this); - - this._readyStateManager = new DocumentReadyStateManager(this.defaultView); - this._rootNode = this; - - const doctype = this.implementation.createDocumentType('html', '', ''); - const documentElement = this.createElement('html'); - const bodyElement = this.createElement('body'); - const headElement = this.createElement('head'); - - this.appendChild(doctype); - this.appendChild(documentElement); - - documentElement.appendChild(headElement); - documentElement.appendChild(bodyElement); - } - /** * Returns document children. */ @@ -989,17 +965,6 @@ export default class Document extends Node implements IDocument { return !!this.activeElement; } - /** - * Triggered by window when it is ready. - */ - public _onWindowReady(): void { - this._readyStateManager.whenComplete().then(() => { - (this.readyState) = DocumentReadyStateEnum.complete; - this.dispatchEvent(new Event('readystatechange')); - this.dispatchEvent(new Event('load', { bubbles: true })); - }); - } - /** * Creates a Processing Instruction node. * diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts index d15eeacce..067c7b487 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts @@ -7,6 +7,7 @@ import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; +import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; /** * Helper class for getting the URL relative to a Location object. @@ -38,7 +39,9 @@ export default class HTMLLinkElementUtility { return; } - (element.ownerDocument)._readyStateManager.startTask(); + (<{ _readyStateManager: DocumentReadyStateManager }>( + (element.ownerDocument.defaultView) + ))._readyStateManager.startTask(); let code: string | null = null; let error: Error | null = null; @@ -49,7 +52,9 @@ export default class HTMLLinkElementUtility { error = e; } - (element.ownerDocument)._readyStateManager.endTask(); + (<{ _readyStateManager: DocumentReadyStateManager }>( + (element.ownerDocument.defaultView) + ))._readyStateManager.endTask(); if (error) { WindowErrorUtility.dispatchError(element, error); diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts index 5bae08daa..dd9f6e490 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts @@ -1,4 +1,3 @@ -import Document from '../document/Document.js'; import Event from '../../event/Event.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; @@ -6,6 +5,7 @@ import ResourceFetch from '../../fetch/ResourceFetch.js'; import HTMLScriptElement from './HTMLScriptElement.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; +import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; /** * Helper class for getting the URL relative to a Location object. @@ -40,7 +40,9 @@ export default class HTMLScriptElementUtility { } if (async) { - (element.ownerDocument)._readyStateManager.startTask(); + (<{ _readyStateManager: DocumentReadyStateManager }>( + (element.ownerDocument.defaultView) + ))._readyStateManager.startTask(); let code: string | null = null; let error: Error | null = null; @@ -51,7 +53,9 @@ export default class HTMLScriptElementUtility { error = e; } - (element.ownerDocument)._readyStateManager.endTask(); + (<{ _readyStateManager: DocumentReadyStateManager }>( + (element.ownerDocument.defaultView) + ))._readyStateManager.endTask(); if (error) { WindowErrorUtility.dispatchError(element, error); diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index f5a0b12d9..345bec7ed 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -141,6 +141,8 @@ import Range from '../range/Range.js'; import XMLHttpRequest from '../xml-http-request/XMLHttpRequest.js'; import IOptionalBrowserSettings from '../browser/IOptionalBrowserSettings.js'; import WindowBrowserSettingsReader from './WindowBrowserSettingsReader.js'; +import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; +import DocumentReadyStateEnum from '../nodes/document/DocumentReadyStateEnum.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -170,10 +172,10 @@ export default class Window extends EventTarget implements IWindow { public readonly ProcessingInstruction: typeof ProcessingInstruction; public readonly Element: typeof Element; public readonly CharacterData: typeof CharacterData; - public readonly Document: typeof Document; - public readonly HTMLDocument: typeof HTMLDocument; - public readonly XMLDocument: typeof XMLDocument; - public readonly SVGDocument: typeof SVGDocument; + public readonly Document = Document; + public readonly HTMLDocument = HTMLDocument; + public readonly XMLDocument = XMLDocument; + public readonly SVGDocument = SVGDocument; // Element classes public readonly HTMLElement: typeof HTMLElement; @@ -486,6 +488,7 @@ export default class Window extends EventTarget implements IWindow { private _setInterval: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; private _clearInterval: (id: NodeJS.Timeout) => void; private _queueMicrotask: (callback: Function) => void; + public readonly _readyStateManager = new DocumentReadyStateManager(this); #browserFrame: BrowserFrame | DetachedBrowserFrame; /** @@ -616,10 +619,6 @@ export default class Window extends EventTarget implements IWindow { this.ProcessingInstruction = classes.ProcessingInstruction; this.Element = classes.Element; this.CharacterData = classes.CharacterData; - this.Document = classes.Document; - this.HTMLDocument = classes.HTMLDocument; - this.XMLDocument = classes.XMLDocument; - this.SVGDocument = classes.SVGDocument; // HTML Element classes this.HTMLElement = classes.HTMLElement; @@ -694,11 +693,27 @@ export default class Window extends EventTarget implements IWindow { this.HTMLParamElement = classes.HTMLElement; this.HTMLTrackElement = classes.HTMLElement; + this._setupVMContext(); + this.document = new this.HTMLDocument(); + (this.document.defaultView) = this; - this._setupVMContext(); + const doctype = this.document.implementation.createDocumentType('html', '', ''); + const documentElement = this.document.createElement('html'); + const bodyElement = this.document.createElement('body'); + const headElement = this.document.createElement('head'); - this.document._onWindowReady(); + this.document.appendChild(doctype); + this.document.appendChild(documentElement); + + documentElement.appendChild(headElement); + documentElement.appendChild(bodyElement); + + this._readyStateManager.whenComplete().then(() => { + (this.document.readyState) = DocumentReadyStateEnum.complete; + this.document.dispatchEvent(new Event('readystatechange')); + this.document.dispatchEvent(new Event('load', { bubbles: true })); + }); } /** diff --git a/packages/happy-dom/src/window/WindowClassFactory.ts b/packages/happy-dom/src/window/WindowClassFactory.ts index f0f3d6286..c5bf873c8 100644 --- a/packages/happy-dom/src/window/WindowClassFactory.ts +++ b/packages/happy-dom/src/window/WindowClassFactory.ts @@ -43,10 +43,6 @@ import ShadowRootImplementation from '../nodes/shadow-root/ShadowRoot.js'; import ProcessingInstructionImplementation from '../nodes/processing-instruction/ProcessingInstruction.js'; import ElementImplementation from '../nodes/element/Element.js'; import CharacterDataImplementation from '../nodes/character-data/CharacterData.js'; -import DocumentImplementation from '../nodes/document/Document.js'; -import HTMLDocumentImplementation from '../nodes/html-document/HTMLDocument.js'; -import XMLDocumentImplementation from '../nodes/xml-document/XMLDocument.js'; -import SVGDocumentImplementation from '../nodes/svg-document/SVGDocument.js'; /** * Some classes need to get access to the window object without having a reference to the window in the constructor. @@ -77,10 +73,6 @@ export default class WindowClassFactory { ProcessingInstruction: typeof ProcessingInstructionImplementation; Element: typeof ElementImplementation; CharacterData: typeof CharacterDataImplementation; - Document: typeof DocumentImplementation; - HTMLDocument: typeof HTMLDocumentImplementation; - XMLDocument: typeof XMLDocumentImplementation; - SVGDocument: typeof SVGDocumentImplementation; // HTML Elements HTMLElement: typeof HTMLElementImplementation; @@ -151,22 +143,6 @@ export default class WindowClassFactory { class CharacterData extends CharacterDataImplementation { public readonly ownerDocument: IDocument = properties.window.document; } - class Document extends DocumentImplementation { - public readonly ownerDocument: IDocument | null = null; - public readonly defaultView: IWindow = properties.window; - } - class HTMLDocument extends HTMLDocumentImplementation { - public readonly ownerDocument: IDocument | null = null; - public readonly defaultView: IWindow = properties.window; - } - class XMLDocument extends XMLDocumentImplementation { - public readonly ownerDocument: IDocument | null = null; - public readonly defaultView: IWindow = properties.window; - } - class SVGDocument extends SVGDocumentImplementation { - public readonly ownerDocument: IDocument | null = null; - public readonly defaultView: IWindow = properties.window; - } // HTML Elements class Audio extends AudioImplementation { @@ -279,10 +255,6 @@ export default class WindowClassFactory { ProcessingInstruction, Element, CharacterData, - Document, - HTMLDocument, - XMLDocument, - SVGDocument, // HTML Elements HTMLElement, diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index 4ab098094..b705f8143 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -6,7 +6,6 @@ import XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js'; import XMLHttpRequestReadyStateEnum from './XMLHttpRequestReadyStateEnum.js'; import Event from '../event/Event.js'; import IDocument from '../nodes/document/IDocument.js'; -import IWindow from '../window/IWindow.js'; import Blob from '../file/Blob.js'; import XMLHttpRequestUpload from './XMLHttpRequestUpload.js'; import DOMException from '../exception/DOMException.js'; From c7b18ec7609f80e8ff7020c957fb7593c326d38b Mon Sep 17 00:00:00 2001 From: David Ortner Date: Sun, 22 Oct 2023 21:01:26 +0200 Subject: [PATCH 12/63] #466@trivial: Continues on implementation. --- packages/happy-dom/src/browser/Browser.ts | 3 +- .../happy-dom/src/browser/BrowserContext.ts | 3 +- .../happy-dom/src/browser/BrowserFrame.ts | 33 +++- packages/happy-dom/src/browser/BrowserPage.ts | 3 +- .../src/browser/DetachedBrowserFrame.ts | 18 +- packages/happy-dom/src/browser/IBrowser.ts | 41 +++++ .../happy-dom/src/browser/IBrowserContext.ts | 37 ++++ .../happy-dom/src/browser/IBrowserFrame.ts | 9 + .../happy-dom/src/browser/IBrowserPage.ts | 51 ++++++ .../AbstractCSSStyleDeclaration.ts | 17 +- .../CSSStyleDeclarationElementStyle.ts | 26 +-- .../dom-implementation/DOMImplementation.ts | 5 +- packages/happy-dom/src/fetch/Fetch.ts | 13 +- packages/happy-dom/src/fetch/Request.ts | 8 +- .../happy-dom/src/nodes/document/Document.ts | 51 +++--- .../happy-dom/src/nodes/document/IDocument.ts | 3 +- .../happy-dom/src/nodes/element/Element.ts | 10 +- .../src/nodes/element/ElementNamedNodeMap.ts | 2 - packages/happy-dom/src/nodes/node/Node.ts | 21 +-- packages/happy-dom/src/window/IWindow.ts | 2 + packages/happy-dom/src/window/Window.ts | 90 ++++----- .../src/window/WindowClassFactory.ts | 172 ++++++++++++++---- .../src/window/WindowErrorUtility.ts | 2 +- packages/happy-dom/test/CustomElement.ts | 4 +- .../DOMImplementation.test.ts | 16 +- .../happy-dom/test/nodes/node/Node.test.ts | 103 +++++------ .../test/nodes/node/NodeUtility.test.ts | 26 +-- packages/happy-dom/test/window/Window.test.ts | 4 +- 28 files changed, 501 insertions(+), 272 deletions(-) create mode 100644 packages/happy-dom/src/browser/IBrowser.ts create mode 100644 packages/happy-dom/src/browser/IBrowserContext.ts create mode 100644 packages/happy-dom/src/browser/IBrowserPage.ts diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index 552015a10..078439d4a 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -3,11 +3,12 @@ import BrowserContext from './BrowserContext.js'; import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; import BrowserSettingsFactory from './BrowserSettingsFactory.js'; import BrowserPage from './BrowserPage.js'; +import IBrowser from './IBrowser.js'; /** * Browser context. */ -export default class Browser { +export default class Browser implements IBrowser { public readonly defaultContext: BrowserContext; public readonly contexts: BrowserContext[]; public readonly settings: IBrowserSettings; diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index 6f757bc32..82ebe57ff 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -1,10 +1,11 @@ import Browser from './Browser.js'; import BrowserPage from './BrowserPage.js'; +import IBrowserContext from './IBrowserContext.js'; /** * Browser context. */ -export default class BrowserContext { +export default class BrowserContext implements IBrowserContext { public pages: BrowserPage[] = []; public browser: Browser; diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 82c2edfe0..ee42d2712 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -5,6 +5,8 @@ import IBrowserFrame from './IBrowserFrame.js'; import Window from '../window/Window.js'; import IBrowserPageViewport from './IBrowserPageViewport.js'; import Event from '../event/Event.js'; +import IBrowserSettings from './IBrowserSettings.js'; +import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; /** * Browser frame. @@ -30,12 +32,41 @@ export default class BrowserFrame implements IBrowserFrame { } /** - * Returns the viewport. + * Returns the content. + * + * @returns Content. */ public get content(): string { return this.window.document.documentElement.outerHTML; } + /** + * Returns virtual console printer. + * + * @returns Virtual console printer. + */ + public get virtualConsolePrinter(): VirtualConsolePrinter { + return this.page.virtualConsolePrinter; + } + + /** + * Returns settings. + * + * @returns Settings. + */ + public get settings(): IBrowserSettings { + return this.page.context.browser.settings; + } + + /** + * Returns console. + * + * @returns Console. + */ + public get console(): Console { + return this.page.context.browser.console; + } + /** * Returns a promise that is resolved when all async tasks are complete. * diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index f29dd020e..7d5d6e171 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -3,11 +3,12 @@ import IBrowserPageViewport from './IBrowserPageViewport.js'; import BrowserFrame from './BrowserFrame.js'; import BrowserContext from './BrowserContext.js'; import VirtualConsole from '../console/VirtualConsole.js'; +import IBrowserPage from './IBrowserPage.js'; /** * Browser page. */ -export default class BrowserPage { +export default class BrowserPage implements IBrowserPage { public readonly virtualConsolePrinter = new VirtualConsolePrinter(); public readonly mainFrame: BrowserFrame; public readonly context: BrowserContext; diff --git a/packages/happy-dom/src/browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/DetachedBrowserFrame.ts index c49d5e355..0ee67e87d 100644 --- a/packages/happy-dom/src/browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/DetachedBrowserFrame.ts @@ -1,13 +1,14 @@ import IWindow from '../window/IWindow.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import IBrowserFrame from './IBrowserFrame.js'; -import Window from '../window/Window.js'; import IBrowserSettings from './IBrowserSettings.js'; -import { VirtualConsolePrinter } from '../index.js'; import BrowserSettingsFactory from './BrowserSettingsFactory.js'; import IBrowserPageViewport from './IBrowserPageViewport.js'; import Event from '../event/Event.js'; import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; +import VirtualConsole from '../console/VirtualConsole.js'; +import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import IBrowserPage from './IBrowserPage.js'; /** * Browser frame. @@ -19,17 +20,24 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public _asyncTaskManager = new AsyncTaskManager(); public readonly virtualConsolePrinter = new VirtualConsolePrinter(); public readonly settings: IBrowserSettings; - public readonly console: Console; + public readonly console: Console | null; + public readonly page: IBrowserPage | null = null; /** * Constructor. * * @param options Options. * @param options.window Window. + * @param [options.console] Console. * @param [options.settings] Browser settings. */ - constructor(options: { window: Window; settings?: IOptionalBrowserSettings }) { + constructor(options: { + window: IWindow; + console?: Console; + settings?: IOptionalBrowserSettings; + }) { this.window = options.window; + this.console = options.console ?? new VirtualConsole(this.virtualConsolePrinter); this.settings = BrowserSettingsFactory.getSettings(options.settings); } @@ -67,7 +75,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public async destroy(): Promise { await Promise.all(this.childFrames.map((frame) => frame.destroy())); await this._asyncTaskManager.destroy(); - (this.window) = null; + (this.window) = null; } /** diff --git a/packages/happy-dom/src/browser/IBrowser.ts b/packages/happy-dom/src/browser/IBrowser.ts new file mode 100644 index 000000000..038703b05 --- /dev/null +++ b/packages/happy-dom/src/browser/IBrowser.ts @@ -0,0 +1,41 @@ +import IBrowserContext from './IBrowserContext.js'; +import IBrowserPage from './IBrowserPage.js'; +import IBrowserSettings from './IBrowserSettings.js'; + +/** + * Browser. + */ +export default interface IBrowser { + readonly defaultContext: IBrowserContext; + readonly contexts: IBrowserContext[]; + readonly settings: IBrowserSettings; + readonly console: Console | null; + + /** + * Aborts all ongoing operations and destroys the browser. + * + * @returns Promise. + */ + close(): Promise; + + /** + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all tasks such as timers are complete. + * + * @returns Promise. + */ + whenComplete(): Promise; + + /** + * Aborts all ongoing operations. + * + * @returns Promise. + */ + abort(): Promise; + + /** + * Creates a new page. + * + * @returns Page. + */ + newPage(): IBrowserPage; +} diff --git a/packages/happy-dom/src/browser/IBrowserContext.ts b/packages/happy-dom/src/browser/IBrowserContext.ts new file mode 100644 index 000000000..d07ff2d28 --- /dev/null +++ b/packages/happy-dom/src/browser/IBrowserContext.ts @@ -0,0 +1,37 @@ +import IBrowser from './IBrowser.js'; +import IBrowserPage from './IBrowserPage.js'; + +/** + * Browser context. + */ +export default interface IBrowserContext { + readonly pages: IBrowserPage[]; + readonly browser: IBrowser; + + /** + * Aborts all ongoing operations and destroys the context. + * + * @returns Promise. + */ + close(): Promise; + + /** + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all tasks such as timers are complete. + * + * @returns Promise. + */ + whenComplete(): Promise; + + /** + * Aborts all ongoing operations. + * + * @returns Promise. + */ + abort(): Promise; + /** + * Creates a new page. + * + * @returns Page. + */ + newPage(): IBrowserPage; +} diff --git a/packages/happy-dom/src/browser/IBrowserFrame.ts b/packages/happy-dom/src/browser/IBrowserFrame.ts index fe685d88a..97b8c40a3 100644 --- a/packages/happy-dom/src/browser/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/IBrowserFrame.ts @@ -1,5 +1,9 @@ +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import IWindow from '../window/IWindow.js'; import IBrowserPageViewport from './IBrowserPageViewport.js'; +import IBrowserSettings from './IBrowserSettings.js'; +import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import IBrowserPage from './IBrowserPage.js'; /** * Browser frame. @@ -9,6 +13,11 @@ export default interface IBrowserFrame { detached: boolean; readonly window: IWindow; readonly content: string; + readonly _asyncTaskManager: AsyncTaskManager; + readonly virtualConsolePrinter: VirtualConsolePrinter; + readonly settings: IBrowserSettings; + readonly console: Console | null; + readonly page: IBrowserPage | null; /** * Returns a promise that is resolved when all async tasks are complete. diff --git a/packages/happy-dom/src/browser/IBrowserPage.ts b/packages/happy-dom/src/browser/IBrowserPage.ts new file mode 100644 index 000000000..dfeb5ce7f --- /dev/null +++ b/packages/happy-dom/src/browser/IBrowserPage.ts @@ -0,0 +1,51 @@ +import IBrowserPageViewport from './IBrowserPageViewport.js'; +import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import IBrowserFrame from './IBrowserFrame.js'; +import IBrowserContext from './IBrowserContext.js'; + +/** + * Browser page. + */ +export default interface IBrowserPage { + readonly virtualConsolePrinter: VirtualConsolePrinter; + readonly mainFrame: IBrowserFrame; + readonly context: IBrowserContext; + readonly console: Console; + readonly frames: IBrowserFrame[]; + readonly content: string; + + /** + * Aborts all ongoing operations and destroys the page. + * + * @returns Promise. + */ + close(): Promise; + + /** + * Returns a promise that is resolved when all async tasks are complete. + * + * @returns Promise. + */ + whenComplete(): Promise; + + /** + * Aborts all ongoing operations. + * + * @returns Promise. + */ + abort(): Promise; + + /** + * Sets the viewport. + * + * @param viewport Viewport. + */ + setViewport(viewport: IBrowserPageViewport): void; + + /** + * Go to a page. + * + * @param url URL. + */ + goto(url: string): Promise; +} diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index 07bc406a6..227b0ac8e 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -20,24 +20,15 @@ export default abstract class AbstractCSSStyleDeclaration { /** * Constructor. * - * @param options Options. * @param [ownerElement] Computed style element. - * @param [options.browserSettings] Browser settings. - * @param [options.browserSettings.disableComputedStyleRendering] Disable computed style rendering. - * @param [options.computed=false] Computed. + * @param [computed] Computed. */ - constructor( - ownerElement?: IElement, - options?: { - browserSettings?: { readonly disableComputedStyleRendering: boolean }; - computed?: boolean; - } - ) { + constructor(ownerElement: IElement = null, computed = false) { this._style = !ownerElement ? new CSSStyleDeclarationPropertyManager() : null; this._ownerElement = ownerElement; - this._computed = ownerElement ? options.computed ?? false : false; + this._computed = ownerElement ? computed : false; this._elementStyle = ownerElement - ? new CSSStyleDeclarationElementStyle(ownerElement, options) + ? new CSSStyleDeclarationElementStyle(ownerElement, this._computed) : null; } diff --git a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 0fce21a1f..18bc358a1 100644 --- a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -16,6 +16,7 @@ import CSSStyleDeclarationCSSParser from '../css-parser/CSSStyleDeclarationCSSPa import QuerySelector from '../../../query-selector/QuerySelector.js'; import CSSMeasurementConverter from '../measurement-converter/CSSMeasurementConverter.js'; import MediaQueryList from '../../../match-media/MediaQueryList.js'; +import WindowBrowserSettingsReader from '../../../window/WindowBrowserSettingsReader.js'; 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; @@ -39,30 +40,18 @@ export default class CSSStyleDeclarationElementStyle { documentCacheID: null }; - readonly #browserSettings: { readonly disableComputedStyleRendering: boolean }; private element: IElement; private computed: boolean; /** * Constructor. * - * @param options Options. - * @param options.element Element. - * @param [options.browserSettings] Browser settings. - * @param [options.browserSettings.disableComputedStyleRendering] Disable computed style rendering. - * @param [options.computed=false] Computed. - * @param element + * @param element Element. + * @param [computed] Computed. */ - constructor( - element: IElement, - options?: { - browserSettings?: { readonly disableComputedStyleRendering: boolean }; - computed?: boolean; - } - ) { + constructor(element: IElement, computed = false) { this.element = element; - this.#browserSettings = options?.browserSettings ?? { disableComputedStyleRendering: false }; - this.computed = options?.computed ?? false; + this.computed = computed; } /** @@ -365,7 +354,10 @@ export default class CSSStyleDeclarationElementStyle { parentFontSize: string | number; parentSize: string | number | null; }): string { - if (this.#browserSettings.disableComputedStyleRendering) { + if ( + WindowBrowserSettingsReader.getSettings(this.element.ownerDocument.defaultView) + .disableComputedStyleRendering + ) { return options.value; } diff --git a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts index c15cdebea..c95c00a9c 100644 --- a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts +++ b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts @@ -24,8 +24,6 @@ export default class DOMImplementation { public createDocument(): IDocument { const documentClass = this._ownerDocument.constructor; // @ts-ignore - documentClass._defaultView = this._ownerDocument.defaultView; - // @ts-ignore return new documentClass(); } @@ -48,8 +46,7 @@ export default class DOMImplementation { publicId: string, systemId: string ): DocumentType { - DocumentType._ownerDocument = this._ownerDocument; - const documentType = new DocumentType(); + const documentType = new this._ownerDocument._defaultView.DocumentType(); documentType.name = qualifiedName; documentType.publicId = publicId; documentType.systemId = systemId; diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index 6081be487..51fddbbb7 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -1,6 +1,5 @@ import IRequestInit from './types/IRequestInit.js'; import IDocument from '../nodes/document/IDocument.js'; -import Document from '../nodes/document/Document.js'; import IResponse from './types/IResponse.js'; import IRequestInfo from './types/IRequestInfo.js'; import Headers from './Headers.js'; @@ -18,6 +17,7 @@ import FetchCORSUtility from './utilities/FetchCORSUtility.js'; import Request from './Request.js'; import Response from './Response.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import CookieJar from '../cookie/CookieJar.js'; const SUPPORTED_SCHEMAS = ['data:', 'http:', 'https:']; const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; @@ -564,10 +564,9 @@ export default class Fetch { this.request.credentials === 'include' || (this.request.credentials === 'same-origin' && !isCORS) ) { - const cookie = (document.defaultView.document)._cookie.getCookieString( - this.ownerDocument.defaultView.location, - false - ); + const cookie = (<{ _cookie: CookieJar }>( + (document.defaultView.document) + ))._cookie.getCookieString(document.defaultView.location, false); if (cookie) { headers.set('Cookie', cookie); } @@ -624,7 +623,9 @@ export default class Fetch { // Handles setting cookie headers to the document. // "set-cookie" and "set-cookie2" are not allowed in response headers according to spec. if (lowerKey === 'set-cookie' || lowerKey === 'set-cookie2') { - (this.ownerDocument)._cookie.addCookieString(this.request._url, header); + (<{ _cookie: CookieJar }>( + (this.ownerDocument.defaultView.document) + ))._cookie.addCookieString(this.request._url, header); } else { headers.append(key, header); } diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts index 179374deb..b700334ad 100644 --- a/packages/happy-dom/src/fetch/Request.ts +++ b/packages/happy-dom/src/fetch/Request.ts @@ -33,7 +33,6 @@ import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; export default class Request implements IRequest { // Needs to be injected by a sub-class. protected readonly _asyncTaskManager: AsyncTaskManager; - protected readonly _ownerDocument: IDocument; // Public properties public readonly method: string; @@ -134,6 +133,13 @@ export default class Request implements IRequest { FetchRequestValidationUtility.validateRedirect(this.redirect); } + /** + * Returns owner document. + */ + protected get _ownerDocument(): IDocument { + throw new Error('_ownerDocument needs to be implemented by sub-class.'); + } + /** * Returns referrer. * diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index e5a741854..cd84e4e3a 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -50,6 +50,8 @@ const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; * Document. */ export default class Document extends Node implements IDocument { + // Needs to be injected by sub-class. + public readonly _defaultView: IWindow; public nodeType = Node.DOCUMENT_NODE; public adoptedStyleSheets: CSSStyleSheet[] = []; public implementation = new DOMImplementation(this); @@ -57,7 +59,6 @@ export default class Document extends Node implements IDocument { public readonly isConnected: boolean = true; public readonly defaultView: IWindow | null = null; public readonly referrer = ''; - public readonly ownerDocument = null; public readonly _windowClass: {} | null = null; public readonly _children: IHTMLCollection = new HTMLCollection(); public _activeElement: IHTMLElement = null; @@ -186,6 +187,15 @@ export default class Document extends Node implements IDocument { public onpaste: (event: Event) => void = null; public onbeforematch: (event: Event) => void = null; + /** + * Returns owner document. + * + * @returns Owner document. + */ + public get ownerDocument(): IDocument { + return null; + } + /** * Returns document children. */ @@ -281,7 +291,7 @@ export default class Document extends Node implements IDocument { * @returns Cookie. */ public get cookie(): string { - return this._cookie.getCookieString(this.defaultView.location, true); + return this._cookie.getCookieString(this._defaultView.location, true); } /** @@ -290,7 +300,7 @@ export default class Document extends Node implements IDocument { * @param cookie Cookie string. */ public set cookie(cookie: string) { - this._cookie.addCookieString(this.defaultView.location, cookie); + this._cookie.addCookieString(this._defaultView.location, cookie); } /** @@ -401,7 +411,7 @@ export default class Document extends Node implements IDocument { * @returns Location. */ public get location(): Location { - return this.defaultView.location; + return this._defaultView.location; } /** @@ -424,7 +434,7 @@ export default class Document extends Node implements IDocument { if (element) { return element.href; } - return this.defaultView.location.href; + return this._defaultView.location.href; } /** @@ -433,7 +443,7 @@ export default class Document extends Node implements IDocument { * @returns the URL of the current document. * */ public get URL(): string { - return this.defaultView.location.href; + return this._defaultView.location.href; } /** @@ -786,21 +796,18 @@ export default class Document extends Node implements IDocument { const tagName = String(qualifiedName).toUpperCase(); let customElementClass; - if (this.defaultView && options && options.is) { - customElementClass = this.defaultView.customElements.get(String(options.is)); - } else if (this.defaultView) { - customElementClass = this.defaultView.customElements.get(tagName); + if (options && options.is) { + customElementClass = this._defaultView.customElements.get(String(options.is)); + } else { + customElementClass = this._defaultView.customElements.get(tagName); } const elementClass: typeof Element = - customElementClass || this.defaultView[ElementTag[tagName]] || HTMLUnknownElement; + customElementClass || this._defaultView[ElementTag[tagName]] || HTMLUnknownElement; const element = new elementClass(); element.tagName = tagName; - // TODO: Should not be necessary as the class should already extend the class created by WindowClassFactory? - (element.ownerDocument) = this; - (element.namespaceURI) = namespaceURI; if (element instanceof Element && options && options.is) { element._isValue = String(options.is); @@ -818,7 +825,7 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createTextNode(data?: string): IText { - return new this.defaultView.Text(data); + return new this._defaultView.Text(data); } /** @@ -828,7 +835,7 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createComment(data?: string): IComment { - return new this.defaultView.Comment(data); + return new this._defaultView.Comment(data); } /** @@ -837,7 +844,7 @@ export default class Document extends Node implements IDocument { * @returns Document fragment. */ public createDocumentFragment(): IDocumentFragment { - return new this.defaultView.DocumentFragment(); + return new this._defaultView.DocumentFragment(); } /** @@ -874,8 +881,8 @@ export default class Document extends Node implements IDocument { * @returns Event. */ public createEvent(type: string): Event { - if (typeof this.defaultView[type] === 'function') { - return new this.defaultView[type]('init'); + if (typeof this._defaultView[type] === 'function') { + return new this._defaultView[type]('init'); } return new Event('init'); } @@ -898,7 +905,7 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public createAttributeNS(namespaceURI: string, qualifiedName: string): IAttr { - const attribute = new this.defaultView.Attr(); + const attribute = new this._defaultView.Attr(); attribute.namespaceURI = namespaceURI; attribute.name = qualifiedName; return attribute; @@ -925,7 +932,7 @@ export default class Document extends Node implements IDocument { * @returns Range. */ public createRange(): Range { - return new this.defaultView.Range(); + return new this._defaultView.Range(); } /** @@ -983,7 +990,7 @@ export default class Document extends Node implements IDocument { `Failed to execute 'createProcessingInstruction' on 'Document': The data provided ('?>') contains '?>'` ); } - const processingInstruction = new this.defaultView.ProcessingInstruction(data); + const processingInstruction = new this._defaultView.ProcessingInstruction(data); processingInstruction.target = target; return processingInstruction; } diff --git a/packages/happy-dom/src/nodes/document/IDocument.ts b/packages/happy-dom/src/nodes/document/IDocument.ts index 1f4c15b36..6b4417902 100644 --- a/packages/happy-dom/src/nodes/document/IDocument.ts +++ b/packages/happy-dom/src/nodes/document/IDocument.ts @@ -27,7 +27,8 @@ import VisibilityStateEnum from './VisibilityStateEnum.js'; * Document. */ export default interface IDocument extends IParentNode { - readonly defaultView: IWindow; + readonly defaultView: IWindow | null; + readonly _defaultView: IWindow; readonly implementation: DOMImplementation; readonly documentElement: IHTMLElement; readonly doctype: IDocumentType; diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 1f1bfae7f..bd2436d06 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -1,6 +1,5 @@ import Node from '../node/Node.js'; import ShadowRoot from '../shadow-root/ShadowRoot.js'; -import Attr from '../attr/Attr.js'; import DOMRect from './DOMRect.js'; import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; import IDOMTokenList from '../../dom-token-list/IDOMTokenList.js'; @@ -364,11 +363,11 @@ export default class Element extends Node implements IElement { public cloneNode(deep = false): IElement { const clone = super.cloneNode(deep); - Attr._ownerDocument = this.ownerDocument; - for (let i = 0, max = this.attributes.length; i < max; i++) { const attribute = this.attributes[i]; - clone.attributes.setNamedItem(Object.assign(new Attr(), attribute)); + clone.attributes.setNamedItem( + Object.assign(new this.ownerDocument.defaultView.Attr(), attribute) + ); } if (deep) { @@ -692,8 +691,7 @@ export default class Element extends Node implements IElement { throw new DOMException('Shadow root has already been attached.'); } - (this._shadowRoot) = new ShadowRoot(); - (this._shadowRoot.ownerDocument) = this.ownerDocument; + (this._shadowRoot) = new this.ownerDocument._defaultView.ShadowRoot(); (this._shadowRoot.host) = this; (this._shadowRoot.mode) = shadowRootInit.mode; (this._shadowRoot)._connectToNode(this); diff --git a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts index 18c3c86b9..de36ef702 100644 --- a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts @@ -3,7 +3,6 @@ import MutationRecord from '../../mutation-observer/MutationRecord.js'; import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; import IAttr from '../attr/IAttr.js'; -import IDocument from '../document/IDocument.js'; import Element from './Element.js'; import HTMLCollection from './HTMLCollection.js'; import IElement from './IElement.js'; @@ -50,7 +49,6 @@ export default class ElementNamedNodeMap extends NamedNodeMap { item.name = this._getAttributeName(item.name); (item.ownerElement) = this._ownerElement; - (item.ownerDocument) = this._ownerElement.ownerDocument; const replacedItem = super.setNamedItem(item); const oldValue = replacedItem ? replacedItem.value : null; diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index f156fe569..65c851fd9 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -15,9 +15,6 @@ import INodeList from './INodeList.js'; * Node. */ export default class Node extends EventTarget implements INode { - // Owner document is set when the Node is created by the Document - public static _ownerDocument: IDocument = null; - // Public properties public static readonly ELEMENT_NODE = NodeTypeEnum.elementNode; public static readonly ATTRIBUTE_NODE = NodeTypeEnum.attributeNode; @@ -51,7 +48,6 @@ export default class Node extends EventTarget implements INode { public readonly DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = NodeDocumentPositionEnum.implementationSpecific; public readonly DOCUMENT_POSITION_PRECEDING = NodeDocumentPositionEnum.preceding; - public readonly ownerDocument: IDocument = null; public readonly parentNode: INode = null; public readonly nodeType: number; public readonly isConnected: boolean = false; @@ -64,14 +60,6 @@ export default class Node extends EventTarget implements INode { public _observers: MutationListener[] = []; public readonly _childNodes: INodeList = new NodeList(); - /** - * Constructor. - */ - constructor() { - super(); - this.ownerDocument = (this.constructor)._ownerDocument; - } - /** * Returns `Symbol.toStringTag`. * @@ -81,6 +69,13 @@ export default class Node extends EventTarget implements INode { return this.constructor.name; } + /** + * Returns owner document. + */ + public get ownerDocument(): IDocument { + throw new Error('Property "ownerDocument" needs to be implemented by sub-class.'); + } + /** * Get child nodes. * @@ -287,8 +282,6 @@ export default class Node extends EventTarget implements INode { } } - (clone.ownerDocument) = this.ownerDocument; - return clone; } diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index fbb2bab54..5f26a240e 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -4,6 +4,7 @@ import IDocument from '../nodes/document/IDocument.js'; import HTMLDocument from '../nodes/html-document/HTMLDocument.js'; import XMLDocument from '../nodes/xml-document/XMLDocument.js'; import SVGDocument from '../nodes/svg-document/SVGDocument.js'; +import DocumentType from '../nodes/document-type/DocumentType.js'; import Node from '../nodes/node/Node.js'; import Text from '../nodes/text/Text.js'; import Comment from '../nodes/comment/Comment.js'; @@ -153,6 +154,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly HTMLDocument: typeof HTMLDocument; readonly XMLDocument: typeof XMLDocument; readonly SVGDocument: typeof SVGDocument; + readonly DocumentType: typeof DocumentType; // Element classes readonly HTMLElement: typeof HTMLElement; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 345bec7ed..7ea81d00f 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -33,6 +33,7 @@ import SVGGraphicsElement from '../nodes/svg-element/SVGGraphicsElement.js'; import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js'; import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; import CharacterData from '../nodes/character-data/CharacterData.js'; +import DocumentType from '../nodes/document-type/DocumentType.js'; import NodeIterator from '../tree-walker/NodeIterator.js'; import TreeWalker from '../tree-walker/TreeWalker.js'; import Event from '../event/Event.js'; @@ -119,7 +120,6 @@ import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; import ValidityState from '../validity-state/ValidityState.js'; import WindowErrorUtility from './WindowErrorUtility.js'; -import VirtualConsole from '../console/VirtualConsole.js'; import ICrossOriginWindow from './ICrossOriginWindow.js'; import Permissions from '../permissions/Permissions.js'; import PermissionStatus from '../permissions/PermissionStatus.js'; @@ -127,8 +127,6 @@ import Clipboard from '../clipboard/Clipboard.js'; import ClipboardItem from '../clipboard/ClipboardItem.js'; import ClipboardEvent from '../event/events/ClipboardEvent.js'; import HappyDOMWindowAPI from './HappyDOMWindowAPI.js'; -import BrowserFrame from '../browser/BrowserFrame.js'; -import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; import Headers from '../fetch/Headers.js'; import WindowClassFactory from './WindowClassFactory.js'; import Audio from '../nodes/html-audio-element/Audio.js'; @@ -143,6 +141,8 @@ import IOptionalBrowserSettings from '../browser/IOptionalBrowserSettings.js'; import WindowBrowserSettingsReader from './WindowBrowserSettingsReader.js'; import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; import DocumentReadyStateEnum from '../nodes/document/DocumentReadyStateEnum.js'; +import IBrowserFrame from '../browser/IBrowserFrame.js'; +import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -172,10 +172,11 @@ export default class Window extends EventTarget implements IWindow { public readonly ProcessingInstruction: typeof ProcessingInstruction; public readonly Element: typeof Element; public readonly CharacterData: typeof CharacterData; - public readonly Document = Document; - public readonly HTMLDocument = HTMLDocument; - public readonly XMLDocument = XMLDocument; - public readonly SVGDocument = SVGDocument; + public readonly Document: typeof Document; + public readonly HTMLDocument: typeof HTMLDocument; + public readonly XMLDocument: typeof XMLDocument; + public readonly SVGDocument: typeof SVGDocument; + public readonly DocumentType: typeof DocumentType; // Element classes public readonly HTMLElement: typeof HTMLElement; @@ -489,7 +490,7 @@ export default class Window extends EventTarget implements IWindow { private _clearInterval: (id: NodeJS.Timeout) => void; private _queueMicrotask: (callback: Function) => void; public readonly _readyStateManager = new DocumentReadyStateManager(this); - #browserFrame: BrowserFrame | DetachedBrowserFrame; + #browserFrame: IBrowserFrame; /** * Constructor. @@ -512,7 +513,7 @@ export default class Window extends EventTarget implements IWindow { url?: string; console?: Console; settings?: IOptionalBrowserSettings; - browserFrame?: BrowserFrame; + browserFrame?: IBrowserFrame; }) { super(); @@ -526,23 +527,17 @@ export default class Window extends EventTarget implements IWindow { if (options?.browserFrame) { this.#browserFrame = options.browserFrame; - this.console = - options?.console ?? new VirtualConsole(this.#browserFrame.page.virtualConsolePrinter); } else { this.#browserFrame = new DetachedBrowserFrame({ window: this, + console: options?.console, settings: options?.settings }); - this.console = - options?.console ?? new VirtualConsole(this.#browserFrame.virtualConsolePrinter); } - WindowBrowserSettingsReader.setSettings( - this, - this.#browserFrame instanceof DetachedBrowserFrame - ? this.#browserFrame.settings - : this.#browserFrame.page.context.browser.settings - ); + this.console = this.#browserFrame.console; + + WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.settings); this.happyDOM = new HappyDOMWindowAPI({ window: this, @@ -591,6 +586,8 @@ export default class Window extends EventTarget implements IWindow { } } + this._setupVMContext(); + const classes = WindowClassFactory.getClasses({ window: this, browserFrame: this.#browserFrame @@ -619,6 +616,11 @@ export default class Window extends EventTarget implements IWindow { this.ProcessingInstruction = classes.ProcessingInstruction; this.Element = classes.Element; this.CharacterData = classes.CharacterData; + this.Document = classes.Document; + this.HTMLDocument = classes.HTMLDocument; + this.XMLDocument = classes.XMLDocument; + this.SVGDocument = classes.SVGDocument; + this.DocumentType = classes.DocumentType; // HTML Element classes this.HTMLElement = classes.HTMLElement; @@ -693,11 +695,11 @@ export default class Window extends EventTarget implements IWindow { this.HTMLParamElement = classes.HTMLElement; this.HTMLTrackElement = classes.HTMLElement; - this._setupVMContext(); - + // Document this.document = new this.HTMLDocument(); (this.document.defaultView) = this; + // Default document elements const doctype = this.document.implementation.createDocumentType('html', '', ''); const documentElement = this.document.createElement('html'); const bodyElement = this.document.createElement('body'); @@ -709,6 +711,7 @@ export default class Window extends EventTarget implements IWindow { documentElement.appendChild(headElement); documentElement.appendChild(bodyElement); + // Ready state manager this._readyStateManager.whenComplete().then(() => { (this.document.readyState) = DocumentReadyStateEnum.complete; this.document.dispatchEvent(new Event('readystatechange')); @@ -768,18 +771,7 @@ export default class Window extends EventTarget implements IWindow { * @returns CSS style declaration. */ public getComputedStyle(element: IElement): CSSStyleDeclaration { - const browserSettings = - this.#browserFrame instanceof DetachedBrowserFrame - ? this.#browserFrame.settings - : this.#browserFrame.page.context.browser.settings; - element['_computedStyle'] = - element['_computedStyle'] || - new CSSStyleDeclaration(element, { - browserSettings: { - disableComputedStyleRendering: browserSettings.disableComputedStyleRendering - }, - computed: true - }); + element['_computedStyle'] = element['_computedStyle'] || new CSSStyleDeclaration(element, true); return element['_computedStyle']; } @@ -848,11 +840,7 @@ export default class Window extends EventTarget implements IWindow { _target?: string, _features?: string ): IWindow | ICrossOriginWindow | null { - const browserSettings = - this.#browserFrame instanceof DetachedBrowserFrame - ? this.#browserFrame.settings - : this.#browserFrame.page.context.browser.settings; - if (browserSettings.disableWindowOpenPageLoading) { + if (this.#browserFrame.settings.disableWindowOpenPageLoading) { return null; } return null; @@ -862,7 +850,7 @@ export default class Window extends EventTarget implements IWindow { * Closes the window. */ public close(): void { - if (this.#browserFrame instanceof BrowserFrame) { + if (this.#browserFrame.page) { if (this.#browserFrame.page.mainFrame === this.#browserFrame) { this.#browserFrame.page.close(); } @@ -890,12 +878,8 @@ export default class Window extends EventTarget implements IWindow { * @returns Timeout ID. */ public setTimeout(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { - const browserSettings = - this.#browserFrame instanceof DetachedBrowserFrame - ? this.#browserFrame.settings - : this.#browserFrame.page.context.browser.settings; const id = this._setTimeout(() => { - if (browserSettings.disableErrorCapturing) { + if (this.#browserFrame.settings.disableErrorCapturing) { callback(...args); } else { WindowErrorUtility.captureError(this, () => callback(...args)); @@ -925,12 +909,8 @@ export default class Window extends EventTarget implements IWindow { * @returns Interval ID. */ public setInterval(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { - const browserSettings = - this.#browserFrame instanceof DetachedBrowserFrame - ? this.#browserFrame.settings - : this.#browserFrame.page.context.browser.settings; const id = this._setInterval(() => { - if (browserSettings.disableErrorCapturing) { + if (this.#browserFrame.settings.disableErrorCapturing) { callback(...args); } else { WindowErrorUtility.captureError( @@ -961,12 +941,8 @@ export default class Window extends EventTarget implements IWindow { * @returns ID. */ public requestAnimationFrame(callback: (timestamp: number) => void): NodeJS.Immediate { - const browserSettings = - this.#browserFrame instanceof DetachedBrowserFrame - ? this.#browserFrame.settings - : this.#browserFrame.page.context.browser.settings; const id = global.setImmediate(() => { - if (browserSettings.disableErrorCapturing) { + if (this.#browserFrame.settings.disableErrorCapturing) { callback(this.performance.now()); } else { WindowErrorUtility.captureError(this, () => callback(this.performance.now())); @@ -995,13 +971,9 @@ export default class Window extends EventTarget implements IWindow { public queueMicrotask(callback: Function): void { let isAborted = false; const taskId = this.#browserFrame._asyncTaskManager.startTask(() => (isAborted = true)); - const browserSettings = - this.#browserFrame instanceof DetachedBrowserFrame - ? this.#browserFrame.settings - : this.#browserFrame.page.context.browser.settings; this._queueMicrotask(() => { if (!isAborted) { - if (browserSettings.disableErrorCapturing) { + if (this.#browserFrame.settings.disableErrorCapturing) { callback(); } else { WindowErrorUtility.captureError(this, <() => unknown>callback); diff --git a/packages/happy-dom/src/window/WindowClassFactory.ts b/packages/happy-dom/src/window/WindowClassFactory.ts index c5bf873c8..cc11b18b3 100644 --- a/packages/happy-dom/src/window/WindowClassFactory.ts +++ b/packages/happy-dom/src/window/WindowClassFactory.ts @@ -43,6 +43,11 @@ import ShadowRootImplementation from '../nodes/shadow-root/ShadowRoot.js'; import ProcessingInstructionImplementation from '../nodes/processing-instruction/ProcessingInstruction.js'; import ElementImplementation from '../nodes/element/Element.js'; import CharacterDataImplementation from '../nodes/character-data/CharacterData.js'; +import DocumentImplementation from '../nodes/document/Document.js'; +import HTMLDocumentImplementation from '../nodes/html-document/HTMLDocument.js'; +import XMLDocumentImplementation from '../nodes/xml-document/XMLDocument.js'; +import SVGDocumentImplementation from '../nodes/svg-document/SVGDocument.js'; +import DocumentTypeImplementation from '../nodes/document-type/DocumentType.js'; /** * Some classes need to get access to the window object without having a reference to the window in the constructor. @@ -73,6 +78,11 @@ export default class WindowClassFactory { ProcessingInstruction: typeof ProcessingInstructionImplementation; Element: typeof ElementImplementation; CharacterData: typeof CharacterDataImplementation; + Document: typeof DocumentImplementation; + HTMLDocument: typeof HTMLDocumentImplementation; + XMLDocument: typeof XMLDocumentImplementation; + SVGDocument: typeof SVGDocumentImplementation; + DocumentType: typeof DocumentTypeImplementation; // HTML Elements HTMLElement: typeof HTMLElementImplementation; @@ -111,115 +121,202 @@ export default class WindowClassFactory { // Nodes class Node extends NodeImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class Attr extends AttrImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class SVGSVGElement extends SVGSVGElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class SVGElement extends SVGElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class SVGGraphicsElement extends SVGGraphicsElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class Text extends TextImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class Comment extends CommentImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class ShadowRoot extends ShadowRootImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class ProcessingInstruction extends ProcessingInstructionImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class Element extends ElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class CharacterData extends CharacterDataImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } + } + class Document extends DocumentImplementation { + public readonly _defaultView: IWindow = properties.window; + } + class HTMLDocument extends HTMLDocumentImplementation { + public readonly _defaultView: IWindow = properties.window; + } + class XMLDocument extends XMLDocumentImplementation { + public readonly _defaultView: IWindow = properties.window; + } + class SVGDocument extends SVGDocumentImplementation { + public readonly _defaultView: IWindow = properties.window; + } + class DocumentType extends DocumentTypeImplementation { + public get ownerDocument(): IDocument { + return properties.window.document; + } } // HTML Elements class Audio extends AudioImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class Image extends ImageImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class DocumentFragment extends DocumentFragmentImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLElement extends HTMLElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLUnknownElement extends HTMLUnknownElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLTemplateElement extends HTMLTemplateElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLFormElement extends HTMLFormElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLInputElement extends HTMLInputElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLSelectElement extends HTMLSelectElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLTextAreaElement extends HTMLTextAreaElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLImageElement extends HTMLImageElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLScriptElement extends HTMLScriptElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLLinkElement extends HTMLLinkElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLStyleElement extends HTMLStyleElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLLabelElement extends HTMLLabelElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLSlotElement extends HTMLSlotElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLMetaElement extends HTMLMetaElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLMediaElement extends HTMLMediaElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLAudioElement extends HTMLAudioElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLVideoElement extends HTMLVideoElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLBaseElement extends HTMLBaseElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLIFrameElement extends HTMLIFrameElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } class HTMLDialogElement extends HTMLDialogElementImplementation { - public readonly ownerDocument: IDocument = properties.window.document; + public get ownerDocument(): IDocument { + return properties.window.document; + } } // Other Classes class Request extends RequestImplementation { protected readonly _asyncTaskManager: AsyncTaskManager = properties.browserFrame._asyncTaskManager; - protected readonly _ownerDocument: IDocument = properties.window.document; + protected get _ownerDocument(): IDocument { + return properties.window.document; + } } class Response extends ResponseImplementation { protected readonly _asyncTaskManager: AsyncTaskManager = @@ -255,6 +352,11 @@ export default class WindowClassFactory { ProcessingInstruction, Element, CharacterData, + Document, + HTMLDocument, + XMLDocument, + SVGDocument, + DocumentType, // HTML Elements HTMLElement, diff --git a/packages/happy-dom/src/window/WindowErrorUtility.ts b/packages/happy-dom/src/window/WindowErrorUtility.ts index 7c76b5482..05d91c863 100644 --- a/packages/happy-dom/src/window/WindowErrorUtility.ts +++ b/packages/happy-dom/src/window/WindowErrorUtility.ts @@ -1,6 +1,6 @@ import IWindow from './IWindow.js'; import ErrorEvent from '../event/events/ErrorEvent.js'; -import { IElement } from '../index.js'; +import IElement from '../nodes/element/IElement.js'; /** * Error utility. diff --git a/packages/happy-dom/test/CustomElement.ts b/packages/happy-dom/test/CustomElement.ts index 9745eba40..0ce4f18b5 100644 --- a/packages/happy-dom/test/CustomElement.ts +++ b/packages/happy-dom/test/CustomElement.ts @@ -1,10 +1,10 @@ -import Window from '../src/window/Window.js'; import IShadowRoot from '../src/nodes/shadow-root/IShadowRoot.js'; +import HTMLElement from '../src/nodes/html-element/HTMLElement.js'; /** * CustomElement test class. */ -export default class CustomElement extends new Window().HTMLElement { +export default class CustomElement extends HTMLElement { public static observedAttributesCallCount = 0; public static shadowRootMode = 'open'; public changedAttributes: Array<{ diff --git a/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts b/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts index c968e40c0..226f0cf41 100644 --- a/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts +++ b/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts @@ -1,19 +1,17 @@ -import Document from '../../src/nodes/document/Document.js'; -import DOMImplementation from '../../src/dom-implementation/DOMImplementation.js'; +import IWindow from '../../src/window/IWindow'; +import Window from '../../src/window/Window'; import { beforeEach, describe, it, expect } from 'vitest'; describe('DOMImplementation', () => { - let ownerDocument: Document; - let domImplementation: DOMImplementation; + let window: IWindow; beforeEach(() => { - ownerDocument = new Document(); - domImplementation = new DOMImplementation(ownerDocument); + window = new Window(); }); describe('createHTMLDocument()', () => { it('Returns a new Document.', () => { - const document = domImplementation.createHTMLDocument(); + const document = window.document.implementation.createHTMLDocument(); expect(document instanceof Document).toBe(true); expect(document.defaultView).toBe(null); }); @@ -21,7 +19,7 @@ describe('DOMImplementation', () => { describe('createDocumentType()', () => { it('Returns a new Document Type.', () => { - const documentType = domImplementation.createDocumentType( + const documentType = window.document.implementation.createDocumentType( 'qualifiedName', 'publicId', 'systemId' @@ -29,7 +27,7 @@ describe('DOMImplementation', () => { expect(documentType.name).toBe('qualifiedName'); expect(documentType.publicId).toBe('publicId'); expect(documentType.systemId).toBe('systemId'); - expect(documentType.ownerDocument).toBe(ownerDocument); + expect(documentType.ownerDocument).toBe(window.document); }); }); }); diff --git a/packages/happy-dom/test/nodes/node/Node.test.ts b/packages/happy-dom/test/nodes/node/Node.test.ts index 96f48b503..fc0cd7318 100644 --- a/packages/happy-dom/test/nodes/node/Node.test.ts +++ b/packages/happy-dom/test/nodes/node/Node.test.ts @@ -13,57 +13,6 @@ import ErrorEvent from '../../../src/event/events/ErrorEvent.js'; import { beforeEach, describe, it, expect } from 'vitest'; import IShadowRoot from '../../../src/nodes/shadow-root/IShadowRoot.js'; -/** - * - */ -class CustomCounterElement extends HTMLElement { - public static output: string[] = []; - - /** - * Constructor. - */ - constructor() { - super(); - this.attachShadow({ mode: 'open' }); - } - - /** - * Connected. - */ - public connectedCallback(): void { - (this.shadowRoot).innerHTML = '
Test
'; - (this.constructor).output.push('Counter:connected'); - } - - /** - * Disconnected. - */ - public disconnectedCallback(): void { - (this.constructor).output.push('Counter:disconnected'); - } -} - -/** - * - */ -class CustomButtonElement extends HTMLElement { - public static output: string[] = []; - - /** - * Connected. - */ - public connectedCallback(): void { - (this.constructor).output.push('Button:connected'); - } - - /** - * Disconnected. - */ - public disconnectedCallback(): void { - (this.constructor).output.push('Button:disconnected'); - } -} - describe('Node', () => { let window: IWindow; let document: IDocument; @@ -72,6 +21,58 @@ describe('Node', () => { beforeEach(() => { window = new Window(); document = window.document; + + /** + * + */ + class CustomCounterElement extends window.HTMLElement { + public static output: string[] = []; + + /** + * Constructor. + */ + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + } + + /** + * Connected. + */ + public connectedCallback(): void { + (this.shadowRoot).innerHTML = '
Test
'; + (this.constructor).output.push('Counter:connected'); + } + + /** + * Disconnected. + */ + public disconnectedCallback(): void { + (this.constructor).output.push('Counter:disconnected'); + } + } + + /** + * + */ + class CustomButtonElement extends window.HTMLElement { + public static output: string[] = []; + + /** + * Connected. + */ + public connectedCallback(): void { + (this.constructor).output.push('Button:connected'); + } + + /** + * Disconnected. + */ + public disconnectedCallback(): void { + (this.constructor).output.push('Button:disconnected'); + } + } + customElementOutput = []; CustomCounterElement.output = customElementOutput; CustomButtonElement.output = customElementOutput; diff --git a/packages/happy-dom/test/nodes/node/NodeUtility.test.ts b/packages/happy-dom/test/nodes/node/NodeUtility.test.ts index 0855623fa..2e7a39b04 100644 --- a/packages/happy-dom/test/nodes/node/NodeUtility.test.ts +++ b/packages/happy-dom/test/nodes/node/NodeUtility.test.ts @@ -2,8 +2,6 @@ import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; import NodeUtility from '../../../src/nodes/node/NodeUtility.js'; import NodeTypeEnum from '../../../src/nodes/node/NodeTypeEnum.js'; -import DOMImplementation from '../../../src/dom-implementation/DOMImplementation.js'; -import IDocument from '../../../src/nodes/document/IDocument.js'; import { beforeEach, describe, it, expect } from 'vitest'; describe('NodeUtility', () => { @@ -170,38 +168,30 @@ describe('NodeUtility', () => { }); describe('w/ document type node', () => { - let document: IDocument; - let implementation: DOMImplementation; - - beforeEach(() => { - document = new Document(); - implementation = new DOMImplementation(document); - }); - it('Returns false if name are different', () => { - const doctype1 = implementation.createDocumentType('html1', 'foo', 'bar'); - const doctype2 = implementation.createDocumentType('html2', 'foo', 'bar'); + const doctype1 = window.document.implementation.createDocumentType('html1', 'foo', 'bar'); + const doctype2 = window.document.implementation.createDocumentType('html2', 'foo', 'bar'); expect(NodeUtility.isEqualNode(doctype1, doctype2)).toEqual(false); }); it('Returns false if public id are different', () => { - const doctype1 = implementation.createDocumentType('html', 'foo1', 'bar'); - const doctype2 = implementation.createDocumentType('html', 'foo2', 'bar'); + const doctype1 = window.document.implementation.createDocumentType('html', 'foo1', 'bar'); + const doctype2 = window.document.implementation.createDocumentType('html', 'foo2', 'bar'); expect(NodeUtility.isEqualNode(doctype1, doctype2)).toEqual(false); }); it('Returns false if system id are different', () => { - const doctype1 = implementation.createDocumentType('html', 'foo', 'bar1'); - const doctype2 = implementation.createDocumentType('html', 'foo', 'bar2'); + const doctype1 = window.document.implementation.createDocumentType('html', 'foo', 'bar1'); + const doctype2 = window.document.implementation.createDocumentType('html', 'foo', 'bar2'); expect(NodeUtility.isEqualNode(doctype1, doctype2)).toEqual(false); }); it('Returns true if doctype are equals', () => { - const doctype1 = implementation.createDocumentType('html', 'foo', 'bar'); - const doctype2 = implementation.createDocumentType('html', 'foo', 'bar'); + const doctype1 = window.document.implementation.createDocumentType('html', 'foo', 'bar'); + const doctype2 = window.document.implementation.createDocumentType('html', 'foo', 'bar'); expect(NodeUtility.isEqualNode(doctype1, doctype2)).toEqual(true); }); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 2b8052d46..bbe4bae07 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -29,7 +29,7 @@ import VirtualConsolePrinter from '../../src/console/VirtualConsolePrinter.js'; import Permissions from '../../src/permissions/Permissions.js'; import Clipboard from '../../src/clipboard/Clipboard.js'; import PackageVersion from '../../src/version.js'; -import { IHTMLDialogElement } from '../../src/index.js'; +import IHTMLDialogElement from '../../src/nodes/html-dialog-element/IHTMLDialogElement.js'; const GET_NAVIGATOR_PLATFORM = (): string => { return ( @@ -945,7 +945,7 @@ describe('Window', () => { isCallbackCalled = true; resolve(null); }); - window.happyDOM.cancelAsync(); + window.happyDOM.abort(); setTimeout(() => { expect(isCallbackCalled).toBe(false); resolve(null); From ac788a9d42c83d05176ed7f8226d4237e795223f Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 24 Oct 2023 18:53:03 +0200 Subject: [PATCH 13/63] #466@trivial: Continues on implementation. --- package-lock.json | 74 --------- packages/happy-dom/src/browser/Browser.ts | 24 +-- .../happy-dom/src/browser/BrowserContext.ts | 22 +-- .../happy-dom/src/browser/BrowserFrame.ts | 53 ++++--- packages/happy-dom/src/browser/BrowserPage.ts | 16 +- .../src/browser/BrowserSettingsFactory.ts | 6 +- .../src/browser/DetachedBrowserFrame.ts | 121 --------------- .../src/browser/IBrowserContextOptions.ts | 27 ---- .../detached-browser/DetachedBrowser.ts | 73 +++++++++ .../DetachedBrowserContext.ts | 63 ++++++++ .../detached-browser/DetachedBrowserFrame.ts | 141 +++++++++++++++++ .../detached-browser/DetachedBrowserPage.ts | 104 +++++++++++++ .../src/browser/{ => types}/IBrowser.ts | 8 +- .../browser/{ => types}/IBrowserContext.ts | 9 +- .../src/browser/{ => types}/IBrowserFrame.ts | 22 +-- .../src/browser/{ => types}/IBrowserPage.ts | 10 +- .../{ => types}/IBrowserPageViewport.ts | 0 .../browser/{ => types}/IBrowserSettings.ts | 0 .../{ => types}/IOptionalBrowserSettings.ts | 0 .../{ => types}/IReadOnlyBrowserSettings.ts | 0 .../CSSStyleDeclarationElementStyle.ts | 6 +- .../dom-implementation/DOMImplementation.ts | 6 +- .../happy-dom/src/dom-parser/DOMParser.ts | 10 +- packages/happy-dom/src/event/Event.ts | 2 +- packages/happy-dom/src/event/EventTarget.ts | 6 +- packages/happy-dom/src/fetch/Fetch.ts | 26 ++-- packages/happy-dom/src/fetch/ResourceFetch.ts | 6 +- packages/happy-dom/src/fetch/Response.ts | 9 +- .../utilities/FetchRequestReferrerUtility.ts | 4 +- packages/happy-dom/src/file/FileReader.ts | 4 +- packages/happy-dom/src/location/Location.ts | 6 +- .../happy-dom/src/nodes/document/Document.ts | 8 +- .../happy-dom/src/nodes/element/Element.ts | 15 +- .../src/nodes/html-element/HTMLElement.ts | 2 +- .../html-iframe-element/HTMLIFrameUtility.ts | 18 +-- .../HTMLLinkElementUtility.ts | 7 +- .../html-script-element/HTMLScriptElement.ts | 10 +- .../HTMLScriptElementUtility.ts | 18 +-- .../HTMLUnknownElement.ts | 4 +- packages/happy-dom/src/nodes/node/Node.ts | 21 ++- .../happy-dom/src/nodes/node/NodeUtility.ts | 4 +- packages/happy-dom/src/range/Range.ts | 9 +- packages/happy-dom/src/selection/Selection.ts | 12 +- .../happy-dom/src/window/HappyDOMWindowAPI.ts | 28 ++-- packages/happy-dom/src/window/IWindow.ts | 10 +- .../src/window/VMGlobalPropertyScript.ts | 2 +- packages/happy-dom/src/window/Window.ts | 61 +++++--- .../src/window/WindowBrowserSettingsReader.ts | 17 ++- .../src/window/WindowClassFactory.ts | 142 +++++++++++------- .../src/window/WindowErrorUtility.ts | 4 +- .../src/window/__BrowserContextLoader.ts | 7 +- .../src/xml-http-request/XMLHttpRequest.ts | 32 ++-- .../DOMImplementation.test.ts | 12 +- .../happy-dom/test/fetch/Response.test.ts | 12 +- .../test/match-media/MediaQueryList.test.ts | 10 +- .../test/nodes/document/Document.test.ts | 3 +- .../HTMLIFrameElement.test.ts | 10 +- packages/happy-dom/test/window/Window.test.ts | 31 ++-- .../xml-http-request/XMLHttpRequest.test.ts | 10 +- 59 files changed, 815 insertions(+), 562 deletions(-) delete mode 100644 packages/happy-dom/src/browser/DetachedBrowserFrame.ts delete mode 100644 packages/happy-dom/src/browser/IBrowserContextOptions.ts create mode 100644 packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts create mode 100644 packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts create mode 100644 packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts create mode 100644 packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts rename packages/happy-dom/src/browser/{ => types}/IBrowser.ts (88%) rename packages/happy-dom/src/browser/{ => types}/IBrowserContext.ts (86%) rename packages/happy-dom/src/browser/{ => types}/IBrowserFrame.ts (61%) rename packages/happy-dom/src/browser/{ => types}/IBrowserPage.ts (83%) rename packages/happy-dom/src/browser/{ => types}/IBrowserPageViewport.ts (100%) rename packages/happy-dom/src/browser/{ => types}/IBrowserSettings.ts (100%) rename packages/happy-dom/src/browser/{ => types}/IOptionalBrowserSettings.ts (100%) rename packages/happy-dom/src/browser/{ => types}/IReadOnlyBrowserSettings.ts (100%) diff --git a/package-lock.json b/package-lock.json index bb5256f88..1d83fc0a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5313,30 +5313,6 @@ "semver": "bin/semver.js" } }, - "node_modules/eslint-plugin-jest": { - "version": "26.9.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-26.9.0.tgz", - "integrity": "sha512-TWJxWGp1J628gxh2KhaH1H1paEdgE2J61BBF1I59c6xWeL5+D1BzMxGDN/nXAfX+aSkR5u80K+XhskK6Gwq9ng==", - "dev": true, - "dependencies": { - "@typescript-eslint/utils": "^5.10.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, "node_modules/eslint-plugin-jsdoc": { "version": "38.1.6", "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-38.1.6.tgz", @@ -5393,15 +5369,6 @@ } } }, - "node_modules/eslint-plugin-turbo": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/eslint-plugin-turbo/-/eslint-plugin-turbo-0.0.7.tgz", - "integrity": "sha512-iajOH8eD4jha3duztGVBD1BEmvNrQBaA/y3HFHf91vMDRYRwH7BpHSDFtxydDpk5ghlhRxG299SFxz7D6z4MBQ==", - "dev": true, - "peerDependencies": { - "eslint": ">6.6.0" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -11946,14 +11913,6 @@ "@types/node": "^16.11.7", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", - "eslint": "^8.11.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-filenames": "^1.3.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^38.0.6", - "eslint-plugin-json": "^3.1.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-turbo": "^0.0.7", "prettier": "^2.6.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -11979,14 +11938,6 @@ "@typescript-eslint/parser": "^5.16.0", "@vitest/ui": "^0.33.0", "@webref/css": "6.6.2", - "eslint": "^8.11.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-filenames": "^1.3.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^38.0.6", - "eslint-plugin-json": "^3.1.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-turbo": "^0.0.7", "prettier": "^2.6.0", "typescript": "^5.0.4", "vitest": "^0.32.4" @@ -12003,14 +11954,6 @@ "@types/node": "^16.11.7", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", - "eslint": "^8.11.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-filenames": "^1.3.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^38.0.6", - "eslint-plugin-json": "^3.1.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-turbo": "^0.0.7", "express": "^4.18.2", "prettier": "^2.6.0" } @@ -12044,15 +11987,6 @@ "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", "cpy": "^8.1.2", - "eslint": "^8.11.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-filenames": "^1.3.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jest": "^26.1.2", - "eslint-plugin-jsdoc": "^38.0.6", - "eslint-plugin-json": "^3.1.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-turbo": "^0.0.7", "express": "^4.18.2", "glob": "^7.2.0", "jest": "^29.4.0", @@ -12078,14 +12012,6 @@ "@types/node": "^16.11.7", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", - "eslint": "^8.11.0", - "eslint-config-prettier": "^8.5.0", - "eslint-plugin-filenames": "^1.3.2", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-jsdoc": "^38.0.6", - "eslint-plugin-json": "^3.1.0", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-turbo": "^0.0.7", "happy-dom": "^0.0.0", "prettier": "^2.6.0", "typescript": "^5.0.4" diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index 078439d4a..b5d2a1c65 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -1,9 +1,9 @@ -import IBrowserSettings from './IBrowserSettings.js'; +import IBrowserSettings from './types/IBrowserSettings.js'; import BrowserContext from './BrowserContext.js'; -import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; +import IOptionalBrowserSettings from './types/IOptionalBrowserSettings.js'; import BrowserSettingsFactory from './BrowserSettingsFactory.js'; import BrowserPage from './BrowserPage.js'; -import IBrowser from './IBrowser.js'; +import IBrowser from './types/IBrowser.js'; /** * Browser context. @@ -23,18 +23,18 @@ export default class Browser implements IBrowser { */ constructor(options?: { settings?: IOptionalBrowserSettings; console?: Console }) { this.console = options?.console || null; + this.settings = BrowserSettingsFactory.getSettings(options?.settings); this.defaultContext = new BrowserContext(this); this.contexts = [this.defaultContext]; - this.settings = BrowserSettingsFactory.getSettings(options?.settings); } /** * Aborts all ongoing operations and destroys the browser. - * - * @returns Promise. */ - public async close(): Promise { - await Promise.all(this.contexts.map((context) => context.close())); + public close(): void { + for (const context of this.contexts) { + context.close(); + } (this.contexts) = []; (this.defaultContext) = null; } @@ -50,11 +50,11 @@ export default class Browser implements IBrowser { /** * Aborts all ongoing operations. - * - * @returns Promise. */ - public async abort(): Promise { - await Promise.all(this.contexts.map((page) => page.abort())); + public abort(): void { + for (const context of this.contexts) { + context.abort(); + } } /** diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index 82ebe57ff..1734fbbf5 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -1,13 +1,13 @@ import Browser from './Browser.js'; import BrowserPage from './BrowserPage.js'; -import IBrowserContext from './IBrowserContext.js'; +import IBrowserContext from './types/IBrowserContext.js'; /** * Browser context. */ export default class BrowserContext implements IBrowserContext { - public pages: BrowserPage[] = []; - public browser: Browser; + public readonly pages: BrowserPage[] = []; + public readonly browser: Browser; /** * Constructor. @@ -20,11 +20,11 @@ export default class BrowserContext implements IBrowserContext { /** * Aborts all ongoing operations and destroys the context. - * - * @returns Promise. */ - public async close(): Promise { - await Promise.all(this.pages.map((page) => page.close())); + public close(): void { + for (const page of this.pages) { + page.close(); + } } /** @@ -38,11 +38,11 @@ export default class BrowserContext implements IBrowserContext { /** * Aborts all ongoing operations. - * - * @returns Promise. */ - public async abort(): Promise { - await Promise.all(this.pages.map((page) => page.abort())); + public abort(): void { + for (const page of this.pages) { + page.abort(); + } } /** diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index ee42d2712..5115ccaf4 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -1,12 +1,11 @@ import IWindow from '../window/IWindow.js'; import BrowserPage from './BrowserPage.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; -import IBrowserFrame from './IBrowserFrame.js'; +import IBrowserFrame from './types/IBrowserFrame.js'; import Window from '../window/Window.js'; -import IBrowserPageViewport from './IBrowserPageViewport.js'; +import IBrowserPageViewport from './types/IBrowserPageViewport.js'; import Event from '../event/Event.js'; -import IBrowserSettings from './IBrowserSettings.js'; -import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import Location from '../location/Location.js'; /** * Browser frame. @@ -41,30 +40,32 @@ export default class BrowserFrame implements IBrowserFrame { } /** - * Returns virtual console printer. + * Sets the content. * - * @returns Virtual console printer. + * @param content Content. */ - public get virtualConsolePrinter(): VirtualConsolePrinter { - return this.page.virtualConsolePrinter; + public set content(content) { + this.window.document['_isFirstWrite'] = true; + this.window.document['_isFirstWriteAfterOpen'] = true; + this.window.document.write(content); } /** - * Returns settings. + * Returns the URL. * - * @returns Settings. + * @returns URL. */ - public get settings(): IBrowserSettings { - return this.page.context.browser.settings; + public get url(): string { + return this.window.location.href; } /** - * Returns console. + * Sets the content. * - * @returns Console. + * @param url URL. */ - public get console(): Console { - return this.page.context.browser.console; + public set url(url) { + (this.window.location) = new Location(url); } /** @@ -81,21 +82,23 @@ export default class BrowserFrame implements IBrowserFrame { * * @returns Promise. */ - public async abort(): Promise { - await Promise.all(this.childFrames.map((frame) => frame.abort())); - await this._asyncTaskManager.abortAll(); + public abort(): void { + for (const frame of this.childFrames) { + frame.abort(); + } + this._asyncTaskManager.abortAll(); } /** * Aborts all ongoing operations and destroys the frame. - * - * @returns Promise. */ - public async destroy(): Promise { - await Promise.all(this.childFrames.map((frame) => frame.destroy())); - await this._asyncTaskManager.destroy(); + public destroy(): void { + for (const frame of this.childFrames) { + frame.destroy(); + } + this._asyncTaskManager.destroy(); (this.page) = null; - (this.window) = null; + (this.window) = null; } /** diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index 7d5d6e171..082b01590 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -1,9 +1,9 @@ import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; -import IBrowserPageViewport from './IBrowserPageViewport.js'; +import IBrowserPageViewport from './types/IBrowserPageViewport.js'; import BrowserFrame from './BrowserFrame.js'; import BrowserContext from './BrowserContext.js'; import VirtualConsole from '../console/VirtualConsole.js'; -import IBrowserPage from './IBrowserPage.js'; +import IBrowserPage from './types/IBrowserPage.js'; /** * Browser page. @@ -41,11 +41,9 @@ export default class BrowserPage implements IBrowserPage { /** * Aborts all ongoing operations and destroys the page. - * - * @returns Promise. */ - public async close(): Promise { - await this.mainFrame.destroy(); + public close(): void { + this.mainFrame.destroy(); const index = this.context.pages.indexOf(this); if (index !== -1) { this.context.pages.splice(index, 1); @@ -66,11 +64,9 @@ export default class BrowserPage implements IBrowserPage { /** * Aborts all ongoing operations. - * - * @returns Promise. */ - public async abort(): Promise { - await this.mainFrame.abort(); + public abort(): void { + this.mainFrame.abort(); } /** diff --git a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts index e718b5bd5..d499c0fca 100644 --- a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts +++ b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts @@ -1,7 +1,7 @@ -import IBrowserSettings from './IBrowserSettings.js'; -import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; +import IBrowserSettings from './types/IBrowserSettings.js'; +import IOptionalBrowserSettings from './types/IOptionalBrowserSettings.js'; import DefaultBrowserSettings from './DefaultBrowserSettings.js'; -import IReadOnlyBrowserSettings from './IReadOnlyBrowserSettings.js'; +import IReadOnlyBrowserSettings from './types/IReadOnlyBrowserSettings.js'; /** * Browser settings utility. diff --git a/packages/happy-dom/src/browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/DetachedBrowserFrame.ts deleted file mode 100644 index 0ee67e87d..000000000 --- a/packages/happy-dom/src/browser/DetachedBrowserFrame.ts +++ /dev/null @@ -1,121 +0,0 @@ -import IWindow from '../window/IWindow.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; -import IBrowserFrame from './IBrowserFrame.js'; -import IBrowserSettings from './IBrowserSettings.js'; -import BrowserSettingsFactory from './BrowserSettingsFactory.js'; -import IBrowserPageViewport from './IBrowserPageViewport.js'; -import Event from '../event/Event.js'; -import IOptionalBrowserSettings from './IOptionalBrowserSettings.js'; -import VirtualConsole from '../console/VirtualConsole.js'; -import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; -import IBrowserPage from './IBrowserPage.js'; - -/** - * Browser frame. - */ -export default class DetachedBrowserFrame implements IBrowserFrame { - public readonly childFrames: DetachedBrowserFrame[] = []; - public detached = false; - public readonly window: IWindow; - public _asyncTaskManager = new AsyncTaskManager(); - public readonly virtualConsolePrinter = new VirtualConsolePrinter(); - public readonly settings: IBrowserSettings; - public readonly console: Console | null; - public readonly page: IBrowserPage | null = null; - - /** - * Constructor. - * - * @param options Options. - * @param options.window Window. - * @param [options.console] Console. - * @param [options.settings] Browser settings. - */ - constructor(options: { - window: IWindow; - console?: Console; - settings?: IOptionalBrowserSettings; - }) { - this.window = options.window; - this.console = options.console ?? new VirtualConsole(this.virtualConsolePrinter); - this.settings = BrowserSettingsFactory.getSettings(options.settings); - } - - /** - * Returns the viewport. - */ - public get content(): string { - return this.window.document.documentElement.outerHTML; - } - - /** - * Returns a promise that is resolved when all async tasks are complete. - * - * @returns Promise. - */ - public async whenComplete(): Promise { - await this._asyncTaskManager.whenComplete(); - } - - /** - * Aborts all ongoing operations. - * - * @returns Promise. - */ - public async abort(): Promise { - await Promise.all(this.childFrames.map((frame) => frame.abort())); - await this._asyncTaskManager.abortAll(); - } - - /** - * Aborts all ongoing operations and destroys the frame. - * - * @returns Promise. - */ - public async destroy(): Promise { - await Promise.all(this.childFrames.map((frame) => frame.destroy())); - await this._asyncTaskManager.destroy(); - (this.window) = null; - } - - /** - * Sets the viewport. - * - * @param viewport Viewport. - */ - public setViewport(viewport: IBrowserPageViewport): void { - if ( - (viewport.width !== undefined && this.window.innerWidth !== viewport.width) || - (viewport.height !== undefined && this.window.innerHeight !== viewport.height) - ) { - if (viewport.width !== undefined && this.window.innerWidth !== viewport.width) { - (this.window.innerWidth) = viewport.width; - (this.window.outerWidth) = viewport.width; - } - - if (viewport.height !== undefined && this.window.innerHeight !== viewport.height) { - (this.window.innerHeight) = viewport.height; - (this.window.outerHeight) = viewport.height; - } - - this.window.dispatchEvent(new Event('resize')); - } - } - - /** - * Go to a page. - * - * @param url URL. - */ - public async goto(url: string): Promise { - await Promise.all(this.childFrames.map((frame) => frame.destroy())); - this._asyncTaskManager.abortAll(); - - this.window.location.href = url; - - const response = await this.window.fetch(url); - const responseText = await response.text(); - - this.window.document.write(responseText); - } -} diff --git a/packages/happy-dom/src/browser/IBrowserContextOptions.ts b/packages/happy-dom/src/browser/IBrowserContextOptions.ts deleted file mode 100644 index 2e4cc0f49..000000000 --- a/packages/happy-dom/src/browser/IBrowserContextOptions.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Browser context options. - */ -export default interface IBrowserContextOptions { - width?: number; - height?: number; - url?: string; - html?: string; - console?: Console; - settings?: { - disableJavaScriptEvaluation?: boolean; - disableJavaScriptFileLoading?: boolean; - disableCSSFileLoading?: boolean; - disableIframePageLoading?: boolean; - disableWindowOpenPageLoading?: boolean; - disableComputedStyleRendering?: boolean; - disableErrorCapturing?: boolean; - enableFileSystemHttpRequests?: boolean; - navigator?: { - userAgent?: string; - }; - device?: { - prefersColorScheme?: string; - mediaType?: string; - }; - }; -} diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts new file mode 100644 index 000000000..2855e0387 --- /dev/null +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts @@ -0,0 +1,73 @@ +import IBrowserSettings from '../types/IBrowserSettings.js'; +import DetachedBrowserContext from './DetachedBrowserContext.js'; +import IOptionalBrowserSettings from '../types/IOptionalBrowserSettings.js'; +import BrowserSettingsFactory from '../BrowserSettingsFactory.js'; +import DetachedBrowserPage from './DetachedBrowserPage.js'; +import IBrowser from '../types/IBrowser.js'; +import IWindow from '../../window/IWindow.js'; + +/** + * Detached browser. + */ +export default class DetachedBrowser implements IBrowser { + public readonly defaultContext: DetachedBrowserContext; + public readonly contexts: DetachedBrowserContext[]; + public readonly settings: IBrowserSettings; + public readonly console: Console | null; + + /** + * Constructor. + * + * @param window Window. + * @param [options] Options. + * @param [options.settings] Browser settings. + * @param [options.console] Console. + */ + constructor( + window: IWindow, + options?: { settings?: IOptionalBrowserSettings; console?: Console } + ) { + this.console = options?.console || null; + this.settings = BrowserSettingsFactory.getSettings(options?.settings); + this.defaultContext = new DetachedBrowserContext(window, this); + this.contexts = [this.defaultContext]; + } + + /** + * Aborts all ongoing operations and destroys the browser. + */ + public close(): void { + for (const context of this.contexts) { + context.close(); + } + (this.contexts) = []; + (this.defaultContext) = null; + } + + /** + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. + * + * @returns Promise. + */ + public async whenComplete(): Promise { + await Promise.all(this.contexts.map((page) => page.whenComplete())); + } + + /** + * Aborts all ongoing operations. + */ + public abort(): void { + for (const context of this.contexts) { + context.abort(); + } + } + + /** + * Creates a new page. + * + * @returns Page. + */ + public newPage(): DetachedBrowserPage { + return this.defaultContext.newPage(); + } +} diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts new file mode 100644 index 000000000..3170295a3 --- /dev/null +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts @@ -0,0 +1,63 @@ +import DetachedBrowser from './DetachedBrowser.js'; +import DetachedBrowserPage from './DetachedBrowserPage.js'; +import IBrowserContext from '../types/IBrowserContext.js'; +import IWindow from '../../window/IWindow.js'; + +/** + * Detached browser context. + */ +export default class DetachedBrowserContext implements IBrowserContext { + public readonly pages: DetachedBrowserPage[]; + public readonly browser: DetachedBrowser; + #windowClass: new () => IWindow; + + /** + * Constructor. + * + * @param window Window. + * @param browser Browser. + */ + constructor(window: IWindow, browser: DetachedBrowser) { + this.browser = browser; + this.pages = [new DetachedBrowserPage(window, this)]; + this.#windowClass = IWindow>window.constructor; + } + + /** + * Aborts all ongoing operations and destroys the context. + */ + public close(): void { + for (const page of this.pages) { + page.close(); + } + } + + /** + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. + * + * @returns Promise. + */ + public async whenComplete(): Promise { + await Promise.all(this.pages.map((page) => page.whenComplete())); + } + + /** + * Aborts all ongoing operations. + */ + public abort(): void { + for (const page of this.pages) { + page.abort(); + } + } + + /** + * Creates a new page. + * + * @returns Page. + */ + public newPage(): DetachedBrowserPage { + const page = new DetachedBrowserPage(new this.#windowClass(), this); + this.pages.push(page); + return page; + } +} diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts new file mode 100644 index 000000000..c86e850c2 --- /dev/null +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -0,0 +1,141 @@ +import IWindow from '../../window/IWindow.js'; +import DetachedBrowserPage from './DetachedBrowserPage.js'; +import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; +import IBrowserFrame from '../types/IBrowserFrame.js'; +import IBrowserPageViewport from '../types/IBrowserPageViewport.js'; +import Event from '../../event/Event.js'; +import Location from '../../location/Location.js'; + +/** + * Browser frame. + */ +export default class BrowserFrame implements IBrowserFrame { + public readonly childFrames: BrowserFrame[] = []; + public detached = false; + public readonly page: DetachedBrowserPage; + public readonly window: IWindow; + public _asyncTaskManager = new AsyncTaskManager(); + + /** + * Constructor. + * + * @param window Window. + * @param page Page. + */ + constructor(window: IWindow, page: DetachedBrowserPage) { + this.window = window; + this.page = page; + } + + /** + * Returns the content. + * + * @returns Content. + */ + public get content(): string { + return this.window.document.documentElement.outerHTML; + } + + /** + * Sets the content. + * + * @param content Content. + */ + public set content(content) { + this.window.document['_isFirstWrite'] = true; + this.window.document['_isFirstWriteAfterOpen'] = true; + this.window.document.write(content); + } + + /** + * Returns the URL. + * + * @returns URL. + */ + public get url(): string { + return this.window.location.href; + } + + /** + * Sets the content. + * + * @param url URL. + */ + public set url(url) { + (this.window.location) = new Location(url); + } + + /** + * Returns a promise that is resolved when all async tasks are complete. + * + * @returns Promise. + */ + public async whenComplete(): Promise { + await this._asyncTaskManager.whenComplete(); + } + + /** + * Aborts all ongoing operations. + * + * @returns Promise. + */ + public abort(): void { + for (const frame of this.childFrames) { + frame.abort(); + } + this._asyncTaskManager.abortAll(); + } + + /** + * Aborts all ongoing operations and destroys the frame. + */ + public destroy(): void { + for (const frame of this.childFrames) { + frame.destroy(); + } + this._asyncTaskManager.destroy(); + (this.page) = null; + (this.window) = null; + } + + /** + * Sets the viewport. + * + * @param viewport Viewport. + */ + public setViewport(viewport: IBrowserPageViewport): void { + if ( + (viewport.width !== undefined && this.window.innerWidth !== viewport.width) || + (viewport.height !== undefined && this.window.innerHeight !== viewport.height) + ) { + if (viewport.width !== undefined && this.window.innerWidth !== viewport.width) { + (this.window.innerWidth) = viewport.width; + (this.window.outerWidth) = viewport.width; + } + + if (viewport.height !== undefined && this.window.innerHeight !== viewport.height) { + (this.window.innerHeight) = viewport.height; + (this.window.outerHeight) = viewport.height; + } + + this.window.dispatchEvent(new Event('resize')); + } + } + + /** + * Go to a page. + * + * @param url URL. + */ + public async goto(url: string): Promise { + await Promise.all(this.childFrames.map((frame) => frame.destroy())); + this._asyncTaskManager.abortAll(); + + this.window.location.href = url; + + const response = await this.window.fetch(url); + const responseText = await response.text(); + + this.window.document.write(responseText); + } +} diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts new file mode 100644 index 000000000..7a75ae394 --- /dev/null +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts @@ -0,0 +1,104 @@ +import VirtualConsolePrinter from '../../console/VirtualConsolePrinter.js'; +import IBrowserPageViewport from '../types/IBrowserPageViewport.js'; +import DetachedBrowserFrame from './DetachedBrowserFrame.js'; +import DetachedBrowserContext from './DetachedBrowserContext.js'; +import VirtualConsole from '../../console/VirtualConsole.js'; +import IBrowserPage from '../types/IBrowserPage.js'; +import IWindow from '../../window/IWindow.js'; + +/** + * Detached browser page. + */ +export default class DetachedBrowserPage implements IBrowserPage { + public readonly virtualConsolePrinter = new VirtualConsolePrinter(); + public readonly mainFrame: DetachedBrowserFrame; + public readonly context: DetachedBrowserContext; + public readonly console: Console; + + /** + * Constructor. + * + * @param window Window. + * @param context Browser context. + */ + constructor(window: IWindow, context: DetachedBrowserContext) { + this.context = context; + this.console = context.browser.console ?? new VirtualConsole(this.virtualConsolePrinter); + this.mainFrame = new DetachedBrowserFrame(window, this); + } + + /** + * Returns frames. + */ + public get frames(): DetachedBrowserFrame[] { + return this._getFrames(this.mainFrame); + } + + /** + * Returns the viewport. + */ + public get content(): string { + return this.mainFrame.window.document.documentElement.outerHTML; + } + + /** + * Aborts all ongoing operations and destroys the page. + */ + public close(): void { + this.mainFrame.destroy(); + const index = this.context.pages.indexOf(this); + if (index !== -1) { + this.context.pages.splice(index, 1); + } + (this.virtualConsolePrinter) = null; + (this.mainFrame) = null; + (this.context) = null; + } + + /** + * Returns a promise that is resolved when all async tasks are complete. + * + * @returns Promise. + */ + public async whenComplete(): Promise { + await this.mainFrame.whenComplete(); + } + + /** + * Aborts all ongoing operations. + */ + public abort(): void { + this.mainFrame.abort(); + } + + /** + * Sets the viewport. + * + * @param viewport Viewport. + */ + public setViewport(viewport: IBrowserPageViewport): void { + this.mainFrame.setViewport(viewport); + } + + /** + * Go to a page. + * + * @param url URL. + */ + public async goto(url: string): Promise { + this.mainFrame.goto(url); + } + + /** + * Returns frames. + * + * @param parent Parent frame. + */ + private _getFrames(parent: DetachedBrowserFrame): DetachedBrowserFrame[] { + let frames = [parent]; + for (const frame of parent.childFrames) { + frames = frames.concat(this._getFrames(frame)); + } + return frames; + } +} diff --git a/packages/happy-dom/src/browser/IBrowser.ts b/packages/happy-dom/src/browser/types/IBrowser.ts similarity index 88% rename from packages/happy-dom/src/browser/IBrowser.ts rename to packages/happy-dom/src/browser/types/IBrowser.ts index 038703b05..5088de404 100644 --- a/packages/happy-dom/src/browser/IBrowser.ts +++ b/packages/happy-dom/src/browser/types/IBrowser.ts @@ -13,10 +13,8 @@ export default interface IBrowser { /** * Aborts all ongoing operations and destroys the browser. - * - * @returns Promise. */ - close(): Promise; + close(): void; /** * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all tasks such as timers are complete. @@ -27,10 +25,8 @@ export default interface IBrowser { /** * Aborts all ongoing operations. - * - * @returns Promise. */ - abort(): Promise; + abort(): void; /** * Creates a new page. diff --git a/packages/happy-dom/src/browser/IBrowserContext.ts b/packages/happy-dom/src/browser/types/IBrowserContext.ts similarity index 86% rename from packages/happy-dom/src/browser/IBrowserContext.ts rename to packages/happy-dom/src/browser/types/IBrowserContext.ts index d07ff2d28..16a609750 100644 --- a/packages/happy-dom/src/browser/IBrowserContext.ts +++ b/packages/happy-dom/src/browser/types/IBrowserContext.ts @@ -10,10 +10,8 @@ export default interface IBrowserContext { /** * Aborts all ongoing operations and destroys the context. - * - * @returns Promise. */ - close(): Promise; + close(): void; /** * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all tasks such as timers are complete. @@ -24,10 +22,9 @@ export default interface IBrowserContext { /** * Aborts all ongoing operations. - * - * @returns Promise. */ - abort(): Promise; + abort(): void; + /** * Creates a new page. * diff --git a/packages/happy-dom/src/browser/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts similarity index 61% rename from packages/happy-dom/src/browser/IBrowserFrame.ts rename to packages/happy-dom/src/browser/types/IBrowserFrame.ts index 97b8c40a3..ba3eef541 100644 --- a/packages/happy-dom/src/browser/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -1,8 +1,6 @@ -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; -import IWindow from '../window/IWindow.js'; +import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; +import IWindow from '../../window/IWindow.js'; import IBrowserPageViewport from './IBrowserPageViewport.js'; -import IBrowserSettings from './IBrowserSettings.js'; -import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; import IBrowserPage from './IBrowserPage.js'; /** @@ -10,13 +8,11 @@ import IBrowserPage from './IBrowserPage.js'; */ export default interface IBrowserFrame { readonly childFrames: IBrowserFrame[]; - detached: boolean; readonly window: IWindow; - readonly content: string; + detached: boolean; + content: string; + url: string; readonly _asyncTaskManager: AsyncTaskManager; - readonly virtualConsolePrinter: VirtualConsolePrinter; - readonly settings: IBrowserSettings; - readonly console: Console | null; readonly page: IBrowserPage | null; /** @@ -28,17 +24,13 @@ export default interface IBrowserFrame { /** * Aborts all ongoing operations. - * - * @returns Promise. */ - abort(): Promise; + abort(): void; /** * Aborts all ongoing operations and destroys the frame. - * - * @returns Promise. */ - destroy(): Promise; + destroy(): void; /** * Sets the viewport. diff --git a/packages/happy-dom/src/browser/IBrowserPage.ts b/packages/happy-dom/src/browser/types/IBrowserPage.ts similarity index 83% rename from packages/happy-dom/src/browser/IBrowserPage.ts rename to packages/happy-dom/src/browser/types/IBrowserPage.ts index dfeb5ce7f..17d072034 100644 --- a/packages/happy-dom/src/browser/IBrowserPage.ts +++ b/packages/happy-dom/src/browser/types/IBrowserPage.ts @@ -1,5 +1,5 @@ import IBrowserPageViewport from './IBrowserPageViewport.js'; -import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import VirtualConsolePrinter from '../../console/VirtualConsolePrinter.js'; import IBrowserFrame from './IBrowserFrame.js'; import IBrowserContext from './IBrowserContext.js'; @@ -16,10 +16,8 @@ export default interface IBrowserPage { /** * Aborts all ongoing operations and destroys the page. - * - * @returns Promise. */ - close(): Promise; + close(): void; /** * Returns a promise that is resolved when all async tasks are complete. @@ -30,10 +28,8 @@ export default interface IBrowserPage { /** * Aborts all ongoing operations. - * - * @returns Promise. */ - abort(): Promise; + abort(): void; /** * Sets the viewport. diff --git a/packages/happy-dom/src/browser/IBrowserPageViewport.ts b/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts similarity index 100% rename from packages/happy-dom/src/browser/IBrowserPageViewport.ts rename to packages/happy-dom/src/browser/types/IBrowserPageViewport.ts diff --git a/packages/happy-dom/src/browser/IBrowserSettings.ts b/packages/happy-dom/src/browser/types/IBrowserSettings.ts similarity index 100% rename from packages/happy-dom/src/browser/IBrowserSettings.ts rename to packages/happy-dom/src/browser/types/IBrowserSettings.ts diff --git a/packages/happy-dom/src/browser/IOptionalBrowserSettings.ts b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts similarity index 100% rename from packages/happy-dom/src/browser/IOptionalBrowserSettings.ts rename to packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts diff --git a/packages/happy-dom/src/browser/IReadOnlyBrowserSettings.ts b/packages/happy-dom/src/browser/types/IReadOnlyBrowserSettings.ts similarity index 100% rename from packages/happy-dom/src/browser/IReadOnlyBrowserSettings.ts rename to packages/happy-dom/src/browser/types/IReadOnlyBrowserSettings.ts diff --git a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 18bc358a1..3d8b62e6a 100644 --- a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -276,7 +276,7 @@ export default class CSSStyleDeclarationElementStyle { return; } - const ownerWindow = this.element.ownerDocument.defaultView; + const ownerWindow = this.element.ownerDocument._defaultView; for (const rule of options.cssRules) { if (rule.type === CSSRuleTypeEnum.styleRule) { @@ -355,7 +355,7 @@ export default class CSSStyleDeclarationElementStyle { parentSize: string | number | null; }): string { if ( - WindowBrowserSettingsReader.getSettings(this.element.ownerDocument.defaultView) + WindowBrowserSettingsReader.getSettings(this.element.ownerDocument._defaultView) .disableComputedStyleRendering ) { return options.value; @@ -368,7 +368,7 @@ export default class CSSStyleDeclarationElementStyle { while ((match = regexp.exec(options.value)) !== null) { if (match[1] !== 'px') { const valueInPixels = CSSMeasurementConverter.toPixels({ - ownerWindow: this.element.ownerDocument.defaultView, + ownerWindow: this.element.ownerDocument._defaultView, value: match[0], rootFontSize: options.rootFontSize, parentFontSize: options.parentFontSize, diff --git a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts index c95c00a9c..59cf8b173 100644 --- a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts +++ b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts @@ -22,16 +22,14 @@ export default class DOMImplementation { * TODO: Not fully implemented. */ public createDocument(): IDocument { - const documentClass = this._ownerDocument.constructor; - // @ts-ignore - return new documentClass(); + return new this._ownerDocument._defaultView.XMLDocument(); } /** * Creates and returns an HTML Document. */ public createHTMLDocument(): IDocument { - return this.createDocument(); + return new this._ownerDocument._defaultView.HTMLDocument(); } /** diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index dc82bd4db..49d1ebc00 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -2,7 +2,6 @@ import IDocument from '../nodes/document/IDocument.js'; import XMLParser from '../xml-parser/XMLParser.js'; import Node from '../nodes/node/Node.js'; import DOMException from '../exception/DOMException.js'; -import IWindow from '../window/IWindow.js'; import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; /** @@ -27,11 +26,8 @@ export default class DOMParser { throw new DOMException('Second parameter "mimeType" is mandatory.'); } - const ownerDocument = this._ownerDocument; const newDocument = this._createDocument(mimeType); - (newDocument.defaultView) = ownerDocument.defaultView; - const root = XMLParser.parse(newDocument, string, { evaluateScripts: true }); let documentElement = null; let documentTypeNode = null; @@ -98,13 +94,13 @@ export default class DOMParser { private _createDocument(mimeType: string): IDocument { switch (mimeType) { case 'text/html': - return new this._ownerDocument.defaultView.HTMLDocument(); + return new this._ownerDocument._defaultView.HTMLDocument(); case 'image/svg+xml': - return new this._ownerDocument.defaultView.SVGDocument(); + return new this._ownerDocument._defaultView.SVGDocument(); case 'text/xml': case 'application/xml': case 'application/xhtml+xml': - return new this._ownerDocument.defaultView.XMLDocument(); + return new this._ownerDocument._defaultView.XMLDocument(); default: throw new DOMException(`Unknown mime type "${mimeType}".`); } diff --git a/packages/happy-dom/src/event/Event.ts b/packages/happy-dom/src/event/Event.ts index c039f9b68..143104596 100644 --- a/packages/happy-dom/src/event/Event.ts +++ b/packages/happy-dom/src/event/Event.ts @@ -95,7 +95,7 @@ export default class Event { ) { eventTarget = (eventTarget).host; } else if ((eventTarget).nodeType === NodeTypeEnum.documentNode) { - eventTarget = ((eventTarget)).defaultView; + eventTarget = ((eventTarget))._defaultView; } else { break; } diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts index cd7ba4399..aae2d1fd2 100644 --- a/packages/happy-dom/src/event/EventTarget.ts +++ b/packages/happy-dom/src/event/EventTarget.ts @@ -261,10 +261,10 @@ export default abstract class EventTarget implements IEventTarget { */ public _getWindow(): IWindow | null { if (((this)).ownerDocument) { - return ((this)).ownerDocument.defaultView; + return ((this)).ownerDocument._defaultView; } - if (((this)).defaultView) { - return ((this)).defaultView; + if (((this))._defaultView) { + return ((this))._defaultView; } if (((this)).document) { return (this); diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index 51fddbbb7..a18631331 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -74,7 +74,7 @@ export default class Fetch { this.asyncTaskManager = options.asyncTaskManager; this.request = typeof options.url === 'string' || options.url instanceof URL - ? new options.ownerDocument.defaultView.Request(options.url, options.init) + ? new options.ownerDocument._defaultView.Request(options.url, options.init) : url; if (options.contentType) { (this.request._contentType) = options.contentType; @@ -109,7 +109,7 @@ export default class Fetch { if (this.request._url.protocol === 'data:') { const result = DataURIParser.parse(this.request.url); - this.response = new this.ownerDocument.defaultView.Response(result.buffer, { + this.response = new this.ownerDocument._defaultView.Response(result.buffer, { headers: { 'Content-Type': result.type } }); resolve(this.response); @@ -199,7 +199,7 @@ export default class Fetch { */ private onError(error: Error): void { this.finalizeRequest(); - this.ownerDocument.defaultView.console.error(error); + this.ownerDocument._defaultView.console.error(error); this.reject( new DOMException( `Fetch to "${this.request.url}" failed. Error: ${error.message}`, @@ -251,7 +251,7 @@ export default class Fetch { nodeResponse.statusCode === 204 || nodeResponse.statusCode === 304 ) { - this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); + this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -273,7 +273,7 @@ export default class Fetch { // Ignore error as it is forwarded to the response body. } }); - this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); + this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -305,7 +305,7 @@ export default class Fetch { }); } - this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); + this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -313,7 +313,7 @@ export default class Fetch { raw.on('end', () => { // Some old IIS servers return zero-length OK deflate responses, so 'data' is never emitted. if (!this.response) { - this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); + this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -329,7 +329,7 @@ export default class Fetch { // Ignore error as it is forwarded to the response body. } }); - this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); + this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -337,7 +337,7 @@ export default class Fetch { } // Otherwise, use response as is - this.response = new this.ownerDocument.defaultView.Response(body, responseOptions); + this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -553,7 +553,7 @@ export default class Fetch { headers.set('Connection', 'close'); if (!headers.has('User-Agent')) { - headers.set('User-Agent', document.defaultView.navigator.userAgent); + headers.set('User-Agent', document._defaultView.navigator.userAgent); } if (this.request._referrer instanceof URL) { @@ -565,8 +565,8 @@ export default class Fetch { (this.request.credentials === 'same-origin' && !isCORS) ) { const cookie = (<{ _cookie: CookieJar }>( - (document.defaultView.document) - ))._cookie.getCookieString(document.defaultView.location, false); + (document._defaultView.document) + ))._cookie.getCookieString(document._defaultView.location, false); if (cookie) { headers.set('Cookie', cookie); } @@ -624,7 +624,7 @@ export default class Fetch { // "set-cookie" and "set-cookie2" are not allowed in response headers according to spec. if (lowerKey === 'set-cookie' || lowerKey === 'set-cookie2') { (<{ _cookie: CookieJar }>( - (this.ownerDocument.defaultView.document) + (this.ownerDocument._defaultView.document) ))._cookie.addCookieString(this.request._url, header); } else { headers.append(key, header); diff --git a/packages/happy-dom/src/fetch/ResourceFetch.ts b/packages/happy-dom/src/fetch/ResourceFetch.ts index 7621d94a6..c4feebd9e 100644 --- a/packages/happy-dom/src/fetch/ResourceFetch.ts +++ b/packages/happy-dom/src/fetch/ResourceFetch.ts @@ -14,7 +14,7 @@ export default class ResourceFetch { * @returns Response. */ public static async fetch(document: IDocument, url: string): Promise { - const response = await document.defaultView.fetch(url); + const response = await document._defaultView.fetch(url); if (!response.ok) { throw new DOMException( `Failed to perform request to "${url}". Status code: ${response.status}` @@ -32,9 +32,9 @@ export default class ResourceFetch { */ public static fetchSync(document: IDocument, url: string): string { // We want to only load SyncRequest when it is needed to improve performance and not have direct dependencies to server side packages. - const absoluteURL = new URL(url, document.defaultView.location).href; + const absoluteURL = new URL(url, document._defaultView.location).href; - const xhr = new document.defaultView.XMLHttpRequest(); + const xhr = new document._defaultView.XMLHttpRequest(); xhr.open('GET', absoluteURL, false); xhr.send(); diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts index 5485d33d2..1e963dfb0 100644 --- a/packages/happy-dom/src/fetch/Response.ts +++ b/packages/happy-dom/src/fetch/Response.ts @@ -161,6 +161,9 @@ export default class Response implements IResponse { (this.bodyUsed) = true; + if (!this._asyncTaskManager) { + debugger; + } const taskID = this._asyncTaskManager.startTask(); let buffer: Buffer; @@ -266,7 +269,7 @@ export default class Response implements IResponse { ); } - return new (this.constructor)(null, { + return new (this)(null, { headers: { location: new URL(url).toString() }, @@ -282,7 +285,7 @@ export default class Response implements IResponse { * @returns Response. */ public static error(): IResponse { - const response = new (this.constructor)(null, { status: 0, statusText: '' }); + const response = new (this)(null, { status: 0, statusText: '' }); (response.type) = 'error'; return response; } @@ -307,7 +310,7 @@ export default class Response implements IResponse { headers.set('content-type', 'application/json'); } - return new (this.constructor)(body, { + return new (this)(body, { status: 200, ...init, headers diff --git a/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts index 8de395d1e..6c4e7cb2c 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts @@ -37,14 +37,14 @@ export default class FetchRequestReferrerUtility { document: IDocument, request: IRequest ): '' | 'no-referrer' | 'client' | URL { - if (request.referrer === 'about:client' && document.defaultView.location.origin === 'null') { + if (request.referrer === 'about:client' && document._defaultView.location.origin === 'null') { return 'no-referrer'; } const requestURL = new URL(request.url); const referrerURL = request.referrer === 'about:client' - ? new URL(document.defaultView.location.href) + ? new URL(document._defaultView.location.href) : new URL(request.referrer); if (REQUEST_REFERRER_UNSUPPORTED_PROTOCOL_REGEXP.test(referrerURL.protocol)) { diff --git a/packages/happy-dom/src/file/FileReader.ts b/packages/happy-dom/src/file/FileReader.ts index ca348ad99..de9057ef8 100644 --- a/packages/happy-dom/src/file/FileReader.ts +++ b/packages/happy-dom/src/file/FileReader.ts @@ -79,7 +79,7 @@ export default class FileReader extends EventTarget { * Aborts the file reader. */ public abort(): void { - const window = this._ownerDocument.defaultView; + const window = this._ownerDocument._defaultView; window.clearTimeout(this._loadTimeout); window.clearTimeout(this._parseTimeout); @@ -110,7 +110,7 @@ export default class FileReader extends EventTarget { * @param [encoding] Encoding. */ private _readFile(blob: Blob, format: FileReaderFormatEnum, encoding: string = null): void { - const window = this._ownerDocument.defaultView; + const window = this._ownerDocument._defaultView; if (this.readyState === FileReaderReadyStateEnum.loading) { throw new DOMException( diff --git a/packages/happy-dom/src/location/Location.ts b/packages/happy-dom/src/location/Location.ts index f32e47d46..5c9ad150d 100644 --- a/packages/happy-dom/src/location/Location.ts +++ b/packages/happy-dom/src/location/Location.ts @@ -8,9 +8,11 @@ import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; export default class Location extends URL { /** * Constructor. + * + * @param [url] URL. */ - constructor() { - super('about:blank'); + constructor(url = 'about:blank') { + super(url); } /** diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index cd84e4e3a..7122283bf 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -805,7 +805,9 @@ export default class Document extends Node implements IDocument { const elementClass: typeof Element = customElementClass || this._defaultView[ElementTag[tagName]] || HTMLUnknownElement; + elementClass._ownerDocument = this; const element = new elementClass(); + elementClass._ownerDocument = null; element.tagName = tagName; (element.namespaceURI) = namespaceURI; @@ -922,7 +924,8 @@ export default class Document extends Node implements IDocument { throw new DOMException('Parameter 1 was not of type Node.'); } const clone = node.cloneNode(deep); - (clone.ownerDocument) = this; + const document = this; + Object.defineProperty(clone, 'ownerDocument', { get: () => document }); return clone; } @@ -947,7 +950,8 @@ export default class Document extends Node implements IDocument { } const adopted = node.parentNode ? node.parentNode.removeChild(node) : node; - (adopted.ownerDocument) = this; + const document = this; + Object.defineProperty(adopted, 'ownerDocument', { get: () => document }); return adopted; } diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index bd2436d06..0e4f20db0 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -14,7 +14,6 @@ import IElement from './IElement.js'; import DOMException from '../../exception/DOMException.js'; import IShadowRoot from '../shadow-root/IShadowRoot.js'; import INode from '../node/INode.js'; -import IDocument from '../document/IDocument.js'; import IHTMLCollection from './IHTMLCollection.js'; import INodeList from '../node/INodeList.js'; import { TInsertAdjacentPositions } from './IElement.js'; @@ -366,7 +365,7 @@ export default class Element extends Node implements IElement { for (let i = 0, max = this.attributes.length; i < max; i++) { const attribute = this.attributes[i]; clone.attributes.setNamedItem( - Object.assign(new this.ownerDocument.defaultView.Attr(), attribute) + Object.assign(new this.ownerDocument._defaultView.Attr(), attribute) ); } @@ -884,7 +883,7 @@ export default class Element extends Node implements IElement { public scroll(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void { if (typeof x === 'object') { if (x.behavior === 'smooth') { - this.ownerDocument.defaultView.setTimeout(() => { + this.ownerDocument._defaultView.setTimeout(() => { if (x.top !== undefined) { (this.scrollTop) = x.top; } @@ -924,7 +923,9 @@ export default class Element extends Node implements IElement { */ public override dispatchEvent(event: Event): boolean { const returnValue = super.dispatchEvent(event); - const browserSettings = WindowBrowserSettingsReader.getSettings(this.ownerDocument.defaultView); + const browserSettings = WindowBrowserSettingsReader.getSettings( + this.ownerDocument._defaultView + ); if ( !browserSettings.disableJavaScriptEvaluation && @@ -936,10 +937,10 @@ export default class Element extends Node implements IElement { if (attribute && !event._immediatePropagationStopped) { if (browserSettings.disableErrorCapturing) { - this.ownerDocument.defaultView.eval(attribute); + this.ownerDocument._defaultView.eval(attribute); } else { - WindowErrorUtility.captureError(this.ownerDocument.defaultView, () => - this.ownerDocument.defaultView.eval(attribute) + WindowErrorUtility.captureError(this.ownerDocument._defaultView, () => + this.ownerDocument._defaultView.eval(attribute) ); } } diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 60e1f7b05..2d303e13a 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -100,7 +100,7 @@ export default class HTMLElement extends Element implements IHTMLElement { for (const childNode of this._childNodes) { if (childNode.nodeType === NodeTypeEnum.elementNode) { const childElement = childNode; - const computedStyle = this.ownerDocument.defaultView.getComputedStyle(childElement); + const computedStyle = this.ownerDocument._defaultView.getComputedStyle(childElement); if (childElement.tagName !== 'SCRIPT' && childElement.tagName !== 'STYLE') { const display = computedStyle.display; diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts index 6ac3013b6..6246b0a35 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts @@ -18,7 +18,7 @@ export default class HTMLIFrameUtility { */ public static async loadPage(element: HTMLIFrameElement): Promise { const browserSettings = WindowBrowserSettingsReader.getSettings( - element.ownerDocument.defaultView + element.ownerDocument._defaultView ); if (element.isConnected && !browserSettings.disableIframePageLoading) { const src = element.src; @@ -26,14 +26,14 @@ export default class HTMLIFrameUtility { if (src) { // To avoid circular dependency, we use a reference to the window class instead of importing it. const contentWindow = new element.ownerDocument['_windowClass']({ - url: new URL(src, element.ownerDocument.defaultView.location.href).href, + url: new URL(src, element.ownerDocument._defaultView.location.href).href, settings: { ...browserSettings } }); - (contentWindow.parent) = element.ownerDocument.defaultView; - (contentWindow.top) = element.ownerDocument.defaultView; + (contentWindow.parent) = element.ownerDocument._defaultView; + (contentWindow.top) = element.ownerDocument._defaultView; if (src === 'about:blank') { element._contentWindow = contentWindow; @@ -54,7 +54,7 @@ export default class HTMLIFrameUtility { return; } - const originURL = element.ownerDocument.defaultView.location; + const originURL = element.ownerDocument._defaultView.location; const targetURL = new URL(src, originURL); const isCORS = (originURL.hostname !== targetURL.hostname && @@ -66,7 +66,7 @@ export default class HTMLIFrameUtility { element._contentWindow = null; try { - const response = await element.ownerDocument.defaultView.fetch(src); + const response = await element.ownerDocument._defaultView.fetch(src); responseText = await response.text(); } catch (error) { element.dispatchEvent( @@ -75,18 +75,18 @@ export default class HTMLIFrameUtility { error }) ); - element.ownerDocument.defaultView.dispatchEvent( + element.ownerDocument._defaultView.dispatchEvent( new ErrorEvent('error', { message: error.message, error }) ); - element.ownerDocument.defaultView.console.error(error); + element.ownerDocument._defaultView.console.error(error); return; } element._contentWindow = isCORS - ? new CrossOriginWindow(element.ownerDocument.defaultView, contentWindow) + ? new CrossOriginWindow(element.ownerDocument._defaultView, contentWindow) : contentWindow; contentWindow.document.write(responseText); element.dispatchEvent(new Event('load')); diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts index 067c7b487..bd8a87f9e 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts @@ -1,4 +1,3 @@ -import Document from '../document/Document.js'; import Event from '../../event/Event.js'; import ResourceFetch from '../../fetch/ResourceFetch.js'; import HTMLLinkElement from './HTMLLinkElement.js'; @@ -24,7 +23,7 @@ export default class HTMLLinkElementUtility { const href = element.getAttribute('href'); const rel = element.getAttribute('rel'); const browserSettings = WindowBrowserSettingsReader.getSettings( - element.ownerDocument.defaultView + element.ownerDocument._defaultView ); if (href !== null && rel && rel.toLowerCase() === 'stylesheet' && element.isConnected) { @@ -40,7 +39,7 @@ export default class HTMLLinkElementUtility { } (<{ _readyStateManager: DocumentReadyStateManager }>( - (element.ownerDocument.defaultView) + (element.ownerDocument._defaultView) ))._readyStateManager.startTask(); let code: string | null = null; @@ -53,7 +52,7 @@ export default class HTMLLinkElementUtility { } (<{ _readyStateManager: DocumentReadyStateManager }>( - (element.ownerDocument.defaultView) + (element.ownerDocument._defaultView) ))._readyStateManager.endTask(); if (error) { diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index df1ddc0a8..c8888fd09 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -172,7 +172,9 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip public override _connectToNode(parentNode: INode = null): void { const isConnected = this.isConnected; const isParentConnected = parentNode ? parentNode.isConnected : false; - const browserSettings = WindowBrowserSettingsReader.getSettings(this.ownerDocument.defaultView); + const browserSettings = WindowBrowserSettingsReader.getSettings( + this.ownerDocument._defaultView + ); super._connectToNode(parentNode); @@ -194,10 +196,10 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip this.ownerDocument['_currentScript'] = this; if (browserSettings.disableErrorCapturing) { - this.ownerDocument.defaultView.eval(textContent); + this.ownerDocument._defaultView.eval(textContent); } else { - WindowErrorUtility.captureError(this.ownerDocument.defaultView, () => - this.ownerDocument.defaultView.eval(textContent) + WindowErrorUtility.captureError(this.ownerDocument._defaultView, () => + this.ownerDocument._defaultView.eval(textContent) ); } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts index dd9f6e490..d5e2eab95 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts @@ -22,7 +22,7 @@ export default class HTMLScriptElementUtility { const src = element.getAttribute('src'); const async = element.getAttribute('async') !== null; const browserSettings = WindowBrowserSettingsReader.getSettings( - element.ownerDocument.defaultView + element.ownerDocument._defaultView ); if ( @@ -41,7 +41,7 @@ export default class HTMLScriptElementUtility { if (async) { (<{ _readyStateManager: DocumentReadyStateManager }>( - (element.ownerDocument.defaultView) + (element.ownerDocument._defaultView) ))._readyStateManager.startTask(); let code: string | null = null; @@ -54,7 +54,7 @@ export default class HTMLScriptElementUtility { } (<{ _readyStateManager: DocumentReadyStateManager }>( - (element.ownerDocument.defaultView) + (element.ownerDocument._defaultView) ))._readyStateManager.endTask(); if (error) { @@ -65,10 +65,10 @@ export default class HTMLScriptElementUtility { } else { element.ownerDocument['_currentScript'] = element; if (browserSettings.disableErrorCapturing) { - element.ownerDocument.defaultView.eval(code); + element.ownerDocument._defaultView.eval(code); } else { - WindowErrorUtility.captureError(element.ownerDocument.defaultView, () => - element.ownerDocument.defaultView.eval(code) + WindowErrorUtility.captureError(element.ownerDocument._defaultView, () => + element.ownerDocument._defaultView.eval(code) ); } element.ownerDocument['_currentScript'] = null; @@ -92,10 +92,10 @@ export default class HTMLScriptElementUtility { } else { element.ownerDocument['_currentScript'] = element; if (browserSettings.disableErrorCapturing) { - element.ownerDocument.defaultView.eval(code); + element.ownerDocument._defaultView.eval(code); } else { - WindowErrorUtility.captureError(element.ownerDocument.defaultView, () => - element.ownerDocument.defaultView.eval(code) + WindowErrorUtility.captureError(element.ownerDocument._defaultView, () => + element.ownerDocument._defaultView.eval(code) ); } element.ownerDocument['_currentScript'] = null; diff --git a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts index 02cb7a737..4c3989137 100644 --- a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts +++ b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts @@ -27,8 +27,8 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem // This element can potentially be a custom element that has not been defined yet // Therefore we need to register a callback for when it is defined in CustomElementRegistry and replace it with the registered element (see #404) - if (tagName.includes('-') && this.ownerDocument.defaultView.customElements._callbacks) { - const callbacks = this.ownerDocument.defaultView.customElements._callbacks; + if (tagName.includes('-') && this.ownerDocument._defaultView.customElements._callbacks) { + const callbacks = this.ownerDocument._defaultView.customElements._callbacks; if (parentNode && !this._customElementDefineCallback) { const callback = (): void => { diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 65c851fd9..bf110e785 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -15,6 +15,9 @@ import INodeList from './INodeList.js'; * Node. */ export default class Node extends EventTarget implements INode { + // Can be set before the Node is created. + public static _ownerDocument: IDocument | null = null; + // Public properties public static readonly ELEMENT_NODE = NodeTypeEnum.elementNode; public static readonly ATTRIBUTE_NODE = NodeTypeEnum.attributeNode; @@ -59,6 +62,18 @@ export default class Node extends EventTarget implements INode { public _textAreaNode: INode = null; public _observers: MutationListener[] = []; public readonly _childNodes: INodeList = new NodeList(); + #ownerDocument: IDocument | null = null; + + /** + * Constructor. + */ + constructor() { + super(); + + if ((this.constructor)._ownerDocument) { + this.#ownerDocument = (this.constructor)._ownerDocument; + } + } /** * Returns `Symbol.toStringTag`. @@ -73,7 +88,7 @@ export default class Node extends EventTarget implements INode { * Returns owner document. */ public get ownerDocument(): IDocument { - throw new Error('Property "ownerDocument" needs to be implemented by sub-class.'); + return this.#ownerDocument; } /** @@ -207,7 +222,7 @@ export default class Node extends EventTarget implements INode { if (base) { return base.href; } - return this.ownerDocument.defaultView.location.href; + return this.ownerDocument._defaultView.location.href; } /** @@ -265,7 +280,9 @@ export default class Node extends EventTarget implements INode { * @returns Cloned node. */ public cloneNode(deep = false): INode { + (this.constructor)._ownerDocument = this.ownerDocument; const clone = new (this.constructor)(); + (this.constructor)._ownerDocument = null; // Document has childNodes directly when it is created if (clone._childNodes.length) { diff --git a/packages/happy-dom/src/nodes/node/NodeUtility.ts b/packages/happy-dom/src/nodes/node/NodeUtility.ts index a8dddb376..69d1a6a34 100644 --- a/packages/happy-dom/src/nodes/node/NodeUtility.ts +++ b/packages/happy-dom/src/nodes/node/NodeUtility.ts @@ -239,8 +239,8 @@ export default class NodeUtility { * @returns "true" if inclusive ancestor. */ public static isInclusiveAncestor( - ancestorNode: INode, - referenceNode: INode, + ancestorNode: INode | null, + referenceNode: INode | null, includeShadowRoots = false ): boolean { if (ancestorNode === null || referenceNode === null) { diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts index a8584a506..2f2b74e4b 100644 --- a/packages/happy-dom/src/range/Range.ts +++ b/packages/happy-dom/src/range/Range.ts @@ -26,8 +26,6 @@ import IRangeBoundaryPoint from './IRangeBoundaryPoint.js'; * https://developer.mozilla.org/en-US/docs/Web/API/Range. */ export default class Range { - // Will be populated by a sub-class in Window. - public readonly _ownerDocument: IDocument; public static readonly END_TO_END: number = RangeHowEnum.endToEnd; public static readonly END_TO_START: number = RangeHowEnum.endToStart; public static readonly START_TO_END: number = RangeHowEnum.startToEnd; @@ -47,6 +45,13 @@ export default class Range { this._end = { node: this._ownerDocument, offset: 0 }; } + /** + * Returns owner document. + */ + public get _ownerDocument(): IDocument { + throw new Error('_ownerDocument needs to be implemented by sub-class.'); + } + /** * Returns start container. * diff --git a/packages/happy-dom/src/selection/Selection.ts b/packages/happy-dom/src/selection/Selection.ts index 47df6082e..7ae1974a0 100644 --- a/packages/happy-dom/src/selection/Selection.ts +++ b/packages/happy-dom/src/selection/Selection.ts @@ -249,7 +249,7 @@ export default class Selection { return; } - const newRange = new this._ownerDocument.defaultView.Range(); + const newRange = new this._ownerDocument._defaultView.Range(); newRange._start.node = node; newRange._start.offset = offset; @@ -285,7 +285,7 @@ export default class Selection { } const { node, offset } = this._range._end; - const newRange = new this._ownerDocument.defaultView.Range(); + const newRange = new this._ownerDocument._defaultView.Range(); newRange._start.node = node; newRange._start.offset = offset; @@ -309,7 +309,7 @@ export default class Selection { } const { node, offset } = this._range._start; - const newRange = new this._ownerDocument.defaultView.Range(); + const newRange = new this._ownerDocument._defaultView.Range(); newRange._start.node = node; newRange._start.offset = offset; @@ -379,7 +379,7 @@ export default class Selection { const anchorNode = this.anchorNode; const anchorOffset = this.anchorOffset; - const newRange = new this._ownerDocument.defaultView.Range(); + const newRange = new this._ownerDocument._defaultView.Range(); newRange._start.node = node; newRange._start.offset = 0; newRange._end.node = node; @@ -435,7 +435,7 @@ export default class Selection { } const length = node.childNodes.length; - const newRange = new this._ownerDocument.defaultView.Range(); + const newRange = new this._ownerDocument._defaultView.Range(); newRange._start.node = node; newRange._start.offset = 0; @@ -479,7 +479,7 @@ export default class Selection { const anchor = { node: anchorNode, offset: anchorOffset }; const focus = { node: focusNode, offset: focusOffset }; - const newRange = new this._ownerDocument.defaultView.Range(); + const newRange = new this._ownerDocument._defaultView.Range(); if (RangeUtility.compareBoundaryPointsPosition(anchor, focus) === -1) { newRange._start = anchor; diff --git a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts index ef237e72f..684985db4 100644 --- a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts +++ b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts @@ -1,18 +1,17 @@ -import IBrowserSettings from '../browser/IBrowserSettings.js'; +import IBrowserSettings from '../browser/types/IBrowserSettings.js'; import IWindow from './IWindow.js'; -import BrowserFrame from '../browser/BrowserFrame.js'; -import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; import BrowserSettingsFactory from '../browser/BrowserSettingsFactory.js'; -import IReadOnlyBrowserSettings from '../browser/IReadOnlyBrowserSettings.js'; -import IBrowserPageViewport from '../browser/IBrowserPageViewport.js'; +import IReadOnlyBrowserSettings from '../browser/types/IReadOnlyBrowserSettings.js'; +import IBrowserPageViewport from '../browser/types/IBrowserPageViewport.js'; +import IBrowserFrame from '../browser/types/IBrowserFrame.js'; /** * API for detached windows to be able to access features of the owner window. */ export default class HappyDOMWindowAPI { #window: IWindow; - #browserFrame?: BrowserFrame | DetachedBrowserFrame; + #browserFrame?: IBrowserFrame; #settings: IBrowserSettings | null = null; /** @@ -22,7 +21,7 @@ export default class HappyDOMWindowAPI { * @param options.window Owner window. * @param options.browserFrame Browser frame. */ - constructor(options: { window: IWindow; browserFrame: BrowserFrame | DetachedBrowserFrame }) { + constructor(options: { window: IWindow; browserFrame: IBrowserFrame }) { this.#window = options.window; this.#browserFrame = options.browserFrame; } @@ -36,9 +35,7 @@ export default class HappyDOMWindowAPI { public get settings(): IReadOnlyBrowserSettings { if (!this.#settings) { this.#settings = BrowserSettingsFactory.getReadOnlySettings( - this.#browserFrame instanceof DetachedBrowserFrame - ? this.#browserFrame.settings - : this.#browserFrame.page.context.browser.settings + this.#browserFrame.page.context.browser.settings ); } return this.#settings; @@ -50,9 +47,6 @@ export default class HappyDOMWindowAPI { * @returns Virtual console printer. */ public get virtualConsolePrinter(): VirtualConsolePrinter { - if (this.#browserFrame instanceof DetachedBrowserFrame) { - return this.#browserFrame.virtualConsolePrinter; - } return this.#browserFrame.page.virtualConsolePrinter; } @@ -80,15 +74,15 @@ export default class HappyDOMWindowAPI { * * @deprecated Use abort() instead. */ - public async cancelAsync(): Promise { - await this.abort(); + public cancelAsync(): void { + this.abort(); } /** * Aborts all async tasks. */ - public async abort(): Promise { - await this.#browserFrame.abort(); + public abort(): void { + this.#browserFrame.abort(); } /** diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index 5f26a240e..54b53c1b5 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -130,6 +130,10 @@ import DetachedWindowAPI from './HappyDOMWindowAPI.js'; import Headers from '../fetch/Headers.js'; import Request from '../fetch/Request.js'; import Response from '../fetch/Response.js'; +import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; +import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; +import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; +import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; /** * Window without dependencies to server side specific packages. @@ -157,6 +161,10 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly DocumentType: typeof DocumentType; // Element classes + readonly HTMLAnchorElement: typeof HTMLAnchorElement; + readonly HTMLButtonElement: typeof HTMLButtonElement; + readonly HTMLOptGroupElement: typeof HTMLOptGroupElement; + readonly HTMLOptionElement: typeof HTMLOptionElement; readonly HTMLElement: typeof HTMLElement; readonly HTMLUnknownElement: typeof HTMLUnknownElement; readonly HTMLTemplateElement: typeof HTMLTemplateElement; @@ -194,10 +202,8 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly HTMLMenuElement: typeof HTMLElement; readonly HTMLDListElement: typeof HTMLElement; readonly HTMLDivElement: typeof HTMLElement; - readonly HTMLAnchorElement: typeof HTMLElement; readonly HTMLAreaElement: typeof HTMLElement; readonly HTMLBRElement: typeof HTMLElement; - readonly HTMLButtonElement: typeof HTMLElement; readonly HTMLCanvasElement: typeof HTMLElement; readonly HTMLDataElement: typeof HTMLElement; readonly HTMLDataListElement: typeof HTMLElement; diff --git a/packages/happy-dom/src/window/VMGlobalPropertyScript.ts b/packages/happy-dom/src/window/VMGlobalPropertyScript.ts index e0a037b12..774dc85e6 100644 --- a/packages/happy-dom/src/window/VMGlobalPropertyScript.ts +++ b/packages/happy-dom/src/window/VMGlobalPropertyScript.ts @@ -48,7 +48,7 @@ this.isFinite = globalThis.isFinite; this.isNaN = globalThis.isNaN; this.parseFloat = globalThis.parseFloat; this.parseInt = globalThis.parseInt; -this.process = globalThis.process; +this.process = null; this.root = globalThis.root; this.undefined = globalThis.undefined; this.unescape = globalThis.unescape; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 7ea81d00f..77d8882c9 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -137,12 +137,16 @@ import FileReader from '../file/FileReader.js'; import Request from '../fetch/Request.js'; import Range from '../range/Range.js'; import XMLHttpRequest from '../xml-http-request/XMLHttpRequest.js'; -import IOptionalBrowserSettings from '../browser/IOptionalBrowserSettings.js'; +import IOptionalBrowserSettings from '../browser/types/IOptionalBrowserSettings.js'; import WindowBrowserSettingsReader from './WindowBrowserSettingsReader.js'; import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; import DocumentReadyStateEnum from '../nodes/document/DocumentReadyStateEnum.js'; -import IBrowserFrame from '../browser/IBrowserFrame.js'; -import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; +import IBrowserFrame from '../browser/types/IBrowserFrame.js'; +import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; +import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; +import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; +import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; +import DetachedBrowser from '../browser/detached-browser/DetachedBrowser.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -179,6 +183,10 @@ export default class Window extends EventTarget implements IWindow { public readonly DocumentType: typeof DocumentType; // Element classes + public readonly HTMLAnchorElement: typeof HTMLAnchorElement; + public readonly HTMLButtonElement: typeof HTMLButtonElement; + public readonly HTMLOptGroupElement: typeof HTMLOptGroupElement; + public readonly HTMLOptionElement: typeof HTMLOptionElement; public readonly HTMLElement: typeof HTMLElement; public readonly HTMLUnknownElement: typeof HTMLUnknownElement; public readonly HTMLTemplateElement: typeof HTMLTemplateElement; @@ -214,10 +222,8 @@ export default class Window extends EventTarget implements IWindow { public readonly HTMLMenuElement: typeof HTMLElement; public readonly HTMLDListElement: typeof HTMLElement; public readonly HTMLDivElement: typeof HTMLElement; - public readonly HTMLAnchorElement: typeof HTMLElement; public readonly HTMLAreaElement: typeof HTMLElement; public readonly HTMLBRElement: typeof HTMLElement; - public readonly HTMLButtonElement: typeof HTMLElement; public readonly HTMLCanvasElement: typeof HTMLElement; public readonly HTMLDataElement: typeof HTMLElement; public readonly HTMLDataListElement: typeof HTMLElement; @@ -357,10 +363,10 @@ export default class Window extends EventTarget implements IWindow { public readonly Plugin = Plugin; public readonly PluginArray = PluginArray; public readonly FileList = FileList; - public readonly DOMRect: typeof DOMRect; - public readonly RadioNodeList: typeof RadioNodeList; - public readonly ValidityState: typeof ValidityState; - public readonly Headers: typeof Headers; + public readonly DOMRect = DOMRect; + public readonly RadioNodeList = RadioNodeList; + public readonly ValidityState = ValidityState; + public readonly Headers = Headers; public readonly Request: typeof Request; public readonly Response: typeof Response; public readonly XMLHttpRequestUpload = XMLHttpRequestUpload; @@ -393,7 +399,6 @@ export default class Window extends EventTarget implements IWindow { public readonly location: Location; public readonly history: History; public readonly navigator: Navigator; - public readonly console: Console; public readonly opener: IWindow | null = null; public readonly self: IWindow = this; public readonly top: IWindow = this; @@ -528,16 +533,13 @@ export default class Window extends EventTarget implements IWindow { if (options?.browserFrame) { this.#browserFrame = options.browserFrame; } else { - this.#browserFrame = new DetachedBrowserFrame({ - window: this, + this.#browserFrame = new DetachedBrowser(this, { console: options?.console, settings: options?.settings - }); + }).defaultContext.pages[0].mainFrame; } - this.console = this.#browserFrame.console; - - WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.settings); + WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); this.happyDOM = new HappyDOMWindowAPI({ window: this, @@ -623,6 +625,10 @@ export default class Window extends EventTarget implements IWindow { this.DocumentType = classes.DocumentType; // HTML Element classes + this.HTMLAnchorElement = classes.HTMLAnchorElement; + this.HTMLButtonElement = classes.HTMLButtonElement; + this.HTMLOptGroupElement = classes.HTMLOptGroupElement; + this.HTMLOptionElement = classes.HTMLOptionElement; this.HTMLElement = classes.HTMLElement; this.HTMLUnknownElement = classes.HTMLUnknownElement; this.HTMLTemplateElement = classes.HTMLTemplateElement; @@ -658,10 +664,8 @@ export default class Window extends EventTarget implements IWindow { this.HTMLMenuElement = classes.HTMLElement; this.HTMLDListElement = classes.HTMLElement; this.HTMLDivElement = classes.HTMLElement; - this.HTMLAnchorElement = classes.HTMLElement; this.HTMLAreaElement = classes.HTMLElement; this.HTMLBRElement = classes.HTMLElement; - this.HTMLButtonElement = classes.HTMLElement; this.HTMLCanvasElement = classes.HTMLElement; this.HTMLDataElement = classes.HTMLElement; this.HTMLDataListElement = classes.HTMLElement; @@ -719,6 +723,15 @@ export default class Window extends EventTarget implements IWindow { }); } + /** + * Returns console. + * + * @returns Console. + */ + public get console(): Console { + return this.#browserFrame.page.console; + } + /** * The number of pixels that the document is currently scrolled horizontally * @@ -840,7 +853,7 @@ export default class Window extends EventTarget implements IWindow { _target?: string, _features?: string ): IWindow | ICrossOriginWindow | null { - if (this.#browserFrame.settings.disableWindowOpenPageLoading) { + if (this.#browserFrame.page.context.browser.settings.disableWindowOpenPageLoading) { return null; } return null; @@ -850,6 +863,8 @@ export default class Window extends EventTarget implements IWindow { * Closes the window. */ public close(): void { + WindowBrowserSettingsReader.removeSettings(this); + if (this.#browserFrame.page) { if (this.#browserFrame.page.mainFrame === this.#browserFrame) { this.#browserFrame.page.close(); @@ -879,7 +894,7 @@ export default class Window extends EventTarget implements IWindow { */ public setTimeout(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { const id = this._setTimeout(() => { - if (this.#browserFrame.settings.disableErrorCapturing) { + if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { callback(...args); } else { WindowErrorUtility.captureError(this, () => callback(...args)); @@ -910,7 +925,7 @@ export default class Window extends EventTarget implements IWindow { */ public setInterval(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { const id = this._setInterval(() => { - if (this.#browserFrame.settings.disableErrorCapturing) { + if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { callback(...args); } else { WindowErrorUtility.captureError( @@ -942,7 +957,7 @@ export default class Window extends EventTarget implements IWindow { */ public requestAnimationFrame(callback: (timestamp: number) => void): NodeJS.Immediate { const id = global.setImmediate(() => { - if (this.#browserFrame.settings.disableErrorCapturing) { + if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { callback(this.performance.now()); } else { WindowErrorUtility.captureError(this, () => callback(this.performance.now())); @@ -973,7 +988,7 @@ export default class Window extends EventTarget implements IWindow { const taskId = this.#browserFrame._asyncTaskManager.startTask(() => (isAborted = true)); this._queueMicrotask(() => { if (!isAborted) { - if (this.#browserFrame.settings.disableErrorCapturing) { + if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { callback(); } else { WindowErrorUtility.captureError(this, <() => unknown>callback); diff --git a/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts b/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts index df5b36bdc..2a65d0c6b 100644 --- a/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts +++ b/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts @@ -1,4 +1,4 @@ -import IBrowserSettings from '../browser/IBrowserSettings.js'; +import IBrowserSettings from '../browser/types/IBrowserSettings.js'; import IWindow from './IWindow.js'; /** @@ -36,4 +36,19 @@ export default class WindowBrowserSettingsReader { window['__happyDOMSettingsID__'] = this.#settings.length; this.#settings.push(settings); } + + /** + * Removes browser settings. + * + * @param window Window. + */ + public static removeSettings(window: IWindow): void { + const id = window['__happyDOMSettingsID__']; + + if (id !== undefined && this.#settings[id]) { + delete this.#settings[id]; + } + + delete window['__happyDOMSettingsID__']; + } } diff --git a/packages/happy-dom/src/window/WindowClassFactory.ts b/packages/happy-dom/src/window/WindowClassFactory.ts index cc11b18b3..8c9edf1b9 100644 --- a/packages/happy-dom/src/window/WindowClassFactory.ts +++ b/packages/happy-dom/src/window/WindowClassFactory.ts @@ -1,5 +1,3 @@ -import BrowserFrame from '../browser/BrowserFrame.js'; -import DetachedBrowserFrame from '../browser/DetachedBrowserFrame.js'; import AudioImplementation from '../nodes/html-audio-element/Audio.js'; import ImageImplementation from '../nodes/html-image-element/Image.js'; import DocumentFragmentImplementation from '../nodes/document-fragment/DocumentFragment.js'; @@ -48,6 +46,11 @@ import HTMLDocumentImplementation from '../nodes/html-document/HTMLDocument.js'; import XMLDocumentImplementation from '../nodes/xml-document/XMLDocument.js'; import SVGDocumentImplementation from '../nodes/svg-document/SVGDocument.js'; import DocumentTypeImplementation from '../nodes/document-type/DocumentType.js'; +import HTMLAnchorElementImplementation from '../nodes/html-anchor-element/HTMLAnchorElement.js'; +import HTMLButtonElementImplementation from '../nodes/html-button-element/HTMLButtonElement.js'; +import HTMLOptGroupElementImplementation from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; +import HTMLOptionElementImplementation from '../nodes/html-option-element/HTMLOptionElement.js'; +import IBrowserFrame from '../browser/types/IBrowserFrame.js'; /** * Some classes need to get access to the window object without having a reference to the window in the constructor. @@ -62,10 +65,7 @@ export default class WindowClassFactory { * @param properties.browserFrame Browser frame. * @returns Classes. */ - public static getClasses(properties: { - window: IWindow; - browserFrame: BrowserFrame | DetachedBrowserFrame; - }): { + public static getClasses(properties: { window: IWindow; browserFrame: IBrowserFrame }): { // Nodes Node: typeof NodeImplementation; Attr: typeof AttrImplementation; @@ -85,6 +85,10 @@ export default class WindowClassFactory { DocumentType: typeof DocumentTypeImplementation; // HTML Elements + HTMLAnchorElement: typeof HTMLAnchorElementImplementation; + HTMLButtonElement: typeof HTMLButtonElementImplementation; + HTMLOptGroupElement: typeof HTMLOptGroupElementImplementation; + HTMLOptionElement: typeof HTMLOptionElementImplementation; HTMLElement: typeof HTMLElementImplementation; HTMLUnknownElement: typeof HTMLUnknownElementImplementation; HTMLTemplateElement: typeof HTMLTemplateElementImplementation; @@ -117,224 +121,246 @@ export default class WindowClassFactory { Range: typeof RangeImplementation; Audio: typeof AudioImplementation; } { + const window = properties.window; + const asyncTaskManager = properties.browserFrame._asyncTaskManager; + /* eslint-disable jsdoc/require-jsdoc */ // Nodes class Node extends NodeImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class Attr extends AttrImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class SVGSVGElement extends SVGSVGElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class SVGElement extends SVGElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class SVGGraphicsElement extends SVGGraphicsElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class Text extends TextImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class Comment extends CommentImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class ShadowRoot extends ShadowRootImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class ProcessingInstruction extends ProcessingInstructionImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class Element extends ElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class CharacterData extends CharacterDataImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class Document extends DocumentImplementation { - public readonly _defaultView: IWindow = properties.window; + public readonly _defaultView: IWindow = window; } class HTMLDocument extends HTMLDocumentImplementation { - public readonly _defaultView: IWindow = properties.window; + public readonly _defaultView: IWindow = window; } class XMLDocument extends XMLDocumentImplementation { - public readonly _defaultView: IWindow = properties.window; + public readonly _defaultView: IWindow = window; } class SVGDocument extends SVGDocumentImplementation { - public readonly _defaultView: IWindow = properties.window; + public readonly _defaultView: IWindow = window; } class DocumentType extends DocumentTypeImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } // HTML Elements + class HTMLAnchorElement extends HTMLAnchorElementImplementation { + public get ownerDocument(): IDocument { + return window.document; + } + } + class HTMLButtonElement extends HTMLButtonElementImplementation { + public get ownerDocument(): IDocument { + return window.document; + } + } + class HTMLOptGroupElement extends HTMLOptGroupElementImplementation { + public get ownerDocument(): IDocument { + return window.document; + } + } + class HTMLOptionElement extends HTMLOptionElementImplementation { + public get ownerDocument(): IDocument { + return window.document; + } + } class Audio extends AudioImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class Image extends ImageImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class DocumentFragment extends DocumentFragmentImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLElement extends HTMLElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLUnknownElement extends HTMLUnknownElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLTemplateElement extends HTMLTemplateElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLFormElement extends HTMLFormElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLInputElement extends HTMLInputElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLSelectElement extends HTMLSelectElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLTextAreaElement extends HTMLTextAreaElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLImageElement extends HTMLImageElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLScriptElement extends HTMLScriptElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLLinkElement extends HTMLLinkElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLStyleElement extends HTMLStyleElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLLabelElement extends HTMLLabelElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLSlotElement extends HTMLSlotElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLMetaElement extends HTMLMetaElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLMediaElement extends HTMLMediaElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLAudioElement extends HTMLAudioElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLVideoElement extends HTMLVideoElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLBaseElement extends HTMLBaseElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLIFrameElement extends HTMLIFrameElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class HTMLDialogElement extends HTMLDialogElementImplementation { public get ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } // Other Classes class Request extends RequestImplementation { - protected readonly _asyncTaskManager: AsyncTaskManager = - properties.browserFrame._asyncTaskManager; + protected readonly _asyncTaskManager: AsyncTaskManager = asyncTaskManager; protected get _ownerDocument(): IDocument { - return properties.window.document; + return window.document; } } class Response extends ResponseImplementation { - protected readonly _asyncTaskManager: AsyncTaskManager = - properties.browserFrame._asyncTaskManager; + protected readonly _asyncTaskManager: AsyncTaskManager = asyncTaskManager; } class XMLHttpRequest extends XMLHttpRequestImplementation { - protected readonly _asyncTaskManager: AsyncTaskManager = - properties.browserFrame._asyncTaskManager; - protected readonly _ownerDocument: IDocument = properties.window.document; + protected readonly _asyncTaskManager: AsyncTaskManager = asyncTaskManager; + protected readonly _ownerDocument: IDocument = window.document; } class FileReader extends FileReaderImplementation { - public readonly _ownerDocument: IDocument = properties.window.document; + public readonly _ownerDocument: IDocument = window.document; } class DOMParser extends DOMParserImplementation { - public readonly _ownerDocument: IDocument = properties.window.document; + public readonly _ownerDocument: IDocument = window.document; } class Range extends RangeImplementation { - public readonly _ownerDocument: IDocument = properties.window.document; + public get _ownerDocument(): IDocument { + return window.document; + } } /* eslint-enable jsdoc/require-jsdoc */ @@ -359,6 +385,10 @@ export default class WindowClassFactory { DocumentType, // HTML Elements + HTMLAnchorElement, + HTMLButtonElement, + HTMLOptGroupElement, + HTMLOptionElement, HTMLElement, HTMLUnknownElement, HTMLTemplateElement, diff --git a/packages/happy-dom/src/window/WindowErrorUtility.ts b/packages/happy-dom/src/window/WindowErrorUtility.ts index 05d91c863..8d2e6a063 100644 --- a/packages/happy-dom/src/window/WindowErrorUtility.ts +++ b/packages/happy-dom/src/window/WindowErrorUtility.ts @@ -56,11 +56,11 @@ export default class WindowErrorUtility { (elementOrWindow).console.error(error); elementOrWindow.dispatchEvent(new ErrorEvent('error', { message: error.message, error })); } else { - (elementOrWindow).ownerDocument.defaultView.console.error(error); + (elementOrWindow).ownerDocument._defaultView.console.error(error); (elementOrWindow).dispatchEvent( new ErrorEvent('error', { message: error.message, error }) ); - (elementOrWindow).ownerDocument.defaultView.dispatchEvent( + (elementOrWindow).ownerDocument._defaultView.dispatchEvent( new ErrorEvent('error', { message: error.message, error }) ); } diff --git a/packages/happy-dom/src/window/__BrowserContextLoader.ts b/packages/happy-dom/src/window/__BrowserContextLoader.ts index 98e0d1753..121527f1d 100644 --- a/packages/happy-dom/src/window/__BrowserContextLoader.ts +++ b/packages/happy-dom/src/window/__BrowserContextLoader.ts @@ -1,5 +1,4 @@ import { URL } from 'url'; -import Document from '../nodes/document/Document.js'; import IWindow from './IWindow.js'; import CrossOriginWindow from './CrossOriginWindow.js'; import WindowErrorUtility from './WindowErrorUtility.js'; @@ -93,7 +92,7 @@ export default class __BrowserContextLoader { !originURL.hostname.endsWith(targetURL.hostname)) || originURL.protocol !== targetURL.protocol; - (contentWindow.document)._readyStateManager.startTask(); + (contentWindow)._readyStateManager.startTask(); ownerWindow .fetch(url, { @@ -102,14 +101,14 @@ export default class __BrowserContextLoader { .then((response) => response.text()) .then((responseText) => { contentWindow.document.write(responseText); - (contentWindow.document)._readyStateManager.endTask(); + (contentWindow)._readyStateManager.endTask(); }) .catch((error) => { WindowErrorUtility.dispatchError( options?.ownerIframeElement ? options.ownerIframeElement : ownerWindow, error ); - (contentWindow.document)._readyStateManager.endTask(); + (contentWindow)._readyStateManager.endTask(); if (!ownerWindow.happyDOM.settings.disableErrorCapturing) { throw error; } diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index b705f8143..d8d266953 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -389,7 +389,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ); } - const { location } = this._ownerDocument.defaultView; + const { location } = this._ownerDocument._defaultView; const url = new URL(this.#internal.settings.url, location); @@ -404,11 +404,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Load files off the local filesystem (file://) if (XMLHttpRequestURLUtility.isLocal(url)) { if ( - !WindowBrowserSettingsReader.getSettings(this._ownerDocument.defaultView) + !WindowBrowserSettingsReader.getSettings(this._ownerDocument._defaultView) .enableFileSystemHttpRequests ) { throw new DOMException( - 'File system is disabled by default for security reasons. To enable it, set the "enableFileSystemHttpRequests" HappyDOM setting to true.', + 'File system is disabled by default for security reasons. To enable it, set the Happy DOM setting "enableFileSystemHttpRequests" option to true.', DOMExceptionNameEnum.securityError ); } @@ -578,7 +578,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Default request headers. */ private _getDefaultRequestHeaders(): { [key: string]: string } { - const { location, navigator, document } = this._ownerDocument.defaultView; + const { location, navigator, document } = this._ownerDocument._defaultView; return { accept: '*/*', @@ -634,7 +634,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseXML = null; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this._ownerDocument.defaultView.location + this._ownerDocument._defaultView.location ).href; // Set Cookies. this._setCookies(this.#internal.state.incommingMessage.headers); @@ -647,7 +647,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ) { const redirectUrl = new URL( this.#internal.state.incommingMessage.headers['location'], - this._ownerDocument.defaultView.location + this._ownerDocument._defaultView.location ); ssl = redirectUrl.protocol === 'https:'; this.#internal.settings.url = redirectUrl.href; @@ -768,7 +768,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Parse the new URL. const redirectUrl = new URL( this.#internal.settings.url, - this._ownerDocument.defaultView.location + this._ownerDocument._defaultView.location ); this.#internal.settings.url = redirectUrl.href; ssl = redirectUrl.protocol === 'https:'; @@ -834,7 +834,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseText = responseText; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this._ownerDocument.defaultView.location + this._ownerDocument._defaultView.location ).href; // Discard the 'end' event if the connection has been aborted this._setState(XMLHttpRequestReadyStateEnum.done); @@ -940,7 +940,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseText = responseText; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this._ownerDocument.defaultView.location + this._ownerDocument._defaultView.location ).href; this._setState(XMLHttpRequestReadyStateEnum.done); @@ -971,7 +971,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { case XMLHttpResponseTypeEnum.blob: try { return { - response: new this._ownerDocument.defaultView.Blob([new Uint8Array(data)], { + response: new this._ownerDocument._defaultView.Blob([new Uint8Array(data)], { type: this.getResponseHeader('content-type') || '' }), responseText: null, @@ -981,7 +981,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return { response: null, responseText: null, responseXML: null }; } case XMLHttpResponseTypeEnum.document: - const window = this._ownerDocument.defaultView; + const window = this._ownerDocument._defaultView; const domParser = new window.DOMParser(); let response: IDocument; @@ -1024,18 +1024,18 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ): void { const originURL = new URL( this.#internal.settings.url, - this._ownerDocument.defaultView.location + this._ownerDocument._defaultView.location ); for (const header of ['set-cookie', 'set-cookie2']) { if (Array.isArray(headers[header])) { for (const cookie of headers[header]) { - (this._ownerDocument.defaultView.document)._cookie.addCookieString( + (this._ownerDocument._defaultView.document)._cookie.addCookieString( originURL, cookie ); } } else if (headers[header]) { - (this._ownerDocument.defaultView.document)._cookie.addCookieString( + (this._ownerDocument._defaultView.document)._cookie.addCookieString( originURL, headers[header] ); @@ -1061,9 +1061,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { error: errorObject }); - this._ownerDocument.defaultView.console.error(errorObject); + this._ownerDocument._defaultView.console.error(errorObject); this.dispatchEvent(event); - this._ownerDocument.defaultView.dispatchEvent(event); + this._ownerDocument._defaultView.dispatchEvent(event); } /** diff --git a/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts b/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts index 226f0cf41..6e669648b 100644 --- a/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts +++ b/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts @@ -1,3 +1,5 @@ +import HTMLDocument from '../../src/nodes/html-document/HTMLDocument'; +import XMLDocument from '../../src/nodes/xml-document/XMLDocument'; import IWindow from '../../src/window/IWindow'; import Window from '../../src/window/Window'; import { beforeEach, describe, it, expect } from 'vitest'; @@ -9,10 +11,18 @@ describe('DOMImplementation', () => { window = new Window(); }); + describe('createDocument()', () => { + it('Returns a new XMLDocument.', () => { + const document = window.document.implementation.createDocument(); + expect(document instanceof XMLDocument).toBe(true); + expect(document.defaultView).toBe(null); + }); + }); + describe('createHTMLDocument()', () => { it('Returns a new Document.', () => { const document = window.document.implementation.createHTMLDocument(); - expect(document instanceof Document).toBe(true); + expect(document instanceof HTMLDocument).toBe(true); expect(document.defaultView).toBe(null); }); }); diff --git a/packages/happy-dom/test/fetch/Response.test.ts b/packages/happy-dom/test/fetch/Response.test.ts index 6eecb8e38..5b1bc9d23 100644 --- a/packages/happy-dom/test/fetch/Response.test.ts +++ b/packages/happy-dom/test/fetch/Response.test.ts @@ -440,14 +440,14 @@ describe('Response', () => { describe('static redirect()', () => { it('Returns a new instance of Response with redirect status set to 302 by default.', async () => { - const response = Response.redirect('https://example.com'); + const response = window.Response.redirect('https://example.com'); expect(response.status).toBe(302); expect(response.headers.get('Location')).toBe('https://example.com/'); }); it('Returns a new instance of Response with redirect status set to 301.', async () => { - const response = Response.redirect('https://example.com', 301); + const response = window.Response.redirect('https://example.com', 301); expect(response.status).toBe(301); expect(response.headers.get('Location')).toBe('https://example.com/'); @@ -457,7 +457,7 @@ describe('Response', () => { let error: Error | null = null; try { - Response.redirect('https://example.com', 200); + window.Response.redirect('https://example.com', 200); } catch (e) { error = e; } @@ -473,7 +473,7 @@ describe('Response', () => { describe('static error()', () => { it('Returns a new instance of Response with type set to error.', async () => { - const response = Response.error(); + const response = window.Response.error(); expect(response.status).toBe(0); expect(response.statusText).toBe(''); @@ -484,7 +484,7 @@ describe('Response', () => { describe('static json()', () => { it('Returns a new instance of Response with JSON body.', async () => { const data = { key1: 'value1', key2: 'value2' }; - const response = Response.json(data); + const response = window.Response.json(data); expect(response.status).toBe(200); expect(response.statusText).toBe(''); @@ -494,7 +494,7 @@ describe('Response', () => { it('Returns a new instance of Response with JSON body and custom init.', async () => { const data = { key1: 'value1', key2: 'value2' }; - const response = Response.json(data, { + const response = window.Response.json(data, { status: 201, statusText: 'OK', headers: { 'Content-Type': 'test' } diff --git a/packages/happy-dom/test/match-media/MediaQueryList.test.ts b/packages/happy-dom/test/match-media/MediaQueryList.test.ts index 37dabf5dd..c897afec4 100644 --- a/packages/happy-dom/test/match-media/MediaQueryList.test.ts +++ b/packages/happy-dom/test/match-media/MediaQueryList.test.ts @@ -61,17 +61,17 @@ describe('MediaQueryList', () => { }); 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 = new Window({ width: 1024, height: 768, settings: { device: { mediaType: '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); - expect(new MediaQueryList({ ownerWindow: window, media: 'print' }).matches).toBe(true); expect( new MediaQueryList({ ownerWindow: window, media: 'print and (min-width: 1024px)' }).matches diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts index 7879c39a2..3ad70c125 100644 --- a/packages/happy-dom/test/nodes/document/Document.test.ts +++ b/packages/happy-dom/test/nodes/document/Document.test.ts @@ -1102,7 +1102,8 @@ describe('Document', () => { const clone = document.cloneNode(false); const clone2 = document.cloneNode(true); - expect(clone.defaultView === window).toBe(true); + expect(clone._defaultView === window).toBe(true); + expect(clone.defaultView === null).toBe(true); expect(clone.children.length).toBe(0); expect(clone2.children.length).toBe(1); expect(clone2.children[0].outerHTML).toBe('
'); diff --git a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts index 967b83780..c50559db9 100644 --- a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts +++ b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts @@ -4,7 +4,7 @@ import IDocument from '../../../src/nodes/document/IDocument.js'; import IHTMLIFrameElement from '../../../src/nodes/html-iframe-element/IHTMLIFrameElement.js'; import IResponse from '../../../src/fetch/types/IResponse.js'; import ErrorEvent from '../../../src/event/events/ErrorEvent.js'; -import IFrameCrossOriginWindow from '../../../src/nodes/html-iframe-element/IFrameCrossOriginWindow.js'; +import CrossOriginWindow from '../../../src/window/CrossOriginWindow.js'; import MessageEvent from '../../../src/event/events/MessageEvent.js'; import DOMExceptionNameEnum from '../../../src/exception/DOMExceptionNameEnum.js'; import DOMException from '../../../src/exception/DOMException.js'; @@ -141,7 +141,7 @@ describe('HTMLIFrameElement', () => { }); }); - it('Returns instance of IFrameCrossOriginWindow for URL with different origin.', async () => { + it('Returns instance of CrossOriginWindow for URL with different origin.', async () => { await new Promise((resolve) => { const iframeOrigin = 'https://other.origin.com'; const iframeSrc = iframeOrigin + '/iframe.html'; @@ -163,7 +163,7 @@ describe('HTMLIFrameElement', () => { const message = 'test'; let triggeredEvent: MessageEvent | null = null; expect(fetchedURL).toBe(iframeSrc); - expect(element.contentWindow instanceof IFrameCrossOriginWindow).toBe(true); + expect(element.contentWindow instanceof CrossOriginWindow).toBe(true); expect(() => element.contentWindow?.location.href).toThrowError( new DOMException( `Blocked a frame with origin "${documentOrigin}" from accessing a cross-origin frame.`, @@ -171,7 +171,7 @@ describe('HTMLIFrameElement', () => { ) ); const targetWindow = ( - (element.contentWindow)['_targetWindow'] + (element.contentWindow)['_targetWindow'] ); expect(element.contentWindow?.self === element.contentWindow).toBe(true); expect(element.contentWindow?.window === element.contentWindow).toBe(true); @@ -179,7 +179,7 @@ describe('HTMLIFrameElement', () => { expect(element.contentWindow?.top === window).toBe(true); targetWindow.addEventListener( 'message', - (event: MessageEvent) => (triggeredEvent = event) + (event) => (triggeredEvent = event) ); element.contentWindow?.postMessage(message, iframeOrigin); expect(triggeredEvent).toBe(null); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index bbe4bae07..1d70d1e85 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -30,6 +30,7 @@ import Permissions from '../../src/permissions/Permissions.js'; import Clipboard from '../../src/clipboard/Clipboard.js'; import PackageVersion from '../../src/version.js'; import IHTMLDialogElement from '../../src/nodes/html-dialog-element/IHTMLDialogElement.js'; +import Browser from '../../src/browser/Browser.js'; const GET_NAVIGATOR_PLATFORM = (): string => { return ( @@ -63,18 +64,22 @@ describe('Window', () => { const thirdWindow = new Window({ url: 'https://localhost:8080' }); for (const className of [ - 'Response', 'Request', - 'Image', 'FileReader', 'DOMParser', - 'Range' + 'Range', + 'Image', + 'Audio', + 'DocumentFragment' ]) { const input = className === 'Request' ? 'test' : undefined; const thirdInstance = new thirdWindow[className](input); const firstInstance = new firstWindow[className](input); const secondInstance = new secondWindow[className](input); - const property = className === 'Image' ? 'ownerDocument' : '_ownerDocument'; + const property = + className === 'Image' || className === 'Audio' || className === 'DocumentFragment' + ? 'ownerDocument' + : '_ownerDocument'; expect(firstInstance[property] === firstWindow.document).toBe(true); expect(secondInstance[property] === secondWindow.document).toBe(true); @@ -130,7 +135,9 @@ describe('Window', () => { expect(windowWithOptions.outerHeight).toBe(1080); expect(windowWithOptions.console).toBe(globalThis.console); expect(windowWithOptions.location.href).toBe('http://localhost:8080/'); - expect(windowWithOptions.happyDOM.virtualConsolePrinter).toBe(null); + expect(windowWithOptions.happyDOM.virtualConsolePrinter).toBeInstanceOf( + VirtualConsolePrinter + ); expect(windowWithOptions.happyDOM.settings.disableJavaScriptEvaluation).toBe(true); expect(windowWithOptions.happyDOM.settings.disableJavaScriptFileLoading).toBe(false); expect(windowWithOptions.happyDOM.settings.disableCSSFileLoading).toBe(false); @@ -423,17 +430,22 @@ describe('Window', () => { describe('get Response()', () => { it('Returns Response class.', () => { - const response = new window.Response(); + const browser = new Browser(); + const page = browser.newPage(); + const response = new page.mainFrame.window.Response(); expect(response instanceof Response).toBe(true); - expect(response['_ownerDocument']).toBe(document); + expect(response['_asyncTaskManager']).toBe(page.mainFrame._asyncTaskManager); }); }); describe('get Request()', () => { it('Returns Request class.', () => { - const request = new window.Request('https://localhost:8080/test/page/'); + const browser = new Browser(); + const page = browser.newPage(); + const request = new page.mainFrame.window.Request('https://localhost:8080/test/page/'); expect(request instanceof Request).toBe(true); - expect(request['_ownerDocument']).toBe(document); + expect(request['_asyncTaskManager']).toBe(page.mainFrame._asyncTaskManager); + expect(request['_ownerDocument']).toBe(page.mainFrame.window.document); }); }); @@ -941,6 +953,7 @@ describe('Window', () => { it('Makes it possible to cancel an ongoing microtask.', async () => { await new Promise((resolve) => { let isCallbackCalled = false; + process.nextTick(() => {}); window.queueMicrotask(() => { isCallbackCalled = true; resolve(null); diff --git a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts index 3e7338325..16a1f5d58 100644 --- a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts +++ b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts @@ -980,9 +980,10 @@ describe('XMLHttpRequest', () => { it('Throws an exception when doing a synchronous request towards a local file with another method than "GET".', () => { window = new Window({ settings: { - enableFileSystemHttpRequests: false + enableFileSystemHttpRequests: true } }); + request = new window.XMLHttpRequest(); request.open('POST', 'file://C:/path/to/file.txt', false); @@ -997,6 +998,7 @@ describe('XMLHttpRequest', () => { enableFileSystemHttpRequests: true } }); + request = new window.XMLHttpRequest(); request.open('POST', 'file://C:/path/to/file.txt', true); @@ -1011,6 +1013,7 @@ describe('XMLHttpRequest', () => { enableFileSystemHttpRequests: true } }); + request = new window.XMLHttpRequest(); const filepath = 'C:/path/to/file.txt'; const fileContent = 'test'; @@ -1037,6 +1040,7 @@ describe('XMLHttpRequest', () => { enableFileSystemHttpRequests: true } }); + request = new window.XMLHttpRequest(); await new Promise((resolve) => { const filepath = 'C:/path/to/file.txt'; @@ -1917,7 +1921,7 @@ describe('XMLHttpRequest', () => { request.send(); request.abort(); - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM.whenComplete(); expect(request.readyState).toBe(XMLHttpRequestReadyStateEnum.unsent); expect(isDestroyed).toBe(true); @@ -1951,7 +1955,7 @@ describe('XMLHttpRequest', () => { request.open('GET', REQUEST_URL, true); request.send(); - window.happyDOM.cancelAsync(); + window.happyDOM.abort(); expect(request.readyState).toBe(XMLHttpRequestReadyStateEnum.unsent); expect(isDestroyed).toBe(true); From ebc5759b08b99eb03bc702d9ef81713436f68663 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 25 Oct 2023 01:27:36 +0200 Subject: [PATCH 14/63] #466@trivial: Continues on implementation. --- .../happy-dom/src/browser/BrowserFrame.ts | 35 +++++- .../detached-browser/DetachedBrowserFrame.ts | 31 ++++- .../src/browser/types/IBrowserFrame.ts | 9 +- .../html-iframe-element/HTMLIFrameElement.ts | 31 ++++- .../HTMLIFrameElementNamedNodeMap.ts | 18 ++- .../html-iframe-element/HTMLIFrameUtility.ts | 96 --------------- .../HTMLIframePageLoader.ts | 112 ++++++++++++++++++ .../happy-dom/src/window/CrossOriginWindow.ts | 7 +- .../src/window/WindowClassFactory.ts | 3 + 9 files changed, 222 insertions(+), 120 deletions(-) delete mode 100644 packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts create mode 100644 packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 5115ccaf4..68bcb0703 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -6,13 +6,14 @@ import Window from '../window/Window.js'; import IBrowserPageViewport from './types/IBrowserPageViewport.js'; import Event from '../event/Event.js'; import Location from '../location/Location.js'; +import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; /** * Browser frame. */ export default class BrowserFrame implements IBrowserFrame { public readonly childFrames: BrowserFrame[] = []; - public detached = false; + public readonly parentFrame: BrowserFrame | null = null; public readonly page: BrowserPage; public readonly window: IWindow; public _asyncTaskManager = new AsyncTaskManager(); @@ -93,10 +94,17 @@ export default class BrowserFrame implements IBrowserFrame { * Aborts all ongoing operations and destroys the frame. */ public destroy(): void { + if (this.parentFrame) { + const index = this.parentFrame.childFrames.indexOf(this); + if (index !== -1) { + this.parentFrame.childFrames.splice(index, 1); + } + } for (const frame of this.childFrames) { frame.destroy(); } this._asyncTaskManager.destroy(); + WindowBrowserSettingsReader.removeSettings(this.window); (this.page) = null; (this.window) = null; } @@ -125,16 +133,35 @@ export default class BrowserFrame implements IBrowserFrame { } } + /** + * Creates a new frame. + * + * @returns Frame. + */ + public newFrame(): IBrowserFrame { + const frame = new BrowserFrame(this.page); + (frame.parentFrame) = this; + this.childFrames.push(frame); + return frame; + } + /** * Go to a page. * * @param url URL. */ public async goto(url: string): Promise { - await Promise.all(this.childFrames.map((frame) => frame.destroy())); - this._asyncTaskManager.abortAll(); + for (const frame of this.childFrames) { + frame.destroy(); + } - this.window.location.href = url; + this._asyncTaskManager.destroy(); + + (this.window) = new Window({ + url, + browserFrame: this, + console: this.page.console + }); const response = await this.window.fetch(url); const responseText = await response.text(); diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index c86e850c2..b347ca35a 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -5,13 +5,14 @@ import IBrowserFrame from '../types/IBrowserFrame.js'; import IBrowserPageViewport from '../types/IBrowserPageViewport.js'; import Event from '../../event/Event.js'; import Location from '../../location/Location.js'; +import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; /** * Browser frame. */ -export default class BrowserFrame implements IBrowserFrame { - public readonly childFrames: BrowserFrame[] = []; - public detached = false; +export default class DetachedBrowserFrame implements IBrowserFrame { + public readonly childFrames: DetachedBrowserFrame[] = []; + public readonly parentFrame: DetachedBrowserFrame | null = null; public readonly page: DetachedBrowserPage; public readonly window: IWindow; public _asyncTaskManager = new AsyncTaskManager(); @@ -90,9 +91,16 @@ export default class BrowserFrame implements IBrowserFrame { * Aborts all ongoing operations and destroys the frame. */ public destroy(): void { + if (this.parentFrame) { + const index = this.parentFrame.childFrames.indexOf(this); + if (index !== -1) { + this.parentFrame.childFrames.splice(index, 1); + } + } for (const frame of this.childFrames) { frame.destroy(); } + WindowBrowserSettingsReader.removeSettings(this.window); this._asyncTaskManager.destroy(); (this.page) = null; (this.window) = null; @@ -122,6 +130,18 @@ export default class BrowserFrame implements IBrowserFrame { } } + /** + * Creates a new frame. + * + * @returns Frame. + */ + public newFrame(): IBrowserFrame { + const frame = new DetachedBrowserFrame(this.window, this.page); + (frame.parentFrame) = this; + this.childFrames.push(frame); + return frame; + } + /** * Go to a page. * @@ -129,13 +149,14 @@ export default class BrowserFrame implements IBrowserFrame { */ public async goto(url: string): Promise { await Promise.all(this.childFrames.map((frame) => frame.destroy())); + this._asyncTaskManager.abortAll(); - this.window.location.href = url; + this.url = url; const response = await this.window.fetch(url); const responseText = await response.text(); - this.window.document.write(responseText); + this.content = responseText; } } diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index ba3eef541..315f165a9 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -9,9 +9,9 @@ import IBrowserPage from './IBrowserPage.js'; export default interface IBrowserFrame { readonly childFrames: IBrowserFrame[]; readonly window: IWindow; - detached: boolean; content: string; url: string; + readonly parentFrame: IBrowserFrame | null; readonly _asyncTaskManager: AsyncTaskManager; readonly page: IBrowserPage | null; @@ -39,6 +39,13 @@ export default interface IBrowserFrame { */ setViewport(viewport: IBrowserPageViewport): void; + /** + * Creates a new frame. + * + * @returns Frame. + */ + newFrame(): IBrowserFrame; + /** * Go to a page. * diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index d9723b9b6..9443001c8 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -4,10 +4,11 @@ import IDocument from '../document/IDocument.js'; import HTMLElement from '../html-element/HTMLElement.js'; import INode from '../node/INode.js'; import IHTMLIFrameElement from './IHTMLIFrameElement.js'; -import HTMLIFrameUtility from './HTMLIFrameUtility.js'; import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import HTMLIFrameElementNamedNodeMap from './HTMLIFrameElementNamedNodeMap.js'; import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; +import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; +import HTMLIframePageLoader from './HTMLIframePageLoader.js'; /** * HTML Iframe Element. @@ -16,14 +17,32 @@ import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement. */ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFrameElement { - public override readonly attributes: INamedNodeMap = new HTMLIFrameElementNamedNodeMap(this); + public override readonly attributes: INamedNodeMap; // Events public onload: (event: Event) => void | null = null; public onerror: (event: Event) => void | null = null; // Internal properties - public _contentWindow: IWindow | ICrossOriginWindow | null = null; + #contentWindowContainer: { window: IWindow | ICrossOriginWindow | null } = { + window: null + }; + #pageLoader: HTMLIframePageLoader; + + /** + * Constructor. + * + * @param browserMainFrame Main browser frame. + */ + constructor(browserMainFrame: IBrowserFrame) { + super(); + this.#pageLoader = new HTMLIframePageLoader({ + element: this, + contentWindowContainer: this.#contentWindowContainer, + browserMainFrame + }); + this.attributes = new HTMLIFrameElementNamedNodeMap(this, this.#pageLoader); + } /** * Returns source. @@ -157,7 +176,7 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram * @returns Content document. */ public get contentDocument(): IDocument | null { - return (this._contentWindow)?.document || null; + return (this.#contentWindowContainer.window)?.document ?? null; } /** @@ -166,7 +185,7 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram * @returns Content window. */ public get contentWindow(): IWindow | ICrossOriginWindow | null { - return this._contentWindow; + return this.#contentWindowContainer.window; } /** @@ -179,7 +198,7 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram super._connectToNode(parentNode); if (isParentConnected && isConnected !== isParentConnected) { - HTMLIFrameUtility.loadPage(this); + this.#pageLoader.loadPage(); } } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts index 5dff9daf5..e85cd28ac 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts @@ -1,7 +1,7 @@ import IAttr from '../attr/IAttr.js'; +import Element from '../element/Element.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLIFrameElement from './HTMLIFrameElement.js'; -import HTMLIFrameUtility from './HTMLIFrameUtility.js'; +import HTMLIframePageLoader from './HTMLIframePageLoader.js'; /** * Named Node Map. @@ -9,7 +9,17 @@ import HTMLIFrameUtility from './HTMLIFrameUtility.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLIFrameElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected _ownerElement: HTMLIFrameElement; + #pageLoader: HTMLIframePageLoader; + + /** + * Constructor. + * + * @param ownerElement Owner element. + */ + constructor(ownerElement: Element, pageLoader: HTMLIframePageLoader) { + super(ownerElement); + this.#pageLoader = pageLoader; + } /** * @override @@ -18,7 +28,7 @@ export default class HTMLIFrameElementNamedNodeMap extends HTMLElementNamedNodeM const replacedAttribute = super.setNamedItem(item); if (item.name === 'src' && item.value && item.value !== replacedAttribute?.value) { - HTMLIFrameUtility.loadPage(this._ownerElement); + this.#pageLoader.loadPage(); } return replacedAttribute || null; diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts deleted file mode 100644 index 6246b0a35..000000000 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameUtility.ts +++ /dev/null @@ -1,96 +0,0 @@ -import URL from '../../url/URL.js'; -import Event from '../../event/Event.js'; -import ErrorEvent from '../../event/events/ErrorEvent.js'; -import IWindow from '../../window/IWindow.js'; -import CrossOriginWindow from '../../window/CrossOriginWindow.js'; -import HTMLIFrameElement from './HTMLIFrameElement.js'; -import WindowErrorUtility from '../../window/WindowErrorUtility.js'; -import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; - -/** - * HTML Iframe Utility. - */ -export default class HTMLIFrameUtility { - /** - * Loads an iframe page. - * - * @param element - */ - public static async loadPage(element: HTMLIFrameElement): Promise { - const browserSettings = WindowBrowserSettingsReader.getSettings( - element.ownerDocument._defaultView - ); - if (element.isConnected && !browserSettings.disableIframePageLoading) { - const src = element.src; - - if (src) { - // To avoid circular dependency, we use a reference to the window class instead of importing it. - const contentWindow = new element.ownerDocument['_windowClass']({ - url: new URL(src, element.ownerDocument._defaultView.location.href).href, - settings: { - ...browserSettings - } - }); - - (contentWindow.parent) = element.ownerDocument._defaultView; - (contentWindow.top) = element.ownerDocument._defaultView; - - if (src === 'about:blank') { - element._contentWindow = contentWindow; - return; - } - - if (src.startsWith('javascript:')) { - element._contentWindow = contentWindow; - if (!browserSettings.disableJavaScriptEvaluation) { - if (browserSettings.disableErrorCapturing) { - (element._contentWindow).eval(src.replace('javascript:', '')); - } else { - WindowErrorUtility.captureError(element, () => - (element._contentWindow).eval(src.replace('javascript:', '')) - ); - } - } - return; - } - - const originURL = element.ownerDocument._defaultView.location; - const targetURL = new URL(src, originURL); - const isCORS = - (originURL.hostname !== targetURL.hostname && - !originURL.hostname.endsWith(targetURL.hostname)) || - originURL.protocol !== targetURL.protocol; - - let responseText: string; - - element._contentWindow = null; - - try { - const response = await element.ownerDocument._defaultView.fetch(src); - responseText = await response.text(); - } catch (error) { - element.dispatchEvent( - new ErrorEvent('error', { - message: error.message, - error - }) - ); - element.ownerDocument._defaultView.dispatchEvent( - new ErrorEvent('error', { - message: error.message, - error - }) - ); - element.ownerDocument._defaultView.console.error(error); - return; - } - - element._contentWindow = isCORS - ? new CrossOriginWindow(element.ownerDocument._defaultView, contentWindow) - : contentWindow; - contentWindow.document.write(responseText); - element.dispatchEvent(new Event('load')); - } - } - } -} diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts new file mode 100644 index 000000000..0ac8f872a --- /dev/null +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts @@ -0,0 +1,112 @@ +import URL from '../../url/URL.js'; +import Event from '../../event/Event.js'; +import IWindow from '../../window/IWindow.js'; +import CrossOriginWindow from '../../window/CrossOriginWindow.js'; +import WindowErrorUtility from '../../window/WindowErrorUtility.js'; +import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; +import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; +import IHTMLIFrameElement from './IHTMLIFrameElement.js'; + +/** + * HTML Iframe page loader. + */ +export default class HTMLIframePageLoader { + #element: IHTMLIFrameElement; + #contentWindowContainer: { window: IWindow | ICrossOriginWindow | null }; + #browserMainFrame: IBrowserFrame; + #browserFrame: IBrowserFrame; + + /** + * Constructor. + * + * @param options Options. + * @param options.element Iframe element. + * @param options.contentWindowContainer Content window container. + * @param options.browserMainFrame Main browser frame. + */ + constructor(options: { + element: IHTMLIFrameElement; + contentWindowContainer: { window: IWindow | ICrossOriginWindow | null }; + browserMainFrame: IBrowserFrame; + }) { + this.#element = options.element; + this.#contentWindowContainer = options.contentWindowContainer; + this.#browserMainFrame = options.browserMainFrame; + } + + /** + * Loads an iframe page. + */ + public async loadPage(): Promise { + const url = this.#element.src; + + if (this.#browserFrame && url !== this.#browserFrame.url) { + this.#browserFrame.destroy(); + this.#browserFrame = null; + } + + this.#contentWindowContainer.window = null; + + if (!url) { + return; + } + + if (!this.#browserMainFrame.page.context.browser.settings.disableIframePageLoading) { + const window = this.#element.ownerDocument._defaultView; + + if (url === 'about:blank') { + this.#browserFrame = this.#browserMainFrame.newFrame(); + (this.#browserFrame.window.parent) = window; + (this.#browserFrame.window.top) = window; + this.#contentWindowContainer.window = this.#browserFrame.window; + return; + } + + if (url.startsWith('javascript:')) { + if (!this.#browserMainFrame.page.context.browser.settings.disableJavaScriptEvaluation) { + if (this.#browserMainFrame.page.context.browser.settings.disableErrorCapturing) { + this.#browserFrame.window.eval(url.replace('javascript:', '')); + } else { + WindowErrorUtility.captureError(this.#browserFrame.window, () => + this.#browserFrame.window.eval(url.replace('javascript:', '')) + ); + } + } + this.#browserFrame = this.#browserMainFrame.newFrame(); + (this.#browserFrame.window.parent) = window; + (this.#browserFrame.window.top) = window; + this.#contentWindowContainer.window = this.#browserFrame.window; + return; + } + + const originURL = window.location; + const targetURL = new URL(url, originURL); + const isCORS = + (originURL.hostname !== targetURL.hostname && + !originURL.hostname.endsWith(targetURL.hostname)) || + originURL.protocol !== targetURL.protocol; + let responseText: string; + + try { + const response = await window.fetch(url); + responseText = await response.text(); + } catch (error) { + WindowErrorUtility.dispatchError(this.#element, error); + return; + } + + this.#browserFrame = this.#browserMainFrame.newFrame(); + (this.#browserFrame.window.parent) = window; + (this.#browserFrame.window.top) = window; + + this.#browserFrame.url = url; + this.#browserFrame.content = responseText; + + this.#contentWindowContainer.window = isCORS + ? new CrossOriginWindow(window, this.#browserFrame.window) + : this.#browserFrame.window; + + this.#element.dispatchEvent(new Event('load')); + } + } +} diff --git a/packages/happy-dom/src/window/CrossOriginWindow.ts b/packages/happy-dom/src/window/CrossOriginWindow.ts index 576f9b6d8..38b820233 100644 --- a/packages/happy-dom/src/window/CrossOriginWindow.ts +++ b/packages/happy-dom/src/window/CrossOriginWindow.ts @@ -14,8 +14,7 @@ export default class CrossOriginWindow extends EventTarget implements ICrossOrig public readonly parent: IWindow; public readonly top: IWindow; public readonly location: Location; - - private _targetWindow: IWindow; + #targetWindow: IWindow; /** * Constructor. @@ -45,7 +44,7 @@ export default class CrossOriginWindow extends EventTarget implements ICrossOrig } } ); - this._targetWindow = target; + this.#targetWindow = target; } /** @@ -56,6 +55,6 @@ export default class CrossOriginWindow extends EventTarget implements ICrossOrig * @param transfer Transfer. Not implemented. */ public postMessage(message: unknown, targetOrigin = '*', transfer?: unknown[]): void { - this._targetWindow.postMessage(message, targetOrigin, transfer); + this.#targetWindow.postMessage(message, targetOrigin, transfer); } } diff --git a/packages/happy-dom/src/window/WindowClassFactory.ts b/packages/happy-dom/src/window/WindowClassFactory.ts index 8c9edf1b9..a6e6140de 100644 --- a/packages/happy-dom/src/window/WindowClassFactory.ts +++ b/packages/happy-dom/src/window/WindowClassFactory.ts @@ -327,6 +327,9 @@ export default class WindowClassFactory { } } class HTMLIFrameElement extends HTMLIFrameElementImplementation { + constructor() { + super(properties.browserFrame); + } public get ownerDocument(): IDocument { return window.document; } From c911169e9b1da1b9f2c5a53642ad761722e535ef Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 25 Oct 2023 01:41:14 +0200 Subject: [PATCH 15/63] #466@trivial: Continues on implementation. --- packages/happy-dom/src/browser/Browser.ts | 33 ++++++++++++++++--- .../detached-browser/DetachedBrowser.ts | 31 ++++++++++++++--- .../happy-dom/src/browser/types/IBrowser.ts | 7 ++++ 3 files changed, 61 insertions(+), 10 deletions(-) diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index b5d2a1c65..19f91c8e4 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -9,7 +9,6 @@ import IBrowser from './types/IBrowser.js'; * Browser context. */ export default class Browser implements IBrowser { - public readonly defaultContext: BrowserContext; public readonly contexts: BrowserContext[]; public readonly settings: IBrowserSettings; public readonly console: Console | null; @@ -24,8 +23,19 @@ export default class Browser implements IBrowser { constructor(options?: { settings?: IOptionalBrowserSettings; console?: Console }) { this.console = options?.console || null; this.settings = BrowserSettingsFactory.getSettings(options?.settings); - this.defaultContext = new BrowserContext(this); - this.contexts = [this.defaultContext]; + this.contexts = [new BrowserContext(this)]; + } + + /** + * Returns the default context. + * + * @returns Default context. + */ + public get defaultContext(): BrowserContext { + if (this.contexts.length === 0) { + throw new Error('No default context. The browser has been closed.'); + } + return this.contexts[0]; } /** @@ -36,7 +46,6 @@ export default class Browser implements IBrowser { context.close(); } (this.contexts) = []; - (this.defaultContext) = null; } /** @@ -57,12 +66,26 @@ export default class Browser implements IBrowser { } } + /** + * Creates a new incognito context. + * + * @returns Context. + */ + public newIncognitoContext(): BrowserContext { + const context = new BrowserContext(this); + this.contexts.push(context); + return context; + } + /** * Creates a new page. * * @returns Page. */ public newPage(): BrowserPage { - return this.defaultContext.newPage(); + if (this.contexts.length === 0) { + throw new Error('No default context. The browser has been closed.'); + } + return this.contexts[0].newPage(); } } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts index 2855e0387..d05c9b151 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts @@ -10,7 +10,6 @@ import IWindow from '../../window/IWindow.js'; * Detached browser. */ export default class DetachedBrowser implements IBrowser { - public readonly defaultContext: DetachedBrowserContext; public readonly contexts: DetachedBrowserContext[]; public readonly settings: IBrowserSettings; public readonly console: Console | null; @@ -29,8 +28,19 @@ export default class DetachedBrowser implements IBrowser { ) { this.console = options?.console || null; this.settings = BrowserSettingsFactory.getSettings(options?.settings); - this.defaultContext = new DetachedBrowserContext(window, this); - this.contexts = [this.defaultContext]; + this.contexts = [new DetachedBrowserContext(window, this)]; + } + + /** + * Returns the default context. + * + * @returns Default context. + */ + public get defaultContext(): DetachedBrowserContext { + if (this.contexts.length === 0) { + throw new Error('No default context. The browser has been closed.'); + } + return this.contexts[0]; } /** @@ -41,7 +51,6 @@ export default class DetachedBrowser implements IBrowser { context.close(); } (this.contexts) = []; - (this.defaultContext) = null; } /** @@ -62,12 +71,24 @@ export default class DetachedBrowser implements IBrowser { } } + /** + * Creates a new incognito context. + * + * @returns Context. + */ + public newIncognitoContext(): DetachedBrowserContext { + throw new Error('Not possible to create a new context on a detached browser.'); + } + /** * Creates a new page. * * @returns Page. */ public newPage(): DetachedBrowserPage { - return this.defaultContext.newPage(); + if (this.contexts.length === 0) { + throw new Error('No default context. The browser has been closed.'); + } + return this.contexts[0].newPage(); } } diff --git a/packages/happy-dom/src/browser/types/IBrowser.ts b/packages/happy-dom/src/browser/types/IBrowser.ts index 5088de404..91113ef0b 100644 --- a/packages/happy-dom/src/browser/types/IBrowser.ts +++ b/packages/happy-dom/src/browser/types/IBrowser.ts @@ -28,6 +28,13 @@ export default interface IBrowser { */ abort(): void; + /** + * Creates a new incognito context. + * + * @returns Context. + */ + newIncognitoContext(): IBrowserContext; + /** * Creates a new page. * From 8ad1a0d84e8805384343a52a61414ada8c7fa9b0 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 27 Oct 2023 15:38:14 +0200 Subject: [PATCH 16/63] #466@trivial: Continues on implementation. --- .../happy-dom/src/browser/BrowserFrame.ts | 24 ++- .../detached-browser/DetachedBrowser.ts | 6 +- .../DetachedBrowserContext.ts | 9 +- .../detached-browser/DetachedBrowserFrame.ts | 18 +- .../detached-browser/DetachedBrowserPage.ts | 5 +- packages/happy-dom/src/location/Location.ts | 29 ++- .../html-anchor-element/HTMLAnchorElement.ts | 26 +++ .../HTMLIframePageLoader.ts | 61 ++++-- .../happy-dom/src/window/CrossOriginWindow.ts | 10 +- .../src/window/ICrossOriginWindow.ts | 4 +- packages/happy-dom/src/window/IWindow.ts | 6 +- packages/happy-dom/src/window/Window.ts | 30 ++- .../happy-dom/src/window/WindowPageOpener.ts | 178 ++++++++++++++++ .../src/window/__BrowserContextLoader.ts | 191 ------------------ 14 files changed, 344 insertions(+), 253 deletions(-) create mode 100644 packages/happy-dom/src/window/WindowPageOpener.ts delete mode 100644 packages/happy-dom/src/window/__BrowserContextLoader.ts diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 68bcb0703..02aa1d463 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -1,4 +1,3 @@ -import IWindow from '../window/IWindow.js'; import BrowserPage from './BrowserPage.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import IBrowserFrame from './types/IBrowserFrame.js'; @@ -7,6 +6,7 @@ import IBrowserPageViewport from './types/IBrowserPageViewport.js'; import Event from '../event/Event.js'; import Location from '../location/Location.js'; import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; +import WindowErrorUtility from '../window/WindowErrorUtility.js'; /** * Browser frame. @@ -15,7 +15,7 @@ export default class BrowserFrame implements IBrowserFrame { public readonly childFrames: BrowserFrame[] = []; public readonly parentFrame: BrowserFrame | null = null; public readonly page: BrowserPage; - public readonly window: IWindow; + public readonly window: Window; public _asyncTaskManager = new AsyncTaskManager(); /** @@ -106,7 +106,7 @@ export default class BrowserFrame implements IBrowserFrame { this._asyncTaskManager.destroy(); WindowBrowserSettingsReader.removeSettings(this.window); (this.page) = null; - (this.window) = null; + (this.window) = null; } /** @@ -157,15 +157,27 @@ export default class BrowserFrame implements IBrowserFrame { this._asyncTaskManager.destroy(); - (this.window) = new Window({ + (this.window) = new Window({ url, browserFrame: this, console: this.page.console }); - const response = await this.window.fetch(url); - const responseText = await response.text(); + this.window._readyStateManager.startTask(); + + let responseText: string; + + try { + const response = await this.window.fetch(url); + responseText = await response.text(); + } catch (error) { + responseText = error.toString(); + this.window._readyStateManager.endTask(); + WindowErrorUtility.dispatchError(this.window, error); + return; + } this.window.document.write(responseText); + this.window._readyStateManager.endTask(); } } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts index d05c9b151..d1deeb8d8 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts @@ -17,18 +17,20 @@ export default class DetachedBrowser implements IBrowser { /** * Constructor. * + * @param windowClass Window class. * @param window Window. * @param [options] Options. * @param [options.settings] Browser settings. * @param [options.console] Console. */ constructor( + windowClass: new () => IWindow, window: IWindow, options?: { settings?: IOptionalBrowserSettings; console?: Console } ) { this.console = options?.console || null; this.settings = BrowserSettingsFactory.getSettings(options?.settings); - this.contexts = [new DetachedBrowserContext(window, this)]; + this.contexts = [new DetachedBrowserContext(windowClass, window, this)]; } /** @@ -73,8 +75,6 @@ export default class DetachedBrowser implements IBrowser { /** * Creates a new incognito context. - * - * @returns Context. */ public newIncognitoContext(): DetachedBrowserContext { throw new Error('Not possible to create a new context on a detached browser.'); diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts index 3170295a3..7bcef09d2 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts @@ -14,13 +14,14 @@ export default class DetachedBrowserContext implements IBrowserContext { /** * Constructor. * + * @param windowClass Window class. * @param window Window. * @param browser Browser. */ - constructor(window: IWindow, browser: DetachedBrowser) { + constructor(windowClass: new () => IWindow, window: IWindow, browser: DetachedBrowser) { + this.#windowClass = windowClass; this.browser = browser; - this.pages = [new DetachedBrowserPage(window, this)]; - this.#windowClass = IWindow>window.constructor; + this.pages = [new DetachedBrowserPage(windowClass, window, this)]; } /** @@ -56,7 +57,7 @@ export default class DetachedBrowserContext implements IBrowserContext { * @returns Page. */ public newPage(): DetachedBrowserPage { - const page = new DetachedBrowserPage(new this.#windowClass(), this); + const page = new DetachedBrowserPage(this.#windowClass, new this.#windowClass(), this); this.pages.push(page); return page; } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index b347ca35a..9bdbae2a4 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -6,6 +6,7 @@ import IBrowserPageViewport from '../types/IBrowserPageViewport.js'; import Event from '../../event/Event.js'; import Location from '../../location/Location.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; +import DocumentReadyStateManager from '../../nodes/document/DocumentReadyStateManager.js'; /** * Browser frame. @@ -16,14 +17,17 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public readonly page: DetachedBrowserPage; public readonly window: IWindow; public _asyncTaskManager = new AsyncTaskManager(); + #windowClass: new () => IWindow; /** * Constructor. * + * @param windowClass Window class. * @param window Window. * @param page Page. */ - constructor(window: IWindow, page: DetachedBrowserPage) { + constructor(windowClass: new () => IWindow, window: IWindow, page: DetachedBrowserPage) { + this.#windowClass = windowClass; this.window = window; this.page = page; } @@ -77,8 +81,6 @@ export default class DetachedBrowserFrame implements IBrowserFrame { /** * Aborts all ongoing operations. - * - * @returns Promise. */ public abort(): void { for (const frame of this.childFrames) { @@ -136,7 +138,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @returns Frame. */ public newFrame(): IBrowserFrame { - const frame = new DetachedBrowserFrame(this.window, this.page); + const frame = new DetachedBrowserFrame(this.#windowClass, new this.#windowClass(), this.page); (frame.parentFrame) = this; this.childFrames.push(frame); return frame; @@ -152,11 +154,19 @@ export default class DetachedBrowserFrame implements IBrowserFrame { this._asyncTaskManager.abortAll(); + const readyStateManager = new DocumentReadyStateManager(this.window); + (<{ _readyStateManager: DocumentReadyStateManager }>(this.window))._readyStateManager = + readyStateManager; + + readyStateManager.startTask(); + this.url = url; const response = await this.window.fetch(url); const responseText = await response.text(); + readyStateManager.endTask(); + this.content = responseText; } } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts index 7a75ae394..bbc617c52 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts @@ -18,13 +18,14 @@ export default class DetachedBrowserPage implements IBrowserPage { /** * Constructor. * + * @param windowClass Window class. * @param window Window. * @param context Browser context. */ - constructor(window: IWindow, context: DetachedBrowserContext) { + constructor(windowClass: new () => IWindow, window: IWindow, context: DetachedBrowserContext) { this.context = context; this.console = context.browser.console ?? new VirtualConsole(this.virtualConsolePrinter); - this.mainFrame = new DetachedBrowserFrame(window, this); + this.mainFrame = new DetachedBrowserFrame(windowClass, window, this); } /** diff --git a/packages/happy-dom/src/location/Location.ts b/packages/happy-dom/src/location/Location.ts index 5c9ad150d..b9ce06287 100644 --- a/packages/happy-dom/src/location/Location.ts +++ b/packages/happy-dom/src/location/Location.ts @@ -1,18 +1,24 @@ import URL from '../url/URL.js'; import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; +import IBrowserFrame from '../browser/types/IBrowserFrame.js'; +import DetachedBrowserFrame from '../browser/detached-browser/DetachedBrowserFrame.js'; +import WindowErrorUtility from '../window/WindowErrorUtility.js'; /** * */ export default class Location extends URL { + #browserFrame: IBrowserFrame | null; + /** * Constructor. * * @param [url] URL. */ - constructor(url = 'about:blank') { + constructor(url = 'about:blank', browserFrame?: IBrowserFrame) { super(url); + this.#browserFrame = browserFrame ?? null; } /** @@ -20,6 +26,22 @@ export default class Location extends URL { */ // @ts-ignore public set href(value: string) { + if (value.startsWith('javascript:')) { + if ( + this.#browserFrame && + !this.#browserFrame.page.context.browser.settings.disableJavaScriptEvaluation + ) { + if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { + this.#browserFrame.window.eval(value.replace('javascript:', '')); + } else { + WindowErrorUtility.captureError(this.#browserFrame.window, () => + this.#browserFrame.window.eval(value.replace('javascript:', '')) + ); + } + } + return; + } + try { super.href = this.hostname ? new URL(value, this).href : value; } catch (e) { @@ -35,6 +57,11 @@ export default class Location extends URL { ); } } + + // When using the Window instance directly and not via the Browser API we should not navigate the browser frame. + if (!(this.#browserFrame instanceof DetachedBrowserFrame)) { + this.#browserFrame?.goto(value); + } } /** diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts index e68d81f98..4e37b6ac9 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -6,6 +6,8 @@ import URL from '../../url/URL.js'; import HTMLAnchorElementUtility from './HTMLAnchorElementUtility.js'; import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import HTMLAnchorElementNamedNodeMap from './HTMLAnchorElementNamedNodeMap.js'; +import Event from '../../event/Event.js'; +import EventPhaseEnum from '../../event/EventPhaseEnum.js'; /** * HTML Anchor Element. @@ -415,4 +417,28 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho public override toString(): string { return this.href; } + + /** + * @override + */ + public override dispatchEvent(event: Event): boolean { + const returnValue = super.dispatchEvent(event); + + if ( + event.type === 'click' && + (event.eventPhase === EventPhaseEnum.atTarget || + event.eventPhase === EventPhaseEnum.bubbling) && + this._formNode && + this.isConnected && + !event.defaultPrevented + ) { + const href = this.href; + if (href) { + // TODO: Add support for "target", "download", "rel", "hreflang", "type", "referrerpolicy", "ping", "referrerpolicy", "relList". + this.ownerDocument._defaultView.location.href = this.href; + } + } + + return returnValue; + } } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts index 0ac8f872a..2dcfd7988 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts @@ -6,6 +6,8 @@ import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; import IHTMLIFrameElement from './IHTMLIFrameElement.js'; +import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; +import IResponse from '../../fetch/types/IResponse.js'; /** * HTML Iframe page loader. @@ -23,6 +25,7 @@ export default class HTMLIframePageLoader { * @param options.element Iframe element. * @param options.contentWindowContainer Content window container. * @param options.browserMainFrame Main browser frame. + * @param options.contentWindowContainer.window Content window. */ constructor(options: { element: IHTMLIFrameElement; @@ -37,7 +40,7 @@ export default class HTMLIframePageLoader { /** * Loads an iframe page. */ - public async loadPage(): Promise { + public loadPage(): void { const url = this.#element.src; if (this.#browserFrame && url !== this.#browserFrame.url) { @@ -81,30 +84,46 @@ export default class HTMLIframePageLoader { const originURL = window.location; const targetURL = new URL(url, originURL); - const isCORS = - (originURL.hostname !== targetURL.hostname && - !originURL.hostname.endsWith(targetURL.hostname)) || - originURL.protocol !== targetURL.protocol; - let responseText: string; - - try { - const response = await window.fetch(url); - responseText = await response.text(); - } catch (error) { - WindowErrorUtility.dispatchError(this.#element, error); - return; - } - this.#browserFrame = this.#browserMainFrame.newFrame(); - (this.#browserFrame.window.parent) = window; - (this.#browserFrame.window.top) = window; + // Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different. + const isSameOrigin = originURL.origin !== targetURL.origin; + this.#browserFrame = this.#browserMainFrame.newFrame(); this.#browserFrame.url = url; - this.#browserFrame.content = responseText; + const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( + (this.#browserMainFrame.window) + ))._readyStateManager; + + readyStateManager.startTask(); + + this.#browserFrame.window + .fetch(url) + .then((response) => { + const xFrameOptions = response.headers.get('X-Frame-Options')?.toLowerCase(); + if (xFrameOptions === 'deny' || (xFrameOptions === 'sameorigin' && !isSameOrigin)) { + throw new Error( + `Refused to display '${url}' in a frame because it set 'X-Frame-Options' to '${xFrameOptions}'.` + ); + } + return response; + }) + .then((response: IResponse) => response.text()) + .then((responseText: string) => { + this.#browserFrame.content = responseText; + readyStateManager.endTask(); + }) + .catch((error) => { + readyStateManager.endTask(); + WindowErrorUtility.dispatchError(this.#element, error); + }); + + const parentWindow = isSameOrigin ? window : new CrossOriginWindow(window); + (this.#browserFrame.window.parent) = parentWindow; + (this.#browserFrame.window.top) = parentWindow; - this.#contentWindowContainer.window = isCORS - ? new CrossOriginWindow(window, this.#browserFrame.window) - : this.#browserFrame.window; + this.#contentWindowContainer.window = isSameOrigin + ? this.#browserFrame.window + : new CrossOriginWindow(this.#browserFrame.window, window); this.#element.dispatchEvent(new Event('load')); } diff --git a/packages/happy-dom/src/window/CrossOriginWindow.ts b/packages/happy-dom/src/window/CrossOriginWindow.ts index 38b820233..559a8ac00 100644 --- a/packages/happy-dom/src/window/CrossOriginWindow.ts +++ b/packages/happy-dom/src/window/CrossOriginWindow.ts @@ -11,21 +11,21 @@ import ICrossOriginWindow from './ICrossOriginWindow.js'; export default class CrossOriginWindow extends EventTarget implements ICrossOriginWindow { public readonly self = this; public readonly window = this; - public readonly parent: IWindow; - public readonly top: IWindow; + public readonly parent: IWindow | ICrossOriginWindow; + public readonly top: IWindow | ICrossOriginWindow; public readonly location: Location; #targetWindow: IWindow; /** * Constructor. * - * @param parent Parent window. * @param target Target window. + * @param [parent] Parent window. */ - constructor(parent: IWindow, target: IWindow) { + constructor(target: IWindow, parent?: IWindow) { super(); - this.parent = parent; + this.parent = parent ?? this; this.top = parent; this.location = new Proxy( {}, diff --git a/packages/happy-dom/src/window/ICrossOriginWindow.ts b/packages/happy-dom/src/window/ICrossOriginWindow.ts index acfcdf2ad..e91fd763e 100644 --- a/packages/happy-dom/src/window/ICrossOriginWindow.ts +++ b/packages/happy-dom/src/window/ICrossOriginWindow.ts @@ -8,8 +8,8 @@ import IEventTarget from '../event/IEventTarget.js'; export default interface ICrossOriginWindow extends IEventTarget { readonly self: ICrossOriginWindow; readonly window: ICrossOriginWindow; - readonly parent: IWindow; - readonly top: IWindow; + readonly parent: IWindow | ICrossOriginWindow; + readonly top: IWindow | ICrossOriginWindow; readonly location: Location; /** diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index 54b53c1b5..36f94a2c7 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -382,9 +382,9 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly navigator: Navigator; readonly console: Console; readonly self: IWindow; - readonly top: IWindow; - readonly opener: IWindow | null; - readonly parent: IWindow; + readonly top: IWindow | ICrossOriginWindow; + readonly opener: IWindow | ICrossOriginWindow | null; + readonly parent: IWindow | ICrossOriginWindow; readonly window: IWindow; readonly globalThis: IWindow; readonly name: string; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 77d8882c9..9b434bce9 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -147,6 +147,7 @@ import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; import DetachedBrowser from '../browser/detached-browser/DetachedBrowser.js'; +import WindowPageOpener from './WindowPageOpener.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -399,10 +400,10 @@ export default class Window extends EventTarget implements IWindow { public readonly location: Location; public readonly history: History; public readonly navigator: Navigator; - public readonly opener: IWindow | null = null; + public readonly opener: IWindow | ICrossOriginWindow | null = null; public readonly self: IWindow = this; - public readonly top: IWindow = this; - public readonly parent: IWindow = this; + public readonly top: IWindow | ICrossOriginWindow = this; + public readonly parent: IWindow | ICrossOriginWindow = this; public readonly window: IWindow = this; public readonly globalThis: IWindow = this; public readonly name: string = ''; @@ -496,6 +497,7 @@ export default class Window extends EventTarget implements IWindow { private _queueMicrotask: (callback: Function) => void; public readonly _readyStateManager = new DocumentReadyStateManager(this); #browserFrame: IBrowserFrame; + #windowPageOpener: WindowPageOpener; /** * Constructor. @@ -533,7 +535,7 @@ export default class Window extends EventTarget implements IWindow { if (options?.browserFrame) { this.#browserFrame = options.browserFrame; } else { - this.#browserFrame = new DetachedBrowser(this, { + this.#browserFrame = new DetachedBrowser(Window, this, { console: options?.console, settings: options?.settings }).defaultContext.pages[0].mainFrame; @@ -541,6 +543,7 @@ export default class Window extends EventTarget implements IWindow { WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); + this.#windowPageOpener = new WindowPageOpener(this.#browserFrame); this.happyDOM = new HappyDOMWindowAPI({ window: this, browserFrame: this.#browserFrame @@ -844,19 +847,24 @@ export default class Window extends EventTarget implements IWindow { /** * Loads a specified resource into a new or existing browsing context (that is, a tab, a window, or an iframe) under a specified name. * - * @param [_url] URL. - * @param [_target] Target. - * @param [_features] Window features. + * @param [url] URL. + * @param [target] Target. + * @param [features] Window features. + * @returns Window. */ public open( - _url?: string, - _target?: string, - _features?: string + url?: string, + target?: string, + features?: string ): IWindow | ICrossOriginWindow | null { if (this.#browserFrame.page.context.browser.settings.disableWindowOpenPageLoading) { return null; } - return null; + return this.#windowPageOpener.openPage({ + url, + target, + features + }); } /** diff --git a/packages/happy-dom/src/window/WindowPageOpener.ts b/packages/happy-dom/src/window/WindowPageOpener.ts new file mode 100644 index 000000000..600655b34 --- /dev/null +++ b/packages/happy-dom/src/window/WindowPageOpener.ts @@ -0,0 +1,178 @@ +import { URL } from 'url'; +import IWindow from './IWindow.js'; +import CrossOriginWindow from './CrossOriginWindow.js'; +import WindowErrorUtility from './WindowErrorUtility.js'; +import Window from './Window.js'; +import IBrowserFrame from '../browser/types/IBrowserFrame.js'; +import FetchCORSUtility from '../fetch/utilities/FetchCORSUtility.js'; +import ICrossOriginWindow from './ICrossOriginWindow.js'; + +/** + * Window page open handler. + */ +export default class WindowPageOpener { + #browserFrame: IBrowserFrame; + + /** + * Constructor. + * + * @param browserFrame Browser frame. + */ + constructor(browserFrame: IBrowserFrame) { + this.#browserFrame = browserFrame; + } + + /** + * Opens a page. + * + * @param [options] Options. + * @param [options.url] URL. + * @param [options.target] Target. + * @param [options.features] Window features. + */ + public openPage(options?: { + url?: string; + target?: string; + features?: string; + }): IWindow | ICrossOriginWindow | null { + const features = this.getWindowFeatures(options?.features || ''); + const url = options?.url || 'about:blank'; + const target = options?.target !== undefined ? String(options.target) : null; + const newPage = this.#browserFrame.page.context.newPage(); + const newWindow = newPage.mainFrame.window; + const originURL = this.#browserFrame.window.location; + const targetURL = new URL(url, originURL); + const isCORS = FetchCORSUtility.isCORS(originURL, targetURL); + + newPage.mainFrame.url = url; + + (newWindow.document.referrer) = !features.noreferrer ? this.#browserFrame.url : ''; + + if (!features.noopener) { + (newWindow.opener) = isCORS + ? new CrossOriginWindow(this.#browserFrame.window) + : this.#browserFrame.window; + } + + if (target) { + (newWindow.name) = target; + } + + if (features?.left) { + (newWindow.screenLeft) = features.left; + (newWindow.screenX) = features.left; + } + + if (features?.top) { + (newWindow.screenTop) = features.top; + (newWindow.screenY) = features.top; + } + + if (url === 'about:blank') { + return features.noopener ? null : newWindow; + } + + if (url.startsWith('javascript:')) { + if (!this.#browserFrame.page.context.browser.settings.disableJavaScriptEvaluation) { + if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { + newWindow.eval(url.replace('javascript:', '')); + } else { + WindowErrorUtility.captureError(newWindow, () => + newWindow.eval(url.replace('javascript:', '')) + ); + } + } + return features.noopener ? null : newWindow; + } + + newWindow._readyStateManager.startTask(); + + newWindow + .fetch(url, { + referrer: features.noreferrer ? 'no-referrer' : undefined + }) + .then((response) => response.text()) + .then((responseText) => { + newPage.mainFrame.content = responseText; + newWindow._readyStateManager.endTask(); + }) + .catch((error) => { + WindowErrorUtility.dispatchError(newWindow, error); + newWindow._readyStateManager.endTask(); + }); + + if (features.noopener) { + return null; + } + + return isCORS ? new CrossOriginWindow(newWindow, this.#browserFrame.window) : newWindow; + } + + /** + * Returns window features. + * + * @param features Window features string. + * @returns Window features. + */ + private getWindowFeatures(features: string): { + popup: boolean; + width: number; + height: number; + left: number; + top: number; + noopener: boolean; + noreferrer: boolean; + } { + const parts = features.split(','); + const result: { + popup: boolean; + width: number; + height: number; + left: number; + top: number; + noopener: boolean; + noreferrer: boolean; + } = { + popup: false, + width: 0, + height: 0, + left: 0, + top: 0, + noopener: false, + noreferrer: false + }; + + for (const part of parts) { + const [key, value] = part.split('='); + switch (key) { + case 'popup': + result.popup = value === 'yes' || value === '1' || value === 'true'; + break; + case 'width': + case 'innerWidth': + result.width = parseInt(value, 10); + break; + case 'height': + case 'innerHeight': + result.height = parseInt(value, 10); + break; + case 'left': + case 'screenX': + result.left = parseInt(value, 10); + break; + case 'top': + case 'screenY': + result.top = parseInt(value, 10); + break; + case 'noopener': + result.noopener = true; + break; + case 'noreferrer': + result.noreferrer = true; + break; + } + } + + return result; + } +} diff --git a/packages/happy-dom/src/window/__BrowserContextLoader.ts b/packages/happy-dom/src/window/__BrowserContextLoader.ts deleted file mode 100644 index 121527f1d..000000000 --- a/packages/happy-dom/src/window/__BrowserContextLoader.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { URL } from 'url'; -import IWindow from './IWindow.js'; -import CrossOriginWindow from './CrossOriginWindow.js'; -import WindowErrorUtility from './WindowErrorUtility.js'; -import ICrossOriginWindow from './ICrossOriginWindow.js'; -import IHTMLElement from '../nodes/html-element/IHTMLElement.js'; -import Window from './Window.js'; -import DetachedWindowAPI from './HappyDOMWindowAPI.js'; - -/** - * Browser context. - */ -export default class __BrowserContextLoader { - /** - * Creates a new browser context for an iframe or a new window using Window.open(). - * - * @param ownerWindow Owner window. - * @param [options] Options. - * @param [options.url] URL. - * @param [options.target] Target. - * @param [options.features] Window features. - * @param [options.ownerIframeElement] Owner iframe element. - */ - public static getBrowserContext( - ownerWindow: IWindow, - options?: { - url?: string; - target?: string; - features?: string; - ownerIframeElement?: IHTMLElement; - } - ): IWindow | ICrossOriginWindow | null { - const features = this.getWindowFeatures(options?.features || ''); - const contentWindow = new (ownerWindow.constructor)({ - url: options?.url ? new URL(options.url, ownerWindow.location.href).href : null, - console: ownerWindow.console, - width: features.width || undefined, - height: features.height || undefined, - settings: { - ...ownerWindow.happyDOM.settings - } - }); - const url = options?.url || 'about:blank'; - const target = options?.target !== undefined ? String(options.target) : null; - - (contentWindow.happyDOM) = contentWindow.happyDOM; - - (contentWindow.document.referrer) = !features.noreferrer - ? ownerWindow.location.href - : ''; - if (!features.noopener) { - (contentWindow.opener) = ownerWindow; - (contentWindow.parent) = ownerWindow; - (contentWindow.top) = ownerWindow; - } - - if (target) { - (contentWindow.name) = target; - } - - if (features?.left) { - (contentWindow.screenLeft) = features.left; - (contentWindow.screenX) = features.left; - } - - if (features?.top) { - (contentWindow.screenTop) = features.top; - (contentWindow.screenY) = features.top; - } - - if (url === 'about:blank') { - return features.noopener ? null : contentWindow; - } - - if (url.startsWith('javascript:')) { - if (!ownerWindow.happyDOM.settings.disableJavaScriptEvaluation) { - if (ownerWindow.happyDOM.settings.disableErrorCapturing) { - contentWindow.eval(url.replace('javascript:', '')); - } else { - WindowErrorUtility.captureError(ownerWindow, () => - contentWindow.eval(url.replace('javascript:', '')) - ); - } - } - return features.noopener ? null : contentWindow; - } - - const originURL = ownerWindow.location; - const targetURL = new URL(url, originURL); - const isCORS = - (originURL.hostname !== targetURL.hostname && - !originURL.hostname.endsWith(targetURL.hostname)) || - originURL.protocol !== targetURL.protocol; - - (contentWindow)._readyStateManager.startTask(); - - ownerWindow - .fetch(url, { - referrer: features.noreferrer ? 'no-referrer' : undefined - }) - .then((response) => response.text()) - .then((responseText) => { - contentWindow.document.write(responseText); - (contentWindow)._readyStateManager.endTask(); - }) - .catch((error) => { - WindowErrorUtility.dispatchError( - options?.ownerIframeElement ? options.ownerIframeElement : ownerWindow, - error - ); - (contentWindow)._readyStateManager.endTask(); - if (!ownerWindow.happyDOM.settings.disableErrorCapturing) { - throw error; - } - }); - - if (features.noopener) { - return null; - } - - return isCORS ? new CrossOriginWindow(ownerWindow, contentWindow) : contentWindow; - } - - /** - * Returns window features. - * - * @param features Window features string. - * @returns Window features. - */ - private static getWindowFeatures(features: string): { - popup: boolean; - width: number; - height: number; - left: number; - top: number; - noopener: boolean; - noreferrer: boolean; - } { - const parts = features.split(','); - const result: { - popup: boolean; - width: number; - height: number; - left: number; - top: number; - noopener: boolean; - noreferrer: boolean; - } = { - popup: false, - width: 0, - height: 0, - left: 0, - top: 0, - noopener: false, - noreferrer: false - }; - - for (const part of parts) { - const [key, value] = part.split('='); - switch (key) { - case 'popup': - result.popup = value === 'yes' || value === '1' || value === 'true'; - break; - case 'width': - case 'innerWidth': - result.width = parseInt(value, 10); - break; - case 'height': - case 'innerHeight': - result.height = parseInt(value, 10); - break; - case 'left': - case 'screenX': - result.left = parseInt(value, 10); - break; - case 'top': - case 'screenY': - result.top = parseInt(value, 10); - break; - case 'noopener': - result.noopener = true; - break; - case 'noreferrer': - result.noreferrer = true; - break; - } - } - - return result; - } -} From f1b7dd2c9e1c35207d751e44888ab4c53c235451 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Sat, 28 Oct 2023 17:06:24 +0200 Subject: [PATCH 17/63] #466@trivial: Continues on implementation. --- .../happy-dom/src/browser/BrowserFrame.ts | 12 +- .../detached-browser/DetachedBrowserFrame.ts | 22 ++- .../src/browser/types/IBrowserFrame.ts | 3 +- .../html-iframe-element/HTMLIFrameElement.ts | 20 +- .../HTMLIFrameElementNamedNodeMap.ts | 6 +- .../HTMLIFrameElementPageLoader.ts | 162 ++++++++++++++++ .../HTMLIframePageLoader.ts | 131 ------------- .../happy-dom/src/window/CrossOriginWindow.ts | 39 ++++ .../happy-dom/src/window/HappyDOMWindowAPI.ts | 22 ++- .../src/window/ICrossOriginWindow.ts | 17 ++ packages/happy-dom/src/window/IWindow.ts | 16 ++ packages/happy-dom/src/window/Window.ts | 82 ++++---- .../src/window/WindowErrorUtility.ts | 3 - ...PageOpener.ts => WindowPageOpenUtility.ts} | 45 ++--- .../HTMLIFrameElement.test.ts | 178 ++++++++++++++---- packages/happy-dom/test/window/Window.test.ts | 37 ++++ 16 files changed, 537 insertions(+), 258 deletions(-) create mode 100644 packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts delete mode 100644 packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts rename packages/happy-dom/src/window/{WindowPageOpener.ts => WindowPageOpenUtility.ts} (80%) diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 02aa1d463..cb3e5fac6 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -7,6 +7,7 @@ import Event from '../event/Event.js'; import Location from '../location/Location.js'; import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; import WindowErrorUtility from '../window/WindowErrorUtility.js'; +import IResponse from '../fetch/types/IResponse.js'; /** * Browser frame. @@ -150,7 +151,7 @@ export default class BrowserFrame implements IBrowserFrame { * * @param url URL. */ - public async goto(url: string): Promise { + public async goto(url: string): Promise { for (const frame of this.childFrames) { frame.destroy(); } @@ -165,19 +166,22 @@ export default class BrowserFrame implements IBrowserFrame { this.window._readyStateManager.startTask(); + let response: IResponse; let responseText: string; try { - const response = await this.window.fetch(url); + response = await this.window.fetch(url); responseText = await response.text(); } catch (error) { - responseText = error.toString(); + this.content = ''; this.window._readyStateManager.endTask(); WindowErrorUtility.dispatchError(this.window, error); - return; + return response || null; } this.window.document.write(responseText); this.window._readyStateManager.endTask(); + + return response; } } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index 9bdbae2a4..b09d63153 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -7,6 +7,8 @@ import Event from '../../event/Event.js'; import Location from '../../location/Location.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; import DocumentReadyStateManager from '../../nodes/document/DocumentReadyStateManager.js'; +import WindowErrorUtility from '../../window/WindowErrorUtility.js'; +import IResponse from '../../fetch/types/IResponse.js'; /** * Browser frame. @@ -102,6 +104,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { for (const frame of this.childFrames) { frame.destroy(); } + (this.window.closed) = true; WindowBrowserSettingsReader.removeSettings(this.window); this._asyncTaskManager.destroy(); (this.page) = null; @@ -149,7 +152,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * * @param url URL. */ - public async goto(url: string): Promise { + public async goto(url: string): Promise { await Promise.all(this.childFrames.map((frame) => frame.destroy())); this._asyncTaskManager.abortAll(); @@ -162,11 +165,22 @@ export default class DetachedBrowserFrame implements IBrowserFrame { this.url = url; - const response = await this.window.fetch(url); - const responseText = await response.text(); + let response: IResponse; + let responseText: string; + + try { + response = await this.window.fetch(url); + responseText = await response.text(); + } catch (error) { + this.content = ''; + readyStateManager.endTask(); + WindowErrorUtility.dispatchError(this.window, error); + return response || null; + } + this.window.document.write(responseText); readyStateManager.endTask(); - this.content = responseText; + return response; } } diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index 315f165a9..ca71dd102 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -2,6 +2,7 @@ import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; import IWindow from '../../window/IWindow.js'; import IBrowserPageViewport from './IBrowserPageViewport.js'; import IBrowserPage from './IBrowserPage.js'; +import IResponse from '../../fetch/types/IResponse.js'; /** * Browser frame. @@ -51,5 +52,5 @@ export default interface IBrowserFrame { * * @param url URL. */ - goto(url: string): Promise; + goto(url: string): Promise; } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index 9443001c8..d3ab9b280 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -8,7 +8,7 @@ import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import HTMLIFrameElementNamedNodeMap from './HTMLIFrameElementNamedNodeMap.js'; import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; -import HTMLIframePageLoader from './HTMLIframePageLoader.js'; +import HTMLIFrameElementPageLoader from './HTMLIFrameElementPageLoader.js'; /** * HTML Iframe Element. @@ -27,19 +27,19 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram #contentWindowContainer: { window: IWindow | ICrossOriginWindow | null } = { window: null }; - #pageLoader: HTMLIframePageLoader; + #pageLoader: HTMLIFrameElementPageLoader; /** * Constructor. * - * @param browserMainFrame Main browser frame. + * @param browserFrame Browser frame. */ - constructor(browserMainFrame: IBrowserFrame) { + constructor(browserFrame: IBrowserFrame) { super(); - this.#pageLoader = new HTMLIframePageLoader({ + this.#pageLoader = new HTMLIFrameElementPageLoader({ element: this, contentWindowContainer: this.#contentWindowContainer, - browserMainFrame + browserParentFrame: browserFrame }); this.attributes = new HTMLIFrameElementNamedNodeMap(this, this.#pageLoader); } @@ -197,8 +197,12 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram super._connectToNode(parentNode); - if (isParentConnected && isConnected !== isParentConnected) { - this.#pageLoader.loadPage(); + if (isConnected !== isParentConnected) { + if (isParentConnected) { + this.#pageLoader.loadPage(); + } else { + this.#pageLoader.unloadPage(); + } } } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts index e85cd28ac..bd621bb3b 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts @@ -1,7 +1,7 @@ import IAttr from '../attr/IAttr.js'; import Element from '../element/Element.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; -import HTMLIframePageLoader from './HTMLIframePageLoader.js'; +import HTMLIFrameElementPageLoader from './HTMLIFrameElementPageLoader.js'; /** * Named Node Map. @@ -9,14 +9,14 @@ import HTMLIframePageLoader from './HTMLIframePageLoader.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLIFrameElementNamedNodeMap extends HTMLElementNamedNodeMap { - #pageLoader: HTMLIframePageLoader; + #pageLoader: HTMLIFrameElementPageLoader; /** * Constructor. * * @param ownerElement Owner element. */ - constructor(ownerElement: Element, pageLoader: HTMLIframePageLoader) { + constructor(ownerElement: Element, pageLoader: HTMLIFrameElementPageLoader) { super(ownerElement); this.#pageLoader = pageLoader; } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts new file mode 100644 index 000000000..c9b2588f7 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts @@ -0,0 +1,162 @@ +import URL from '../../url/URL.js'; +import Event from '../../event/Event.js'; +import IWindow from '../../window/IWindow.js'; +import CrossOriginWindow from '../../window/CrossOriginWindow.js'; +import WindowErrorUtility from '../../window/WindowErrorUtility.js'; +import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; +import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; +import IHTMLIFrameElement from './IHTMLIFrameElement.js'; +import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; +import IResponse from '../../fetch/types/IResponse.js'; +import DOMException from '../../exception/DOMException.js'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; + +/** + * HTML Iframe page loader. + */ +export default class HTMLIFrameElementPageLoader { + #element: IHTMLIFrameElement; + #contentWindowContainer: { window: IWindow | ICrossOriginWindow | null }; + #browserParentFrame: IBrowserFrame; + #browserIFrame: IBrowserFrame; + + /** + * Constructor. + * + * @param options Options. + * @param options.element Iframe element. + * @param options.browserParentFrame Main browser frame. + * @param options.contentWindowContainer Content window container. + * @param options.contentWindowContainer.window Content window. + */ + constructor(options: { + element: IHTMLIFrameElement; + browserParentFrame: IBrowserFrame; + contentWindowContainer: { window: IWindow | ICrossOriginWindow | null }; + }) { + this.#element = options.element; + this.#contentWindowContainer = options.contentWindowContainer; + this.#browserParentFrame = options.browserParentFrame; + } + + /** + * Loads an iframe page. + */ + public loadPage(): void { + if (!this.#element.isConnected) { + if (this.#browserIFrame) { + this.#browserIFrame.destroy(); + this.#browserIFrame = null; + } + this.#contentWindowContainer.window = null; + return; + } + + let url = this.#element.src || 'about:blank'; + + if (url !== 'about:blank' && !url.startsWith('javascript:')) { + url = new URL(this.#element.src, this.#browserParentFrame.window.location.href).href; + } + + if (this.#browserIFrame && url === this.#browserIFrame.url) { + return; + } + + if (this.#browserIFrame) { + this.#browserIFrame.destroy(); + this.#browserIFrame = null; + } + + this.#contentWindowContainer.window = null; + + if (this.#browserParentFrame.page.context.browser.settings.disableIframePageLoading) { + WindowErrorUtility.dispatchError( + this.#element, + new DOMException( + `Failed to load iframe page "${url}". Iframe page loading is disabled.`, + DOMExceptionNameEnum.notSupportedError + ) + ); + return; + } + + const window = this.#element.ownerDocument._defaultView; + this.#browserIFrame = this.#browserParentFrame.newFrame(); + + if (url === 'about:blank' || url.startsWith('javascript:')) { + (this.#browserIFrame.window.parent) = window; + (this.#browserIFrame.window.top) = window; + this.#contentWindowContainer.window = this.#browserIFrame.window; + + if ( + url !== 'about:blank' && + !this.#browserParentFrame.page.context.browser.settings.disableJavaScriptEvaluation + ) { + if (this.#browserParentFrame.page.context.browser.settings.disableErrorCapturing) { + this.#browserIFrame.window.eval(url.replace('javascript:', '')); + } else { + WindowErrorUtility.captureError(this.#browserIFrame.window, () => + this.#browserIFrame.window.eval(url.replace('javascript:', '')) + ); + } + } + + this.#element.dispatchEvent(new Event('load')); + return; + } + + const originURL = window.location; + const targetURL = new URL(url, originURL); + + // Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different. + const isSameOrigin = originURL.origin === targetURL.origin; + + this.#browserIFrame.url = url; + const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( + (this.#browserParentFrame.window) + ))._readyStateManager; + + readyStateManager.startTask(); + + const parentWindow = isSameOrigin ? window : new CrossOriginWindow(window); + (this.#browserIFrame.window.parent) = parentWindow; + (this.#browserIFrame.window.top) = parentWindow; + + this.#contentWindowContainer.window = isSameOrigin + ? this.#browserIFrame.window + : new CrossOriginWindow(this.#browserIFrame.window, window); + + this.#browserIFrame.window + .fetch(url) + .then((response) => { + const xFrameOptions = response.headers.get('X-Frame-Options')?.toLowerCase(); + if (xFrameOptions === 'deny' || (xFrameOptions === 'sameorigin' && !isSameOrigin)) { + throw new Error( + `Refused to display '${url}' in a frame because it set 'X-Frame-Options' to '${xFrameOptions}'.` + ); + } + return response; + }) + .then((response: IResponse) => response.text()) + .then((responseText: string) => { + this.#browserIFrame.content = responseText; + readyStateManager.endTask(); + this.#element.dispatchEvent(new Event('load')); + }) + .catch((error) => { + readyStateManager.endTask(); + WindowErrorUtility.dispatchError(this.#element, error); + }); + } + + /** + * Unloads an iframe page. + */ + public unloadPage(): void { + if (this.#browserIFrame) { + this.#browserIFrame.destroy(); + this.#browserIFrame = null; + } + this.#contentWindowContainer.window = null; + } +} diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts deleted file mode 100644 index 2dcfd7988..000000000 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIframePageLoader.ts +++ /dev/null @@ -1,131 +0,0 @@ -import URL from '../../url/URL.js'; -import Event from '../../event/Event.js'; -import IWindow from '../../window/IWindow.js'; -import CrossOriginWindow from '../../window/CrossOriginWindow.js'; -import WindowErrorUtility from '../../window/WindowErrorUtility.js'; -import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; -import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; -import IHTMLIFrameElement from './IHTMLIFrameElement.js'; -import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; -import IResponse from '../../fetch/types/IResponse.js'; - -/** - * HTML Iframe page loader. - */ -export default class HTMLIframePageLoader { - #element: IHTMLIFrameElement; - #contentWindowContainer: { window: IWindow | ICrossOriginWindow | null }; - #browserMainFrame: IBrowserFrame; - #browserFrame: IBrowserFrame; - - /** - * Constructor. - * - * @param options Options. - * @param options.element Iframe element. - * @param options.contentWindowContainer Content window container. - * @param options.browserMainFrame Main browser frame. - * @param options.contentWindowContainer.window Content window. - */ - constructor(options: { - element: IHTMLIFrameElement; - contentWindowContainer: { window: IWindow | ICrossOriginWindow | null }; - browserMainFrame: IBrowserFrame; - }) { - this.#element = options.element; - this.#contentWindowContainer = options.contentWindowContainer; - this.#browserMainFrame = options.browserMainFrame; - } - - /** - * Loads an iframe page. - */ - public loadPage(): void { - const url = this.#element.src; - - if (this.#browserFrame && url !== this.#browserFrame.url) { - this.#browserFrame.destroy(); - this.#browserFrame = null; - } - - this.#contentWindowContainer.window = null; - - if (!url) { - return; - } - - if (!this.#browserMainFrame.page.context.browser.settings.disableIframePageLoading) { - const window = this.#element.ownerDocument._defaultView; - - if (url === 'about:blank') { - this.#browserFrame = this.#browserMainFrame.newFrame(); - (this.#browserFrame.window.parent) = window; - (this.#browserFrame.window.top) = window; - this.#contentWindowContainer.window = this.#browserFrame.window; - return; - } - - if (url.startsWith('javascript:')) { - if (!this.#browserMainFrame.page.context.browser.settings.disableJavaScriptEvaluation) { - if (this.#browserMainFrame.page.context.browser.settings.disableErrorCapturing) { - this.#browserFrame.window.eval(url.replace('javascript:', '')); - } else { - WindowErrorUtility.captureError(this.#browserFrame.window, () => - this.#browserFrame.window.eval(url.replace('javascript:', '')) - ); - } - } - this.#browserFrame = this.#browserMainFrame.newFrame(); - (this.#browserFrame.window.parent) = window; - (this.#browserFrame.window.top) = window; - this.#contentWindowContainer.window = this.#browserFrame.window; - return; - } - - const originURL = window.location; - const targetURL = new URL(url, originURL); - - // Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different. - const isSameOrigin = originURL.origin !== targetURL.origin; - - this.#browserFrame = this.#browserMainFrame.newFrame(); - this.#browserFrame.url = url; - const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( - (this.#browserMainFrame.window) - ))._readyStateManager; - - readyStateManager.startTask(); - - this.#browserFrame.window - .fetch(url) - .then((response) => { - const xFrameOptions = response.headers.get('X-Frame-Options')?.toLowerCase(); - if (xFrameOptions === 'deny' || (xFrameOptions === 'sameorigin' && !isSameOrigin)) { - throw new Error( - `Refused to display '${url}' in a frame because it set 'X-Frame-Options' to '${xFrameOptions}'.` - ); - } - return response; - }) - .then((response: IResponse) => response.text()) - .then((responseText: string) => { - this.#browserFrame.content = responseText; - readyStateManager.endTask(); - }) - .catch((error) => { - readyStateManager.endTask(); - WindowErrorUtility.dispatchError(this.#element, error); - }); - - const parentWindow = isSameOrigin ? window : new CrossOriginWindow(window); - (this.#browserFrame.window.parent) = parentWindow; - (this.#browserFrame.window.top) = parentWindow; - - this.#contentWindowContainer.window = isSameOrigin - ? this.#browserFrame.window - : new CrossOriginWindow(this.#browserFrame.window, window); - - this.#element.dispatchEvent(new Event('load')); - } - } -} diff --git a/packages/happy-dom/src/window/CrossOriginWindow.ts b/packages/happy-dom/src/window/CrossOriginWindow.ts index 559a8ac00..d13c78585 100644 --- a/packages/happy-dom/src/window/CrossOriginWindow.ts +++ b/packages/happy-dom/src/window/CrossOriginWindow.ts @@ -47,6 +47,45 @@ export default class CrossOriginWindow extends EventTarget implements ICrossOrig this.#targetWindow = target; } + /** + * Returns the opener. + * + * @returns Opener. + */ + public get opener(): IWindow | ICrossOriginWindow | null { + return this.#targetWindow.opener; + } + + /** + * Returns the closed state. + * + * @returns Closed state. + */ + public get closed(): boolean { + return this.#targetWindow.closed; + } + + /** + * Shifts focus away from the window. + */ + public blur(): void { + this.#targetWindow.blur(); + } + + /** + * Gives focus to the window. + */ + public focus(): void { + this.#targetWindow.focus(); + } + + /** + * Closes the window. + */ + public close(): void { + this.#targetWindow.close(); + } + /** * Safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it. * diff --git a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts index 684985db4..385a31ce8 100644 --- a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts +++ b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts @@ -1,29 +1,25 @@ import IBrowserSettings from '../browser/types/IBrowserSettings.js'; -import IWindow from './IWindow.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; import BrowserSettingsFactory from '../browser/BrowserSettingsFactory.js'; import IReadOnlyBrowserSettings from '../browser/types/IReadOnlyBrowserSettings.js'; import IBrowserPageViewport from '../browser/types/IBrowserPageViewport.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; +import DetachedBrowserFrame from '../browser/detached-browser/DetachedBrowserFrame.js'; /** * API for detached windows to be able to access features of the owner window. */ export default class HappyDOMWindowAPI { - #window: IWindow; #browserFrame?: IBrowserFrame; #settings: IBrowserSettings | null = null; /** * Constructor. * - * @param options Options. - * @param options.window Owner window. - * @param options.browserFrame Browser frame. + * @param browserFrame Browser frame. */ - constructor(options: { window: IWindow; browserFrame: IBrowserFrame }) { - this.#window = options.window; - this.#browserFrame = options.browserFrame; + constructor(browserFrame: IBrowserFrame) { + this.#browserFrame = browserFrame; } /** @@ -86,12 +82,18 @@ export default class HappyDOMWindowAPI { } /** - * Sets the URL. + * Sets the URL on a detached window. + * It will throw an exception if the window is not detached as a script could potentially use this method to bypass CORS. * * @param url URL. */ public setURL(url: string): void { - this.#window.location.href = url; + if (!(this.#browserFrame instanceof DetachedBrowserFrame)) { + throw new Error( + 'Only detached browser frames can use the setURL() method for security reasons. Use the Browser API instead for setting URL.' + ); + } + this.#browserFrame.url = url; } /** diff --git a/packages/happy-dom/src/window/ICrossOriginWindow.ts b/packages/happy-dom/src/window/ICrossOriginWindow.ts index e91fd763e..543677222 100644 --- a/packages/happy-dom/src/window/ICrossOriginWindow.ts +++ b/packages/happy-dom/src/window/ICrossOriginWindow.ts @@ -11,6 +11,23 @@ export default interface ICrossOriginWindow extends IEventTarget { readonly parent: IWindow | ICrossOriginWindow; readonly top: IWindow | ICrossOriginWindow; readonly location: Location; + readonly opener: IWindow | ICrossOriginWindow | null; + readonly closed: boolean; + + /** + * Shifts focus away from the window. + */ + blur(): void; + + /** + * Gives focus to the window. + */ + focus(): void; + + /** + * Closes the window. + */ + close(): void; /** * Safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it. diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index 36f94a2c7..beaffb03a 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -405,6 +405,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly scrollX: number; readonly scrollY: number; readonly crypto: typeof webcrypto; + readonly closed: boolean; /** * Returns an object containing the values of all CSS properties of an element. @@ -437,6 +438,16 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { */ scrollTo(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void; + /** + * Shifts focus away from the window. + */ + blur(): void; + + /** + * Gives focus to the window. + */ + focus(): void; + /** * Loads a specified resource into a new or existing browsing context (that is, a tab, a window, or an iframe) under a specified name. * @@ -446,6 +457,11 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { */ open(url?: string, target?: string, windowFeatures?: string): IWindow | ICrossOriginWindow | null; + /** + * Closes the window. + */ + close(): void; + /** * Returns a new MediaQueryList object that can then be used to determine if the document matches the media query string. * diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 9b434bce9..b93a3483d 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -147,7 +147,7 @@ import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; import DetachedBrowser from '../browser/detached-browser/DetachedBrowser.js'; -import WindowPageOpener from './WindowPageOpener.js'; +import WindowPageOpenUtility from './WindowPageOpenUtility.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -400,10 +400,10 @@ export default class Window extends EventTarget implements IWindow { public readonly location: Location; public readonly history: History; public readonly navigator: Navigator; - public readonly opener: IWindow | ICrossOriginWindow | null = null; + public readonly opener: IWindow | null = null; public readonly self: IWindow = this; - public readonly top: IWindow | ICrossOriginWindow = this; - public readonly parent: IWindow | ICrossOriginWindow = this; + public readonly top: IWindow = this; + public readonly parent: IWindow = this; public readonly window: IWindow = this; public readonly globalThis: IWindow = this; public readonly name: string = ''; @@ -421,6 +421,7 @@ export default class Window extends EventTarget implements IWindow { public readonly screenX: number = 0; public readonly screenY: number = 0; public readonly crypto = webcrypto; + public readonly closed = false; // Node.js Globals public Array: typeof Array; @@ -488,16 +489,15 @@ export default class Window extends EventTarget implements IWindow { // Used for tracking capture event listeners to improve performance when they are not used. // See EventTarget class. public _captureEventListenerCount: { [eventType: string]: number } = {}; + public readonly _readyStateManager = new DocumentReadyStateManager(this); // Private properties - private _setTimeout: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; - private _clearTimeout: (id: NodeJS.Timeout) => void; - private _setInterval: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; - private _clearInterval: (id: NodeJS.Timeout) => void; - private _queueMicrotask: (callback: Function) => void; - public readonly _readyStateManager = new DocumentReadyStateManager(this); + #setTimeout: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; + #clearTimeout: (id: NodeJS.Timeout) => void; + #setInterval: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; + #clearInterval: (id: NodeJS.Timeout) => void; + #queueMicrotask: (callback: Function) => void; #browserFrame: IBrowserFrame; - #windowPageOpener: WindowPageOpener; /** * Constructor. @@ -543,11 +543,7 @@ export default class Window extends EventTarget implements IWindow { WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); - this.#windowPageOpener = new WindowPageOpener(this.#browserFrame); - this.happyDOM = new HappyDOMWindowAPI({ - window: this, - browserFrame: this.#browserFrame - }); + this.happyDOM = new HappyDOMWindowAPI(this.#browserFrame); if (options) { if (options.width !== undefined) { @@ -571,11 +567,11 @@ export default class Window extends EventTarget implements IWindow { } } - this._setTimeout = ORIGINAL_SET_TIMEOUT; - this._clearTimeout = ORIGINAL_CLEAR_TIMEOUT; - this._setInterval = ORIGINAL_SET_INTERVAL; - this._clearInterval = ORIGINAL_CLEAR_INTERVAL; - this._queueMicrotask = ORIGINAL_QUEUE_MICROTASK; + this.#setTimeout = ORIGINAL_SET_TIMEOUT; + this.#clearTimeout = ORIGINAL_CLEAR_TIMEOUT; + this.#setInterval = ORIGINAL_SET_INTERVAL; + this.#clearInterval = ORIGINAL_CLEAR_INTERVAL; + this.#queueMicrotask = ORIGINAL_QUEUE_MICROTASK; // Binds all methods to "this", so that it will use the correct context when called globally. for (const key of Object.getOwnPropertyNames(Window.prototype).concat( @@ -844,6 +840,20 @@ export default class Window extends EventTarget implements IWindow { this.scroll(x, y); } + /** + * Shifts focus away from the window. + */ + public blur(): void { + // TODO: Implement. + } + + /** + * Gives focus to the window. + */ + public focus(): void { + // TODO: Implement. + } + /** * Loads a specified resource into a new or existing browsing context (that is, a tab, a window, or an iframe) under a specified name. * @@ -860,7 +870,7 @@ export default class Window extends EventTarget implements IWindow { if (this.#browserFrame.page.context.browser.settings.disableWindowOpenPageLoading) { return null; } - return this.#windowPageOpener.openPage({ + return WindowPageOpenUtility.openPage(this.#browserFrame, { url, target, features @@ -871,12 +881,8 @@ export default class Window extends EventTarget implements IWindow { * Closes the window. */ public close(): void { - WindowBrowserSettingsReader.removeSettings(this); - - if (this.#browserFrame.page) { - if (this.#browserFrame.page.mainFrame === this.#browserFrame) { - this.#browserFrame.page.close(); - } + if (this.#browserFrame.page.mainFrame === this.#browserFrame) { + this.#browserFrame.page.close(); } else { this.#browserFrame.destroy(); } @@ -901,7 +907,7 @@ export default class Window extends EventTarget implements IWindow { * @returns Timeout ID. */ public setTimeout(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { - const id = this._setTimeout(() => { + const id = this.#setTimeout(() => { if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { callback(...args); } else { @@ -919,7 +925,7 @@ export default class Window extends EventTarget implements IWindow { * @param id ID of the timeout. */ public clearTimeout(id: NodeJS.Timeout): void { - this._clearTimeout(id); + this.#clearTimeout(id); this.#browserFrame._asyncTaskManager.endTimer(id); } @@ -932,7 +938,7 @@ export default class Window extends EventTarget implements IWindow { * @returns Interval ID. */ public setInterval(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { - const id = this._setInterval(() => { + const id = this.#setInterval(() => { if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { callback(...args); } else { @@ -953,7 +959,7 @@ export default class Window extends EventTarget implements IWindow { * @param id ID of the interval. */ public clearInterval(id: NodeJS.Timeout): void { - this._clearInterval(id); + this.#clearInterval(id); this.#browserFrame._asyncTaskManager.endTimer(id); } @@ -994,7 +1000,7 @@ export default class Window extends EventTarget implements IWindow { public queueMicrotask(callback: Function): void { let isAborted = false; const taskId = this.#browserFrame._asyncTaskManager.startTask(() => (isAborted = true)); - this._queueMicrotask(() => { + this.#queueMicrotask(() => { if (!isAborted) { if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { callback(); @@ -1072,12 +1078,16 @@ export default class Window extends EventTarget implements IWindow { ); } - this.window.setTimeout(() => + this.setTimeout(() => this.dispatchEvent( new MessageEvent('message', { data: message, - origin: this.parent.location.origin, - source: this.parent, + origin: this.#browserFrame.parentFrame + ? this.#browserFrame.parentFrame.window.location.origin + : this.#browserFrame.window.location.origin, + source: this.#browserFrame.parentFrame + ? this.#browserFrame.parentFrame.window + : this.#browserFrame.window, lastEventId: '' }) ) diff --git a/packages/happy-dom/src/window/WindowErrorUtility.ts b/packages/happy-dom/src/window/WindowErrorUtility.ts index 8d2e6a063..27c363760 100644 --- a/packages/happy-dom/src/window/WindowErrorUtility.ts +++ b/packages/happy-dom/src/window/WindowErrorUtility.ts @@ -60,9 +60,6 @@ export default class WindowErrorUtility { (elementOrWindow).dispatchEvent( new ErrorEvent('error', { message: error.message, error }) ); - (elementOrWindow).ownerDocument._defaultView.dispatchEvent( - new ErrorEvent('error', { message: error.message, error }) - ); } } } diff --git a/packages/happy-dom/src/window/WindowPageOpener.ts b/packages/happy-dom/src/window/WindowPageOpenUtility.ts similarity index 80% rename from packages/happy-dom/src/window/WindowPageOpener.ts rename to packages/happy-dom/src/window/WindowPageOpenUtility.ts index 600655b34..af71a0fe6 100644 --- a/packages/happy-dom/src/window/WindowPageOpener.ts +++ b/packages/happy-dom/src/window/WindowPageOpenUtility.ts @@ -10,48 +10,41 @@ import ICrossOriginWindow from './ICrossOriginWindow.js'; /** * Window page open handler. */ -export default class WindowPageOpener { - #browserFrame: IBrowserFrame; - - /** - * Constructor. - * - * @param browserFrame Browser frame. - */ - constructor(browserFrame: IBrowserFrame) { - this.#browserFrame = browserFrame; - } - +export default class WindowPageOpenUtility { /** * Opens a page. * + * @param browserFrame Browser frame. * @param [options] Options. * @param [options.url] URL. * @param [options.target] Target. * @param [options.features] Window features. */ - public openPage(options?: { - url?: string; - target?: string; - features?: string; - }): IWindow | ICrossOriginWindow | null { + public static openPage( + browserFrame: IBrowserFrame, + options?: { + url?: string; + target?: string; + features?: string; + } + ): IWindow | ICrossOriginWindow | null { const features = this.getWindowFeatures(options?.features || ''); const url = options?.url || 'about:blank'; const target = options?.target !== undefined ? String(options.target) : null; - const newPage = this.#browserFrame.page.context.newPage(); + const newPage = browserFrame.page.context.newPage(); const newWindow = newPage.mainFrame.window; - const originURL = this.#browserFrame.window.location; + const originURL = browserFrame.window.location; const targetURL = new URL(url, originURL); const isCORS = FetchCORSUtility.isCORS(originURL, targetURL); newPage.mainFrame.url = url; - (newWindow.document.referrer) = !features.noreferrer ? this.#browserFrame.url : ''; + (newWindow.document.referrer) = !features.noreferrer ? browserFrame.url : ''; if (!features.noopener) { (newWindow.opener) = isCORS - ? new CrossOriginWindow(this.#browserFrame.window) - : this.#browserFrame.window; + ? new CrossOriginWindow(browserFrame.window) + : browserFrame.window; } if (target) { @@ -73,8 +66,8 @@ export default class WindowPageOpener { } if (url.startsWith('javascript:')) { - if (!this.#browserFrame.page.context.browser.settings.disableJavaScriptEvaluation) { - if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { + if (!browserFrame.page.context.browser.settings.disableJavaScriptEvaluation) { + if (browserFrame.page.context.browser.settings.disableErrorCapturing) { newWindow.eval(url.replace('javascript:', '')); } else { WindowErrorUtility.captureError(newWindow, () => @@ -105,7 +98,7 @@ export default class WindowPageOpener { return null; } - return isCORS ? new CrossOriginWindow(newWindow, this.#browserFrame.window) : newWindow; + return isCORS ? new CrossOriginWindow(newWindow, browserFrame.window) : newWindow; } /** @@ -114,7 +107,7 @@ export default class WindowPageOpener { * @param features Window features string. * @returns Window features. */ - private getWindowFeatures(features: string): { + private static getWindowFeatures(features: string): { popup: boolean; width: number; height: number; diff --git a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts index c50559db9..8f8a7bf0c 100644 --- a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts +++ b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts @@ -10,6 +10,8 @@ import DOMExceptionNameEnum from '../../../src/exception/DOMExceptionNameEnum.js import DOMException from '../../../src/exception/DOMException.js'; import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest'; import IRequestInfo from '../../../src/fetch/types/IRequestInfo.js'; +import Headers from '../../../src/fetch/Headers.js'; +import Browser from '../../../src/browser/Browser.js'; describe('HTMLIFrameElement', () => { let window: IWindow; @@ -51,6 +53,7 @@ describe('HTMLIFrameElement', () => { describe('get contentWindow()', () => { it('Returns content window for "about:blank".', () => { element.src = 'about:blank'; + expect(element.contentWindow).toBe(null); expect(element.contentDocument).toBe(null); document.body.appendChild(element); expect(element.contentWindow === element.contentDocument?.defaultView).toBe(true); @@ -65,17 +68,114 @@ describe('HTMLIFrameElement', () => { expect(element.contentDocument?.documentElement.scrollTop).toBe(20); }); + it(`Does'nt load anything if the Happy DOM setting "disableIframePageLoading" is set to true.`, () => { + const browser = new Browser({ settings: { disableIframePageLoading: true } }); + const page = browser.newPage(); + const window = page.mainFrame.window; + const document = window.document; + const element = document.createElement('iframe'); + + element.src = 'https://localhost:8080/iframe.html'; + document.body.appendChild(element); + expect(element.contentWindow === null).toBe(true); + expect(element.contentDocument === null).toBe(true); + }); + + it(`Dispatches an error event if the response of the iframe page has an "x-frame-options" header set to "deny".`, async () => { + await new Promise((resolve) => { + const responseHTML = 'Test'; + let fetchedURL: string | null = null; + + vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { + fetchedURL = url; + return Promise.resolve(({ + text: () => Promise.resolve(responseHTML), + ok: true, + headers: new Headers({ 'x-frame-options': 'deny' }) + })); + }); + + window.happyDOM.setURL('https://localhost:8080'); + element.src = 'https://localhost:8080/iframe.html'; + element.addEventListener('error', (event) => { + expect((event).message).toBe( + `Refused to display 'https://localhost:8080/iframe.html' in a frame because it set 'X-Frame-Options' to 'deny'.` + ); + expect((event).message === (event).error?.message).toBe(true); + resolve(null); + }); + document.body.appendChild(element); + }); + }); + + it(`Dispatches an error event if the response of the iframe page has an "x-frame-options" header set to "sameorigin" when the origin is different.`, async () => { + await new Promise((resolve) => { + const responseHTML = 'Test'; + let fetchedURL: string | null = null; + + vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { + fetchedURL = url; + return Promise.resolve(({ + text: () => Promise.resolve(responseHTML), + ok: true, + headers: new Headers({ 'x-frame-options': 'sameorigin' }) + })); + }); + + window.happyDOM.setURL('https://localhost:3000'); + element.src = 'https://localhost:8080/iframe.html'; + element.addEventListener('error', (event) => { + expect((event).message).toBe( + `Refused to display 'https://localhost:8080/iframe.html' in a frame because it set 'X-Frame-Options' to 'sameorigin'.` + ); + expect((event).message === (event).error?.message).toBe(true); + resolve(null); + }); + document.body.appendChild(element); + }); + }); + + it('Returns content window for URL with same origin when the response has an "x-frame-options" set to "sameorigin".', async () => { + await new Promise((resolve) => { + const responseHTML = 'Test'; + let fetchedURL: string | null = null; + + vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { + fetchedURL = url; + return Promise.resolve(({ + text: () => Promise.resolve(responseHTML), + ok: true, + headers: new Headers({ 'x-frame-options': 'sameorigin' }) + })); + }); + + window.happyDOM.setURL('https://localhost:8080'); + element.src = 'https://localhost:8080/iframe.html'; + element.addEventListener('load', () => { + expect(element.contentDocument?.location.href).toBe('https://localhost:8080/iframe.html'); + expect(fetchedURL).toBe('https://localhost:8080/iframe.html'); + expect(element.contentWindow === element.contentDocument?.defaultView).toBe(true); + expect(`${element.contentDocument?.documentElement.innerHTML}`).toBe( + responseHTML + ); + resolve(null); + }); + document.body.appendChild(element); + }); + }); + it('Returns content window for URL with same origin.', async () => { await new Promise((resolve) => { const responseHTML = 'Test'; let fetchedURL: string | null = null; - vi.spyOn(window, 'fetch').mockImplementation((url: IRequestInfo) => { + vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { fetchedURL = url; - return Promise.resolve({ + return Promise.resolve(({ text: () => Promise.resolve(responseHTML), - ok: true - }); + ok: true, + headers: new Headers() + })); }); window.happyDOM.setURL('https://localhost:8080'); @@ -98,12 +198,13 @@ describe('HTMLIFrameElement', () => { const responseHTML = 'Test'; let fetchedURL: string | null = null; - vi.spyOn(window, 'fetch').mockImplementation((url: IRequestInfo) => { + vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { fetchedURL = url; - return Promise.resolve({ + return Promise.resolve(({ text: () => Promise.resolve(responseHTML), - ok: true - }); + ok: true, + headers: new Headers() + })); }); window.happyDOM.setURL('https://localhost:8080'); @@ -118,45 +219,60 @@ describe('HTMLIFrameElement', () => { it('Returns content window for without protocol.', async () => { await new Promise((resolve) => { + const browser = new Browser(); + const page = browser.newPage(); + const window = page.mainFrame.window; + const document = window.document; + const element = document.createElement('iframe'); const responseHTML = 'Test'; let fetchedURL: string | null = null; - vi.spyOn(window, 'fetch').mockImplementation((url: IRequestInfo) => { + page.mainFrame.url = 'https://localhost:8080'; + + vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { fetchedURL = url; - return Promise.resolve({ + return Promise.resolve(({ text: () => Promise.resolve(responseHTML), - ok: true - }); + ok: true, + headers: new Headers() + })); }); - window.happyDOM.setURL('https://localhost:8080'); element.src = '//www.github.com/iframe.html'; element.addEventListener('load', () => { - expect((element.contentWindow?.['_targetWindow']).document.location.href).toBe( - 'https://www.github.com/iframe.html' - ); + expect(page.mainFrame.childFrames[0].url).toBe('https://www.github.com/iframe.html'); resolve(null); }); + document.body.appendChild(element); }); }); it('Returns instance of CrossOriginWindow for URL with different origin.', async () => { await new Promise((resolve) => { + const browser = new Browser(); + const page = browser.newPage(); + const window = page.mainFrame.window; + const document = window.document; + const element = document.createElement('iframe'); const iframeOrigin = 'https://other.origin.com'; const iframeSrc = iframeOrigin + '/iframe.html'; const documentOrigin = 'https://localhost:8080'; let fetchedURL: string | null = null; - vi.spyOn(window, 'fetch').mockImplementation((url: IRequestInfo): Promise => { - fetchedURL = url; - return Promise.resolve({ - text: () => Promise.resolve('Test'), - ok: true - }); - }); + page.mainFrame.url = documentOrigin; + + vi.spyOn(Window.prototype, 'fetch').mockImplementation( + (url: IRequestInfo): Promise => { + fetchedURL = url; + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + ok: true, + headers: new Headers() + })); + } + ); - window.happyDOM.setURL(documentOrigin); document.body.appendChild(element); element.src = iframeSrc; element.addEventListener('load', () => { @@ -170,14 +286,11 @@ describe('HTMLIFrameElement', () => { DOMExceptionNameEnum.securityError ) ); - const targetWindow = ( - (element.contentWindow)['_targetWindow'] - ); expect(element.contentWindow?.self === element.contentWindow).toBe(true); expect(element.contentWindow?.window === element.contentWindow).toBe(true); expect(element.contentWindow?.parent === window).toBe(true); expect(element.contentWindow?.top === window).toBe(true); - targetWindow.addEventListener( + page.mainFrame.childFrames[0].window.addEventListener( 'message', (event) => (triggeredEvent = event) ); @@ -200,11 +313,12 @@ describe('HTMLIFrameElement', () => { await new Promise((resolve) => { const error = new Error('Error'); - vi.spyOn(window, 'fetch').mockImplementation(() => { - return Promise.resolve({ + vi.spyOn(Window.prototype, 'fetch').mockImplementation(() => { + return Promise.resolve(({ text: () => Promise.reject(error), - ok: true - }); + ok: true, + headers: new Headers() + })); }); element.src = 'https://localhost:8080/iframe.html'; diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 1d70d1e85..728075f4d 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -1534,4 +1534,41 @@ describe('Window', () => { ); }); }); + + describe('open()', () => { + it('Opens a new window without URL.', () => { + const newWindow = window.open(); + expect(newWindow).toBeInstanceOf(Window); + expect(newWindow?.location.href).toBe('about:blank'); + }); + + it('Opens a new window with URL.', () => { + window.happyDOM.setURL('https://localhost:8080/test/'); + const newWindow = window.open('/path/to/file.html'); + expect(newWindow).toBeInstanceOf(Window); + expect(newWindow?.location.href).toBe('https://localhost:8080/path/to/file.html'); + }); + + it('Opens a new window with the Browser API.', () => { + const browser = new Browser(); + const page = browser.newPage(); + + page.mainFrame.url = 'https://localhost:8080/test/'; + + const newWindow = page.mainFrame.window.open('/path/to/file.html'); + + expect(browser.defaultContext.pages.length).toBe(2); + expect(browser.defaultContext.pages[0]).toBe(page); + expect(browser.defaultContext.pages[1].mainFrame.window).toBe(newWindow); + expect(browser.defaultContext.pages[1].mainFrame.url).toBe( + 'https://localhost:8080/path/to/file.html' + ); + + newWindow?.close(); + + expect(browser.defaultContext.pages.length).toBe(1); + expect(browser.defaultContext.pages[0]).toBe(page); + expect(newWindow?.closed).toBe(true); + }); + }); }); From c3dff3f9131e01e46c7ec70c3e3c384cfba4bc30 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 1 Nov 2023 01:56:23 +0100 Subject: [PATCH 18/63] #466@trivial: Continues on implementation. --- .../happy-dom/src/browser/BrowserFrame.ts | 98 +-------- .../src/browser/BrowserFrameUtility.ts | 130 ++++++++++++ packages/happy-dom/src/browser/BrowserPage.ts | 28 ++- .../detached-browser/DetachedBrowser.ts | 14 +- .../DetachedBrowserContext.ts | 20 +- .../detached-browser/DetachedBrowserFrame.ts | 117 ++--------- .../detached-browser/DetachedBrowserPage.ts | 40 +++- .../src/browser/types/IBrowserFrame.ts | 24 +-- .../src/browser/types/IGoToOptions.ts | 9 + packages/happy-dom/src/fetch/Fetch.ts | 19 +- .../src/fetch/utilities/FetchCORSUtility.ts | 7 +- packages/happy-dom/src/location/Location.ts | 37 +++- .../happy-dom/src/nodes/element/Element.ts | 5 +- .../HTMLIFrameElementPageLoader.ts | 16 +- .../html-script-element/HTMLScriptElement.ts | 7 +- .../HTMLScriptElementUtility.ts | 1 + .../happy-dom/src/window/CrossOriginWindow.ts | 1 + .../happy-dom/src/window/HappyDOMWindowAPI.ts | 2 +- packages/happy-dom/src/window/IWindow.ts | 2 +- packages/happy-dom/src/window/Window.ts | 15 +- .../src/window/WindowPageOpenUtility.ts | 123 ++++++----- packages/happy-dom/test/window/Window.test.ts | 192 +++++++++++++----- 22 files changed, 542 insertions(+), 365 deletions(-) create mode 100644 packages/happy-dom/src/browser/BrowserFrameUtility.ts create mode 100644 packages/happy-dom/src/browser/types/IGoToOptions.ts diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index cb3e5fac6..ce20370ae 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -2,12 +2,10 @@ import BrowserPage from './BrowserPage.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import IBrowserFrame from './types/IBrowserFrame.js'; import Window from '../window/Window.js'; -import IBrowserPageViewport from './types/IBrowserPageViewport.js'; -import Event from '../event/Event.js'; import Location from '../location/Location.js'; -import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; -import WindowErrorUtility from '../window/WindowErrorUtility.js'; import IResponse from '../fetch/types/IResponse.js'; +import BrowserFrameUtility from './BrowserFrameUtility.js'; +import IGoToOptions from './types/IGoToOptions.js'; /** * Browser frame. @@ -67,7 +65,7 @@ export default class BrowserFrame implements IBrowserFrame { * @param url URL. */ public set url(url) { - (this.window.location) = new Location(url); + (this.window.location) = new Location(url, this); } /** @@ -91,97 +89,13 @@ export default class BrowserFrame implements IBrowserFrame { this._asyncTaskManager.abortAll(); } - /** - * Aborts all ongoing operations and destroys the frame. - */ - public destroy(): void { - if (this.parentFrame) { - const index = this.parentFrame.childFrames.indexOf(this); - if (index !== -1) { - this.parentFrame.childFrames.splice(index, 1); - } - } - for (const frame of this.childFrames) { - frame.destroy(); - } - this._asyncTaskManager.destroy(); - WindowBrowserSettingsReader.removeSettings(this.window); - (this.page) = null; - (this.window) = null; - } - - /** - * Sets the viewport. - * - * @param viewport Viewport. - */ - public setViewport(viewport: IBrowserPageViewport): void { - if ( - (viewport.width !== undefined && this.window.innerWidth !== viewport.width) || - (viewport.height !== undefined && this.window.innerHeight !== viewport.height) - ) { - if (viewport.width !== undefined && this.window.innerWidth !== viewport.width) { - (this.window.innerWidth) = viewport.width; - (this.window.outerWidth) = viewport.width; - } - - if (viewport.height !== undefined && this.window.innerHeight !== viewport.height) { - (this.window.innerHeight) = viewport.height; - (this.window.outerHeight) = viewport.height; - } - - this.window.dispatchEvent(new Event('resize')); - } - } - - /** - * Creates a new frame. - * - * @returns Frame. - */ - public newFrame(): IBrowserFrame { - const frame = new BrowserFrame(this.page); - (frame.parentFrame) = this; - this.childFrames.push(frame); - return frame; - } - /** * Go to a page. * * @param url URL. + * @param [options] Options. */ - public async goto(url: string): Promise { - for (const frame of this.childFrames) { - frame.destroy(); - } - - this._asyncTaskManager.destroy(); - - (this.window) = new Window({ - url, - browserFrame: this, - console: this.page.console - }); - - this.window._readyStateManager.startTask(); - - let response: IResponse; - let responseText: string; - - try { - response = await this.window.fetch(url); - responseText = await response.text(); - } catch (error) { - this.content = ''; - this.window._readyStateManager.endTask(); - WindowErrorUtility.dispatchError(this.window, error); - return response || null; - } - - this.window.document.write(responseText); - this.window._readyStateManager.endTask(); - - return response; + public async goto(url: string, options?: IGoToOptions): Promise { + return await BrowserFrameUtility.goto(Window, this, url, options); } } diff --git a/packages/happy-dom/src/browser/BrowserFrameUtility.ts b/packages/happy-dom/src/browser/BrowserFrameUtility.ts new file mode 100644 index 000000000..7412cfd65 --- /dev/null +++ b/packages/happy-dom/src/browser/BrowserFrameUtility.ts @@ -0,0 +1,130 @@ +import BrowserPage from './BrowserPage.js'; +import IBrowserFrame from './types/IBrowserFrame.js'; +import Window from '../window/Window.js'; +import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; +import IBrowserPage from './types/IBrowserPage.js'; +import IGoToOptions from './types/IGoToOptions.js'; +import IResponse from '../fetch/types/IResponse.js'; +import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; +import IWindow from '../window/IWindow.js'; +import WindowErrorUtility from '../window/WindowErrorUtility.js'; + +/** + * Browser frame utility. + */ +export default class BrowserFrameUtility { + /** + * Aborts all ongoing operations and destroys the frame. + */ + public static closeFrame(frame: IBrowserFrame): void { + if (!frame.window) { + return; + } + + if (frame.parentFrame) { + const index = frame.parentFrame.childFrames.indexOf(frame); + if (index !== -1) { + frame.parentFrame.childFrames.splice(index, 1); + } + } + + for (const childFrame of frame.childFrames) { + this.closeFrame(childFrame); + } + + (frame.window.closed) = true; + frame._asyncTaskManager.destroy(); + WindowBrowserSettingsReader.removeSettings(frame.window); + (frame.page) = null; + (frame.window) = null; + } + + /** + * Creates a new frame. + * + * @param parentFrame Parent frame. + * @returns Frame. + */ + public static newFrame(parentFrame: IBrowserFrame): IBrowserFrame { + const frame = new ( IBrowserFrame>parentFrame.constructor)( + parentFrame.page + ); + (frame.parentFrame) = parentFrame; + parentFrame.childFrames.push(frame); + return frame; + } + + /** + * Go to a page. + * + * @param windowClass Window class. + * @param frame Frame. + * @param url URL. + * @param [options] Options. + * @returns Response. + */ + public static async goto( + windowClass: new (options: { + browserFrame: IBrowserFrame; + console: Console; + url?: string; + }) => IWindow, + frame: IBrowserFrame, + url: string, + options?: IGoToOptions + ): Promise { + if (url.startsWith('javascript:')) { + frame.window.location.href = url; + return null; + } + + for (const childFrame of frame.childFrames) { + BrowserFrameUtility.closeFrame(childFrame); + } + + (frame.window.closed) = true; + frame._asyncTaskManager.destroy(); + WindowBrowserSettingsReader.removeSettings(frame.window); + + (frame.window) = new windowClass({ + browserFrame: frame, + console: frame.page.console + }); + + if (options?.referrer) { + (frame.window.document.referrer) = options.referrer; + } + + if (!url || url.startsWith('about:')) { + return null; + } + + frame.url = url; + + const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( + (frame.window) + ))._readyStateManager; + + readyStateManager.startTask(); + + let response: IResponse; + let responseText: string; + + try { + response = await frame.window.fetch(url, { + referrer: options?.referrer, + referrerPolicy: options?.referrerPolicy + }); + responseText = await response.text(); + } catch (error) { + readyStateManager.endTask(); + WindowErrorUtility.dispatchError(frame.window, error); + return response || null; + } + + frame.window.document.write(responseText); + readyStateManager.endTask(); + + return response; + } +} diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index 082b01590..62208d0b6 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -4,6 +4,8 @@ import BrowserFrame from './BrowserFrame.js'; import BrowserContext from './BrowserContext.js'; import VirtualConsole from '../console/VirtualConsole.js'; import IBrowserPage from './types/IBrowserPage.js'; +import BrowserFrameUtility from './BrowserFrameUtility.js'; +import Event from '../event/Event.js'; /** * Browser page. @@ -43,11 +45,17 @@ export default class BrowserPage implements IBrowserPage { * Aborts all ongoing operations and destroys the page. */ public close(): void { - this.mainFrame.destroy(); + if (!this.mainFrame) { + return; + } + + BrowserFrameUtility.closeFrame(this.mainFrame); + const index = this.context.pages.indexOf(this); if (index !== -1) { this.context.pages.splice(index, 1); } + (this.virtualConsolePrinter) = null; (this.mainFrame) = null; (this.context) = null; @@ -75,9 +83,23 @@ export default class BrowserPage implements IBrowserPage { * @param viewport Viewport. */ public setViewport(viewport: IBrowserPageViewport): void { - this.mainFrame.setViewport(viewport); - } + if ( + (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) || + (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) + ) { + if (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) { + (this.mainFrame.window.innerWidth) = viewport.width; + (this.mainFrame.window.outerWidth) = viewport.width; + } + + if (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) { + (this.mainFrame.window.innerHeight) = viewport.height; + (this.mainFrame.window.outerHeight) = viewport.height; + } + this.mainFrame.window.dispatchEvent(new Event('resize')); + } + } /** * Go to a page. * diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts index d1deeb8d8..d6c9179f4 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts @@ -5,6 +5,7 @@ import BrowserSettingsFactory from '../BrowserSettingsFactory.js'; import DetachedBrowserPage from './DetachedBrowserPage.js'; import IBrowser from '../types/IBrowser.js'; import IWindow from '../../window/IWindow.js'; +import IBrowserFrame from '../types/IBrowserFrame.js'; /** * Detached browser. @@ -13,6 +14,12 @@ export default class DetachedBrowser implements IBrowser { public readonly contexts: DetachedBrowserContext[]; public readonly settings: IBrowserSettings; public readonly console: Console | null; + public readonly detachedWindowClass: new (options: { + browserFrame: IBrowserFrame; + console: Console; + url?: string; + }) => IWindow; + public readonly detachedWindow: IWindow; /** * Constructor. @@ -28,9 +35,11 @@ export default class DetachedBrowser implements IBrowser { window: IWindow, options?: { settings?: IOptionalBrowserSettings; console?: Console } ) { + this.detachedWindowClass = windowClass; + this.detachedWindow = window; this.console = options?.console || null; this.settings = BrowserSettingsFactory.getSettings(options?.settings); - this.contexts = [new DetachedBrowserContext(windowClass, window, this)]; + this.contexts = [new DetachedBrowserContext(this)]; } /** @@ -53,6 +62,9 @@ export default class DetachedBrowser implements IBrowser { context.close(); } (this.contexts) = []; + (this.console) = null; + (this.detachedWindow) = null; + ( IWindow | null>this.detachedWindowClass) = null; } /** diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts index 7bcef09d2..e49fad363 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts @@ -1,7 +1,6 @@ import DetachedBrowser from './DetachedBrowser.js'; import DetachedBrowserPage from './DetachedBrowserPage.js'; import IBrowserContext from '../types/IBrowserContext.js'; -import IWindow from '../../window/IWindow.js'; /** * Detached browser context. @@ -9,28 +8,33 @@ import IWindow from '../../window/IWindow.js'; export default class DetachedBrowserContext implements IBrowserContext { public readonly pages: DetachedBrowserPage[]; public readonly browser: DetachedBrowser; - #windowClass: new () => IWindow; /** * Constructor. * - * @param windowClass Window class. - * @param window Window. * @param browser Browser. */ - constructor(windowClass: new () => IWindow, window: IWindow, browser: DetachedBrowser) { - this.#windowClass = windowClass; + constructor(browser: DetachedBrowser) { this.browser = browser; - this.pages = [new DetachedBrowserPage(windowClass, window, this)]; + this.pages = [new DetachedBrowserPage(this)]; } /** * Aborts all ongoing operations and destroys the context. */ public close(): void { + if (!this.browser) { + return; + } for (const page of this.pages) { page.close(); } + const browser = this.browser; + (this.pages) = []; + (this.browser) = null; + if (browser.defaultContext === this) { + browser.close(); + } } /** @@ -57,7 +61,7 @@ export default class DetachedBrowserContext implements IBrowserContext { * @returns Page. */ public newPage(): DetachedBrowserPage { - const page = new DetachedBrowserPage(this.#windowClass, new this.#windowClass(), this); + const page = new DetachedBrowserPage(this); this.pages.push(page); return page; } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index b09d63153..d16e1db89 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -2,13 +2,10 @@ import IWindow from '../../window/IWindow.js'; import DetachedBrowserPage from './DetachedBrowserPage.js'; import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; import IBrowserFrame from '../types/IBrowserFrame.js'; -import IBrowserPageViewport from '../types/IBrowserPageViewport.js'; -import Event from '../../event/Event.js'; import Location from '../../location/Location.js'; -import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; -import DocumentReadyStateManager from '../../nodes/document/DocumentReadyStateManager.js'; -import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import IResponse from '../../fetch/types/IResponse.js'; +import BrowserFrameUtility from '../BrowserFrameUtility.js'; +import IGoToOptions from '../types/IGoToOptions.js'; /** * Browser frame. @@ -19,18 +16,16 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public readonly page: DetachedBrowserPage; public readonly window: IWindow; public _asyncTaskManager = new AsyncTaskManager(); - #windowClass: new () => IWindow; /** * Constructor. * - * @param windowClass Window class. - * @param window Window. * @param page Page. */ - constructor(windowClass: new () => IWindow, window: IWindow, page: DetachedBrowserPage) { - this.#windowClass = windowClass; - this.window = window; + constructor(page: DetachedBrowserPage) { + this.window = page.mainFrame + ? new page.context.browser.detachedWindowClass({ browserFrame: this, console: page.console }) + : page.context.browser.detachedWindow; this.page = page; } @@ -69,7 +64,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @param url URL. */ public set url(url) { - (this.window.location) = new Location(url); + (this.window.location) = new Location(url, this); } /** @@ -91,96 +86,26 @@ export default class DetachedBrowserFrame implements IBrowserFrame { this._asyncTaskManager.abortAll(); } - /** - * Aborts all ongoing operations and destroys the frame. - */ - public destroy(): void { - if (this.parentFrame) { - const index = this.parentFrame.childFrames.indexOf(this); - if (index !== -1) { - this.parentFrame.childFrames.splice(index, 1); - } - } - for (const frame of this.childFrames) { - frame.destroy(); - } - (this.window.closed) = true; - WindowBrowserSettingsReader.removeSettings(this.window); - this._asyncTaskManager.destroy(); - (this.page) = null; - (this.window) = null; - } - - /** - * Sets the viewport. - * - * @param viewport Viewport. - */ - public setViewport(viewport: IBrowserPageViewport): void { - if ( - (viewport.width !== undefined && this.window.innerWidth !== viewport.width) || - (viewport.height !== undefined && this.window.innerHeight !== viewport.height) - ) { - if (viewport.width !== undefined && this.window.innerWidth !== viewport.width) { - (this.window.innerWidth) = viewport.width; - (this.window.outerWidth) = viewport.width; - } - - if (viewport.height !== undefined && this.window.innerHeight !== viewport.height) { - (this.window.innerHeight) = viewport.height; - (this.window.outerHeight) = viewport.height; - } - - this.window.dispatchEvent(new Event('resize')); - } - } - - /** - * Creates a new frame. - * - * @returns Frame. - */ - public newFrame(): IBrowserFrame { - const frame = new DetachedBrowserFrame(this.#windowClass, new this.#windowClass(), this.page); - (frame.parentFrame) = this; - this.childFrames.push(frame); - return frame; - } - /** * Go to a page. * * @param url URL. + * @param [options] Options. */ - public async goto(url: string): Promise { - await Promise.all(this.childFrames.map((frame) => frame.destroy())); - - this._asyncTaskManager.abortAll(); - - const readyStateManager = new DocumentReadyStateManager(this.window); - (<{ _readyStateManager: DocumentReadyStateManager }>(this.window))._readyStateManager = - readyStateManager; - - readyStateManager.startTask(); - - this.url = url; - - let response: IResponse; - let responseText: string; - - try { - response = await this.window.fetch(url); - responseText = await response.text(); - } catch (error) { - this.content = ''; - readyStateManager.endTask(); - WindowErrorUtility.dispatchError(this.window, error); - return response || null; + public async goto(url: string, options?: IGoToOptions): Promise { + if ( + this.page.context === this.page.context.browser.defaultContext && + this.page.context.pages[0] === this.page && + this.page.mainFrame === this + ) { + throw new Error('The main frame cannot be navigated in a detached browser.'); } - this.window.document.write(responseText); - readyStateManager.endTask(); - - return response; + return await BrowserFrameUtility.goto( + this.page.context.browser.detachedWindowClass, + this, + url, + options + ); } } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts index bbc617c52..f1e36cdc7 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts @@ -4,7 +4,8 @@ import DetachedBrowserFrame from './DetachedBrowserFrame.js'; import DetachedBrowserContext from './DetachedBrowserContext.js'; import VirtualConsole from '../../console/VirtualConsole.js'; import IBrowserPage from '../types/IBrowserPage.js'; -import IWindow from '../../window/IWindow.js'; +import BrowserFrameUtility from '../BrowserFrameUtility.js'; +import Event from '../../event/Event.js'; /** * Detached browser page. @@ -18,14 +19,12 @@ export default class DetachedBrowserPage implements IBrowserPage { /** * Constructor. * - * @param windowClass Window class. - * @param window Window. * @param context Browser context. */ - constructor(windowClass: new () => IWindow, window: IWindow, context: DetachedBrowserContext) { + constructor(context: DetachedBrowserContext) { this.context = context; this.console = context.browser.console ?? new VirtualConsole(this.virtualConsolePrinter); - this.mainFrame = new DetachedBrowserFrame(windowClass, window, this); + this.mainFrame = new DetachedBrowserFrame(this); } /** @@ -46,14 +45,26 @@ export default class DetachedBrowserPage implements IBrowserPage { * Aborts all ongoing operations and destroys the page. */ public close(): void { - this.mainFrame.destroy(); + if (!this.mainFrame) { + return; + } + + BrowserFrameUtility.closeFrame(this.mainFrame); + const index = this.context.pages.indexOf(this); if (index !== -1) { this.context.pages.splice(index, 1); } + + const context = this.context; + (this.virtualConsolePrinter) = null; (this.mainFrame) = null; (this.context) = null; + + if (context.pages[0] === this) { + context.close(); + } } /** @@ -78,7 +89,22 @@ export default class DetachedBrowserPage implements IBrowserPage { * @param viewport Viewport. */ public setViewport(viewport: IBrowserPageViewport): void { - this.mainFrame.setViewport(viewport); + if ( + (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) || + (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) + ) { + if (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) { + (this.mainFrame.window.innerWidth) = viewport.width; + (this.mainFrame.window.outerWidth) = viewport.width; + } + + if (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) { + (this.mainFrame.window.innerHeight) = viewport.height; + (this.mainFrame.window.outerHeight) = viewport.height; + } + + this.mainFrame.window.dispatchEvent(new Event('resize')); + } } /** diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index ca71dd102..22329740d 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -1,8 +1,8 @@ import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; import IWindow from '../../window/IWindow.js'; -import IBrowserPageViewport from './IBrowserPageViewport.js'; import IBrowserPage from './IBrowserPage.js'; import IResponse from '../../fetch/types/IResponse.js'; +import IGoToOptions from './IGoToOptions.js'; /** * Browser frame. @@ -28,29 +28,11 @@ export default interface IBrowserFrame { */ abort(): void; - /** - * Aborts all ongoing operations and destroys the frame. - */ - destroy(): void; - - /** - * Sets the viewport. - * - * @param viewport Viewport. - */ - setViewport(viewport: IBrowserPageViewport): void; - - /** - * Creates a new frame. - * - * @returns Frame. - */ - newFrame(): IBrowserFrame; - /** * Go to a page. * * @param url URL. + * @param [options] Options. */ - goto(url: string): Promise; + goto(url: string, options?: IGoToOptions): Promise; } diff --git a/packages/happy-dom/src/browser/types/IGoToOptions.ts b/packages/happy-dom/src/browser/types/IGoToOptions.ts new file mode 100644 index 000000000..9ca27a6f4 --- /dev/null +++ b/packages/happy-dom/src/browser/types/IGoToOptions.ts @@ -0,0 +1,9 @@ +import IRequestReferrerPolicy from '../../fetch/types/IRequestReferrerPolicy.js'; + +/** + * Go to options. + */ +export default interface IGoToOptions { + referrer?: string; + referrerPolicy?: IRequestReferrerPolicy; +} diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index a18631331..5f534013a 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -89,7 +89,7 @@ export default class Fetch { */ public send(): Promise { return new Promise((resolve, reject) => { - const taskID = this.asyncTaskManager.startTask(() => this.abort()); + const taskID = this.asyncTaskManager.startTask(() => this.onAsyncTaskManagerAbort()); if (this.resolve) { throw new Error('Fetch already sent.'); @@ -208,6 +208,23 @@ export default class Fetch { ); } + /** + * Triggered when the async task manager aborts. + */ + private onAsyncTaskManagerAbort(): void { + const error = new DOMException('The operation was aborted.', DOMExceptionNameEnum.abortError); + + if (this.request.body) { + this.request.body.destroy(error); + } + + if (!this.response || !this.response.body) { + return; + } + + this.response.body.emit('error', error); + } + /** * Event listener for request "response" event. * diff --git a/packages/happy-dom/src/fetch/utilities/FetchCORSUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchCORSUtility.ts index c15f3daf2..9249ede19 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchCORSUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchCORSUtility.ts @@ -1,3 +1,5 @@ +import { URL } from 'url'; + /** * Fetch CORS utility. */ @@ -8,7 +10,10 @@ export default class FetchCORSUtility { * @param originURL Origin URL. * @param targetURL Target URL. */ - public static isCORS(originURL, targetURL): boolean { + public static isCORS(originURL: URL, targetURL: URL): boolean { + if (targetURL.protocol === 'about:' || targetURL.protocol === 'javascript:') { + return false; + } return ( (originURL.hostname !== targetURL.hostname && !originURL.hostname.endsWith(targetURL.hostname)) || diff --git a/packages/happy-dom/src/location/Location.ts b/packages/happy-dom/src/location/Location.ts index b9ce06287..0aaa49927 100644 --- a/packages/happy-dom/src/location/Location.ts +++ b/packages/happy-dom/src/location/Location.ts @@ -2,11 +2,12 @@ import URL from '../url/URL.js'; import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; -import DetachedBrowserFrame from '../browser/detached-browser/DetachedBrowserFrame.js'; import WindowErrorUtility from '../window/WindowErrorUtility.js'; +import DetachedBrowserFrame from '../browser/detached-browser/DetachedBrowserFrame.js'; +import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; /** - * + * Location. */ export default class Location extends URL { #browserFrame: IBrowserFrame | null; @@ -31,13 +32,25 @@ export default class Location extends URL { this.#browserFrame && !this.#browserFrame.page.context.browser.settings.disableJavaScriptEvaluation ) { - if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { - this.#browserFrame.window.eval(value.replace('javascript:', '')); - } else { - WindowErrorUtility.captureError(this.#browserFrame.window, () => - this.#browserFrame.window.eval(value.replace('javascript:', '')) - ); - } + const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( + (this.#browserFrame.window) + ))._readyStateManager; + + readyStateManager.startTask(); + + this.#browserFrame.page.mainFrame.window.setTimeout(() => { + const code = '//# sourceURL=' + super.href + '\n' + value.replace('javascript:', ''); + + if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { + this.#browserFrame.window.eval(code); + } else { + WindowErrorUtility.captureError(this.#browserFrame.window, () => + this.#browserFrame.window.eval(code) + ); + } + + readyStateManager.endTask(); + }); } return; } @@ -59,9 +72,11 @@ export default class Location extends URL { } // When using the Window instance directly and not via the Browser API we should not navigate the browser frame. - if (!(this.#browserFrame instanceof DetachedBrowserFrame)) { - this.#browserFrame?.goto(value); + if (this.#browserFrame instanceof DetachedBrowserFrame) { + return; } + + this.#browserFrame?.goto(value); } /** diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 0e4f20db0..abf6fe426 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -936,11 +936,12 @@ export default class Element extends Node implements IElement { const attribute = this.getAttribute('on' + event.type); if (attribute && !event._immediatePropagationStopped) { + const code = `//# sourceURL=${this.ownerDocument._defaultView.location.href}\n${attribute}`; if (browserSettings.disableErrorCapturing) { - this.ownerDocument._defaultView.eval(attribute); + this.ownerDocument._defaultView.eval(code); } else { WindowErrorUtility.captureError(this.ownerDocument._defaultView, () => - this.ownerDocument._defaultView.eval(attribute) + this.ownerDocument._defaultView.eval(code) ); } } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts index c9b2588f7..6f171b2c6 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts @@ -10,6 +10,7 @@ import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js' import IResponse from '../../fetch/types/IResponse.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; +import BrowserFrameUtility from '../../browser/BrowserFrameUtility.js'; /** * HTML Iframe page loader. @@ -45,7 +46,7 @@ export default class HTMLIFrameElementPageLoader { public loadPage(): void { if (!this.#element.isConnected) { if (this.#browserIFrame) { - this.#browserIFrame.destroy(); + BrowserFrameUtility.closeFrame(this.#browserIFrame); this.#browserIFrame = null; } this.#contentWindowContainer.window = null; @@ -55,7 +56,7 @@ export default class HTMLIFrameElementPageLoader { let url = this.#element.src || 'about:blank'; if (url !== 'about:blank' && !url.startsWith('javascript:')) { - url = new URL(this.#element.src, this.#browserParentFrame.window.location.href).href; + url = new URL(this.#element.src, this.#browserParentFrame.window.location).href; } if (this.#browserIFrame && url === this.#browserIFrame.url) { @@ -63,7 +64,7 @@ export default class HTMLIFrameElementPageLoader { } if (this.#browserIFrame) { - this.#browserIFrame.destroy(); + BrowserFrameUtility.closeFrame(this.#browserIFrame); this.#browserIFrame = null; } @@ -81,7 +82,7 @@ export default class HTMLIFrameElementPageLoader { } const window = this.#element.ownerDocument._defaultView; - this.#browserIFrame = this.#browserParentFrame.newFrame(); + this.#browserIFrame = BrowserFrameUtility.newFrame(this.#browserParentFrame); if (url === 'about:blank' || url.startsWith('javascript:')) { (this.#browserIFrame.window.parent) = window; @@ -92,11 +93,12 @@ export default class HTMLIFrameElementPageLoader { url !== 'about:blank' && !this.#browserParentFrame.page.context.browser.settings.disableJavaScriptEvaluation ) { + const code = '//# sourceURL=about:blank\n' + url.replace('javascript:', ''); if (this.#browserParentFrame.page.context.browser.settings.disableErrorCapturing) { - this.#browserIFrame.window.eval(url.replace('javascript:', '')); + this.#browserIFrame.window.eval(code); } else { WindowErrorUtility.captureError(this.#browserIFrame.window, () => - this.#browserIFrame.window.eval(url.replace('javascript:', '')) + this.#browserIFrame.window.eval(code) ); } } @@ -154,7 +156,7 @@ export default class HTMLIFrameElementPageLoader { */ public unloadPage(): void { if (this.#browserIFrame) { - this.#browserIFrame.destroy(); + BrowserFrameUtility.closeFrame(this.#browserIFrame); this.#browserIFrame = null; } this.#contentWindowContainer.window = null; diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index c8888fd09..b14762487 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -195,11 +195,14 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip ) { this.ownerDocument['_currentScript'] = this; + const code = + `//# sourceURL=${this.ownerDocument._defaultView.location.href}\n` + textContent; + if (browserSettings.disableErrorCapturing) { - this.ownerDocument._defaultView.eval(textContent); + this.ownerDocument._defaultView.eval(code); } else { WindowErrorUtility.captureError(this.ownerDocument._defaultView, () => - this.ownerDocument._defaultView.eval(textContent) + this.ownerDocument._defaultView.eval(code) ); } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts index d5e2eab95..f2af9e634 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts @@ -64,6 +64,7 @@ export default class HTMLScriptElementUtility { } } else { element.ownerDocument['_currentScript'] = element; + code = '//# sourceURL=' + src + '\n' + code; if (browserSettings.disableErrorCapturing) { element.ownerDocument._defaultView.eval(code); } else { diff --git a/packages/happy-dom/src/window/CrossOriginWindow.ts b/packages/happy-dom/src/window/CrossOriginWindow.ts index d13c78585..317312bf5 100644 --- a/packages/happy-dom/src/window/CrossOriginWindow.ts +++ b/packages/happy-dom/src/window/CrossOriginWindow.ts @@ -31,6 +31,7 @@ export default class CrossOriginWindow extends EventTarget implements ICrossOrig {}, { get: () => { + debugger; throw new DOMException( `Blocked a frame with origin "${this.parent.location.origin}" from accessing a cross-origin frame.`, DOMExceptionNameEnum.securityError diff --git a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts index 385a31ce8..d37d93ea7 100644 --- a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts +++ b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts @@ -117,7 +117,7 @@ export default class HappyDOMWindowAPI { * @param viewport Viewport. */ public setViewport(viewport: IBrowserPageViewport): void { - this.#browserFrame.setViewport(viewport); + this.#browserFrame.page.setViewport(viewport); } /** diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index beaffb03a..b5d8a6a84 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -387,7 +387,6 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly parent: IWindow | ICrossOriginWindow; readonly window: IWindow; readonly globalThis: IWindow; - readonly name: string; readonly screen: Screen; readonly innerWidth: number; readonly innerHeight: number; @@ -406,6 +405,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly scrollY: number; readonly crypto: typeof webcrypto; readonly closed: boolean; + name: string; /** * Returns an object containing the values of all CSS properties of an element. diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index b93a3483d..e91f25e43 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -406,7 +406,6 @@ export default class Window extends EventTarget implements IWindow { public readonly parent: IWindow = this; public readonly window: IWindow = this; public readonly globalThis: IWindow = this; - public readonly name: string = ''; public readonly screen: Screen; public readonly devicePixelRatio = 1; public readonly sessionStorage: Storage; @@ -422,6 +421,8 @@ export default class Window extends EventTarget implements IWindow { public readonly screenY: number = 0; public readonly crypto = webcrypto; public readonly closed = false; + public readonly console: Console; + public name: string = ''; // Node.js Globals public Array: typeof Array; @@ -543,6 +544,7 @@ export default class Window extends EventTarget implements IWindow { WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); + this.console = this.#browserFrame.page.console; this.happyDOM = new HappyDOMWindowAPI(this.#browserFrame); if (options) { @@ -722,15 +724,6 @@ export default class Window extends EventTarget implements IWindow { }); } - /** - * Returns console. - * - * @returns Console. - */ - public get console(): Console { - return this.#browserFrame.page.console; - } - /** * The number of pixels that the document is currently scrolled horizontally * @@ -883,8 +876,6 @@ export default class Window extends EventTarget implements IWindow { public close(): void { if (this.#browserFrame.page.mainFrame === this.#browserFrame) { this.#browserFrame.page.close(); - } else { - this.#browserFrame.destroy(); } } diff --git a/packages/happy-dom/src/window/WindowPageOpenUtility.ts b/packages/happy-dom/src/window/WindowPageOpenUtility.ts index af71a0fe6..31b0192e9 100644 --- a/packages/happy-dom/src/window/WindowPageOpenUtility.ts +++ b/packages/happy-dom/src/window/WindowPageOpenUtility.ts @@ -1,11 +1,10 @@ import { URL } from 'url'; import IWindow from './IWindow.js'; import CrossOriginWindow from './CrossOriginWindow.js'; -import WindowErrorUtility from './WindowErrorUtility.js'; -import Window from './Window.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; import FetchCORSUtility from '../fetch/utilities/FetchCORSUtility.js'; import ICrossOriginWindow from './ICrossOriginWindow.js'; +import DetachedBrowserFrame from '../browser/detached-browser/DetachedBrowserFrame.js'; /** * Window page open handler. @@ -29,76 +28,94 @@ export default class WindowPageOpenUtility { } ): IWindow | ICrossOriginWindow | null { const features = this.getWindowFeatures(options?.features || ''); - const url = options?.url || 'about:blank'; const target = options?.target !== undefined ? String(options.target) : null; - const newPage = browserFrame.page.context.newPage(); - const newWindow = newPage.mainFrame.window; - const originURL = browserFrame.window.location; - const targetURL = new URL(url, originURL); - const isCORS = FetchCORSUtility.isCORS(originURL, targetURL); + let targetFrame: IBrowserFrame; - newPage.mainFrame.url = url; + // When using the Window instance directly and not via the Browser API we should not navigate the browser frame. + if ( + browserFrame instanceof DetachedBrowserFrame && + (target === '_self' || target === '_top' || target === '_parent') + ) { + return null; + } - (newWindow.document.referrer) = !features.noreferrer ? browserFrame.url : ''; + switch (target) { + case '_self': + targetFrame = browserFrame; + break; + case '_top': + targetFrame = browserFrame.page.mainFrame; + break; + case '_parent': + targetFrame = browserFrame.parentFrame ?? browserFrame; + break; + case '_blank': + default: + const newPage = browserFrame.page.context.newPage(); + targetFrame = newPage.mainFrame; + break; + } - if (!features.noopener) { - (newWindow.opener) = isCORS - ? new CrossOriginWindow(browserFrame.window) - : browserFrame.window; + let url = options.url || 'about:blank'; + + if (url.startsWith('about:') || url.startsWith('javascript:')) { + url = new URL(url, browserFrame.window.location).href; } - if (target) { - (newWindow.name) = target; + targetFrame.goto(url, { + referrer: features.noreferrer ? 'no-referrer' : undefined + }); + + if (url.startsWith('javascript:')) { + return targetFrame.window; } - if (features?.left) { - (newWindow.screenLeft) = features.left; - (newWindow.screenX) = features.left; + if (targetFrame === browserFrame) { + return null; } - if (features?.top) { - (newWindow.screenTop) = features.top; - (newWindow.screenY) = features.top; + if (features.popup && target !== '_self' && target !== '_top' && target !== '_parent') { + if (features?.width || features?.height) { + targetFrame.page.setViewport({ + width: features?.width, + height: features?.height + }); + } + + if (features?.left) { + (targetFrame.window.screenLeft) = features.left; + (targetFrame.window.screenX) = features.left; + } + + if (features?.top) { + (targetFrame.window.screenTop) = features.top; + (targetFrame.window.screenY) = features.top; + } } - if (url === 'about:blank') { - return features.noopener ? null : newWindow; + if (target) { + (targetFrame.window.name) = target; } - if (url.startsWith('javascript:')) { - if (!browserFrame.page.context.browser.settings.disableJavaScriptEvaluation) { - if (browserFrame.page.context.browser.settings.disableErrorCapturing) { - newWindow.eval(url.replace('javascript:', '')); - } else { - WindowErrorUtility.captureError(newWindow, () => - newWindow.eval(url.replace('javascript:', '')) - ); - } - } - return features.noopener ? null : newWindow; + const originURL = browserFrame.window.location; + const targetURL = new URL(targetFrame.url, originURL); + const isCORS = FetchCORSUtility.isCORS(originURL, targetURL); + + if (!features.noopener && !features.noreferrer) { + (targetFrame.window.opener) = isCORS + ? new CrossOriginWindow(browserFrame.window) + : browserFrame.window; } - newWindow._readyStateManager.startTask(); - - newWindow - .fetch(url, { - referrer: features.noreferrer ? 'no-referrer' : undefined - }) - .then((response) => response.text()) - .then((responseText) => { - newPage.mainFrame.content = responseText; - newWindow._readyStateManager.endTask(); - }) - .catch((error) => { - WindowErrorUtility.dispatchError(newWindow, error); - newWindow._readyStateManager.endTask(); - }); - - if (features.noopener) { + if (features.noopener || features.noreferrer) { return null; } - return isCORS ? new CrossOriginWindow(newWindow, browserFrame.window) : newWindow; + if (isCORS) { + return new CrossOriginWindow(targetFrame.window, browserFrame.window); + } + + return targetFrame.window; } /** diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 728075f4d..ac3a868d2 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -31,6 +31,9 @@ import Clipboard from '../../src/clipboard/Clipboard.js'; import PackageVersion from '../../src/version.js'; import IHTMLDialogElement from '../../src/nodes/html-dialog-element/IHTMLDialogElement.js'; import Browser from '../../src/browser/Browser.js'; +import ICrossOriginWindow from '../../src/window/ICrossOriginWindow.js'; +import CrossOriginWindow from '../../src/window/CrossOriginWindow.js'; +import BrowserFrameUtility from '../../src/browser/BrowserFrameUtility.js'; const GET_NAVIGATOR_PLATFORM = (): string => { return ( @@ -1350,42 +1353,31 @@ describe('Window', () => { }); }); - it('Triggers "error" event if there are problems loading resources.', async () => { + it('Triggers "error" when an error occurs in the executed code.', async () => { await new Promise((resolve) => { - const cssURL = '/path/to/file.css'; - const jsURL = '/path/to/file.js'; const errorEvents: ErrorEvent[] = []; - vi.spyOn(ResourceFetch, 'fetch').mockImplementation( - async (_document: IDocument, url: string) => { - throw new Error(url); - } - ); - window.addEventListener('error', (event) => { errorEvents.push(event); }); const script = document.createElement('script'); - script.async = true; - script.src = jsURL; - - const link = document.createElement('link'); - link.href = cssURL; - link.rel = 'stylesheet'; - + script.innerText = 'throw new Error("Script error");'; document.body.appendChild(script); - document.body.appendChild(link); + + window.setTimeout(() => { + throw new Error('Timeout error'); + }); setTimeout(() => { expect(errorEvents.length).toBe(2); expect(errorEvents[0].target).toBe(window); - expect((errorEvents[0].error).message).toBe(jsURL); + expect((errorEvents[0].error).message).toBe('Script error'); expect(errorEvents[1].target).toBe(window); - expect((errorEvents[1].error).message).toBe(cssURL); + expect((errorEvents[1].error).message).toBe('Timeout error'); resolve(null); - }, 0); + }, 10); }); }); }); @@ -1456,34 +1448,35 @@ describe('Window', () => { describe('postMessage()', () => { it('Posts a message.', async () => { await new Promise((resolve) => { + const browser = new Browser(); + const page = browser.newPage(); + const frame = BrowserFrameUtility.newFrame(page.mainFrame); + const message = 'test'; - const parentOrigin = 'https://localhost:8080'; - const parent = new Window({ - url: parentOrigin - }); let triggeredEvent: MessageEvent | null = null; - (window.parent) = parent; + page.mainFrame.url = 'https://localhost:8080/test/'; - window.addEventListener('message', (event) => (triggeredEvent = event)); - window.postMessage(message); + frame.url = 'https://localhost:8080/iframe.html'; + frame.window.addEventListener('message', (event) => (triggeredEvent = event)); + frame.window.postMessage(message); expect(triggeredEvent).toBe(null); setTimeout(() => { expect((triggeredEvent).data).toBe(message); - expect((triggeredEvent).origin).toBe(parentOrigin); - expect((triggeredEvent).source).toBe(parent); + expect((triggeredEvent).origin).toBe('https://localhost:8080'); + expect((triggeredEvent).source).toBe(page.mainFrame.window); expect((triggeredEvent).lastEventId).toBe(''); triggeredEvent = null; - window.postMessage(message, '*'); + frame.window.postMessage(message, '*'); expect(triggeredEvent).toBe(null); setTimeout(() => { expect((triggeredEvent).data).toBe(message); - expect((triggeredEvent).origin).toBe(parentOrigin); - expect((triggeredEvent).source).toBe(parent); + expect((triggeredEvent).origin).toBe('https://localhost:8080'); + expect((triggeredEvent).source).toBe(page.mainFrame.window); expect((triggeredEvent).lastEventId).toBe(''); resolve(null); }, 10); @@ -1537,38 +1530,145 @@ describe('Window', () => { describe('open()', () => { it('Opens a new window without URL.', () => { - const newWindow = window.open(); + const newWindow = window.open(); + expect(newWindow).toBeInstanceOf(Window); + expect(newWindow.location.href).toBe('about:blank'); + }); + + it('Opens a URL with Javascript.', async () => { + const newWindow = window.open(`javascript:document.write('Test');`); + expect(newWindow).toBeInstanceOf(Window); + expect(newWindow.location.href).toBe('about:blank'); + await new Promise((resolve) => setTimeout(resolve, 1)); + expect(newWindow.document.body.innerHTML).toBe('Test'); + }); + + it('Dispatches error event when the Javascript code is invalid.', async () => { + const newWindow = window.open(`javascript:document.write(test);`); + let errorEvent: ErrorEvent | null = null; + window.addEventListener('error', (event) => (errorEvent = event)); expect(newWindow).toBeInstanceOf(Window); - expect(newWindow?.location.href).toBe('about:blank'); + expect(newWindow.location.href).toBe('about:blank'); + await new Promise((resolve) => setTimeout(resolve, 1)); + expect(String(((errorEvent)).error)).toBe( + 'ReferenceError: test is not defined' + ); }); - it('Opens a new window with URL.', () => { + it('Opens a new window with URL.', async () => { + const html = 'Test'; + let request: IRequest | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + text: () => new Promise((resolve) => setTimeout(() => resolve(html))) + }); + }); + window.happyDOM.setURL('https://localhost:8080/test/'); - const newWindow = window.open('/path/to/file.html'); + + const newWindow = window.open('/path/to/file.html'); + expect(newWindow).toBeInstanceOf(Window); + expect(newWindow.location.href).toBe('https://localhost:8080/path/to/file.html'); + expect(((request)).url).toBe('https://localhost:8080/path/to/file.html'); + + await new Promise((resolve) => { + newWindow.addEventListener('load', () => { + expect(newWindow.document.body.innerHTML).toBe('Test'); + resolve(null); + }); + }); + }); + + it('Sets width, height, top and left when popup is set as a feature.', () => { + const newWindow = ( + window.open('', '', 'popup=yes,width=100,height=200,top=300,left=400') + ); expect(newWindow).toBeInstanceOf(Window); - expect(newWindow?.location.href).toBe('https://localhost:8080/path/to/file.html'); + expect(newWindow.innerWidth).toBe(100); + expect(newWindow.innerHeight).toBe(200); + expect(newWindow.screenLeft).toBe(400); + expect(newWindow.screenX).toBe(400); + expect(newWindow.screenTop).toBe(300); + expect(newWindow.screenY).toBe(300); }); - it('Opens a new window with the Browser API.', () => { + it(`Doesn't Sets width, height, top and left when popup is set as a feature.`, () => { + const newWindow = window.open('', '', 'width=100,height=200,top=300,left=400'); + expect(newWindow).toBeInstanceOf(Window); + expect(newWindow.innerWidth).toBe(1024); + expect(newWindow.innerHeight).toBe(768); + expect(newWindow.screenLeft).toBe(0); + expect(newWindow.screenX).toBe(0); + expect(newWindow.screenTop).toBe(0); + expect(newWindow.screenY).toBe(0); + }); + + it('Sets the target as name on the Window instance.', () => { + const newWindow = window.open('', 'test'); + expect(newWindow).toBeInstanceOf(Window); + expect(newWindow.name).toBe('test'); + }); + + it(`Doesn't set opener if "noopener" has been specified as a feature without an URL.`, () => { const browser = new Browser(); const page = browser.newPage(); + const newWindow = page.mainFrame.window.open('', '', 'noopener'); + expect(newWindow).toBe(null); + expect(browser.defaultContext.pages[1].mainFrame.window.opener).toBe(null); + }); - page.mainFrame.url = 'https://localhost:8080/test/'; + it(`Doesn't set opener if "noopener" has been specified as a feature when opening an URL.`, () => { + const browser = new Browser(); + const page = browser.newPage(); + page.mainFrame.url = 'https://www.github.com/happy-dom/'; + const newWindow = page.mainFrame.window.open('/test/', '', 'noopener'); + expect(newWindow).toBe(null); + expect(browser.defaultContext.pages[1].mainFrame.window.opener).toBe(null); + }); + + it('Opens a new window with a CORS URL.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const html = 'Test'; + let request: IRequest | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + text: () => new Promise((resolve) => setTimeout(() => resolve(html))) + }); + }); + + page.mainFrame.url = 'https://www.github.com/happy-dom/'; - const newWindow = page.mainFrame.window.open('/path/to/file.html'); + const newWindow = ( + page.mainFrame.window.open('https://developer.mozilla.org/en-US/docs/Web/API/Window/open') + ); expect(browser.defaultContext.pages.length).toBe(2); expect(browser.defaultContext.pages[0]).toBe(page); - expect(browser.defaultContext.pages[1].mainFrame.window).toBe(newWindow); + expect(browser.defaultContext.pages[1].mainFrame.window === newWindow).toBe(false); expect(browser.defaultContext.pages[1].mainFrame.url).toBe( - 'https://localhost:8080/path/to/file.html' + 'https://developer.mozilla.org/en-US/docs/Web/API/Window/open' ); + expect(newWindow instanceof CrossOriginWindow).toBe(true); - newWindow?.close(); + await new Promise((resolve) => { + browser.defaultContext.pages[1].mainFrame.window.addEventListener('load', () => { + expect(browser.defaultContext.pages[1].mainFrame.content).toBe( + 'Test' + ); - expect(browser.defaultContext.pages.length).toBe(1); - expect(browser.defaultContext.pages[0]).toBe(page); - expect(newWindow?.closed).toBe(true); + newWindow.close(); + + expect(browser.defaultContext.pages.length).toBe(1); + expect(browser.defaultContext.pages[0]).toBe(page); + expect(newWindow.closed).toBe(true); + resolve(null); + }); + }); }); }); }); From 1fc61c66c0c7979d19d6c4e9fb181f8b1f85a817 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 2 Nov 2023 01:19:59 +0100 Subject: [PATCH 19/63] #466@trivial: Continues on implementation. --- packages/happy-dom/src/browser/BrowserFrame.ts | 3 ++- .../happy-dom/src/browser/BrowserFrameUtility.ts | 2 +- .../browser/detached-browser/DetachedBrowserFrame.ts | 5 +++-- packages/happy-dom/src/nodes/document/Document.ts | 12 ++++++++++++ packages/happy-dom/src/window/Window.ts | 2 +- .../happy-dom/src/window/WindowPageOpenUtility.ts | 2 +- .../happy-dom/test/nodes/document/Document.test.ts | 8 ++++++++ 7 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index ce20370ae..82263c900 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -46,7 +46,8 @@ export default class BrowserFrame implements IBrowserFrame { */ public set content(content) { this.window.document['_isFirstWrite'] = true; - this.window.document['_isFirstWriteAfterOpen'] = true; + this.window.document['_isFirstWriteAfterOpen'] = false; + this.window.document.open(); this.window.document.write(content); } diff --git a/packages/happy-dom/src/browser/BrowserFrameUtility.ts b/packages/happy-dom/src/browser/BrowserFrameUtility.ts index 7412cfd65..31a1b2483 100644 --- a/packages/happy-dom/src/browser/BrowserFrameUtility.ts +++ b/packages/happy-dom/src/browser/BrowserFrameUtility.ts @@ -122,7 +122,7 @@ export default class BrowserFrameUtility { return response || null; } - frame.window.document.write(responseText); + frame.content = responseText; readyStateManager.endTask(); return response; diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index d16e1db89..98054e23a 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -23,10 +23,10 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @param page Page. */ constructor(page: DetachedBrowserPage) { + this.page = page; this.window = page.mainFrame ? new page.context.browser.detachedWindowClass({ browserFrame: this, console: page.console }) : page.context.browser.detachedWindow; - this.page = page; } /** @@ -45,7 +45,8 @@ export default class DetachedBrowserFrame implements IBrowserFrame { */ public set content(content) { this.window.document['_isFirstWrite'] = true; - this.window.document['_isFirstWriteAfterOpen'] = true; + this.window.document['_isFirstWriteAfterOpen'] = false; + this.window.document.open(); this.window.document.write(content); } diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 7122283bf..1784f883f 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -694,6 +694,18 @@ export default class Document extends Node implements IDocument { } this.appendChild(documentElement); + + const head = ParentNodeUtility.getElementByTagName(this, 'head'); + let body = ParentNodeUtility.getElementByTagName(this, 'body'); + + if (!body) { + body = this.createElement('body'); + documentElement.appendChild(this.createElement('body')); + } + + if (!head) { + documentElement.insertBefore(this.createElement('head'), body); + } } else { const rootBody = ParentNodeUtility.getElementByTagName(root, 'body'); const body = ParentNodeUtility.getElementByTagName(this, 'body'); diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index e91f25e43..d9657d47a 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -526,7 +526,6 @@ export default class Window extends EventTarget implements IWindow { super(); this.customElements = new CustomElementRegistry(); - this.location = new Location(); this.navigator = new Navigator(this); this.history = new History(); this.screen = new Screen(); @@ -545,6 +544,7 @@ export default class Window extends EventTarget implements IWindow { WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); this.console = this.#browserFrame.page.console; + this.location = new Location('about:blank', this.#browserFrame); this.happyDOM = new HappyDOMWindowAPI(this.#browserFrame); if (options) { diff --git a/packages/happy-dom/src/window/WindowPageOpenUtility.ts b/packages/happy-dom/src/window/WindowPageOpenUtility.ts index 31b0192e9..5872b604d 100644 --- a/packages/happy-dom/src/window/WindowPageOpenUtility.ts +++ b/packages/happy-dom/src/window/WindowPageOpenUtility.ts @@ -58,7 +58,7 @@ export default class WindowPageOpenUtility { let url = options.url || 'about:blank'; - if (url.startsWith('about:') || url.startsWith('javascript:')) { + if (!url.startsWith('about:') && !url.startsWith('javascript:')) { url = new URL(url, browserFrame.window.location).href; } diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts index 3ad70c125..8ccedcfb2 100644 --- a/packages/happy-dom/test/nodes/document/Document.test.ts +++ b/packages/happy-dom/test/nodes/document/Document.test.ts @@ -839,6 +839,14 @@ describe('Document', () => { `.replace(/[\s]/gm, '') ); }); + + it('Adds elements outside of the tag to the tag.', () => { + const html = `Test>`; + document.write(html); + expect(document.documentElement.outerHTML).toBe( + 'Test>' + ); + }); }); describe('open()', () => { From eda600f2c48a6531e38759e6b445cfb89bad75ce Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 2 Nov 2023 01:25:03 +0100 Subject: [PATCH 20/63] #466@trivial: Continues on implementation. --- .../HTMLScriptElementUtility.ts | 59 ++++++------------- 1 file changed, 19 insertions(+), 40 deletions(-) diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts index f2af9e634..4233ea2c6 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts @@ -39,14 +39,14 @@ export default class HTMLScriptElementUtility { return; } + let code: string | null = null; + let error: Error | null = null; + if (async) { (<{ _readyStateManager: DocumentReadyStateManager }>( (element.ownerDocument._defaultView) ))._readyStateManager.startTask(); - let code: string | null = null; - let error: Error | null = null; - try { code = await ResourceFetch.fetch(element.ownerDocument, src); } catch (e) { @@ -56,52 +56,31 @@ export default class HTMLScriptElementUtility { (<{ _readyStateManager: DocumentReadyStateManager }>( (element.ownerDocument._defaultView) ))._readyStateManager.endTask(); - - if (error) { - WindowErrorUtility.dispatchError(element, error); - if (browserSettings.disableErrorCapturing) { - throw error; - } - } else { - element.ownerDocument['_currentScript'] = element; - code = '//# sourceURL=' + src + '\n' + code; - if (browserSettings.disableErrorCapturing) { - element.ownerDocument._defaultView.eval(code); - } else { - WindowErrorUtility.captureError(element.ownerDocument._defaultView, () => - element.ownerDocument._defaultView.eval(code) - ); - } - element.ownerDocument['_currentScript'] = null; - element.dispatchEvent(new Event('load')); - } } else { - let code: string | null = null; - let error: Error | null = null; - try { code = ResourceFetch.fetchSync(element.ownerDocument, src); } catch (e) { error = e; } + } - if (error) { - WindowErrorUtility.dispatchError(element, error); - if (browserSettings.disableErrorCapturing) { - throw error; - } + if (error) { + WindowErrorUtility.dispatchError(element, error); + if (browserSettings.disableErrorCapturing) { + throw error; + } + } else { + element.ownerDocument['_currentScript'] = element; + code = '//# sourceURL=' + src + '\n' + code; + if (browserSettings.disableErrorCapturing) { + element.ownerDocument._defaultView.eval(code); } else { - element.ownerDocument['_currentScript'] = element; - if (browserSettings.disableErrorCapturing) { - element.ownerDocument._defaultView.eval(code); - } else { - WindowErrorUtility.captureError(element.ownerDocument._defaultView, () => - element.ownerDocument._defaultView.eval(code) - ); - } - element.ownerDocument['_currentScript'] = null; - element.dispatchEvent(new Event('load')); + WindowErrorUtility.captureError(element.ownerDocument._defaultView, () => + element.ownerDocument._defaultView.eval(code) + ); } + element.ownerDocument['_currentScript'] = null; + element.dispatchEvent(new Event('load')); } } } From 81255d52e2d2d87bdced04c10c330e5027da6aca Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 3 Nov 2023 18:24:21 +0100 Subject: [PATCH 21/63] #466@trivial: Continues on implementation. --- .../src/browser/BrowserFrameUtility.ts | 114 +++++++++++++++++- .../src/browser/DefaultBrowserSettings.ts | 4 +- .../detached-browser/DetachedBrowserFrame.ts | 8 -- .../src/browser/types/IBrowserSettings.ts | 1 + .../browser/types/IOptionalBrowserSettings.ts | 1 + .../src/fetch/utilities/FetchCORSUtility.ts | 6 +- packages/happy-dom/src/location/Location.ts | 61 +--------- .../happy-dom/src/nodes/element/Element.ts | 1 + .../html-anchor-element/HTMLAnchorElement.ts | 11 +- .../HTMLIFrameElementPageLoader.ts | 2 +- .../HTMLUnknownElement.ts | 1 + .../happy-dom/src/window/CrossOriginWindow.ts | 2 +- .../happy-dom/src/window/HappyDOMWindowAPI.ts | 3 +- packages/happy-dom/src/window/Window.ts | 2 +- .../src/window/WindowPageOpenUtility.ts | 47 ++++---- .../HTMLAnchorElement.test.ts | 96 ++++++++++++++- packages/happy-dom/test/window/Window.test.ts | 89 +++++++++++++- 17 files changed, 341 insertions(+), 108 deletions(-) diff --git a/packages/happy-dom/src/browser/BrowserFrameUtility.ts b/packages/happy-dom/src/browser/BrowserFrameUtility.ts index 31a1b2483..d6d70d1ce 100644 --- a/packages/happy-dom/src/browser/BrowserFrameUtility.ts +++ b/packages/happy-dom/src/browser/BrowserFrameUtility.ts @@ -8,6 +8,10 @@ import IResponse from '../fetch/types/IResponse.js'; import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; import IWindow from '../window/IWindow.js'; import WindowErrorUtility from '../window/WindowErrorUtility.js'; +import DetachedBrowserFrame from './detached-browser/DetachedBrowserFrame.js'; +import { URL } from 'url'; +import DOMException from '../exception/DOMException.js'; +import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; /** * Browser frame utility. @@ -73,8 +77,38 @@ export default class BrowserFrameUtility { url: string, options?: IGoToOptions ): Promise { + url = this.getRelativeURL(frame, url); + if (url.startsWith('javascript:')) { - frame.window.location.href = url; + if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { + const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( + (frame.window) + ))._readyStateManager; + + readyStateManager.startTask(); + + frame.page.mainFrame.window.setTimeout(() => { + const code = '//# sourceURL=' + frame.url + '\n' + url.replace('javascript:', ''); + + if (frame.page.context.browser.settings.disableErrorCapturing) { + frame.window.eval(code); + } else { + WindowErrorUtility.captureError(frame.window, () => frame.window.eval(code)); + } + + readyStateManager.endTask(); + }); + } + return null; + } + + if ( + this.isDetachedMainFrame(frame) || + !this.isBrowserNavigationAllowed(frame, frame.url, url) + ) { + if (frame.page.context.browser.settings.browserNavigation.includes('url-set-fallback')) { + frame.url = url; + } return null; } @@ -82,6 +116,7 @@ export default class BrowserFrameUtility { BrowserFrameUtility.closeFrame(childFrame); } + (frame.childFrames) = []; (frame.window.closed) = true; frame._asyncTaskManager.destroy(); WindowBrowserSettingsReader.removeSettings(frame.window); @@ -127,4 +162,81 @@ export default class BrowserFrameUtility { return response; } + + /** + * Returns true if the frame is a detached main frame. + * + * @param frame Frame. + * @returns True if the frame is a detached main frame. + */ + public static isBrowserNavigationAllowed( + frame: IBrowserFrame, + fromURL: string, + toURL: string + ): boolean { + const settings = frame.page.context.browser.settings; + + if (settings.browserNavigation.includes('deny')) { + return false; + } + + if ( + settings.browserNavigation.includes('sameorigin') && + new URL(fromURL).origin !== new URL(toURL).origin + ) { + return false; + } + + if (settings.browserNavigation.includes('child-only') && frame.page.mainFrame === frame) { + return false; + } + + return true; + } + + /** + * Returns true if the frame is a detached main frame. + * + * @param frame Frame. + * @returns True if the frame is a detached main frame. + */ + public static isDetachedMainFrame(frame: IBrowserFrame): boolean { + return ( + frame instanceof DetachedBrowserFrame && + frame.page.context === frame.page.context.browser.defaultContext && + frame.page.context.pages[0] === frame.page && + frame.page.mainFrame === frame + ); + } + + /** + * Returns relative URL. + * + * @param frame Frame. + * @param url URL. + * @returns Relative URL. + */ + public static getRelativeURL(frame: IBrowserFrame, url: string): string { + url = url || 'about:blank'; + + if (url.startsWith('about:') || url.startsWith('javascript:')) { + return url; + } + + try { + return new URL(url, frame.window.location).href; + } catch (e) { + if (frame.window.location.hostname) { + throw new DOMException( + `Failed to construct URL from string "${url}".`, + DOMExceptionNameEnum.uriMismatchError + ); + } else { + throw new DOMException( + `Failed to construct URL from string "${url}" relative to URL "${frame.window.location.href}".`, + DOMExceptionNameEnum.uriMismatchError + ); + } + } + } } diff --git a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts index d976f43d6..4479a5445 100644 --- a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts +++ b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts @@ -1,6 +1,7 @@ import PackageVersion from '../version.js'; +import IBrowserSettings from './types/IBrowserSettings.js'; -export default { +export default { disableJavaScriptEvaluation: false, disableJavaScriptFileLoading: false, disableCSSFileLoading: false, @@ -9,6 +10,7 @@ export default { disableComputedStyleRendering: false, disableErrorCapturing: false, enableFileSystemHttpRequests: false, + browserNavigation: ['allow'], navigator: { userAgent: `Mozilla/5.0 (X11; ${ process.platform.charAt(0).toUpperCase() + process.platform.slice(1) + ' ' + process.arch diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index 98054e23a..3768fbc36 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -94,14 +94,6 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @param [options] Options. */ public async goto(url: string, options?: IGoToOptions): Promise { - if ( - this.page.context === this.page.context.browser.defaultContext && - this.page.context.pages[0] === this.page && - this.page.mainFrame === this - ) { - throw new Error('The main frame cannot be navigated in a detached browser.'); - } - return await BrowserFrameUtility.goto( this.page.context.browser.detachedWindowClass, this, diff --git a/packages/happy-dom/src/browser/types/IBrowserSettings.ts b/packages/happy-dom/src/browser/types/IBrowserSettings.ts index 4339bdbaa..a3ca08fca 100644 --- a/packages/happy-dom/src/browser/types/IBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IBrowserSettings.ts @@ -10,6 +10,7 @@ export default interface IBrowserSettings { disableComputedStyleRendering: boolean; disableErrorCapturing: boolean; enableFileSystemHttpRequests: boolean; + browserNavigation: Array<'allow' | 'deny' | 'sameorigin' | 'child-only' | 'url-set-fallback'>; navigator: { userAgent: string; }; diff --git a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts index 5832e17b2..955401b56 100644 --- a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts @@ -10,6 +10,7 @@ export default interface IOptionalBrowserSettings { disableComputedStyleRendering?: boolean; disableErrorCapturing?: boolean; enableFileSystemHttpRequests?: boolean; + browserNavigation?: Array<'allow' | 'deny' | 'sameorigin' | 'child-only' | 'url-set-fallback'>; navigator?: { userAgent?: string; }; diff --git a/packages/happy-dom/src/fetch/utilities/FetchCORSUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchCORSUtility.ts index 9249ede19..a3c8e75b8 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchCORSUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchCORSUtility.ts @@ -10,10 +10,14 @@ export default class FetchCORSUtility { * @param originURL Origin URL. * @param targetURL Target URL. */ - public static isCORS(originURL: URL, targetURL: URL): boolean { + public static isCORS(originURL: URL | string, targetURL: URL | string): boolean { + originURL = typeof originURL === 'string' ? new URL(originURL) : originURL; + targetURL = typeof targetURL === 'string' ? new URL(targetURL) : targetURL; + if (targetURL.protocol === 'about:' || targetURL.protocol === 'javascript:') { return false; } + return ( (originURL.hostname !== targetURL.hostname && !originURL.hostname.endsWith(targetURL.hostname)) || diff --git a/packages/happy-dom/src/location/Location.ts b/packages/happy-dom/src/location/Location.ts index 0aaa49927..01d6b6f46 100644 --- a/packages/happy-dom/src/location/Location.ts +++ b/packages/happy-dom/src/location/Location.ts @@ -1,10 +1,6 @@ import URL from '../url/URL.js'; -import DOMException from '../exception/DOMException.js'; -import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; -import WindowErrorUtility from '../window/WindowErrorUtility.js'; -import DetachedBrowserFrame from '../browser/detached-browser/DetachedBrowserFrame.js'; -import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; +import BrowserFrameUtility from '../browser/BrowserFrameUtility.js'; /** * Location. @@ -18,7 +14,7 @@ export default class Location extends URL { * @param [url] URL. */ constructor(url = 'about:blank', browserFrame?: IBrowserFrame) { - super(url); + super(browserFrame ? BrowserFrameUtility.getRelativeURL(browserFrame, url) : url); this.#browserFrame = browserFrame ?? null; } @@ -27,55 +23,6 @@ export default class Location extends URL { */ // @ts-ignore public set href(value: string) { - if (value.startsWith('javascript:')) { - if ( - this.#browserFrame && - !this.#browserFrame.page.context.browser.settings.disableJavaScriptEvaluation - ) { - const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( - (this.#browserFrame.window) - ))._readyStateManager; - - readyStateManager.startTask(); - - this.#browserFrame.page.mainFrame.window.setTimeout(() => { - const code = '//# sourceURL=' + super.href + '\n' + value.replace('javascript:', ''); - - if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { - this.#browserFrame.window.eval(code); - } else { - WindowErrorUtility.captureError(this.#browserFrame.window, () => - this.#browserFrame.window.eval(code) - ); - } - - readyStateManager.endTask(); - }); - } - return; - } - - try { - super.href = this.hostname ? new URL(value, this).href : value; - } catch (e) { - if (this.hostname) { - throw new DOMException( - `Failed to construct URL from string "${value}".`, - DOMExceptionNameEnum.uriMismatchError - ); - } else { - throw new DOMException( - `Failed to construct URL from string "${value}" relative to URL "${super.href}".`, - DOMExceptionNameEnum.uriMismatchError - ); - } - } - - // When using the Window instance directly and not via the Browser API we should not navigate the browser frame. - if (this.#browserFrame instanceof DetachedBrowserFrame) { - return; - } - this.#browserFrame?.goto(value); } @@ -109,10 +56,8 @@ export default class Location extends URL { /** * Reloads the resource from the current URL. - * - * Note: Will do nothing as reloading is not supported in server-dom. */ public reload(): void { - // Do nothing + this.#browserFrame?.goto(this.href); } } diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index abf6fe426..49d0a84cc 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -928,6 +928,7 @@ export default class Element extends Node implements IElement { ); if ( + browserSettings && !browserSettings.disableJavaScriptEvaluation && (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts index 4e37b6ac9..0bff8db6b 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -428,14 +428,13 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho event.type === 'click' && (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && - this._formNode && this.isConnected && - !event.defaultPrevented + !event.defaultPrevented && + this._url ) { - const href = this.href; - if (href) { - // TODO: Add support for "target", "download", "rel", "hreflang", "type", "referrerpolicy", "ping", "referrerpolicy", "relList". - this.ownerDocument._defaultView.location.href = this.href; + this.ownerDocument._defaultView.open(this._url.toString(), this.target || '_self'); + if (this.ownerDocument._defaultView.closed) { + event.stopImmediatePropagation(); } } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts index 6f171b2c6..49d7f3449 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts @@ -111,7 +111,7 @@ export default class HTMLIFrameElementPageLoader { const targetURL = new URL(url, originURL); // Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different. - const isSameOrigin = originURL.origin === targetURL.origin; + const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; this.#browserIFrame.url = url; const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( diff --git a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts index 4c3989137..082fd1ff8 100644 --- a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts +++ b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts @@ -76,6 +76,7 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem } if (newElement.isConnected && newElement.connectedCallback) { + debugger; newElement.connectedCallback(); } diff --git a/packages/happy-dom/src/window/CrossOriginWindow.ts b/packages/happy-dom/src/window/CrossOriginWindow.ts index 317312bf5..995f81f28 100644 --- a/packages/happy-dom/src/window/CrossOriginWindow.ts +++ b/packages/happy-dom/src/window/CrossOriginWindow.ts @@ -26,7 +26,7 @@ export default class CrossOriginWindow extends EventTarget implements ICrossOrig super(); this.parent = parent ?? this; - this.top = parent; + this.top = parent ?? this; this.location = new Proxy( {}, { diff --git a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts index d37d93ea7..08f98707d 100644 --- a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts +++ b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts @@ -83,8 +83,9 @@ export default class HappyDOMWindowAPI { /** * Sets the URL on a detached window. - * It will throw an exception if the window is not detached as a script could potentially use this method to bypass CORS. + * It will throw an exception if the window is not detached, as a script could potentially use this method to bypass CORS. * + * @deprecated Use the Browser API instead for setting URL runtime. * @param url URL. */ public setURL(url: string): void { diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index d9657d47a..eccdc1d24 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -565,7 +565,7 @@ export default class Window extends EventTarget implements IWindow { } if (options.url !== undefined) { - this.location.href = options.url; + this.#browserFrame.url = options.url; } } diff --git a/packages/happy-dom/src/window/WindowPageOpenUtility.ts b/packages/happy-dom/src/window/WindowPageOpenUtility.ts index 5872b604d..670944b28 100644 --- a/packages/happy-dom/src/window/WindowPageOpenUtility.ts +++ b/packages/happy-dom/src/window/WindowPageOpenUtility.ts @@ -4,7 +4,7 @@ import CrossOriginWindow from './CrossOriginWindow.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; import FetchCORSUtility from '../fetch/utilities/FetchCORSUtility.js'; import ICrossOriginWindow from './ICrossOriginWindow.js'; -import DetachedBrowserFrame from '../browser/detached-browser/DetachedBrowserFrame.js'; +import BrowserFrameUtility from '../browser/BrowserFrameUtility.js'; /** * Window page open handler. @@ -29,16 +29,9 @@ export default class WindowPageOpenUtility { ): IWindow | ICrossOriginWindow | null { const features = this.getWindowFeatures(options?.features || ''); const target = options?.target !== undefined ? String(options.target) : null; + const originURL = browserFrame.window.location; let targetFrame: IBrowserFrame; - // When using the Window instance directly and not via the Browser API we should not navigate the browser frame. - if ( - browserFrame instanceof DetachedBrowserFrame && - (target === '_self' || target === '_top' || target === '_parent') - ) { - return null; - } - switch (target) { case '_self': targetFrame = browserFrame; @@ -56,22 +49,17 @@ export default class WindowPageOpenUtility { break; } - let url = options.url || 'about:blank'; - - if (!url.startsWith('about:') && !url.startsWith('javascript:')) { - url = new URL(url, browserFrame.window.location).href; - } - - targetFrame.goto(url, { + targetFrame.goto(options.url, { referrer: features.noreferrer ? 'no-referrer' : undefined }); - if (url.startsWith('javascript:')) { - return targetFrame.window; + // When using the Window instance directly and not via the Browser API we should not navigate the browser frame. + if (BrowserFrameUtility.isDetachedMainFrame(targetFrame)) { + return null; } - if (targetFrame === browserFrame) { - return null; + if (options.url.startsWith('javascript:')) { + return targetFrame.window; } if (features.popup && target !== '_self' && target !== '_top' && target !== '_parent') { @@ -93,15 +81,24 @@ export default class WindowPageOpenUtility { } } - if (target) { + if ( + target && + target !== '_self' && + target !== '_top' && + target !== '_parent' && + target !== '_blank' + ) { (targetFrame.window.name) = target; } - const originURL = browserFrame.window.location; - const targetURL = new URL(targetFrame.url, originURL); - const isCORS = FetchCORSUtility.isCORS(originURL, targetURL); + const isCORS = FetchCORSUtility.isCORS(originURL, targetFrame.url); - if (!features.noopener && !features.noreferrer) { + if ( + !features.noopener && + !features.noreferrer && + browserFrame.window && + targetFrame.window !== browserFrame.window + ) { (targetFrame.window.opener) = isCORS ? new CrossOriginWindow(browserFrame.window) : browserFrame.window; diff --git a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts index a56106d93..6bc5cd1c0 100644 --- a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts +++ b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts @@ -2,7 +2,11 @@ import Window from '../../../src/window/Window.js'; import IWindow from '../../../src/window/IWindow.js'; import IDocument from '../../../src/nodes/document/IDocument.js'; import IHTMLAnchorElement from '../../../src/nodes/html-anchor-element/IHTMLAnchorElement.js'; -import { beforeEach, describe, it, expect } from 'vitest'; +import { beforeEach, describe, it, expect, vi } from 'vitest'; +import PointerEvent from '../../../src/event/events/PointerEvent.js'; +import IRequest from '../../../src/fetch/types/IRequest.js'; +import IResponse from '../../../src/fetch/types/IResponse.js'; +import Fetch from '../../../src/fetch/Fetch.js'; const BLOB_URL = 'blob:https://mozilla.org'; @@ -400,4 +404,94 @@ describe('HTMLAnchorElement', () => { expect(element.hash).toBe(''); }); }); + + describe('dispatchEvent()', () => { + it(`Doesn't change the location when a "click" event is dispatched inside the main frame of a detached browser.`, () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + throw new Error('Fetch should not be called.'); + }); + const element = document.createElement('a'); + element.href = 'https://www.example.com'; + document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + expect(window.location.href).toBe('https://www.somesite.com/test.html'); + }); + + it('Changes the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "url-set-fallback" is set.', () => { + const window = new Window({ + settings: { + browserNavigation: ['allow', 'url-set-fallback'] + } + }); + document = window.document; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + throw new Error('Fetch should not be called.'); + }); + const element = document.createElement('a'); + element.href = 'https://www.example.com'; + document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + expect(window.location.href).toBe('https://www.example.com/'); + }); + + it('Opens a window when a "click" event is dispatched on an element with target set to "_blank" inside the main frame of a detached browser.', () => { + let request: IRequest | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const element = document.createElement('a'); + element.target = '_blank'; + element.href = 'https://www.example.com'; + document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + expect(((request)).url).toBe('https://www.example.com/'); + }); + + it('Navigates the browser when a "click" event is dispatched on an element inside a non-main frame of a detached browser.', () => { + let request: IRequest | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const newWindow = window.open(); + + const element = newWindow.document.createElement('a'); + element.href = 'https://www.example.com'; + newWindow.document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + expect(((request)).url).toBe('https://www.example.com/'); + expect(newWindow.closed).toBe(true); + }); + + it(`Doesn't navigate the browser when a "click" event is dispatched on an element inside a non-main frame of a detached browser when the Happy DOM setting "deny" is set to "true".`, () => { + const window = new Window({ + settings: { + browserNavigation: ['deny', 'url-set-fallback'] + } + }); + document = window.document; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + throw new Error('Fetch should not be called.'); + }); + + const newWindow = window.open(); + + const element = newWindow.document.createElement('a'); + element.href = 'https://www.example.com'; + newWindow.document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + expect(newWindow.closed).toBe(false); + expect(newWindow.location.href).toBe('https://www.example.com/'); + }); + }); }); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index ac3a868d2..2fe800a6c 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -34,6 +34,7 @@ import Browser from '../../src/browser/Browser.js'; import ICrossOriginWindow from '../../src/window/ICrossOriginWindow.js'; import CrossOriginWindow from '../../src/window/CrossOriginWindow.js'; import BrowserFrameUtility from '../../src/browser/BrowserFrameUtility.js'; +import IHTMLIFrameElement from '../../src/nodes/html-iframe-element/IHTMLIFrameElement.js'; const GET_NAVIGATOR_PLATFORM = (): string => { return ( @@ -1620,14 +1621,93 @@ describe('Window', () => { }); it(`Doesn't set opener if "noopener" has been specified as a feature when opening an URL.`, () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + const browser = new Browser(); const page = browser.newPage(); - page.mainFrame.url = 'https://www.github.com/happy-dom/'; + page.mainFrame.url = 'https://www.github.com/capricorn86/happy-dom/'; const newWindow = page.mainFrame.window.open('/test/', '', 'noopener'); expect(newWindow).toBe(null); expect(browser.defaultContext.pages[1].mainFrame.window.opener).toBe(null); }); + it(`Doesn't navigate the browser if the target is the main frame of a detached browser.`, () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + throw new Error('This should not be called.'); + }); + + window.happyDOM.setURL('https://www.github.com/'); + + expect(window.open('/capricorn86/happy-dom/', '_self')).toBe(null); + expect(window.location.href).toBe('https://www.github.com/capricorn86/happy-dom/'); + + expect(window.open('/capricorn86/happy-dom/2/', '_top')).toBe(null); + expect(window.location.href).toBe('https://www.github.com/capricorn86/happy-dom/2/'); + + expect(window.open('/capricorn86/happy-dom/3/', '_parent')).toBe(null); + expect(window.location.href).toBe('https://www.github.com/capricorn86/happy-dom/3/'); + }); + + it(`Navigates the "_top" frame of a detached browser.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + window.happyDOM.setURL('https://localhost:8080'); + + const newWindow = window.open('/test/1/', '_blank'); + + expect(newWindow.name).toBe(''); + newWindow.document.write(''); + + const iframe = newWindow.document.querySelector('iframe'); + (iframe.contentWindow).happyDOM.setURL('https://localhost:8080'); + const newWindow2 = ( + (iframe.contentWindow).open('https://localhost:8080/test/2/', '_top') + ); + + expect(newWindow2.name).toBe(''); + expect(newWindow2.location.href).toBe('https://localhost:8080/test/2/'); + + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect(newWindow2.document.body.innerHTML).toBe('Test'); + }); + + it(`Navigates the "_parent" frame of a detached browser.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + window.happyDOM.setURL('https://localhost:8080'); + + const newWindow = window.open('/test/1/', '_blank'); + + expect(newWindow.name).toBe(''); + newWindow.document.write(''); + + const iframe = newWindow.document.querySelector('iframe'); + (iframe.contentWindow).happyDOM.setURL('https://localhost:8080'); + const newWindow2 = ( + (iframe.contentWindow).open('https://localhost:8080/test/2/', '_parent') + ); + + expect(newWindow2.name).toBe(''); + expect(newWindow2.location.href).toBe('https://localhost:8080/test/2/'); + + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect(newWindow2.document.body.innerHTML).toBe('Test'); + }); + it('Opens a new window with a CORS URL.', async () => { const browser = new Browser(); const page = browser.newPage(); @@ -1641,19 +1721,22 @@ describe('Window', () => { }); }); - page.mainFrame.url = 'https://www.github.com/happy-dom/'; + page.mainFrame.url = 'https://www.github.com/capricorn86/happy-dom/'; const newWindow = ( page.mainFrame.window.open('https://developer.mozilla.org/en-US/docs/Web/API/Window/open') ); + expect(newWindow instanceof CrossOriginWindow).toBe(true); expect(browser.defaultContext.pages.length).toBe(2); expect(browser.defaultContext.pages[0]).toBe(page); expect(browser.defaultContext.pages[1].mainFrame.window === newWindow).toBe(false); expect(browser.defaultContext.pages[1].mainFrame.url).toBe( 'https://developer.mozilla.org/en-US/docs/Web/API/Window/open' ); - expect(newWindow instanceof CrossOriginWindow).toBe(true); + expect(((request)).url).toBe( + 'https://developer.mozilla.org/en-US/docs/Web/API/Window/open' + ); await new Promise((resolve) => { browser.defaultContext.pages[1].mainFrame.window.addEventListener('load', () => { From 6d49f2c97180d11732ab8ffe4a99a1edf3a1d98a Mon Sep 17 00:00:00 2001 From: David Ortner Date: Sat, 11 Nov 2023 18:25:38 +0100 Subject: [PATCH 22/63] #466@trivial: Continues on implementation. --- packages/happy-dom/src/browser/Browser.ts | 12 +- .../happy-dom/src/browser/BrowserContext.ts | 7 +- .../happy-dom/src/browser/BrowserFrame.ts | 21 +- .../src/browser/BrowserFrameUtility.ts | 52 +++- packages/happy-dom/src/browser/BrowserPage.ts | 95 +++---- .../src/browser/BrowserPageUtility.ts | 69 +++++ .../src/browser/DefaultBrowserSettings.ts | 5 +- .../detached-browser/DetachedBrowser.ts | 9 +- .../DetachedBrowserContext.ts | 7 +- .../detached-browser/DetachedBrowserFrame.ts | 30 ++- .../detached-browser/DetachedBrowserPage.ts | 99 ++++---- .../happy-dom/src/browser/types/IBrowser.ts | 2 + .../src/browser/types/IBrowserFrame.ts | 11 +- .../src/browser/types/IBrowserPage.ts | 14 +- .../src/browser/types/IBrowserPageViewport.ts | 1 - .../src/browser/types/IBrowserSettings.ts | 3 +- .../src/browser/types/IGoToOptions.ts | 4 + .../browser/types/IOptionalBrowserSettings.ts | 5 +- .../browser/types/IReadOnlyBrowserSettings.ts | 3 + packages/happy-dom/src/location/Location.ts | 22 +- packages/happy-dom/src/navigator/Navigator.ts | 2 +- .../html-anchor-element/HTMLAnchorElement.ts | 1 - .../happy-dom/src/window/HappyDOMWindowAPI.ts | 1 - packages/happy-dom/src/window/IWindow.ts | 6 + packages/happy-dom/src/window/Window.ts | 6 +- .../src/window/WindowPageOpenUtility.ts | 5 +- .../happy-dom/test/browser/Browser.test.ts | 167 ++++++++++++ .../test/browser/BrowserContext.test.ts | 87 +++++++ .../test/browser/BrowserFrame.test.ts | 237 ++++++++++++++++++ .../test/browser/BrowserPage.test.ts | 223 ++++++++++++++++ .../detached-browser/DetachedBrowser.test.ts | 149 +++++++++++ .../DetachedBrowserContext.test.ts | 93 +++++++ .../DetachedBrowserPage.test.ts | 224 +++++++++++++++++ .../happy-dom/test/location/Location.test.ts | 114 ++++++--- .../HTMLAnchorElement.test.ts | 156 ++++++++++-- packages/happy-dom/test/window/Window.test.ts | 4 +- 36 files changed, 1735 insertions(+), 211 deletions(-) create mode 100644 packages/happy-dom/src/browser/BrowserPageUtility.ts create mode 100644 packages/happy-dom/test/browser/Browser.test.ts create mode 100644 packages/happy-dom/test/browser/BrowserContext.test.ts create mode 100644 packages/happy-dom/test/browser/BrowserFrame.test.ts create mode 100644 packages/happy-dom/test/browser/BrowserPage.test.ts create mode 100644 packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts create mode 100644 packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts create mode 100644 packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index 19f91c8e4..f20662c06 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -6,7 +6,9 @@ import BrowserPage from './BrowserPage.js'; import IBrowser from './types/IBrowser.js'; /** - * Browser context. + * Browser. + * + * Much of the interface for the browser has been taken from Puppeteer and Playwright, so that the API is familiar. */ export default class Browser implements IBrowser { public readonly contexts: BrowserContext[]; @@ -54,6 +56,9 @@ export default class Browser implements IBrowser { * @returns Promise. */ public async whenComplete(): Promise { + if (this.contexts.length === 0) { + throw new Error('No default context. The browser has been closed.'); + } await Promise.all(this.contexts.map((page) => page.whenComplete())); } @@ -61,7 +66,7 @@ export default class Browser implements IBrowser { * Aborts all ongoing operations. */ public abort(): void { - for (const context of this.contexts) { + for (const context of this.contexts.slice()) { context.abort(); } } @@ -72,6 +77,9 @@ export default class Browser implements IBrowser { * @returns Context. */ public newIncognitoContext(): BrowserContext { + if (this.contexts.length === 0) { + throw new Error('No default context. The browser has been closed.'); + } const context = new BrowserContext(this); this.contexts.push(context); return context; diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index 1734fbbf5..4b07d587a 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -22,9 +22,14 @@ export default class BrowserContext implements IBrowserContext { * Aborts all ongoing operations and destroys the context. */ public close(): void { - for (const page of this.pages) { + const index = this.browser.contexts.indexOf(this); + this.browser.contexts.splice(index, 1); + for (const page of this.pages.slice()) { page.close(); } + if (this.browser.contexts.length === 0) { + this.browser.close(); + } } /** diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 82263c900..4e41ffb58 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -6,6 +6,7 @@ import Location from '../location/Location.js'; import IResponse from '../fetch/types/IResponse.js'; import BrowserFrameUtility from './BrowserFrameUtility.js'; import IGoToOptions from './types/IGoToOptions.js'; +import { Script } from 'vm'; /** * Browser frame. @@ -66,7 +67,10 @@ export default class BrowserFrame implements IBrowserFrame { * @param url URL. */ public set url(url) { - (this.window.location) = new Location(url, this); + (this.window.location) = new Location( + this, + BrowserFrameUtility.getRelativeURL(this, url) + ); } /** @@ -75,7 +79,10 @@ export default class BrowserFrame implements IBrowserFrame { * @returns Promise. */ public async whenComplete(): Promise { - await this._asyncTaskManager.whenComplete(); + await Promise.all([ + this._asyncTaskManager.whenComplete(), + ...this.childFrames.map((frame) => frame.whenComplete()) + ]); } /** @@ -90,6 +97,16 @@ export default class BrowserFrame implements IBrowserFrame { this._asyncTaskManager.abortAll(); } + /** + * Evaluates code or a VM Script in the page's context. + * + * @param script Script. + * @returns Result. + */ + public evaluate(script: string | Script): any { + return BrowserFrameUtility.evaluate(this, script); + } + /** * Go to a page. * diff --git a/packages/happy-dom/src/browser/BrowserFrameUtility.ts b/packages/happy-dom/src/browser/BrowserFrameUtility.ts index d6d70d1ce..275cebf8b 100644 --- a/packages/happy-dom/src/browser/BrowserFrameUtility.ts +++ b/packages/happy-dom/src/browser/BrowserFrameUtility.ts @@ -12,6 +12,9 @@ import DetachedBrowserFrame from './detached-browser/DetachedBrowserFrame.js'; import { URL } from 'url'; import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; +import Location from '../location/Location.js'; +import AbortController from '../fetch/AbortController.js'; +import { Script } from 'vm'; /** * Browser frame utility. @@ -32,7 +35,7 @@ export default class BrowserFrameUtility { } } - for (const childFrame of frame.childFrames) { + for (const childFrame of frame.childFrames.slice()) { this.closeFrame(childFrame); } @@ -58,6 +61,20 @@ export default class BrowserFrameUtility { return frame; } + /** + * Returns frames. + * + * @param parentFrame Parent frame. + * @returns Frames, including the parent. + */ + public static getFrames(parentFrame: IBrowserFrame): IBrowserFrame[] { + let frames = [parentFrame]; + for (const frame of parentFrame.childFrames) { + frames = frames.concat(this.getFrames(frame)); + } + return frames; + } + /** * Go to a page. * @@ -107,7 +124,7 @@ export default class BrowserFrameUtility { !this.isBrowserNavigationAllowed(frame, frame.url, url) ) { if (frame.page.context.browser.settings.browserNavigation.includes('url-set-fallback')) { - frame.url = url; + (frame.window.location) = new Location(frame, url); } return null; } @@ -123,7 +140,8 @@ export default class BrowserFrameUtility { (frame.window) = new windowClass({ browserFrame: frame, - console: frame.page.console + console: frame.page.console, + url }); if (options?.referrer) { @@ -134,29 +152,37 @@ export default class BrowserFrameUtility { return null; } - frame.url = url; - const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( (frame.window) ))._readyStateManager; readyStateManager.startTask(); + let abortController = new AbortController(); let response: IResponse; let responseText: string; + const timeout = frame.window.setTimeout( + () => abortController.abort('Request timed out.'), + options?.timeout ?? 30000 + ); + try { response = await frame.window.fetch(url, { referrer: options?.referrer, - referrerPolicy: options?.referrerPolicy + referrerPolicy: options?.referrerPolicy, + signal: abortController.signal }); responseText = await response.text(); } catch (error) { + // TODO: Throw error as it can't be retrieved otherwise + frame.window.clearTimeout(timeout); readyStateManager.endTask(); WindowErrorUtility.dispatchError(frame.window, error); return response || null; } + frame.window.clearTimeout(timeout); frame.content = responseText; readyStateManager.endTask(); @@ -187,7 +213,7 @@ export default class BrowserFrameUtility { return false; } - if (settings.browserNavigation.includes('child-only') && frame.page.mainFrame === frame) { + if (settings.browserNavigation.includes('allow-children') && frame.page.mainFrame === frame) { return false; } @@ -239,4 +265,16 @@ export default class BrowserFrameUtility { } } } + + /** + * Evaluates code or a VM Script in the frame's context. + * + * @param frame Frame. + * @param script Script. + * @returns Result. + */ + public static evaluate(frame: IBrowserFrame, script: string | Script): any { + script = typeof script === 'string' ? new Script(script) : script; + return script.runInContext(frame.window); + } } diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index 62208d0b6..d82233794 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -5,7 +5,10 @@ import BrowserContext from './BrowserContext.js'; import VirtualConsole from '../console/VirtualConsole.js'; import IBrowserPage from './types/IBrowserPage.js'; import BrowserFrameUtility from './BrowserFrameUtility.js'; -import Event from '../event/Event.js'; +import { Script } from 'vm'; +import IGoToOptions from './types/IGoToOptions.js'; +import IResponse from '../fetch/types/IResponse.js'; +import BrowserPageUtility from './BrowserPageUtility.js'; /** * Browser page. @@ -31,34 +34,48 @@ export default class BrowserPage implements IBrowserPage { * Returns frames. */ public get frames(): BrowserFrame[] { - return this._getFrames(this.mainFrame); + return BrowserFrameUtility.getFrames(this.mainFrame); } /** * Returns the viewport. */ public get content(): string { - return this.mainFrame.window.document.documentElement.outerHTML; + return this.mainFrame.content; } /** - * Aborts all ongoing operations and destroys the page. + * Sets the content. + * + * @param content Content. */ - public close(): void { - if (!this.mainFrame) { - return; - } + public set content(content) { + this.mainFrame.content = content; + } - BrowserFrameUtility.closeFrame(this.mainFrame); + /** + * Returns the URL. + * + * @returns URL. + */ + public get url(): string { + return this.mainFrame.url; + } - const index = this.context.pages.indexOf(this); - if (index !== -1) { - this.context.pages.splice(index, 1); - } + /** + * Sets the content. + * + * @param url URL. + */ + public set url(url) { + this.mainFrame.url = url; + } - (this.virtualConsolePrinter) = null; - (this.mainFrame) = null; - (this.context) = null; + /** + * Aborts all ongoing operations and destroys the page. + */ + public close(): void { + BrowserPageUtility.closePage(this); } /** @@ -78,47 +95,31 @@ export default class BrowserPage implements IBrowserPage { } /** - * Sets the viewport. + * Evaluates code or a VM Script in the page's context. * - * @param viewport Viewport. + * @param script Script. + * @returns Result. */ - public setViewport(viewport: IBrowserPageViewport): void { - if ( - (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) || - (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) - ) { - if (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) { - (this.mainFrame.window.innerWidth) = viewport.width; - (this.mainFrame.window.outerWidth) = viewport.width; - } - - if (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) { - (this.mainFrame.window.innerHeight) = viewport.height; - (this.mainFrame.window.outerHeight) = viewport.height; - } - - this.mainFrame.window.dispatchEvent(new Event('resize')); - } + public evaluate(script: string | Script): any { + return this.mainFrame.evaluate(script); } + /** - * Go to a page. + * Sets the viewport. * - * @param url URL. + * @param viewport Viewport. */ - public async goto(url: string): Promise { - this.mainFrame.goto(url); + public setViewport(viewport: IBrowserPageViewport): void { + BrowserPageUtility.setViewport(this, viewport); } /** - * Returns frames. + * Go to a page. * - * @param parent Parent frame. + * @param url URL. + * @param [options] Options. */ - private _getFrames(parent: BrowserFrame): BrowserFrame[] { - let frames = [parent]; - for (const frame of parent.childFrames) { - frames = frames.concat(this._getFrames(frame)); - } - return frames; + public async goto(url: string, options?: IGoToOptions): Promise { + return await this.mainFrame.goto(url, options); } } diff --git a/packages/happy-dom/src/browser/BrowserPageUtility.ts b/packages/happy-dom/src/browser/BrowserPageUtility.ts new file mode 100644 index 000000000..d07e983c0 --- /dev/null +++ b/packages/happy-dom/src/browser/BrowserPageUtility.ts @@ -0,0 +1,69 @@ +import IBrowserPage from './types/IBrowserPage.js'; +import IBrowserPageViewport from './types/IBrowserPageViewport.js'; +import Event from '../event/Event.js'; +import BrowserFrameUtility from './BrowserFrameUtility.js'; +import IVirtualConsolePrinter from '../console/types/IVirtualConsolePrinter.js'; +import IBrowserFrame from './types/IBrowserFrame.js'; +import IBrowserContext from './types/IBrowserContext.js'; + +/** + * Browser page utility. + */ +export default class BrowserPageUtility { + /** + * Sets the viewport. + * + * @param page Page. + * @param viewport Viewport. + */ + public static setViewport(page: IBrowserPage, viewport: IBrowserPageViewport): void { + if ( + (viewport.width !== undefined && page.mainFrame.window.innerWidth !== viewport.width) || + (viewport.height !== undefined && page.mainFrame.window.innerHeight !== viewport.height) + ) { + if (viewport.width !== undefined && page.mainFrame.window.innerWidth !== viewport.width) { + (page.mainFrame.window.innerWidth) = viewport.width; + (page.mainFrame.window.outerWidth) = viewport.width; + } + + if (viewport.height !== undefined && page.mainFrame.window.innerHeight !== viewport.height) { + (page.mainFrame.window.innerHeight) = viewport.height; + (page.mainFrame.window.outerHeight) = viewport.height; + } + + page.mainFrame.window.dispatchEvent(new Event('resize')); + } + + if (viewport.deviceScaleFactor !== undefined) { + (page.mainFrame.window.devicePixelRatio) = viewport.deviceScaleFactor; + } + } + + /** + * Aborts all ongoing operations and destroys the page. + * + * @param page Page. + */ + public static closePage(page: IBrowserPage): void { + if (!page.mainFrame) { + return; + } + + BrowserFrameUtility.closeFrame(page.mainFrame); + + const index = page.context.pages.indexOf(page); + if (index !== -1) { + page.context.pages.splice(index, 1); + } + + const context = page.context; + + (page.virtualConsolePrinter) = null; + (page.mainFrame) = null; + (page.context) = null; + + if (context.pages[0] === page) { + context.close(); + } + } +} diff --git a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts index 4479a5445..fb7dafc78 100644 --- a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts +++ b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts @@ -10,11 +10,12 @@ export default { disableComputedStyleRendering: false, disableErrorCapturing: false, enableFileSystemHttpRequests: false, - browserNavigation: ['allow'], + browserNavigation: ['allow', 'url-set-fallback'], navigator: { userAgent: `Mozilla/5.0 (X11; ${ process.platform.charAt(0).toUpperCase() + process.platform.slice(1) + ' ' + process.arch - }) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}` + }) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}`, + maxTouchPoints: 0 }, device: { prefersColorScheme: 'light', diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts index d6c9179f4..39b030b24 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts @@ -8,7 +8,9 @@ import IWindow from '../../window/IWindow.js'; import IBrowserFrame from '../types/IBrowserFrame.js'; /** - * Detached browser. + * Detached browser used when constructing a Window instance without a browser. + * + * Much of the interface for the browser has been taken from Puppeteer and Playwright, so that the API is familiar. */ export default class DetachedBrowser implements IBrowser { public readonly contexts: DetachedBrowserContext[]; @@ -39,7 +41,8 @@ export default class DetachedBrowser implements IBrowser { this.detachedWindow = window; this.console = options?.console || null; this.settings = BrowserSettingsFactory.getSettings(options?.settings); - this.contexts = [new DetachedBrowserContext(this)]; + this.contexts = []; + this.contexts.push(new DetachedBrowserContext(this)); } /** @@ -58,7 +61,7 @@ export default class DetachedBrowser implements IBrowser { * Aborts all ongoing operations and destroys the browser. */ public close(): void { - for (const context of this.contexts) { + for (const context of this.contexts.slice()) { context.close(); } (this.contexts) = []; diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts index e49fad363..206085224 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts @@ -3,7 +3,7 @@ import DetachedBrowserPage from './DetachedBrowserPage.js'; import IBrowserContext from '../types/IBrowserContext.js'; /** - * Detached browser context. + * Detached browser context used when constructing a Window instance without a browser. */ export default class DetachedBrowserContext implements IBrowserContext { public readonly pages: DetachedBrowserPage[]; @@ -16,7 +16,8 @@ export default class DetachedBrowserContext implements IBrowserContext { */ constructor(browser: DetachedBrowser) { this.browser = browser; - this.pages = [new DetachedBrowserPage(this)]; + this.pages = []; + this.pages.push(new DetachedBrowserPage(this)); } /** @@ -26,7 +27,7 @@ export default class DetachedBrowserContext implements IBrowserContext { if (!this.browser) { return; } - for (const page of this.pages) { + for (const page of this.pages.slice()) { page.close(); } const browser = this.browser; diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index 3768fbc36..ed6acb5a3 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -6,9 +6,10 @@ import Location from '../../location/Location.js'; import IResponse from '../../fetch/types/IResponse.js'; import BrowserFrameUtility from '../BrowserFrameUtility.js'; import IGoToOptions from '../types/IGoToOptions.js'; +import { Script } from 'vm'; /** - * Browser frame. + * Browser frame used when constructing a Window instance without a browser. */ export default class DetachedBrowserFrame implements IBrowserFrame { public readonly childFrames: DetachedBrowserFrame[] = []; @@ -24,8 +25,11 @@ export default class DetachedBrowserFrame implements IBrowserFrame { */ constructor(page: DetachedBrowserPage) { this.page = page; - this.window = page.mainFrame - ? new page.context.browser.detachedWindowClass({ browserFrame: this, console: page.console }) + this.window = page.context.browser.contexts[0]?.pages[0]?.mainFrame + ? new page.context.browser.detachedWindowClass({ + browserFrame: this, + console: page.console + }) : page.context.browser.detachedWindow; } @@ -65,7 +69,10 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @param url URL. */ public set url(url) { - (this.window.location) = new Location(url, this); + (this.window.location) = new Location( + this, + BrowserFrameUtility.getRelativeURL(this, url) + ); } /** @@ -74,7 +81,10 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @returns Promise. */ public async whenComplete(): Promise { - await this._asyncTaskManager.whenComplete(); + await Promise.all([ + this._asyncTaskManager.whenComplete(), + ...this.childFrames.map((frame) => frame.whenComplete()) + ]); } /** @@ -87,6 +97,16 @@ export default class DetachedBrowserFrame implements IBrowserFrame { this._asyncTaskManager.abortAll(); } + /** + * Evaluates code or a VM Script in the page's context. + * + * @param script Script. + * @returns Result. + */ + public evaluate(script: string | Script): any { + return BrowserFrameUtility.evaluate(this, script); + } + /** * Go to a page. * diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts index f1e36cdc7..3d13952bd 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts @@ -5,10 +5,13 @@ import DetachedBrowserContext from './DetachedBrowserContext.js'; import VirtualConsole from '../../console/VirtualConsole.js'; import IBrowserPage from '../types/IBrowserPage.js'; import BrowserFrameUtility from '../BrowserFrameUtility.js'; -import Event from '../../event/Event.js'; +import { Script } from 'vm'; +import IGoToOptions from '../types/IGoToOptions.js'; +import IResponse from '../../fetch/types/IResponse.js'; +import BrowserPageUtility from '../BrowserPageUtility.js'; /** - * Detached browser page. + * Detached browser page used when constructing a Window instance without a browser. */ export default class DetachedBrowserPage implements IBrowserPage { public readonly virtualConsolePrinter = new VirtualConsolePrinter(); @@ -31,37 +34,50 @@ export default class DetachedBrowserPage implements IBrowserPage { * Returns frames. */ public get frames(): DetachedBrowserFrame[] { - return this._getFrames(this.mainFrame); + return BrowserFrameUtility.getFrames(this.mainFrame); } /** * Returns the viewport. */ public get content(): string { - return this.mainFrame.window.document.documentElement.outerHTML; + return this.mainFrame.content; } /** - * Aborts all ongoing operations and destroys the page. + * Sets the content. + * + * @param content Content. */ - public close(): void { - if (!this.mainFrame) { - return; - } + public set content(content) { + this.mainFrame.content = content; + } - BrowserFrameUtility.closeFrame(this.mainFrame); + /** + * Returns the URL. + * + * @returns URL. + */ + public get url(): string { + return this.mainFrame.url; + } - const index = this.context.pages.indexOf(this); - if (index !== -1) { - this.context.pages.splice(index, 1); - } + /** + * Sets the content. + * + * @param url URL. + */ + public set url(url) { + this.mainFrame.url = url; + } + /** + * Aborts all ongoing operations and destroys the page. + */ + public close(): void { const context = this.context; - - (this.virtualConsolePrinter) = null; - (this.mainFrame) = null; - (this.context) = null; - + BrowserPageUtility.closePage(this); + // As we are in a detached page, a context or browser should not exist without a page as there are no references to them. if (context.pages[0] === this) { context.close(); } @@ -84,48 +100,31 @@ export default class DetachedBrowserPage implements IBrowserPage { } /** - * Sets the viewport. + * Evaluates code or a VM Script in the page's context. * - * @param viewport Viewport. + * @param script Script. + * @returns Result. */ - public setViewport(viewport: IBrowserPageViewport): void { - if ( - (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) || - (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) - ) { - if (viewport.width !== undefined && this.mainFrame.window.innerWidth !== viewport.width) { - (this.mainFrame.window.innerWidth) = viewport.width; - (this.mainFrame.window.outerWidth) = viewport.width; - } - - if (viewport.height !== undefined && this.mainFrame.window.innerHeight !== viewport.height) { - (this.mainFrame.window.innerHeight) = viewport.height; - (this.mainFrame.window.outerHeight) = viewport.height; - } - - this.mainFrame.window.dispatchEvent(new Event('resize')); - } + public evaluate(script: string | Script): any { + return this.mainFrame.evaluate(script); } /** - * Go to a page. + * Sets the viewport. * - * @param url URL. + * @param viewport Viewport. */ - public async goto(url: string): Promise { - this.mainFrame.goto(url); + public setViewport(viewport: IBrowserPageViewport): void { + BrowserPageUtility.setViewport(this, viewport); } /** - * Returns frames. + * Go to a page. * - * @param parent Parent frame. + * @param url URL. + * @param [options] Options. */ - private _getFrames(parent: DetachedBrowserFrame): DetachedBrowserFrame[] { - let frames = [parent]; - for (const frame of parent.childFrames) { - frames = frames.concat(this._getFrames(frame)); - } - return frames; + public async goto(url: string, options?: IGoToOptions): Promise { + return await this.mainFrame.goto(url, options); } } diff --git a/packages/happy-dom/src/browser/types/IBrowser.ts b/packages/happy-dom/src/browser/types/IBrowser.ts index 91113ef0b..26a1bea3a 100644 --- a/packages/happy-dom/src/browser/types/IBrowser.ts +++ b/packages/happy-dom/src/browser/types/IBrowser.ts @@ -4,6 +4,8 @@ import IBrowserSettings from './IBrowserSettings.js'; /** * Browser. + * + * Much of the interface for the browser has been taken from Puppeteer and Playwright, so that the API is familiar. */ export default interface IBrowser { readonly defaultContext: IBrowserContext; diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index 22329740d..5015d19cf 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -3,6 +3,7 @@ import IWindow from '../../window/IWindow.js'; import IBrowserPage from './IBrowserPage.js'; import IResponse from '../../fetch/types/IResponse.js'; import IGoToOptions from './IGoToOptions.js'; +import { Script } from 'vm'; /** * Browser frame. @@ -14,7 +15,7 @@ export default interface IBrowserFrame { url: string; readonly parentFrame: IBrowserFrame | null; readonly _asyncTaskManager: AsyncTaskManager; - readonly page: IBrowserPage | null; + readonly page: IBrowserPage; /** * Returns a promise that is resolved when all async tasks are complete. @@ -28,6 +29,14 @@ export default interface IBrowserFrame { */ abort(): void; + /** + * Evaluates code or a VM Script in the page's context. + * + * @param script Script. + * @returns Result. + */ + evaluate(script: string | Script): any; + /** * Go to a page. * diff --git a/packages/happy-dom/src/browser/types/IBrowserPage.ts b/packages/happy-dom/src/browser/types/IBrowserPage.ts index 17d072034..b2f938e7e 100644 --- a/packages/happy-dom/src/browser/types/IBrowserPage.ts +++ b/packages/happy-dom/src/browser/types/IBrowserPage.ts @@ -2,6 +2,9 @@ import IBrowserPageViewport from './IBrowserPageViewport.js'; import VirtualConsolePrinter from '../../console/VirtualConsolePrinter.js'; import IBrowserFrame from './IBrowserFrame.js'; import IBrowserContext from './IBrowserContext.js'; +import { Script } from 'vm'; +import IGoToOptions from './IGoToOptions.js'; +import IResponse from '../../fetch/types/IResponse.js'; /** * Browser page. @@ -12,7 +15,8 @@ export default interface IBrowserPage { readonly context: IBrowserContext; readonly console: Console; readonly frames: IBrowserFrame[]; - readonly content: string; + content: string; + url: string; /** * Aborts all ongoing operations and destroys the page. @@ -31,6 +35,11 @@ export default interface IBrowserPage { */ abort(): void; + /** + * Evaluates code or a VM Script in the page's context. + */ + evaluate(script: string | Script): any; + /** * Sets the viewport. * @@ -42,6 +51,7 @@ export default interface IBrowserPage { * Go to a page. * * @param url URL. + * @param [options] Options. */ - goto(url: string): Promise; + goto(url: string, options?: IGoToOptions): Promise; } diff --git a/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts b/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts index ba5bba338..654493a06 100644 --- a/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts +++ b/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts @@ -2,5 +2,4 @@ export default interface IBrowserPageViewport { width?: number; height?: number; deviceScaleFactor?: number; - hasTouch?: boolean; } diff --git a/packages/happy-dom/src/browser/types/IBrowserSettings.ts b/packages/happy-dom/src/browser/types/IBrowserSettings.ts index a3ca08fca..1c51722c7 100644 --- a/packages/happy-dom/src/browser/types/IBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IBrowserSettings.ts @@ -10,9 +10,10 @@ export default interface IBrowserSettings { disableComputedStyleRendering: boolean; disableErrorCapturing: boolean; enableFileSystemHttpRequests: boolean; - browserNavigation: Array<'allow' | 'deny' | 'sameorigin' | 'child-only' | 'url-set-fallback'>; + browserNavigation: Array<'allow' | 'deny' | 'sameorigin' | 'allow-children' | 'url-set-fallback'>; navigator: { userAgent: string; + maxTouchPoints: number; }; device: { prefersColorScheme: string; diff --git a/packages/happy-dom/src/browser/types/IGoToOptions.ts b/packages/happy-dom/src/browser/types/IGoToOptions.ts index 9ca27a6f4..22c801601 100644 --- a/packages/happy-dom/src/browser/types/IGoToOptions.ts +++ b/packages/happy-dom/src/browser/types/IGoToOptions.ts @@ -6,4 +6,8 @@ import IRequestReferrerPolicy from '../../fetch/types/IRequestReferrerPolicy.js' export default interface IGoToOptions { referrer?: string; referrerPolicy?: IRequestReferrerPolicy; + /** + * Timeout in ms. Default is 30000ms. + */ + timeout?: number; } diff --git a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts index 955401b56..9a0802462 100644 --- a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts @@ -10,9 +10,12 @@ export default interface IOptionalBrowserSettings { disableComputedStyleRendering?: boolean; disableErrorCapturing?: boolean; enableFileSystemHttpRequests?: boolean; - browserNavigation?: Array<'allow' | 'deny' | 'sameorigin' | 'child-only' | 'url-set-fallback'>; + browserNavigation?: Array< + 'allow' | 'deny' | 'sameorigin' | 'allow-children' | 'url-set-fallback' + >; navigator?: { userAgent?: string; + maxTouchPoints?: number; }; device?: { prefersColorScheme?: string; diff --git a/packages/happy-dom/src/browser/types/IReadOnlyBrowserSettings.ts b/packages/happy-dom/src/browser/types/IReadOnlyBrowserSettings.ts index 0235fac19..2479bc6ef 100644 --- a/packages/happy-dom/src/browser/types/IReadOnlyBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IReadOnlyBrowserSettings.ts @@ -10,6 +10,9 @@ export default interface IReadOnlyBrowserSettings { readonly disableComputedStyleRendering: boolean; readonly disableErrorCapturing: boolean; readonly enableFileSystemHttpRequests: boolean; + readonly browserNavigation: Array< + 'allow' | 'deny' | 'sameorigin' | 'allow-children' | 'url-set-fallback' + >; readonly navigator: { readonly userAgent: string; }; diff --git a/packages/happy-dom/src/location/Location.ts b/packages/happy-dom/src/location/Location.ts index 01d6b6f46..7a60f2446 100644 --- a/packages/happy-dom/src/location/Location.ts +++ b/packages/happy-dom/src/location/Location.ts @@ -1,29 +1,29 @@ import URL from '../url/URL.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; -import BrowserFrameUtility from '../browser/BrowserFrameUtility.js'; /** * Location. */ export default class Location extends URL { - #browserFrame: IBrowserFrame | null; + #browserFrame: IBrowserFrame; /** * Constructor. * - * @param [url] URL. + * @param browserFrame Browser frame. + * @param url URL. */ - constructor(url = 'about:blank', browserFrame?: IBrowserFrame) { - super(browserFrame ? BrowserFrameUtility.getRelativeURL(browserFrame, url) : url); - this.#browserFrame = browserFrame ?? null; + constructor(browserFrame: IBrowserFrame, url: string) { + super(url); + this.#browserFrame = browserFrame; } /** * Override set href. */ // @ts-ignore - public set href(value: string) { - this.#browserFrame?.goto(value); + public set href(url: string) { + this.#browserFrame.goto(url); } /** @@ -39,7 +39,7 @@ export default class Location extends URL { * @param url URL. */ public replace(url: string): void { - this.href = url; + this.#browserFrame.goto(url); } /** @@ -51,13 +51,13 @@ export default class Location extends URL { * @see this.replace() */ public assign(url: string): void { - this.href = url; + this.#browserFrame.goto(url); } /** * Reloads the resource from the current URL. */ public reload(): void { - this.#browserFrame?.goto(this.href); + this.#browserFrame.goto(this.href); } } diff --git a/packages/happy-dom/src/navigator/Navigator.ts b/packages/happy-dom/src/navigator/Navigator.ts index 75c97f24a..b3bb9dade 100644 --- a/packages/happy-dom/src/navigator/Navigator.ts +++ b/packages/happy-dom/src/navigator/Navigator.ts @@ -75,7 +75,7 @@ export default class Navigator { * Maximum number of simultaneous touch contact points are supported by the current device. */ public get maxTouchPoints(): number { - return 0; + return WindowBrowserSettingsReader.getSettings(this.#ownerWindow).navigator.maxTouchPoints; } /** diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts index 0bff8db6b..067669c64 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -428,7 +428,6 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho event.type === 'click' && (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && - this.isConnected && !event.defaultPrevented && this._url ) { diff --git a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts index 08f98707d..0d6ea9fb8 100644 --- a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts +++ b/packages/happy-dom/src/window/HappyDOMWindowAPI.ts @@ -85,7 +85,6 @@ export default class HappyDOMWindowAPI { * Sets the URL on a detached window. * It will throw an exception if the window is not detached, as a script could potentially use this method to bypass CORS. * - * @deprecated Use the Browser API instead for setting URL runtime. * @param url URL. */ public setURL(url: string): void { diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index b5d8a6a84..e67580e5f 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -33,6 +33,7 @@ import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; import Image from '../nodes/html-image-element/Image.js'; +import Audio from '../nodes/html-audio-element/Audio.js'; import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; import CharacterData from '../nodes/character-data/CharacterData.js'; import NodeIterator from '../tree-walker/NodeIterator.js'; @@ -109,6 +110,7 @@ import Attr from '../nodes/attr/Attr.js'; import NamedNodeMap from '../named-node-map/NamedNodeMap.js'; import { Performance } from 'perf_hooks'; import IElement from '../nodes/element/IElement.js'; +import SVGGraphicsElement from '../nodes/svg-element/SVGGraphicsElement.js'; import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction.js'; import RequestInfo from '../fetch/types/IRequestInfo.js'; import FileList from '../nodes/html-input-element/FileList.js'; @@ -147,6 +149,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly Attr: typeof Attr; readonly SVGSVGElement: typeof SVGSVGElement; readonly SVGElement: typeof SVGElement; + readonly SVGGraphicsElement: typeof SVGGraphicsElement; readonly Text: typeof Text; readonly Comment: typeof Comment; readonly ShadowRoot: typeof ShadowRoot; @@ -303,6 +306,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { // Other classes readonly Image: typeof Image; + readonly Audio: typeof Audio; readonly NamedNodeMap: typeof NamedNodeMap; readonly EventTarget: typeof EventTarget; readonly DataTransfer: typeof DataTransfer; @@ -353,6 +357,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly FileList: typeof FileList; readonly ReadableStream: typeof Stream.Readable; readonly WritableStream: typeof Stream.Writable; + readonly TransformStream: typeof Stream.Transform; readonly FormData: typeof FormData; readonly AbortController: typeof AbortController; readonly AbortSignal: typeof AbortSignal; @@ -388,6 +393,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly window: IWindow; readonly globalThis: IWindow; readonly screen: Screen; + readonly devicePixelRatio: number; readonly innerWidth: number; readonly innerHeight: number; readonly outerWidth: number; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index eccdc1d24..60ad3e9b5 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -544,8 +544,8 @@ export default class Window extends EventTarget implements IWindow { WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); this.console = this.#browserFrame.page.console; - this.location = new Location('about:blank', this.#browserFrame); this.happyDOM = new HappyDOMWindowAPI(this.#browserFrame); + this.location = new Location(this.#browserFrame, options?.url ?? 'about:blank'); if (options) { if (options.width !== undefined) { @@ -563,10 +563,6 @@ export default class Window extends EventTarget implements IWindow { this.innerHeight = options.innerHeight; this.outerHeight = options.innerHeight; } - - if (options.url !== undefined) { - this.#browserFrame.url = options.url; - } } this.#setTimeout = ORIGINAL_SET_TIMEOUT; diff --git a/packages/happy-dom/src/window/WindowPageOpenUtility.ts b/packages/happy-dom/src/window/WindowPageOpenUtility.ts index 670944b28..af57e4046 100644 --- a/packages/happy-dom/src/window/WindowPageOpenUtility.ts +++ b/packages/happy-dom/src/window/WindowPageOpenUtility.ts @@ -30,6 +30,7 @@ export default class WindowPageOpenUtility { const features = this.getWindowFeatures(options?.features || ''); const target = options?.target !== undefined ? String(options.target) : null; const originURL = browserFrame.window.location; + const url = BrowserFrameUtility.getRelativeURL(browserFrame, options.url); let targetFrame: IBrowserFrame; switch (target) { @@ -49,7 +50,7 @@ export default class WindowPageOpenUtility { break; } - targetFrame.goto(options.url, { + targetFrame.goto(BrowserFrameUtility.getRelativeURL(browserFrame, url), { referrer: features.noreferrer ? 'no-referrer' : undefined }); @@ -58,7 +59,7 @@ export default class WindowPageOpenUtility { return null; } - if (options.url.startsWith('javascript:')) { + if (url.startsWith('javascript:')) { return targetFrame.window; } diff --git a/packages/happy-dom/test/browser/Browser.test.ts b/packages/happy-dom/test/browser/Browser.test.ts new file mode 100644 index 000000000..f2acf213f --- /dev/null +++ b/packages/happy-dom/test/browser/Browser.test.ts @@ -0,0 +1,167 @@ +import Browser from '../../src/browser/Browser'; +import BrowserContext from '../../src/browser/BrowserContext'; +import BrowserPage from '../../src/browser/BrowserPage'; +import DefaultBrowserSettings from '../../src/browser/DefaultBrowserSettings'; +import { describe, it, expect, afterEach, vi } from 'vitest'; + +describe('Browser', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('get contexts()', () => { + it('Returns the contexts.', () => { + const browser = new Browser(); + expect(browser.contexts.length).toBe(1); + expect(browser.contexts[0]).toBe(browser.defaultContext); + + const incognitoContext = browser.newIncognitoContext(); + expect(browser.contexts.length).toBe(2); + expect(browser.contexts[0]).toBe(browser.defaultContext); + expect(browser.contexts[1]).toBe(incognitoContext); + + incognitoContext.close(); + + expect(browser.contexts.length).toBe(1); + expect(browser.contexts[0]).toBe(browser.defaultContext); + + browser.defaultContext.close(); + + expect(browser.contexts.length).toBe(0); + }); + }); + + describe('get settings()', () => { + it('Returns the settings.', () => { + expect(new Browser().settings).toEqual(DefaultBrowserSettings); + }); + + it('Returns the settings with custom settings.', () => { + const settings = { + disableJavaScriptEvaluation: true, + navigator: { + userAgent: 'test' + } + }; + expect(new Browser({ settings }).settings).toEqual({ + ...DefaultBrowserSettings, + ...settings, + navigator: { + ...DefaultBrowserSettings.navigator, + ...settings.navigator + } + }); + }); + }); + + describe('get console()', () => { + it('Returns "null" if no console is provided.', () => { + expect(new Browser().console).toBe(null); + }); + + it('Returns console sent into the constructor.', () => { + expect(new Browser({ console }).console).toBe(console); + }); + }); + + describe('get defaultContext()', () => { + it('Returns the default context.', () => { + const browser = new Browser(); + expect(browser.defaultContext instanceof BrowserContext).toBe(true); + expect(browser.contexts[0]).toBe(browser.defaultContext); + }); + + it('Throws an error if the browser has been closed.', () => { + const browser = new Browser(); + browser.close(); + expect(() => browser.defaultContext).toThrow( + 'No default context. The browser has been closed.' + ); + }); + }); + + describe('close()', () => { + it('Closes the browser.', () => { + const browser = new Browser(); + const originalClose = browser.defaultContext.close; + let isContextClosed = false; + + vi.spyOn(browser.defaultContext, 'close').mockImplementation(() => { + isContextClosed = true; + originalClose.call(browser.defaultContext); + }); + + browser.close(); + expect(browser.contexts.length).toBe(0); + expect(isContextClosed).toBe(true); + }); + }); + + describe('whenComplete()', () => { + it('Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.', async () => { + const browser = new Browser(); + const page1 = browser.newPage(); + const page2 = browser.newPage(); + const page3 = browser.newIncognitoContext().newPage(); + page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + page3.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); + await browser.whenComplete(); + expect(page1.mainFrame.window['test']).toBe(1); + expect(page2.mainFrame.window['test']).toBe(2); + expect(page3.mainFrame.window['test']).toBe(3); + }); + }); + + describe('abort()', () => { + it('Aborts all ongoing operations.', async () => { + const browser = new Browser(); + const page1 = browser.newPage(); + const page2 = browser.newPage(); + const page3 = browser.newIncognitoContext().newPage(); + page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + page3.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); + browser.abort(); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(page1.mainFrame.window['test']).toBeUndefined(); + expect(page2.mainFrame.window['test']).toBeUndefined(); + expect(page3.mainFrame.window['test']).toBeUndefined(); + }); + }); + + describe('newIncognitoContext()', () => { + it('Creates a new incognito context.', () => { + const browser = new Browser(); + const context = browser.newIncognitoContext(); + expect(context instanceof BrowserContext).toBe(true); + expect(browser.contexts.length).toBe(2); + expect(browser.contexts[1]).toBe(context); + }); + + it('Throws an error if the browser has been closed.', () => { + const browser = new Browser(); + browser.close(); + expect(() => browser.newIncognitoContext()).toThrow( + 'No default context. The browser has been closed.' + ); + }); + }); + + describe('newPage()', () => { + it('Creates a new page.', () => { + const browser = new Browser(); + const page = browser.newPage(); + expect(page instanceof BrowserPage).toBe(true); + expect(browser.contexts.length).toBe(1); + expect(browser.contexts[0].pages.length).toBe(1); + expect(browser.contexts[0].pages[0]).toBe(page); + }); + + it('Throws an error if the browser has been closed.', () => { + const browser = new Browser(); + browser.close(); + expect(() => browser.newPage()).toThrow('No default context. The browser has been closed.'); + }); + }); +}); diff --git a/packages/happy-dom/test/browser/BrowserContext.test.ts b/packages/happy-dom/test/browser/BrowserContext.test.ts new file mode 100644 index 000000000..62dfe3fdd --- /dev/null +++ b/packages/happy-dom/test/browser/BrowserContext.test.ts @@ -0,0 +1,87 @@ +import Browser from '../../src/browser/Browser'; +import BrowserPage from '../../src/browser/BrowserPage'; +import { describe, it, expect, afterEach, vi } from 'vitest'; + +describe('BrowserContext', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('get pages()', () => { + it('Returns the pages.', () => { + const browser = new Browser(); + expect(browser.defaultContext.pages.length).toBe(0); + const page = browser.defaultContext.newPage(); + expect(browser.defaultContext.pages.length).toBe(1); + expect(browser.defaultContext.pages[0]).toBe(page); + }); + }); + + describe('get browser()', () => { + it('Returns the browser.', () => { + const browser = new Browser(); + expect(browser.defaultContext.browser).toBe(browser); + }); + }); + + describe('close()', () => { + it('Closes the context.', () => { + const browser = new Browser(); + const context = browser.defaultContext; + const page1 = context.newPage(); + const page2 = context.newPage(); + const originalClose1 = page1.close; + const originalClose2 = page2.close; + let pagesClosed = 0; + vi.spyOn(page1, 'close').mockImplementation(() => { + pagesClosed++; + originalClose1.call(page1); + }); + vi.spyOn(page2, 'close').mockImplementation(() => { + pagesClosed++; + originalClose2.call(page2); + }); + expect(browser.contexts.length).toBe(1); + context.close(); + expect(browser.contexts.length).toBe(0); + expect(pagesClosed).toBe(2); + }); + }); + + describe('whenComplete()', () => { + it('Waits for all pages to complete.', async () => { + const browser = new Browser(); + const page1 = browser.newPage(); + const page2 = browser.newPage(); + page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + await browser.defaultContext.whenComplete(); + expect(page1.mainFrame.window['test']).toBe(1); + expect(page2.mainFrame.window['test']).toBe(2); + }); + }); + + describe('abort()', () => { + it('Aborts all ongoing operations.', async () => { + const browser = new Browser(); + const page1 = browser.newPage(); + const page2 = browser.newPage(); + page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + browser.defaultContext.abort(); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(page1.mainFrame.window['test']).toBeUndefined(); + expect(page2.mainFrame.window['test']).toBeUndefined(); + }); + }); + + describe('newPage()', () => { + it('Creates a new page.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + expect(page instanceof BrowserPage).toBe(true); + expect(browser.defaultContext.pages.length).toBe(1); + expect(browser.defaultContext.pages[0]).toBe(page); + }); + }); +}); diff --git a/packages/happy-dom/test/browser/BrowserFrame.test.ts b/packages/happy-dom/test/browser/BrowserFrame.test.ts new file mode 100644 index 000000000..0bf81df7a --- /dev/null +++ b/packages/happy-dom/test/browser/BrowserFrame.test.ts @@ -0,0 +1,237 @@ +import { Script } from 'vm'; +import Browser from '../../src/browser/Browser'; +import BrowserFrameUtility from '../../src/browser/BrowserFrameUtility'; +import Event from '../../src/event/Event'; +import ErrorEvent from '../../src/event/events/ErrorEvent'; +import Window from '../../src/window/Window'; +import IRequest from '../../src/fetch/types/IRequest'; +import IResponse from '../../src/fetch/types/IResponse'; +import { describe, it, expect, afterEach, vi } from 'vitest'; +import Fetch from '../../src/fetch/Fetch'; + +describe('BrowserFrame', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('get childFrames()', () => { + it('Returns child frames.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + expect(page.mainFrame.childFrames).toEqual([]); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + expect(page.mainFrame.childFrames).toEqual([frame1, frame2]); + }); + }); + + describe('get parentFrame()', () => { + it('Returns the parent frame.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + expect(page.mainFrame.parentFrame).toBe(null); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(frame1); + expect(frame2.parentFrame).toBe(frame1); + expect(frame1.parentFrame).toBe(page.mainFrame); + expect(page.mainFrame.parentFrame).toBe(null); + }); + }); + + describe('get page()', () => { + it('Returns the page.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + expect(page.mainFrame.page).toBe(page); + }); + }); + + describe('get window()', () => { + it('Returns the window.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + expect(page.mainFrame.window).toBeInstanceOf(Window); + expect(page.mainFrame.window.console).toBe(page.console); + }); + }); + + describe('get content()', () => { + it('Returns the document HTML content.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + page.mainFrame.window.document.write('
test
'); + expect(page.content).toBe('
test
'); + }); + }); + + describe('set content()', () => { + it('Sets the document HTML content.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + page.mainFrame.content = '
test
'; + expect(page.mainFrame.window.document.documentElement.outerHTML).toBe( + '
test
' + ); + }); + + it('Removes listeners and child nodes before setting the document HTML content.', () => { + const browser = new Browser({ settings: { disableErrorCapturing: true } }); + const page = browser.defaultContext.newPage(); + page.mainFrame.content = '
test
'; + page.mainFrame.window.document.addEventListener('load', () => { + throw new Error('Should not be called'); + }); + page.mainFrame.window.document.addEventListener('error', () => { + throw new Error('Should not be called'); + }); + page.mainFrame.content = '
test
'; + page.mainFrame.window.document.dispatchEvent(new Event('load')); + page.mainFrame.window.document.dispatchEvent(new Event('error')); + expect(page.mainFrame.window.document.documentElement.outerHTML).toBe( + '
test
' + ); + }); + }); + + describe('get url()', () => { + it('Returns the document URL.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + page.mainFrame.url = 'http://localhost:3000'; + expect(page.mainFrame.url).toBe('http://localhost:3000/'); + }); + }); + + describe('set url()', () => { + it('Sets the document URL.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + const location = page.mainFrame.window.location; + page.mainFrame.url = 'http://localhost:3000'; + expect(page.mainFrame.window.location.href).toBe('http://localhost:3000/'); + expect(page.mainFrame.window.location).not.toBe(location); + }); + }); + + describe('whenComplete()', () => { + it('Waits for all pages to complete.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + page.mainFrame.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + frame1.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + frame2.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); + await page.whenComplete(); + expect(page.mainFrame.window['test']).toBe(1); + expect(frame1.window['test']).toBe(2); + expect(frame2.window['test']).toBe(3); + }); + }); + + describe('abort()', () => { + it('Aborts all ongoing operations.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + page.mainFrame.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + page.abort(); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(page.mainFrame.window['test']).toBeUndefined(); + expect(frame1.window['test']).toBeUndefined(); + expect(frame2.window['test']).toBeUndefined(); + }); + }); + + describe('evaluate()', () => { + it("Evaluates a code string in the frame's context.", () => { + const browser = new Browser(); + const page = browser.newPage(); + expect(page.mainFrame.evaluate('globalThis.test = 1')).toBe(1); + expect(page.mainFrame.window['test']).toBe(1); + }); + + it("Evaluates a VM script in the frame's context.", () => { + const browser = new Browser(); + const page = browser.newPage(); + expect(page.mainFrame.evaluate(new Script('globalThis.test = 1'))).toBe(1); + expect(page.mainFrame.window['test']).toBe(1); + }); + }); + + describe('goto()', () => { + it('Navigates to a URL.', async () => { + let request: IRequest | null = null; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + url: request?.url, + text: () => + new Promise((resolve) => setTimeout(() => resolve('Test'), 1)) + }); + }); + + const browser = new Browser(); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + const response = await page.mainFrame.goto('http://localhost:3000', { + referrer: 'http://localhost:3000/referrer', + referrerPolicy: 'no-referrer-when-downgrade' + }); + + expect((response).url).toBe('http://localhost:3000/'); + expect(((request)).referrer).toBe('http://localhost:3000/referrer'); + expect(((request)).referrerPolicy).toBe('no-referrer-when-downgrade'); + expect(page.mainFrame.url).toBe('http://localhost:3000/'); + expect(page.mainFrame.window).not.toBe(oldWindow); + expect(oldWindow.location.href).toBe('about:blank'); + expect(page.mainFrame.window.location.href).toBe('http://localhost:3000/'); + expect(page.mainFrame.window.document.body.innerHTML).toBe('Test'); + }); + + it('Navigates to a URL with "javascript:" as protocol.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + const response = await page.mainFrame.goto('javascript:document.write("test");'); + + expect(response).toBeNull(); + expect(page.mainFrame.url).toBe('about:blank'); + expect(page.mainFrame.window).toBe(oldWindow); + + expect(page.mainFrame.window.document.body.innerHTML).toBe(''); + + await new Promise((resolve) => setTimeout(resolve, 2)); + + expect(page.mainFrame.window.document.body.innerHTML).toBe('test'); + }); + + it('Navigates to a URL with "about:" as protocol.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + const response = await page.mainFrame.goto('about:blank'); + + expect(response).toBeNull(); + expect(page.mainFrame.url).toBe('about:blank'); + expect(page.mainFrame.window).not.toBe(oldWindow); + }); + + it('Aborts request if it times out.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + const response = await page.mainFrame.goto('http://localhost:9999', { + timeout: 1 + }); + + expect(response).toBeNull(); + expect(page.mainFrame.url).toBe('http://localhost:9999/'); + expect(page.mainFrame.window).not.toBe(oldWindow); + expect(page.mainFrame.window.document.body.innerHTML).toBe(''); + }); + }); +}); diff --git a/packages/happy-dom/test/browser/BrowserPage.test.ts b/packages/happy-dom/test/browser/BrowserPage.test.ts new file mode 100644 index 000000000..b3f924564 --- /dev/null +++ b/packages/happy-dom/test/browser/BrowserPage.test.ts @@ -0,0 +1,223 @@ +import Browser from '../../src/browser/Browser'; +import BrowserFrame from '../../src/browser/BrowserFrame'; +import Window from '../../src/window/Window'; +import VirtualConsolePrinter from '../../src/console/VirtualConsolePrinter'; +import VirtualConsole from '../../src/console/VirtualConsole'; +import BrowserFrameUtility from '../../src/browser/BrowserFrameUtility'; +import IResponse from '../../src/fetch/types/IResponse'; +import { describe, it, expect, afterEach, vi } from 'vitest'; +import IGoToOptions from '../../src/browser/types/IGoToOptions'; + +describe('BrowserPage', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('get virtualConsolePrinter()', () => { + it('Returns the virtual console printer.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + expect(page.virtualConsolePrinter).toBeInstanceOf(VirtualConsolePrinter); + }); + }); + + describe('get mainFrame()', () => { + it('Returns the mainFrame.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + expect(page.mainFrame).toBeInstanceOf(BrowserFrame); + expect(page.mainFrame.window).toBeInstanceOf(Window); + }); + }); + + describe('get context()', () => { + it('Returns the context.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + expect(page.context).toBe(browser.defaultContext); + }); + }); + + describe('get console()', () => { + it('Returns a virtual console by default.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + expect(page.console).toBeInstanceOf(VirtualConsole); + page.console.log('test'); + expect(page.virtualConsolePrinter.readAsString()).toBe('test\n'); + }); + + it('Returns the browser console if set.', () => { + const browser = new Browser({ console }); + const page = browser.defaultContext.newPage(); + expect(page.console).toBe(console); + }); + }); + + describe('get frames()', () => { + it('Returns the frames.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + expect(page.frames).toEqual([page.mainFrame, frame1, frame2]); + }); + }); + + describe('get content()', () => { + it('Returns the document HTML content.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + page.mainFrame.window.document.write('
test
'); + expect(page.content).toBe('
test
'); + }); + }); + + describe('set content()', () => { + it('Sets the document HTML content.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + page.content = '
test
'; + expect(page.mainFrame.window.document.documentElement.outerHTML).toBe( + '
test
' + ); + }); + }); + + describe('get url()', () => { + it('Returns the document URL.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + page.mainFrame.url = 'http://localhost:3000'; + expect(page.url).toBe('http://localhost:3000/'); + }); + }); + + describe('set url()', () => { + it('Sets the document URL.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + page.url = 'http://localhost:3000'; + expect(page.mainFrame.window.location.href).toBe('http://localhost:3000/'); + }); + }); + + describe('close()', () => { + it('Closes the page.', () => { + const browser = new Browser(); + const page = browser.defaultContext.newPage(); + const mainFrame = BrowserFrameUtility.newFrame(page.mainFrame); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + + page.close(); + + expect(browser.defaultContext.pages.length).toBe(0); + + expect(page.virtualConsolePrinter).toBe(null); + expect(page.context).toBe(null); + expect(page.mainFrame).toBe(null); + expect(mainFrame.window).toBe(null); + expect(frame1.window).toBe(null); + expect(frame2.window).toBe(null); + }); + }); + + describe('whenComplete()', () => { + it('Waits for all pages to complete.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + await page.whenComplete(); + expect(frame1.window['test']).toBe(1); + expect(frame2.window['test']).toBe(2); + }); + }); + + describe('abort()', () => { + it('Aborts all ongoing operations.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + page.abort(); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(frame1.window['test']).toBeUndefined(); + expect(frame2.window['test']).toBeUndefined(); + }); + }); + + describe('evaluate()', () => { + it("Evaluates code in the page's context.", () => { + const browser = new Browser(); + const page = browser.newPage(); + let evaluatedCode: string | null = null; + vi.spyOn(page.mainFrame, 'evaluate').mockImplementation((code) => { + evaluatedCode = code; + return 'returnValue'; + }); + expect(page.evaluate('test')).toBe('returnValue'); + expect(evaluatedCode).toBe('test'); + }); + }); + + describe('setViewport()', () => { + it('Sets the viewport width.', () => { + const browser = new Browser(); + const page = browser.newPage(); + page.setViewport({ width: 100 }); + expect(page.mainFrame.window.innerWidth).toBe(100); + expect(page.mainFrame.window.outerWidth).toBe(100); + }); + + it('Sets the viewport height.', () => { + const browser = new Browser(); + const page = browser.newPage(); + page.setViewport({ height: 100 }); + expect(page.mainFrame.window.innerHeight).toBe(100); + expect(page.mainFrame.window.outerHeight).toBe(100); + }); + + it('Sets the viewport width and height.', () => { + const browser = new Browser(); + const page = browser.newPage(); + page.setViewport({ width: 100, height: 100 }); + expect(page.mainFrame.window.innerWidth).toBe(100); + expect(page.mainFrame.window.outerWidth).toBe(100); + expect(page.mainFrame.window.innerHeight).toBe(100); + expect(page.mainFrame.window.outerHeight).toBe(100); + }); + + it('Sets the viewport device scale factor.', () => { + const browser = new Browser(); + const page = browser.newPage(); + page.setViewport({ deviceScaleFactor: 2 }); + expect(page.mainFrame.window.devicePixelRatio).toBe(2); + }); + }); + + describe('goto()', () => { + it('Goes to a page.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + let usedURL: string | null = null; + let usedOptions: IGoToOptions | null = null; + + vi.spyOn(page.mainFrame, 'goto').mockImplementation((url, options) => { + usedURL = url; + usedOptions = options; + return Promise.resolve({ url }); + }); + + const response = await page.goto('http://localhost:3000', { timeout: 10000 }); + expect((response).url).toBe('http://localhost:3000'); + expect(usedURL).toBe('http://localhost:3000'); + expect(usedOptions).toEqual({ timeout: 10000 }); + }); + }); +}); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts new file mode 100644 index 000000000..af8118afc --- /dev/null +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts @@ -0,0 +1,149 @@ +import DetachedBrowser from '../../../src/browser/detached-browser/DetachedBrowser'; +import DetachedBrowserContext from '../../../src/browser/detached-browser/DetachedBrowserContext'; +import DetachedBrowserPage from '../../../src/browser/detached-browser/DetachedBrowserPage'; +import DefaultBrowserSettings from '../../../src/browser/DefaultBrowserSettings'; +import Window from '../../../src/window/Window'; +import { describe, it, expect, afterEach, vi } from 'vitest'; + +describe('DetachedBrowser', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('get contexts()', () => { + it('Returns the contexts.', () => { + const browser = new DetachedBrowser(Window, new Window()); + expect(browser.contexts.length).toBe(1); + expect(browser.contexts[0]).toBe(browser.defaultContext); + + expect(browser.contexts.length).toBe(1); + expect(browser.contexts[0]).toBe(browser.defaultContext); + + browser.defaultContext.close(); + + expect(browser.contexts.length).toBe(0); + }); + }); + + describe('get settings()', () => { + it('Returns the settings.', () => { + expect(new DetachedBrowser(Window, new Window()).settings).toEqual(DefaultBrowserSettings); + }); + + it('Returns the settings with custom settings.', () => { + const settings = { + disableJavaScriptEvaluation: true, + navigator: { + userAgent: 'test' + } + }; + expect(new DetachedBrowser(Window, new Window(), { settings }).settings).toEqual({ + ...DefaultBrowserSettings, + ...settings, + navigator: { + ...DefaultBrowserSettings.navigator, + ...settings.navigator + } + }); + }); + }); + + describe('get console()', () => { + it('Returns "null" if no console is provided.', () => { + expect(new DetachedBrowser(Window, new Window()).console).toBe(null); + }); + + it('Returns console sent into the constructor.', () => { + expect(new DetachedBrowser(Window, new Window(), { console }).console).toBe(console); + }); + }); + + describe('get defaultContext()', () => { + it('Returns the default context.', () => { + const browser = new DetachedBrowser(Window, new Window()); + expect(browser.defaultContext instanceof DetachedBrowserContext).toBe(true); + expect(browser.contexts[0]).toBe(browser.defaultContext); + }); + + it('Throws an error if the browser has been closed.', () => { + const browser = new DetachedBrowser(Window, new Window()); + browser.close(); + expect(() => browser.defaultContext).toThrow( + 'No default context. The browser has been closed.' + ); + }); + }); + + describe('close()', () => { + it('Closes the browser.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const originalClose = browser.defaultContext.close; + let isContextClosed = false; + + vi.spyOn(browser.defaultContext, 'close').mockImplementation(() => { + isContextClosed = true; + originalClose.call(browser.defaultContext); + }); + + browser.close(); + expect(browser.contexts.length).toBe(0); + expect(isContextClosed).toBe(true); + }); + }); + + describe('whenComplete()', () => { + it('Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page1 = browser.newPage(); + const page2 = browser.newPage(); + page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + await browser.whenComplete(); + expect(page1.mainFrame.window['test']).toBe(1); + expect(page2.mainFrame.window['test']).toBe(2); + }); + }); + + describe('abort()', () => { + it('Aborts all ongoing operations.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page1 = browser.newPage(); + const page2 = browser.newPage(); + page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + browser.abort(); + await new Promise((resolve) => setTimeout(resolve, 30)); + expect(page1.mainFrame.window['test']).toBeUndefined(); + expect(page2.mainFrame.window['test']).toBeUndefined(); + }); + }); + + describe('newIncognitoContext()', () => { + it('Throws an error as it is not possible to create a new incognito context inside a detached browser.', () => { + const browser = new DetachedBrowser(Window, new Window()); + browser.close(); + expect(() => browser.newIncognitoContext()).toThrow( + 'Not possible to create a new context on a detached browser.' + ); + }); + }); + + describe('newPage()', () => { + it('Creates a new page.', () => { + const window = new Window(); + const browser = new DetachedBrowser(Window, window); + const page = browser.newPage(); + expect(page instanceof DetachedBrowserPage).toBe(true); + expect(browser.contexts.length).toBe(1); + expect(browser.contexts[0].pages.length).toBe(2); + expect(browser.contexts[0].pages[0].mainFrame.window).toBe(window); + expect(browser.contexts[0].pages[1]).toBe(page); + }); + + it('Throws an error if the browser has been closed.', () => { + const browser = new DetachedBrowser(Window, new Window()); + browser.close(); + expect(() => browser.newPage()).toThrow('No default context. The browser has been closed.'); + }); + }); +}); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts new file mode 100644 index 000000000..a68a9b83a --- /dev/null +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts @@ -0,0 +1,93 @@ +import DetachedBrowser from '../../../src/browser/detached-browser/DetachedBrowser'; +import DetachedBrowserPage from '../../../src/browser/detached-browser/DetachedBrowserPage'; +import Window from '../../../src/window/Window'; +import { describe, it, expect, afterEach, vi } from 'vitest'; + +describe('DetachedBrowserContext', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('get pages()', () => { + it('Returns the pages.', () => { + const window = new Window(); + const browser = new DetachedBrowser(Window, window); + expect(browser.defaultContext.pages.length).toBe(1); + expect(browser.defaultContext.pages[0].mainFrame.window).toBe(window); + const page = browser.defaultContext.newPage(); + expect(browser.defaultContext.pages.length).toBe(2); + expect(browser.defaultContext.pages[0].mainFrame.window).toBe(window); + expect(browser.defaultContext.pages[1]).toBe(page); + }); + }); + + describe('get browser()', () => { + it('Returns the browser.', () => { + const browser = new DetachedBrowser(Window, new Window()); + expect(browser.defaultContext.browser).toBe(browser); + }); + }); + + describe('close()', () => { + it('Closes the context.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const context = browser.defaultContext; + const page1 = context.newPage(); + const page2 = context.newPage(); + const originalClose1 = page1.close; + const originalClose2 = page2.close; + let pagesClosed = 0; + vi.spyOn(page1, 'close').mockImplementation(() => { + pagesClosed++; + originalClose1.call(page1); + }); + vi.spyOn(page2, 'close').mockImplementation(() => { + pagesClosed++; + originalClose2.call(page2); + }); + expect(browser.contexts.length).toBe(1); + context.close(); + expect(browser.contexts.length).toBe(0); + expect(pagesClosed).toBe(2); + }); + }); + + describe('whenComplete()', () => { + it('Waits for all pages to complete.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page1 = browser.newPage(); + const page2 = browser.newPage(); + page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + await browser.defaultContext.whenComplete(); + expect(page1.mainFrame.window['test']).toBe(1); + expect(page2.mainFrame.window['test']).toBe(2); + }); + }); + + describe('abort()', () => { + it('Aborts all ongoing operations.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page1 = browser.newPage(); + const page2 = browser.newPage(); + page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + browser.defaultContext.abort(); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(page1.mainFrame.window['test']).toBeUndefined(); + expect(page2.mainFrame.window['test']).toBeUndefined(); + }); + }); + + describe('newPage()', () => { + it('Creates a new page.', () => { + const window = new Window(); + const browser = new DetachedBrowser(Window, window); + const page = browser.defaultContext.newPage(); + expect(page instanceof DetachedBrowserPage).toBe(true); + expect(browser.defaultContext.pages.length).toBe(2); + expect(browser.defaultContext.pages[0].mainFrame.window).toBe(window); + expect(browser.defaultContext.pages[1]).toBe(page); + }); + }); +}); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts new file mode 100644 index 000000000..3f8ec36d0 --- /dev/null +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts @@ -0,0 +1,224 @@ +import DetachedBrowser from '../../../src/browser/detached-browser/DetachedBrowser'; +import DetachedBrowserFrame from '../../../src/browser/detached-browser/DetachedBrowserFrame'; +import Window from '../../../src/window/Window'; +import VirtualConsolePrinter from '../../../src/console/VirtualConsolePrinter'; +import VirtualConsole from '../../../src/console/VirtualConsole'; +import BrowserFrameUtility from '../../../src/browser/BrowserFrameUtility'; +import IResponse from '../../../src/fetch/types/IResponse'; +import { describe, it, expect, afterEach, vi } from 'vitest'; +import IGoToOptions from '../../../src/browser/types/IGoToOptions'; + +describe('DetachedBrowserPage', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('get virtualConsolePrinter()', () => { + it('Returns the virtual console printer.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + expect(page.virtualConsolePrinter).toBeInstanceOf(VirtualConsolePrinter); + }); + }); + + describe('get mainFrame()', () => { + it('Returns the mainFrame.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + expect(page.mainFrame).toBeInstanceOf(DetachedBrowserFrame); + expect(page.mainFrame.window).toBeInstanceOf(Window); + }); + }); + + describe('get context()', () => { + it('Returns the context.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + expect(page.context).toBe(browser.defaultContext); + }); + }); + + describe('get console()', () => { + it('Returns a virtual console by default.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + expect(page.console).toBeInstanceOf(VirtualConsole); + page.console.log('test'); + expect(page.virtualConsolePrinter.readAsString()).toBe('test\n'); + }); + + it('Returns the browser console if set.', () => { + const browser = new DetachedBrowser(Window, new Window(), { console }); + const page = browser.defaultContext.newPage(); + expect(page.console).toBe(console); + }); + }); + + describe('get frames()', () => { + it('Returns the frames.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + expect(page.frames).toEqual([page.mainFrame, frame1, frame2]); + }); + }); + + describe('get content()', () => { + it('Returns the document HTML content.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + page.mainFrame.window.document.write('
test
'); + expect(page.content).toBe('
test
'); + }); + }); + + describe('set content()', () => { + it('Sets the document HTML content.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + page.content = '
test
'; + expect(page.mainFrame.window.document.documentElement.outerHTML).toBe( + '
test
' + ); + }); + }); + + describe('get url()', () => { + it('Returns the document URL.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + page.mainFrame.url = 'http://localhost:3000'; + expect(page.url).toBe('http://localhost:3000/'); + }); + }); + + describe('set url()', () => { + it('Sets the document URL.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + page.url = 'http://localhost:3000'; + expect(page.mainFrame.window.location.href).toBe('http://localhost:3000/'); + }); + }); + + describe('close()', () => { + it('Closes the page.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + const mainFrame = BrowserFrameUtility.newFrame(page.mainFrame); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + + page.close(); + + // There is always one page in a detached browser context. + expect(browser.defaultContext.pages.length).toBe(1); + + expect(page.virtualConsolePrinter).toBe(null); + expect(page.context).toBe(null); + expect(page.mainFrame).toBe(null); + expect(mainFrame.window).toBe(null); + expect(frame1.window).toBe(null); + expect(frame2.window).toBe(null); + }); + }); + + describe('whenComplete()', () => { + it('Waits for all pages to complete.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + await page.whenComplete(); + expect(frame1.window['test']).toBe(1); + expect(frame2.window['test']).toBe(2); + }); + }); + + describe('abort()', () => { + it('Aborts all ongoing operations.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + page.abort(); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(frame1.window['test']).toBeUndefined(); + expect(frame2.window['test']).toBeUndefined(); + }); + }); + + describe('evaluate()', () => { + it("Evaluates code in the page's context.", () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + let evaluatedCode: string | null = null; + vi.spyOn(page.mainFrame, 'evaluate').mockImplementation((code) => { + evaluatedCode = code; + return 'returnValue'; + }); + expect(page.evaluate('test')).toBe('returnValue'); + expect(evaluatedCode).toBe('test'); + }); + }); + + describe('setViewport()', () => { + it('Sets the viewport width.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + page.setViewport({ width: 100 }); + expect(page.mainFrame.window.innerWidth).toBe(100); + expect(page.mainFrame.window.outerWidth).toBe(100); + }); + + it('Sets the viewport height.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + page.setViewport({ height: 100 }); + expect(page.mainFrame.window.innerHeight).toBe(100); + expect(page.mainFrame.window.outerHeight).toBe(100); + }); + + it('Sets the viewport width and height.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + page.setViewport({ width: 100, height: 100 }); + expect(page.mainFrame.window.innerWidth).toBe(100); + expect(page.mainFrame.window.outerWidth).toBe(100); + expect(page.mainFrame.window.innerHeight).toBe(100); + expect(page.mainFrame.window.outerHeight).toBe(100); + }); + + it('Sets the viewport device scale factor.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + page.setViewport({ deviceScaleFactor: 2 }); + expect(page.mainFrame.window.devicePixelRatio).toBe(2); + }); + }); + + describe('goto()', () => { + it('Goes to a page.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + let usedURL: string | null = null; + let usedOptions: IGoToOptions | null = null; + + vi.spyOn(page.mainFrame, 'goto').mockImplementation((url, options) => { + usedURL = url; + usedOptions = options; + return Promise.resolve({ url }); + }); + + const response = await page.goto('http://localhost:3000', { timeout: 10000 }); + expect((response).url).toBe('http://localhost:3000'); + expect(usedURL).toBe('http://localhost:3000'); + expect(usedOptions).toEqual({ timeout: 10000 }); + }); + }); +}); diff --git a/packages/happy-dom/test/location/Location.test.ts b/packages/happy-dom/test/location/Location.test.ts index 83d45374c..1f9b664d6 100644 --- a/packages/happy-dom/test/location/Location.test.ts +++ b/packages/happy-dom/test/location/Location.test.ts @@ -1,62 +1,106 @@ -import DOMException from '../../src/exception/DOMException.js'; -import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum.js'; +import Browser from '../../src/browser/Browser.js'; +import BrowserFrame from '../../src/browser/BrowserFrame.js'; +import IBrowserFrame from '../../src/browser/types/IBrowserFrame.js'; +import IGoToOptions from '../../src/browser/types/IGoToOptions.js'; +import IResponse from '../../src/fetch/types/IResponse.js'; import Location from '../../src/location/Location.js'; -import { beforeEach, describe, it, expect } from 'vitest'; +import { beforeEach, describe, it, expect, vi } from 'vitest'; const HREF = 'https://google.com/some-path/?key=value&key2=value2#hash'; describe('Location', () => { + let browserFrame: IBrowserFrame; let location: Location; beforeEach(() => { - location = new Location(); + browserFrame = new BrowserFrame(new Browser().newPage()); + location = new Location(browserFrame, 'about:blank'); }); - describe('replace()', () => { - it('Replaces the url.', () => { - location.replace(HREF); - expect(location.href).toBe(HREF); + describe('set href()', () => { + it('Calls browserFrame.goto() to navigate to the URL.', () => { + let calledURL: string | null = null; + let calledOptions: IGoToOptions | undefined = undefined; + + vi.spyOn(browserFrame, 'goto').mockImplementation( + async (url, options?: IGoToOptions): Promise => { + calledURL = url; + calledOptions = options; + return null; + } + ); + + location.href = HREF; + + expect(calledURL).toBe(HREF); + expect(calledOptions).toBeUndefined(); }); }); - describe('assign()', () => { - it('Assign the url.', () => { - location.assign(HREF); - expect(location.href).toBe(HREF); + describe('get href()', () => { + it('Returns the URL.', () => { + expect(location.href).toBe('about:blank'); + expect(new Location(browserFrame, HREF).href).toBe(HREF); }); }); - describe('reload()', () => { - it('Does nothing.', () => { + describe('replace()', () => { + it('Calls browserFrame.goto() to navigate to the URL.', () => { + let calledURL: string | null = null; + let calledOptions: IGoToOptions | undefined = undefined; + + vi.spyOn(browserFrame, 'goto').mockImplementation( + async (url, options?: IGoToOptions): Promise => { + calledURL = url; + calledOptions = options; + return null; + } + ); + location.replace(HREF); - location.reload(); - expect(location.href).toBe(HREF); + + expect(calledURL).toBe(HREF); + expect(calledOptions).toBeUndefined(); }); }); - describe('href', () => { - it('Successully sets a relative URL.', () => { - location.href = HREF; - expect(location.href).toBe(HREF); - location.href = '/foo'; - expect(location.href).toBe('https://google.com/foo'); - }); + describe('assign()', () => { + it('Calls browserFrame.goto() to navigate to the URL.', () => { + let calledURL: string | null = null; + let calledOptions: IGoToOptions | undefined = undefined; + + vi.spyOn(browserFrame, 'goto').mockImplementation( + async (url, options?: IGoToOptions): Promise => { + calledURL = url; + calledOptions = options; + return null; + } + ); + + location.assign(HREF); - it('Fails when it is not possible to construct a relative URL.', () => { - let error: Error | null = null; + expect(calledURL).toBe(HREF); + expect(calledOptions).toBeUndefined(); + }); + }); - try { - location.href = '/foo'; - } catch (e) { - error = e; - } + describe('reload()', () => { + it('Reloads the page by calling browserFrame.goto() with the same URL.', () => { + let calledURL: string | null = null; + let calledOptions: IGoToOptions | undefined = undefined; - expect(error).toEqual( - new DOMException( - `Failed to construct URL from string "/foo" relative to URL "about:blank".`, - DOMExceptionNameEnum.uriMismatchError - ) + vi.spyOn(browserFrame, 'goto').mockImplementation( + async (url, options?: IGoToOptions): Promise => { + calledURL = url; + calledOptions = options; + return null; + } ); + + location.reload(); + + expect(calledURL).toBe('about:blank'); + expect(calledOptions).toBeUndefined(); }); }); }); diff --git a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts index 6bc5cd1c0..1b0445c7d 100644 --- a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts +++ b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts @@ -7,6 +7,7 @@ import PointerEvent from '../../../src/event/events/PointerEvent.js'; import IRequest from '../../../src/fetch/types/IRequest.js'; import IResponse from '../../../src/fetch/types/IResponse.js'; import Fetch from '../../../src/fetch/Fetch.js'; +import Browser from '../../../src/browser/Browser.js'; const BLOB_URL = 'blob:https://mozilla.org'; @@ -406,27 +407,154 @@ describe('HTMLAnchorElement', () => { }); describe('dispatchEvent()', () => { - it(`Doesn't change the location when a "click" event is dispatched inside the main frame of a detached browser.`, () => { + it('Navigates the browser when a "click" event is dispatched on an element.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const window = page.mainFrame.window; + + let request: IRequest | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const element = window.document.createElement('a'); + element.href = 'https://www.example.com'; + window.document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + + const newWindow = page.mainFrame.window; + + expect(newWindow === window).toBe(false); + expect(newWindow.location.href).toBe('https://www.example.com/'); + + await browser.whenComplete(); + + expect(newWindow.document.body.innerHTML).toBe('Test'); + + newWindow.close(); + + expect(newWindow.closed).toBe(true); + }); + + it('Navigates the browser when a "click" event is dispatched on an element with target "_blank".', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const window = page.mainFrame.window; + + let request: IRequest | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const element = window.document.createElement('a'); + element.href = 'https://www.example.com'; + element.target = '_blank'; + window.document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + + const newWindow = browser.defaultContext.pages[1].mainFrame.window; + + expect(newWindow === window).toBe(false); + expect(newWindow.location.href).toBe('https://www.example.com/'); + + await browser.whenComplete(); + + expect(newWindow.document.body.innerHTML).toBe('Test'); + + newWindow.close(); + + expect(newWindow.closed).toBe(true); + }); + + it('Navigates the browser when a "click" event is dispatched on an element, even if the element is not connected to DOM.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const window = page.mainFrame.window; + + let request: IRequest | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const element = window.document.createElement('a'); + element.href = 'https://www.example.com'; + element.dispatchEvent(new PointerEvent('click')); + + const newWindow = page.mainFrame.window; + + expect(newWindow === window).toBe(false); + expect(newWindow.location.href).toBe('https://www.example.com/'); + + await browser.whenComplete(); + + expect(newWindow.document.body.innerHTML).toBe('Test'); + }); + + it(`Doesn't navigate or change the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "browserNavigation" is set to ["deny"].`, () => { + const window = new Window({ + settings: { + browserNavigation: ['deny'] + } + }); + document = window.document; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { throw new Error('Fetch should not be called.'); }); + const element = document.createElement('a'); element.href = 'https://www.example.com'; document.body.appendChild(element); element.dispatchEvent(new PointerEvent('click')); - expect(window.location.href).toBe('https://www.somesite.com/test.html'); + expect(window.location.href).toBe('about:blank'); }); - it('Changes the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "url-set-fallback" is set.', () => { + it(`Doesn't navigate, but changes the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "browserNavigation" is set to ["deny", "url-set-fallback"].`, () => { + const window = new Window({ + settings: { + browserNavigation: ['deny', 'url-set-fallback'] + } + }); + document = window.document; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + throw new Error('Fetch should not be called.'); + }); + + const newWindow = window.open(); + + const element = newWindow.document.createElement('a'); + element.href = 'https://www.example.com'; + newWindow.document.body.appendChild(element); + element.dispatchEvent(new PointerEvent('click')); + expect(newWindow.closed).toBe(false); + expect(newWindow.location.href).toBe('https://www.example.com/'); + }); + + it('Changes the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "browserNavigation" is set to ["allow", "url-set-fallback"].', () => { const window = new Window({ settings: { browserNavigation: ['allow', 'url-set-fallback'] } }); document = window.document; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { throw new Error('Fetch should not be called.'); }); + const element = document.createElement('a'); element.href = 'https://www.example.com'; document.body.appendChild(element); @@ -471,27 +599,5 @@ describe('HTMLAnchorElement', () => { expect(((request)).url).toBe('https://www.example.com/'); expect(newWindow.closed).toBe(true); }); - - it(`Doesn't navigate the browser when a "click" event is dispatched on an element inside a non-main frame of a detached browser when the Happy DOM setting "deny" is set to "true".`, () => { - const window = new Window({ - settings: { - browserNavigation: ['deny', 'url-set-fallback'] - } - }); - document = window.document; - - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { - throw new Error('Fetch should not be called.'); - }); - - const newWindow = window.open(); - - const element = newWindow.document.createElement('a'); - element.href = 'https://www.example.com'; - newWindow.document.body.appendChild(element); - element.dispatchEvent(new PointerEvent('click')); - expect(newWindow.closed).toBe(false); - expect(newWindow.location.href).toBe('https://www.example.com/'); - }); }); }); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 2fe800a6c..b1a4a5d4d 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -1293,7 +1293,7 @@ describe('Window', () => { setTimeout(() => { expect((loadEvent).target).toBe(document); resolve(null); - }, 1); + }, 10); }); }); @@ -1550,7 +1550,7 @@ describe('Window', () => { window.addEventListener('error', (event) => (errorEvent = event)); expect(newWindow).toBeInstanceOf(Window); expect(newWindow.location.href).toBe('about:blank'); - await new Promise((resolve) => setTimeout(resolve, 1)); + await new Promise((resolve) => setTimeout(resolve, 10)); expect(String(((errorEvent)).error)).toBe( 'ReferenceError: test is not defined' ); From 4c4f732d878a2c3c839cb18bd593bcea893046a2 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 14 Nov 2023 00:41:08 +0100 Subject: [PATCH 23/63] #466@trivial: Continues on implementation. --- .../src/browser/BrowserFrameUtility.ts | 9 ++- packages/happy-dom/src/fetch/Fetch.ts | 16 +++- packages/happy-dom/src/location/Location.ts | 15 ++-- .../HTMLIFrameElementPageLoader.ts | 1 + .../src/window/WindowPageOpenUtility.ts | 9 ++- .../test/browser/BrowserFrame.test.ts | 73 ++++++++++++++++++- .../happy-dom/test/location/Location.test.ts | 56 ++++++++++++++ packages/happy-dom/test/window/Window.test.ts | 23 +++++- 8 files changed, 177 insertions(+), 25 deletions(-) diff --git a/packages/happy-dom/src/browser/BrowserFrameUtility.ts b/packages/happy-dom/src/browser/BrowserFrameUtility.ts index 275cebf8b..eb8948514 100644 --- a/packages/happy-dom/src/browser/BrowserFrameUtility.ts +++ b/packages/happy-dom/src/browser/BrowserFrameUtility.ts @@ -78,6 +78,7 @@ export default class BrowserFrameUtility { /** * Go to a page. * + * @throws Error if the request can't be resolved (because of SSL error or similar). It will not throw if the response is not ok. * @param windowClass Window class. * @param frame Frame. * @param url URL. @@ -175,17 +176,19 @@ export default class BrowserFrameUtility { }); responseText = await response.text(); } catch (error) { - // TODO: Throw error as it can't be retrieved otherwise frame.window.clearTimeout(timeout); readyStateManager.endTask(); - WindowErrorUtility.dispatchError(frame.window, error); - return response || null; + throw error; } frame.window.clearTimeout(timeout); frame.content = responseText; readyStateManager.endTask(); + if (!response.ok) { + frame.page.console.error(`GET ${url} ${response.status} (${response.statusText})`); + } + return response; } diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index 5f534013a..24aaac02e 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -18,6 +18,7 @@ import Request from './Request.js'; import Response from './Response.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import CookieJar from '../cookie/CookieJar.js'; +import AbortSignal from './AbortSignal.js'; const SUPPORTED_SCHEMAS = ['data:', 'http:', 'https:']; const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; @@ -186,10 +187,12 @@ export default class Fetch { /** * Event listener for signal "abort" event. + * + * @param event Event. */ - private onSignalAbort(): void { + private onSignalAbort(event: Event): void { this.finalizeRequest(); - this.abort(); + this.abort((event.target)?.reason); } /** @@ -664,9 +667,14 @@ export default class Fetch { /** * Aborts the request. + * + * @param reason Reason. */ - private abort(): void { - const error = new DOMException('The operation was aborted.', DOMExceptionNameEnum.abortError); + private abort(reason?: string): void { + const error = new DOMException( + 'The operation was aborted.' + (reason ? ' ' + reason : ''), + DOMExceptionNameEnum.abortError + ); if (this.request.body) { this.request.body.destroy(error); diff --git a/packages/happy-dom/src/location/Location.ts b/packages/happy-dom/src/location/Location.ts index 7a60f2446..59ec3d2e2 100644 --- a/packages/happy-dom/src/location/Location.ts +++ b/packages/happy-dom/src/location/Location.ts @@ -23,7 +23,7 @@ export default class Location extends URL { */ // @ts-ignore public set href(url: string) { - this.#browserFrame.goto(url); + this.#browserFrame.goto(url).catch((error) => this.#browserFrame.page.console.error(error)); } /** @@ -39,25 +39,24 @@ export default class Location extends URL { * @param url URL. */ public replace(url: string): void { - this.#browserFrame.goto(url); + this.href = url; } /** * Loads the resource at the URL provided in parameter. * - * Note: Will do the same thing as "replace()" as server-dom does not support loading the URL. - * - * @param url - * @see this.replace() + * @param url URL. */ public assign(url: string): void { - this.#browserFrame.goto(url); + this.href = url; } /** * Reloads the resource from the current URL. */ public reload(): void { - this.#browserFrame.goto(this.href); + this.#browserFrame + .goto(this.href) + .catch((error) => this.#browserFrame.page.console.error(error)); } } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts index 49d7f3449..a4f05d791 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts @@ -128,6 +128,7 @@ export default class HTMLIFrameElementPageLoader { ? this.#browserIFrame.window : new CrossOriginWindow(this.#browserIFrame.window, window); + // TODO: Use BrowserFrame.goto() this.#browserIFrame.window .fetch(url) .then((response) => { diff --git a/packages/happy-dom/src/window/WindowPageOpenUtility.ts b/packages/happy-dom/src/window/WindowPageOpenUtility.ts index af57e4046..c17a123d8 100644 --- a/packages/happy-dom/src/window/WindowPageOpenUtility.ts +++ b/packages/happy-dom/src/window/WindowPageOpenUtility.ts @@ -1,4 +1,3 @@ -import { URL } from 'url'; import IWindow from './IWindow.js'; import CrossOriginWindow from './CrossOriginWindow.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; @@ -50,9 +49,11 @@ export default class WindowPageOpenUtility { break; } - targetFrame.goto(BrowserFrameUtility.getRelativeURL(browserFrame, url), { - referrer: features.noreferrer ? 'no-referrer' : undefined - }); + targetFrame + .goto(BrowserFrameUtility.getRelativeURL(browserFrame, url), { + referrer: features.noreferrer ? 'no-referrer' : undefined + }) + .catch((error) => targetFrame.page.console.error(error)); // When using the Window instance directly and not via the Browser API we should not navigate the browser frame. if (BrowserFrameUtility.isDetachedMainFrame(targetFrame)) { diff --git a/packages/happy-dom/test/browser/BrowserFrame.test.ts b/packages/happy-dom/test/browser/BrowserFrame.test.ts index 0bf81df7a..66d85caf8 100644 --- a/packages/happy-dom/test/browser/BrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/BrowserFrame.test.ts @@ -2,12 +2,13 @@ import { Script } from 'vm'; import Browser from '../../src/browser/Browser'; import BrowserFrameUtility from '../../src/browser/BrowserFrameUtility'; import Event from '../../src/event/Event'; -import ErrorEvent from '../../src/event/events/ErrorEvent'; import Window from '../../src/window/Window'; import IRequest from '../../src/fetch/types/IRequest'; import IResponse from '../../src/fetch/types/IResponse'; import { describe, it, expect, afterEach, vi } from 'vitest'; import Fetch from '../../src/fetch/Fetch'; +import DOMException from '../../src/exception/DOMException'; +import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum'; describe('BrowserFrame', () => { afterEach(() => { @@ -224,11 +225,75 @@ describe('BrowserFrame', () => { const browser = new Browser(); const page = browser.newPage(); const oldWindow = page.mainFrame.window; - const response = await page.mainFrame.goto('http://localhost:9999', { - timeout: 1 + let error: Error | null = null; + try { + await page.mainFrame.goto('http://localhost:9999', { + timeout: 1 + }); + } catch (e) { + error = e; + } + + expect(error).toEqual( + new DOMException( + 'The operation was aborted. Request timed out.', + DOMExceptionNameEnum.abortError + ) + ); + + expect(page.mainFrame.url).toBe('http://localhost:9999/'); + expect(page.mainFrame.window).not.toBe(oldWindow); + expect(page.mainFrame.window.document.body.innerHTML).toBe(''); + }); + + it('Handles error status code in response.', async () => { + let request: IRequest | null = null; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + url: request?.url, + status: 404, + statusText: 'Not Found', + text: () => + new Promise((resolve) => + setTimeout(() => resolve('404 error'), 1) + ) + }); }); - expect(response).toBeNull(); + const browser = new Browser(); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + const response = await page.mainFrame.goto('http://localhost:3000'); + + expect(page.mainFrame.url).toBe('http://localhost:3000/'); + expect(page.mainFrame.window).not.toBe(oldWindow); + expect(page.mainFrame.window.location.href).toBe('http://localhost:3000/'); + expect(page.mainFrame.window.document.body.innerHTML).toBe('404 error'); + + expect(((response)).status).toBe(404); + expect(page.virtualConsolePrinter.readAsString()).toBe( + 'GET http://localhost:3000/ 404 (Not Found)\n' + ); + }); + + it('Handles reject when performing fetch.', async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Error')); + }); + + const browser = new Browser(); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + let error: Error | null = null; + try { + await page.mainFrame.goto('http://localhost:9999'); + } catch (e) { + error = e; + } + + expect(error).toEqual(new Error('Error')); + expect(page.mainFrame.url).toBe('http://localhost:9999/'); expect(page.mainFrame.window).not.toBe(oldWindow); expect(page.mainFrame.window.document.body.innerHTML).toBe(''); diff --git a/packages/happy-dom/test/location/Location.test.ts b/packages/happy-dom/test/location/Location.test.ts index 1f9b664d6..dd60f6398 100644 --- a/packages/happy-dom/test/location/Location.test.ts +++ b/packages/happy-dom/test/location/Location.test.ts @@ -35,6 +35,20 @@ describe('Location', () => { expect(calledURL).toBe(HREF); expect(calledOptions).toBeUndefined(); }); + + it('Handles promise rejections.', async () => { + vi.spyOn(browserFrame, 'goto').mockImplementation((): Promise => { + return Promise.reject(new Error('Test error')); + }); + + location.href = HREF; + + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect( + browserFrame.page.virtualConsolePrinter.readAsString().startsWith('Error: Test error\n') + ).toBe(true); + }); }); describe('get href()', () => { @@ -62,6 +76,20 @@ describe('Location', () => { expect(calledURL).toBe(HREF); expect(calledOptions).toBeUndefined(); }); + + it('Handles promise rejections.', async () => { + vi.spyOn(browserFrame, 'goto').mockImplementation((): Promise => { + return Promise.reject(new Error('Test error')); + }); + + location.replace(HREF); + + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect( + browserFrame.page.virtualConsolePrinter.readAsString().startsWith('Error: Test error\n') + ).toBe(true); + }); }); describe('assign()', () => { @@ -82,6 +110,20 @@ describe('Location', () => { expect(calledURL).toBe(HREF); expect(calledOptions).toBeUndefined(); }); + + it('Handles promise rejections.', async () => { + vi.spyOn(browserFrame, 'goto').mockImplementation((): Promise => { + return Promise.reject(new Error('Test error')); + }); + + location.assign(HREF); + + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect( + browserFrame.page.virtualConsolePrinter.readAsString().startsWith('Error: Test error\n') + ).toBe(true); + }); }); describe('reload()', () => { @@ -102,5 +144,19 @@ describe('Location', () => { expect(calledURL).toBe('about:blank'); expect(calledOptions).toBeUndefined(); }); + + it('Handles promise rejections.', async () => { + vi.spyOn(browserFrame, 'goto').mockImplementation((): Promise => { + return Promise.reject(new Error('Test error')); + }); + + location.reload(); + + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect( + browserFrame.page.virtualConsolePrinter.readAsString().startsWith('Error: Test error\n') + ).toBe(true); + }); }); }); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index b1a4a5d4d..45809b5b3 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -1547,10 +1547,10 @@ describe('Window', () => { it('Dispatches error event when the Javascript code is invalid.', async () => { const newWindow = window.open(`javascript:document.write(test);`); let errorEvent: ErrorEvent | null = null; - window.addEventListener('error', (event) => (errorEvent = event)); + newWindow.addEventListener('error', (event) => (errorEvent = event)); expect(newWindow).toBeInstanceOf(Window); expect(newWindow.location.href).toBe('about:blank'); - await new Promise((resolve) => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 20)); expect(String(((errorEvent)).error)).toBe( 'ReferenceError: test is not defined' ); @@ -1753,5 +1753,24 @@ describe('Window', () => { }); }); }); + + it("Outputs error to the console if the request can't be resolved.", async () => { + const browser = new Browser(); + const page = browser.newPage(); + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Test error')); + }); + + page.mainFrame.window.open('https://www.github.com/'); + + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect( + browser.defaultContext.pages[1].virtualConsolePrinter + .readAsString() + .startsWith('Error: Test error\n') + ).toBe(true); + }); }); }); From e119f9fe2b66d5d85eb2ced8d9c58ed14462373c Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 15 Nov 2023 00:36:12 +0100 Subject: [PATCH 24/63] #466@trivial: Continues on implementation. --- .../happy-dom/src/browser/BrowserFrame.ts | 2 +- .../src/browser/BrowserFrameUtility.ts | 48 ++++++++++++------- .../detached-browser/DetachedBrowserFrame.ts | 2 +- .../HTMLIFrameElementPageLoader.ts | 43 +++++------------ .../src/window/WindowPageOpenUtility.ts | 6 +-- 5 files changed, 46 insertions(+), 55 deletions(-) diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 4e41ffb58..378dc774f 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -69,7 +69,7 @@ export default class BrowserFrame implements IBrowserFrame { public set url(url) { (this.window.location) = new Location( this, - BrowserFrameUtility.getRelativeURL(this, url) + BrowserFrameUtility.getRelativeURL(this, url).href ); } diff --git a/packages/happy-dom/src/browser/BrowserFrameUtility.ts b/packages/happy-dom/src/browser/BrowserFrameUtility.ts index eb8948514..c3b71d49c 100644 --- a/packages/happy-dom/src/browser/BrowserFrameUtility.ts +++ b/packages/happy-dom/src/browser/BrowserFrameUtility.ts @@ -95,9 +95,10 @@ export default class BrowserFrameUtility { url: string, options?: IGoToOptions ): Promise { - url = this.getRelativeURL(frame, url); + const originURL = new URL(frame.url); + const targetURL = this.getRelativeURL(frame, url); - if (url.startsWith('javascript:')) { + if (targetURL.protocol === 'javascript') { if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( (frame.window) @@ -106,7 +107,8 @@ export default class BrowserFrameUtility { readyStateManager.startTask(); frame.page.mainFrame.window.setTimeout(() => { - const code = '//# sourceURL=' + frame.url + '\n' + url.replace('javascript:', ''); + const code = + '//# sourceURL=' + frame.url + '\n' + targetURL.href.replace('javascript:', ''); if (frame.page.context.browser.settings.disableErrorCapturing) { frame.window.eval(code); @@ -122,10 +124,10 @@ export default class BrowserFrameUtility { if ( this.isDetachedMainFrame(frame) || - !this.isBrowserNavigationAllowed(frame, frame.url, url) + !this.isBrowserNavigationAllowed(frame, originURL, targetURL) ) { if (frame.page.context.browser.settings.browserNavigation.includes('url-set-fallback')) { - (frame.window.location) = new Location(frame, url); + (frame.window.location) = new Location(frame, targetURL.href); } return null; } @@ -142,14 +144,14 @@ export default class BrowserFrameUtility { (frame.window) = new windowClass({ browserFrame: frame, console: frame.page.console, - url + url: targetURL.href }); if (options?.referrer) { (frame.window.document.referrer) = options.referrer; } - if (!url || url.startsWith('about:')) { + if (targetURL.protocol === 'about') { return null; } @@ -169,11 +171,24 @@ export default class BrowserFrameUtility { ); try { - response = await frame.window.fetch(url, { + response = await frame.window.fetch(targetURL.href, { referrer: options?.referrer, referrerPolicy: options?.referrerPolicy, signal: abortController.signal }); + + // Handles the "X-Frame-Options" header for child frames. + if (frame.parentFrame) { + const xFrameOptions = response.headers.get('X-Frame-Options')?.toLowerCase(); + const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; + + if (xFrameOptions === 'deny' || (xFrameOptions === 'sameorigin' && !isSameOrigin)) { + throw new Error( + `Refused to display '${url}' in a frame because it set 'X-Frame-Options' to '${xFrameOptions}'.` + ); + } + } + responseText = await response.text(); } catch (error) { frame.window.clearTimeout(timeout); @@ -186,7 +201,7 @@ export default class BrowserFrameUtility { readyStateManager.endTask(); if (!response.ok) { - frame.page.console.error(`GET ${url} ${response.status} (${response.statusText})`); + frame.page.console.error(`GET ${targetURL.href} ${response.status} (${response.statusText})`); } return response; @@ -200,8 +215,8 @@ export default class BrowserFrameUtility { */ public static isBrowserNavigationAllowed( frame: IBrowserFrame, - fromURL: string, - toURL: string + fromURL: URL, + toURL: URL ): boolean { const settings = frame.page.context.browser.settings; @@ -209,10 +224,7 @@ export default class BrowserFrameUtility { return false; } - if ( - settings.browserNavigation.includes('sameorigin') && - new URL(fromURL).origin !== new URL(toURL).origin - ) { + if (settings.browserNavigation.includes('sameorigin') && fromURL.origin !== toURL.origin) { return false; } @@ -245,15 +257,15 @@ export default class BrowserFrameUtility { * @param url URL. * @returns Relative URL. */ - public static getRelativeURL(frame: IBrowserFrame, url: string): string { + public static getRelativeURL(frame: IBrowserFrame, url: string): URL { url = url || 'about:blank'; if (url.startsWith('about:') || url.startsWith('javascript:')) { - return url; + return new URL(url); } try { - return new URL(url, frame.window.location).href; + return new URL(url, frame.window.location); } catch (e) { if (frame.window.location.hostname) { throw new DOMException( diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index ed6acb5a3..be5d896d3 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -71,7 +71,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public set url(url) { (this.window.location) = new Location( this, - BrowserFrameUtility.getRelativeURL(this, url) + BrowserFrameUtility.getRelativeURL(this, url).href ); } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts index a4f05d791..2ddb2f0ae 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts @@ -53,35 +53,31 @@ export default class HTMLIFrameElementPageLoader { return; } - let url = this.#element.src || 'about:blank'; - - if (url !== 'about:blank' && !url.startsWith('javascript:')) { - url = new URL(this.#element.src, this.#browserParentFrame.window.location).href; - } + const window = this.#element.ownerDocument._defaultView; + const originURL = this.#browserParentFrame.window.location; + const targetURL = BrowserFrameUtility.getRelativeURL( + this.#browserParentFrame, + this.#element.src + ); + // Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different. + const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; + const parentWindow = isSameOrigin ? window : new CrossOriginWindow(window); - if (this.#browserIFrame && url === this.#browserIFrame.url) { + if (this.#browserIFrame && originURL.href === targetURL.href) { return; } - if (this.#browserIFrame) { - BrowserFrameUtility.closeFrame(this.#browserIFrame); - this.#browserIFrame = null; - } - - this.#contentWindowContainer.window = null; - if (this.#browserParentFrame.page.context.browser.settings.disableIframePageLoading) { WindowErrorUtility.dispatchError( this.#element, new DOMException( - `Failed to load iframe page "${url}". Iframe page loading is disabled.`, + `Failed to load iframe page "${targetURL.href}". Iframe page loading is disabled.`, DOMExceptionNameEnum.notSupportedError ) ); return; } - const window = this.#element.ownerDocument._defaultView; this.#browserIFrame = BrowserFrameUtility.newFrame(this.#browserParentFrame); if (url === 'about:blank' || url.startsWith('javascript:')) { @@ -107,23 +103,6 @@ export default class HTMLIFrameElementPageLoader { return; } - const originURL = window.location; - const targetURL = new URL(url, originURL); - - // Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different. - const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; - - this.#browserIFrame.url = url; - const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( - (this.#browserParentFrame.window) - ))._readyStateManager; - - readyStateManager.startTask(); - - const parentWindow = isSameOrigin ? window : new CrossOriginWindow(window); - (this.#browserIFrame.window.parent) = parentWindow; - (this.#browserIFrame.window.top) = parentWindow; - this.#contentWindowContainer.window = isSameOrigin ? this.#browserIFrame.window : new CrossOriginWindow(this.#browserIFrame.window, window); diff --git a/packages/happy-dom/src/window/WindowPageOpenUtility.ts b/packages/happy-dom/src/window/WindowPageOpenUtility.ts index c17a123d8..9989b6343 100644 --- a/packages/happy-dom/src/window/WindowPageOpenUtility.ts +++ b/packages/happy-dom/src/window/WindowPageOpenUtility.ts @@ -29,7 +29,7 @@ export default class WindowPageOpenUtility { const features = this.getWindowFeatures(options?.features || ''); const target = options?.target !== undefined ? String(options.target) : null; const originURL = browserFrame.window.location; - const url = BrowserFrameUtility.getRelativeURL(browserFrame, options.url); + const targetURL = BrowserFrameUtility.getRelativeURL(browserFrame, options.url); let targetFrame: IBrowserFrame; switch (target) { @@ -50,7 +50,7 @@ export default class WindowPageOpenUtility { } targetFrame - .goto(BrowserFrameUtility.getRelativeURL(browserFrame, url), { + .goto(targetURL.href, { referrer: features.noreferrer ? 'no-referrer' : undefined }) .catch((error) => targetFrame.page.console.error(error)); @@ -60,7 +60,7 @@ export default class WindowPageOpenUtility { return null; } - if (url.startsWith('javascript:')) { + if (targetURL.protocol === 'javascript') { return targetFrame.window; } From bd6853bace545765703737fce700a3ea39624d7c Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 15 Nov 2023 00:36:25 +0100 Subject: [PATCH 25/63] #466@trivial: Continues on implementation. --- .../HTMLIFrameElementPageLoader.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts index 2ddb2f0ae..4379fe7b8 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts @@ -59,9 +59,6 @@ export default class HTMLIFrameElementPageLoader { this.#browserParentFrame, this.#element.src ); - // Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different. - const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; - const parentWindow = isSameOrigin ? window : new CrossOriginWindow(window); if (this.#browserIFrame && originURL.href === targetURL.href) { return; @@ -78,8 +75,19 @@ export default class HTMLIFrameElementPageLoader { return; } + // Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different. + const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; + const parentWindow = isSameOrigin ? window : new CrossOriginWindow(window); + this.#browserIFrame = BrowserFrameUtility.newFrame(this.#browserParentFrame); + this.#browserIFrame.goto(targetURL.href).then((response) => { + this.#contentWindowContainer.window = isSameOrigin + ? this.#browserIFrame.window + : new CrossOriginWindow(this.#browserIFrame.window, window); + this.#element.dispatchEvent(new Event('load')); + }); + if (url === 'about:blank' || url.startsWith('javascript:')) { (this.#browserIFrame.window.parent) = window; (this.#browserIFrame.window.top) = window; From 91498b875c004b6dbfb584ede48b13903785b9b2 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Sat, 18 Nov 2023 18:51:40 +0100 Subject: [PATCH 26/63] #466@trivial: Continues on implementation. --- packages/happy-dom/src/browser/Browser.ts | 6 +- .../happy-dom/src/browser/BrowserContext.ts | 5 +- .../happy-dom/src/browser/BrowserFrame.ts | 6 +- .../src/browser/BrowserFrameUtility.ts | 90 ++++-- packages/happy-dom/src/browser/BrowserPage.ts | 5 +- .../src/browser/BrowserSettingsFactory.ts | 22 -- .../src/browser/DefaultBrowserSettings.ts | 3 +- .../detached-browser/DetachedBrowser.ts | 6 +- .../DetachedBrowserContext.ts | 5 +- .../detached-browser/DetachedBrowserFrame.ts | 6 +- .../detached-browser/DetachedBrowserPage.ts | 5 +- .../browser/types/BrowserNavigationEnum.ts | 10 + .../happy-dom/src/browser/types/IBrowser.ts | 4 +- .../src/browser/types/IBrowserContext.ts | 4 +- .../src/browser/types/IBrowserFrame.ts | 1 + .../src/browser/types/IBrowserSettings.ts | 4 +- .../browser/types/IOptionalBrowserSettings.ts | 6 +- .../browser/types/IReadOnlyBrowserSettings.ts | 23 -- .../HTMLIFrameElementPageLoader.ts | 65 +--- .../happy-dom/src/window/CrossOriginWindow.ts | 1 - ...pyDOMWindowAPI.ts => DetachedWindowAPI.ts} | 70 ++--- packages/happy-dom/src/window/IWindow.ts | 4 +- packages/happy-dom/src/window/Window.ts | 29 +- .../src/window/WindowPageOpenUtility.ts | 4 +- .../happy-dom/test/browser/Browser.test.ts | 7 + .../test/browser/BrowserContext.test.ts | 7 + .../test/browser/BrowserFrame.test.ts | 249 ++++++++++++++- .../detached-browser/DetachedBrowser.test.ts | 8 + .../DetachedBrowserContext.test.ts | 8 + .../HTMLAnchorElement.test.ts | 7 +- .../HTMLIFrameElement.test.ts | 75 +++-- .../test/window/DetachedWindowAPI.test.ts | 279 +++++++++++++++++ packages/happy-dom/test/window/Window.test.ts | 284 +++++------------- 33 files changed, 856 insertions(+), 452 deletions(-) create mode 100644 packages/happy-dom/src/browser/types/BrowserNavigationEnum.ts delete mode 100644 packages/happy-dom/src/browser/types/IReadOnlyBrowserSettings.ts rename packages/happy-dom/src/window/{HappyDOMWindowAPI.ts => DetachedWindowAPI.ts} (64%) create mode 100644 packages/happy-dom/test/window/DetachedWindowAPI.test.ts diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index f20662c06..e31131b22 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -4,6 +4,7 @@ import IOptionalBrowserSettings from './types/IOptionalBrowserSettings.js'; import BrowserSettingsFactory from './BrowserSettingsFactory.js'; import BrowserPage from './BrowserPage.js'; import IBrowser from './types/IBrowser.js'; +import BrowserFrame from './BrowserFrame.js'; /** * Browser. @@ -88,12 +89,13 @@ export default class Browser implements IBrowser { /** * Creates a new page. * + * @param [opener] Opener. * @returns Page. */ - public newPage(): BrowserPage { + public newPage(opener?: BrowserFrame): BrowserPage { if (this.contexts.length === 0) { throw new Error('No default context. The browser has been closed.'); } - return this.contexts[0].newPage(); + return this.contexts[0].newPage(opener); } } diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index 4b07d587a..73cbfae76 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -1,4 +1,5 @@ import Browser from './Browser.js'; +import BrowserFrame from './BrowserFrame.js'; import BrowserPage from './BrowserPage.js'; import IBrowserContext from './types/IBrowserContext.js'; @@ -53,10 +54,12 @@ export default class BrowserContext implements IBrowserContext { /** * Creates a new page. * + * @param [opener] Opener. * @returns Page. */ - public newPage(): BrowserPage { + public newPage(opener?: BrowserFrame): BrowserPage { const page = new BrowserPage(this); + ((page.mainFrame.opener)) = opener || null; this.pages.push(page); return page; } diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 378dc774f..7c9951e83 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -14,6 +14,7 @@ import { Script } from 'vm'; export default class BrowserFrame implements IBrowserFrame { public readonly childFrames: BrowserFrame[] = []; public readonly parentFrame: BrowserFrame | null = null; + public readonly opener: BrowserFrame | null = null; public readonly page: BrowserPage; public readonly window: Window; public _asyncTaskManager = new AsyncTaskManager(); @@ -112,8 +113,9 @@ export default class BrowserFrame implements IBrowserFrame { * * @param url URL. * @param [options] Options. + * @returns Response. */ - public async goto(url: string, options?: IGoToOptions): Promise { - return await BrowserFrameUtility.goto(Window, this, url, options); + public goto(url: string, options?: IGoToOptions): Promise { + return BrowserFrameUtility.goto(Window, this, url, options); } } diff --git a/packages/happy-dom/src/browser/BrowserFrameUtility.ts b/packages/happy-dom/src/browser/BrowserFrameUtility.ts index c3b71d49c..b8c29c0e0 100644 --- a/packages/happy-dom/src/browser/BrowserFrameUtility.ts +++ b/packages/happy-dom/src/browser/BrowserFrameUtility.ts @@ -15,6 +15,7 @@ import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import Location from '../location/Location.js'; import AbortController from '../fetch/AbortController.js'; import { Script } from 'vm'; +import BrowserNavigationEnum from './types/BrowserNavigationEnum.js'; /** * Browser frame utility. @@ -42,8 +43,9 @@ export default class BrowserFrameUtility { (frame.window.closed) = true; frame._asyncTaskManager.destroy(); WindowBrowserSettingsReader.removeSettings(frame.window); - (frame.page) = null; - (frame.window) = null; + (frame.page) = null; + (frame.window) = null; + (frame.opener) = null; } /** @@ -95,10 +97,9 @@ export default class BrowserFrameUtility { url: string, options?: IGoToOptions ): Promise { - const originURL = new URL(frame.url); const targetURL = this.getRelativeURL(frame, url); - if (targetURL.protocol === 'javascript') { + if (targetURL.protocol === 'javascript:') { if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( (frame.window) @@ -106,29 +107,33 @@ export default class BrowserFrameUtility { readyStateManager.startTask(); - frame.page.mainFrame.window.setTimeout(() => { - const code = - '//# sourceURL=' + frame.url + '\n' + targetURL.href.replace('javascript:', ''); + // The browser will wait for the next tick before executing the script. + await new Promise((resolve) => frame.page.mainFrame.window.setTimeout(resolve)); - if (frame.page.context.browser.settings.disableErrorCapturing) { - frame.window.eval(code); - } else { - WindowErrorUtility.captureError(frame.window, () => frame.window.eval(code)); - } + const code = + '//# sourceURL=' + frame.url + '\n' + targetURL.href.replace('javascript:', ''); - readyStateManager.endTask(); - }); + if (frame.page.context.browser.settings.disableErrorCapturing) { + frame.window.eval(code); + } else { + WindowErrorUtility.captureError(frame.window, () => frame.window.eval(code)); + } + + readyStateManager.endTask(); } + return null; } - if ( - this.isDetachedMainFrame(frame) || - !this.isBrowserNavigationAllowed(frame, originURL, targetURL) - ) { - if (frame.page.context.browser.settings.browserNavigation.includes('url-set-fallback')) { + if (this.isDetachedMainFrame(frame) || !this.isBrowserNavigationAllowed(frame, targetURL)) { + if ( + frame.page.context.browser.settings.browserNavigation.includes( + BrowserNavigationEnum.setURLFallback + ) + ) { (frame.window.location) = new Location(frame, targetURL.href); } + return null; } @@ -151,7 +156,7 @@ export default class BrowserFrameUtility { (frame.window.document.referrer) = options.referrer; } - if (targetURL.protocol === 'about') { + if (targetURL.protocol === 'about:') { return null; } @@ -179,6 +184,7 @@ export default class BrowserFrameUtility { // Handles the "X-Frame-Options" header for child frames. if (frame.parentFrame) { + const originURL = frame.parentFrame.window.location; const xFrameOptions = response.headers.get('X-Frame-Options')?.toLowerCase(); const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; @@ -213,26 +219,52 @@ export default class BrowserFrameUtility { * @param frame Frame. * @returns True if the frame is a detached main frame. */ - public static isBrowserNavigationAllowed( - frame: IBrowserFrame, - fromURL: URL, - toURL: URL - ): boolean { + public static isBrowserNavigationAllowed(frame: IBrowserFrame, toURL: URL): boolean { const settings = frame.page.context.browser.settings; + let fromURL = frame.page.mainFrame.window.location; + if (frame.opener) { + fromURL = frame.opener.window.location; + } + if (frame.parentFrame) { + fromURL = frame.parentFrame.window.location; + } + + if (settings.browserNavigation.includes(BrowserNavigationEnum.all)) { + return true; + } - if (settings.browserNavigation.includes('deny')) { + if ( + settings.browserNavigation.includes(BrowserNavigationEnum.sameOrigin) && + fromURL.protocol !== 'about:' && + toURL.protocol !== 'about:' && + toURL.protocol !== 'javascript:' && + fromURL.origin !== toURL.origin + ) { return false; } - if (settings.browserNavigation.includes('sameorigin') && fromURL.origin !== toURL.origin) { + if ( + settings.browserNavigation.includes(BrowserNavigationEnum.mainFrame) && + frame.page.mainFrame !== frame + ) { return false; } - if (settings.browserNavigation.includes('allow-children') && frame.page.mainFrame === frame) { + if ( + settings.browserNavigation.includes(BrowserNavigationEnum.childFrame) && + frame.page.mainFrame === frame + ) { return false; } - return true; + if ( + settings.browserNavigation.includes(BrowserNavigationEnum.allowChildPages) && + !!frame.opener + ) { + return true; + } + + return false; } /** diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index d82233794..cebbc1f17 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -118,8 +118,9 @@ export default class BrowserPage implements IBrowserPage { * * @param url URL. * @param [options] Options. + * @returns Response. */ - public async goto(url: string, options?: IGoToOptions): Promise { - return await this.mainFrame.goto(url, options); + public goto(url: string, options?: IGoToOptions): Promise { + return this.mainFrame.goto(url, options); } } diff --git a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts index d499c0fca..9296cff7c 100644 --- a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts +++ b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts @@ -1,7 +1,6 @@ import IBrowserSettings from './types/IBrowserSettings.js'; import IOptionalBrowserSettings from './types/IOptionalBrowserSettings.js'; import DefaultBrowserSettings from './DefaultBrowserSettings.js'; -import IReadOnlyBrowserSettings from './types/IReadOnlyBrowserSettings.js'; /** * Browser settings utility. @@ -28,25 +27,4 @@ export default class BrowserSettingsFactory { } }; } - /** - * Returns readonly browser settings. - * - * @param [settings] Browser settings. - * @param [freezeObject] "true" to freeze the object. - * @returns Settings. - */ - public static getReadOnlySettings(settings?: IOptionalBrowserSettings): IReadOnlyBrowserSettings { - return Object.freeze({ - ...DefaultBrowserSettings, - ...settings, - navigator: Object.freeze({ - ...DefaultBrowserSettings.navigator, - ...settings?.navigator - }), - device: Object.freeze({ - ...DefaultBrowserSettings.device, - ...settings?.device - }) - }); - } } diff --git a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts index fb7dafc78..7265d3af5 100644 --- a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts +++ b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts @@ -1,4 +1,5 @@ import PackageVersion from '../version.js'; +import BrowserNavigationEnum from './types/BrowserNavigationEnum.js'; import IBrowserSettings from './types/IBrowserSettings.js'; export default { @@ -10,7 +11,7 @@ export default { disableComputedStyleRendering: false, disableErrorCapturing: false, enableFileSystemHttpRequests: false, - browserNavigation: ['allow', 'url-set-fallback'], + browserNavigation: [BrowserNavigationEnum.allow, BrowserNavigationEnum.setURLFallback], navigator: { userAgent: `Mozilla/5.0 (X11; ${ process.platform.charAt(0).toUpperCase() + process.platform.slice(1) + ' ' + process.arch diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts index 39b030b24..76047effb 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts @@ -6,6 +6,7 @@ import DetachedBrowserPage from './DetachedBrowserPage.js'; import IBrowser from '../types/IBrowser.js'; import IWindow from '../../window/IWindow.js'; import IBrowserFrame from '../types/IBrowserFrame.js'; +import DetachedBrowserFrame from './DetachedBrowserFrame.js'; /** * Detached browser used when constructing a Window instance without a browser. @@ -98,12 +99,13 @@ export default class DetachedBrowser implements IBrowser { /** * Creates a new page. * + * @param [opener] Opener. * @returns Page. */ - public newPage(): DetachedBrowserPage { + public newPage(opener?: DetachedBrowserFrame): DetachedBrowserPage { if (this.contexts.length === 0) { throw new Error('No default context. The browser has been closed.'); } - return this.contexts[0].newPage(); + return this.contexts[0].newPage(opener); } } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts index 206085224..6fd76b94d 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts @@ -1,6 +1,7 @@ import DetachedBrowser from './DetachedBrowser.js'; import DetachedBrowserPage from './DetachedBrowserPage.js'; import IBrowserContext from '../types/IBrowserContext.js'; +import DetachedBrowserFrame from './DetachedBrowserFrame.js'; /** * Detached browser context used when constructing a Window instance without a browser. @@ -59,10 +60,12 @@ export default class DetachedBrowserContext implements IBrowserContext { /** * Creates a new page. * + * @param [opener] Opener. * @returns Page. */ - public newPage(): DetachedBrowserPage { + public newPage(opener?: DetachedBrowserFrame): DetachedBrowserPage { const page = new DetachedBrowserPage(this); + ((page.mainFrame.opener)) = opener || null; this.pages.push(page); return page; } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index be5d896d3..9d956f3e8 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -14,6 +14,7 @@ import { Script } from 'vm'; export default class DetachedBrowserFrame implements IBrowserFrame { public readonly childFrames: DetachedBrowserFrame[] = []; public readonly parentFrame: DetachedBrowserFrame | null = null; + public readonly opener: DetachedBrowserFrame | null = null; public readonly page: DetachedBrowserPage; public readonly window: IWindow; public _asyncTaskManager = new AsyncTaskManager(); @@ -112,9 +113,10 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * * @param url URL. * @param [options] Options. + * @returns Response. */ - public async goto(url: string, options?: IGoToOptions): Promise { - return await BrowserFrameUtility.goto( + public goto(url: string, options?: IGoToOptions): Promise { + return BrowserFrameUtility.goto( this.page.context.browser.detachedWindowClass, this, url, diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts index 3d13952bd..0bb7a5b6a 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts @@ -123,8 +123,9 @@ export default class DetachedBrowserPage implements IBrowserPage { * * @param url URL. * @param [options] Options. + * @returns Response. */ - public async goto(url: string, options?: IGoToOptions): Promise { - return await this.mainFrame.goto(url, options); + public goto(url: string, options?: IGoToOptions): Promise { + return this.mainFrame.goto(url, options); } } diff --git a/packages/happy-dom/src/browser/types/BrowserNavigationEnum.ts b/packages/happy-dom/src/browser/types/BrowserNavigationEnum.ts new file mode 100644 index 000000000..20a251ce1 --- /dev/null +++ b/packages/happy-dom/src/browser/types/BrowserNavigationEnum.ts @@ -0,0 +1,10 @@ +enum BrowserNavigationEnum { + all = 'all', + sameOrigin = 'sameorigin', + mainFrame = 'main-frame', + childFrame = 'child-frame', + childPage = 'child-page', + urlFallback = 'set-url-fallback' +} + +export default BrowserNavigationEnum; diff --git a/packages/happy-dom/src/browser/types/IBrowser.ts b/packages/happy-dom/src/browser/types/IBrowser.ts index 26a1bea3a..075b6669d 100644 --- a/packages/happy-dom/src/browser/types/IBrowser.ts +++ b/packages/happy-dom/src/browser/types/IBrowser.ts @@ -1,4 +1,5 @@ import IBrowserContext from './IBrowserContext.js'; +import IBrowserFrame from './IBrowserFrame.js'; import IBrowserPage from './IBrowserPage.js'; import IBrowserSettings from './IBrowserSettings.js'; @@ -40,7 +41,8 @@ export default interface IBrowser { /** * Creates a new page. * + * @param [opener] Opener. * @returns Page. */ - newPage(): IBrowserPage; + newPage(opener?: IBrowserFrame): IBrowserPage; } diff --git a/packages/happy-dom/src/browser/types/IBrowserContext.ts b/packages/happy-dom/src/browser/types/IBrowserContext.ts index 16a609750..a0a1669dd 100644 --- a/packages/happy-dom/src/browser/types/IBrowserContext.ts +++ b/packages/happy-dom/src/browser/types/IBrowserContext.ts @@ -1,4 +1,5 @@ import IBrowser from './IBrowser.js'; +import IBrowserFrame from './IBrowserFrame.js'; import IBrowserPage from './IBrowserPage.js'; /** @@ -28,7 +29,8 @@ export default interface IBrowserContext { /** * Creates a new page. * + * @param [opener] Opener. * @returns Page. */ - newPage(): IBrowserPage; + newPage(opener?: IBrowserFrame): IBrowserPage; } diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index 5015d19cf..aa0b1cf32 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -14,6 +14,7 @@ export default interface IBrowserFrame { content: string; url: string; readonly parentFrame: IBrowserFrame | null; + readonly opener: IBrowserFrame | null; readonly _asyncTaskManager: AsyncTaskManager; readonly page: IBrowserPage; diff --git a/packages/happy-dom/src/browser/types/IBrowserSettings.ts b/packages/happy-dom/src/browser/types/IBrowserSettings.ts index 1c51722c7..097a6ceef 100644 --- a/packages/happy-dom/src/browser/types/IBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IBrowserSettings.ts @@ -1,3 +1,5 @@ +import BrowserNavigationEnum from './BrowserNavigationEnum.js'; + /** * Browser settings. */ @@ -10,7 +12,7 @@ export default interface IBrowserSettings { disableComputedStyleRendering: boolean; disableErrorCapturing: boolean; enableFileSystemHttpRequests: boolean; - browserNavigation: Array<'allow' | 'deny' | 'sameorigin' | 'allow-children' | 'url-set-fallback'>; + browserNavigation: BrowserNavigationEnum[]; navigator: { userAgent: string; maxTouchPoints: number; diff --git a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts index 9a0802462..4fd64d9c0 100644 --- a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts @@ -1,3 +1,5 @@ +import BrowserNavigationEnum from './BrowserNavigationEnum.js'; + /** * Browser settings. */ @@ -10,9 +12,7 @@ export default interface IOptionalBrowserSettings { disableComputedStyleRendering?: boolean; disableErrorCapturing?: boolean; enableFileSystemHttpRequests?: boolean; - browserNavigation?: Array< - 'allow' | 'deny' | 'sameorigin' | 'allow-children' | 'url-set-fallback' - >; + browserNavigation?: BrowserNavigationEnum[]; navigator?: { userAgent?: string; maxTouchPoints?: number; diff --git a/packages/happy-dom/src/browser/types/IReadOnlyBrowserSettings.ts b/packages/happy-dom/src/browser/types/IReadOnlyBrowserSettings.ts deleted file mode 100644 index 2479bc6ef..000000000 --- a/packages/happy-dom/src/browser/types/IReadOnlyBrowserSettings.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Browser settings. - */ -export default interface IReadOnlyBrowserSettings { - readonly disableJavaScriptEvaluation: boolean; - readonly disableJavaScriptFileLoading: boolean; - readonly disableCSSFileLoading: boolean; - readonly disableIframePageLoading: boolean; - readonly disableWindowOpenPageLoading: boolean; - readonly disableComputedStyleRendering: boolean; - readonly disableErrorCapturing: boolean; - readonly enableFileSystemHttpRequests: boolean; - readonly browserNavigation: Array< - 'allow' | 'deny' | 'sameorigin' | 'allow-children' | 'url-set-fallback' - >; - readonly navigator: { - readonly userAgent: string; - }; - readonly device: { - readonly prefersColorScheme: string; - readonly mediaType: string; - }; -} diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts index 4379fe7b8..f52854507 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts @@ -1,4 +1,3 @@ -import URL from '../../url/URL.js'; import Event from '../../event/Event.js'; import IWindow from '../../window/IWindow.js'; import CrossOriginWindow from '../../window/CrossOriginWindow.js'; @@ -6,8 +5,6 @@ import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; import IHTMLIFrameElement from './IHTMLIFrameElement.js'; -import DocumentReadyStateManager from '../document/DocumentReadyStateManager.js'; -import IResponse from '../../fetch/types/IResponse.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import BrowserFrameUtility from '../../browser/BrowserFrameUtility.js'; @@ -60,7 +57,7 @@ export default class HTMLIFrameElementPageLoader { this.#element.src ); - if (this.#browserIFrame && originURL.href === targetURL.href) { + if (this.#browserIFrame && this.#browserIFrame.window.location.href === targetURL.href) { return; } @@ -79,64 +76,20 @@ export default class HTMLIFrameElementPageLoader { const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; const parentWindow = isSameOrigin ? window : new CrossOriginWindow(window); - this.#browserIFrame = BrowserFrameUtility.newFrame(this.#browserParentFrame); + this.#browserIFrame = + this.#browserIFrame ?? BrowserFrameUtility.newFrame(this.#browserParentFrame); - this.#browserIFrame.goto(targetURL.href).then((response) => { - this.#contentWindowContainer.window = isSameOrigin - ? this.#browserIFrame.window - : new CrossOriginWindow(this.#browserIFrame.window, window); - this.#element.dispatchEvent(new Event('load')); - }); + ((this.#browserIFrame.window.top)) = parentWindow; + ((this.#browserIFrame.window.parent)) = parentWindow; - if (url === 'about:blank' || url.startsWith('javascript:')) { - (this.#browserIFrame.window.parent) = window; - (this.#browserIFrame.window.top) = window; - this.#contentWindowContainer.window = this.#browserIFrame.window; - - if ( - url !== 'about:blank' && - !this.#browserParentFrame.page.context.browser.settings.disableJavaScriptEvaluation - ) { - const code = '//# sourceURL=about:blank\n' + url.replace('javascript:', ''); - if (this.#browserParentFrame.page.context.browser.settings.disableErrorCapturing) { - this.#browserIFrame.window.eval(code); - } else { - WindowErrorUtility.captureError(this.#browserIFrame.window, () => - this.#browserIFrame.window.eval(code) - ); - } - } - - this.#element.dispatchEvent(new Event('load')); - return; - } + this.#browserIFrame + .goto(targetURL.href) + .then(() => this.#element.dispatchEvent(new Event('load'))) + .catch((error) => WindowErrorUtility.dispatchError(this.#element, error)); this.#contentWindowContainer.window = isSameOrigin ? this.#browserIFrame.window : new CrossOriginWindow(this.#browserIFrame.window, window); - - // TODO: Use BrowserFrame.goto() - this.#browserIFrame.window - .fetch(url) - .then((response) => { - const xFrameOptions = response.headers.get('X-Frame-Options')?.toLowerCase(); - if (xFrameOptions === 'deny' || (xFrameOptions === 'sameorigin' && !isSameOrigin)) { - throw new Error( - `Refused to display '${url}' in a frame because it set 'X-Frame-Options' to '${xFrameOptions}'.` - ); - } - return response; - }) - .then((response: IResponse) => response.text()) - .then((responseText: string) => { - this.#browserIFrame.content = responseText; - readyStateManager.endTask(); - this.#element.dispatchEvent(new Event('load')); - }) - .catch((error) => { - readyStateManager.endTask(); - WindowErrorUtility.dispatchError(this.#element, error); - }); } /** diff --git a/packages/happy-dom/src/window/CrossOriginWindow.ts b/packages/happy-dom/src/window/CrossOriginWindow.ts index 995f81f28..4ac016290 100644 --- a/packages/happy-dom/src/window/CrossOriginWindow.ts +++ b/packages/happy-dom/src/window/CrossOriginWindow.ts @@ -31,7 +31,6 @@ export default class CrossOriginWindow extends EventTarget implements ICrossOrig {}, { get: () => { - debugger; throw new DOMException( `Blocked a frame with origin "${this.parent.location.origin}" from accessing a cross-origin frame.`, DOMExceptionNameEnum.securityError diff --git a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts b/packages/happy-dom/src/window/DetachedWindowAPI.ts similarity index 64% rename from packages/happy-dom/src/window/HappyDOMWindowAPI.ts rename to packages/happy-dom/src/window/DetachedWindowAPI.ts index 0d6ea9fb8..899a33094 100644 --- a/packages/happy-dom/src/window/HappyDOMWindowAPI.ts +++ b/packages/happy-dom/src/window/DetachedWindowAPI.ts @@ -1,17 +1,13 @@ -import IBrowserSettings from '../browser/types/IBrowserSettings.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; -import BrowserSettingsFactory from '../browser/BrowserSettingsFactory.js'; -import IReadOnlyBrowserSettings from '../browser/types/IReadOnlyBrowserSettings.js'; import IBrowserPageViewport from '../browser/types/IBrowserPageViewport.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; -import DetachedBrowserFrame from '../browser/detached-browser/DetachedBrowserFrame.js'; +import IBrowserSettings from '../browser/types/IBrowserSettings.js'; /** - * API for detached windows to be able to access features of the owner window. + * API for detached windows to be able to access features of the browser. */ -export default class HappyDOMWindowAPI { +export default class DetachedWindowAPI { #browserFrame?: IBrowserFrame; - #settings: IBrowserSettings | null = null; /** * Constructor. @@ -25,16 +21,11 @@ export default class HappyDOMWindowAPI { /** * Returns settings. * - * @deprecated Settings should not be read or written from Window. Use the Browser class instead to access settings. + * @deprecated Depreacted for security reasons and will be removed in the future. Use Browser API instead to access settings (e.g. new Browser()). * @returns Settings. */ - public get settings(): IReadOnlyBrowserSettings { - if (!this.#settings) { - this.#settings = BrowserSettingsFactory.getReadOnlySettings( - this.#browserFrame.page.context.browser.settings - ); - } - return this.#settings; + public get settings(): IBrowserSettings { + return this.#browserFrame.page.context.browser.settings; } /** @@ -49,53 +40,57 @@ export default class HappyDOMWindowAPI { /** * Waits for all async tasks to complete. * - * @deprecated Use whenComplete() instead. * @returns Promise. */ - public async whenAsyncComplete(): Promise { - return await this.whenComplete(); + public whenComplete(): Promise { + return this.#browserFrame.whenComplete(); } /** * Waits for all async tasks to complete. * + * @deprecated Use whenComplete() instead. * @returns Promise. */ - public async whenComplete(): Promise { - return await this.#browserFrame.whenComplete(); + public whenAsyncComplete(): Promise { + return this.whenComplete(); } /** * Aborts all async tasks. - * - * @deprecated Use abort() instead. */ - public cancelAsync(): void { - this.abort(); + public abort(): void { + this.#browserFrame.abort(); } /** * Aborts all async tasks. + * + * @deprecated Use abort() instead. */ - public abort(): void { - this.#browserFrame.abort(); + public cancelAsync(): void { + this.abort(); } /** - * Sets the URL on a detached window. - * It will throw an exception if the window is not detached, as a script could potentially use this method to bypass CORS. + * Sets the URL without navigating the browser. * + * @deprecated Depreacted for security reasons and will be removed in the future. Use Browser API instead to change URL (e.g. new Browser()). * @param url URL. */ public setURL(url: string): void { - if (!(this.#browserFrame instanceof DetachedBrowserFrame)) { - throw new Error( - 'Only detached browser frames can use the setURL() method for security reasons. Use the Browser API instead for setting URL.' - ); - } this.#browserFrame.url = url; } + /** + * Sets the viewport. + * + * @param viewport Viewport. + */ + public setViewport(viewport: IBrowserPageViewport): void { + this.#browserFrame.page.setViewport(viewport); + } + /** * Sets the window size. * @@ -111,15 +106,6 @@ export default class HappyDOMWindowAPI { }); } - /** - * Sets the viewport. - * - * @param viewport Viewport. - */ - public setViewport(viewport: IBrowserPageViewport): void { - this.#browserFrame.page.setViewport(viewport); - } - /** * Sets the window width. * diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index e67580e5f..353fcc03b 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -128,7 +128,7 @@ import PermissionStatus from '../permissions/PermissionStatus.js'; import Clipboard from '../clipboard/Clipboard.js'; import ClipboardItem from '../clipboard/ClipboardItem.js'; import ClipboardEvent from '../event/events/ClipboardEvent.js'; -import DetachedWindowAPI from './HappyDOMWindowAPI.js'; +import DetachedWindowAPI from './DetachedWindowAPI.js'; import Headers from '../fetch/Headers.js'; import Request from '../fetch/Request.js'; import Response from '../fetch/Response.js'; @@ -142,7 +142,7 @@ import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js */ export default interface IWindow extends IEventTarget, INodeJSGlobal { // Detached Window API. - readonly happyDOM: DetachedWindowAPI; + readonly happyDOM?: DetachedWindowAPI; // Nodes readonly Node: typeof Node; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 60ad3e9b5..46cc467b2 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -126,7 +126,7 @@ import PermissionStatus from '../permissions/PermissionStatus.js'; import Clipboard from '../clipboard/Clipboard.js'; import ClipboardItem from '../clipboard/ClipboardItem.js'; import ClipboardEvent from '../event/events/ClipboardEvent.js'; -import HappyDOMWindowAPI from './HappyDOMWindowAPI.js'; +import DetachedWindowAPI from './DetachedWindowAPI.js'; import Headers from '../fetch/Headers.js'; import WindowClassFactory from './WindowClassFactory.js'; import Audio from '../nodes/html-audio-element/Audio.js'; @@ -163,7 +163,7 @@ const ORIGINAL_QUEUE_MICROTASK = queueMicrotask; */ export default class Window extends EventTarget implements IWindow { // Detached Window API. - public readonly happyDOM: HappyDOMWindowAPI; + public readonly happyDOM?: DetachedWindowAPI; // Nodes public readonly Node: typeof Node; @@ -421,7 +421,6 @@ export default class Window extends EventTarget implements IWindow { public readonly screenY: number = 0; public readonly crypto = webcrypto; public readonly closed = false; - public readonly console: Console; public name: string = ''; // Node.js Globals @@ -539,12 +538,11 @@ export default class Window extends EventTarget implements IWindow { console: options?.console, settings: options?.settings }).defaultContext.pages[0].mainFrame; + this.happyDOM = new DetachedWindowAPI(this.#browserFrame); } WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); - this.console = this.#browserFrame.page.console; - this.happyDOM = new HappyDOMWindowAPI(this.#browserFrame); this.location = new Location(this.#browserFrame, options?.url ?? 'about:blank'); if (options) { @@ -721,9 +719,18 @@ export default class Window extends EventTarget implements IWindow { } /** - * The number of pixels that the document is currently scrolled horizontally + * Returns the console. * - * @returns number + * @returns Console. + */ + public get console(): Console { + return this.#browserFrame.page.console; + } + + /** + * The number of pixels that the document is currently scrolled horizontally. + * + * @returns Scroll X. */ public get scrollX(): number { return this.document?.documentElement?.scrollLeft ?? 0; @@ -732,16 +739,16 @@ export default class Window extends EventTarget implements IWindow { /** * The read-only Window property pageXOffset is an alias for scrollX. * - * @returns number + * @returns Scroll X. */ public get pageXOffset(): number { return this.scrollX; } /** - * The number of pixels that the document is currently scrolled vertically + * The number of pixels that the document is currently scrolled vertically. * - * @returns number + * @returns Scroll Y. */ public get scrollY(): number { return this.document?.documentElement?.scrollTop ?? 0; @@ -750,7 +757,7 @@ export default class Window extends EventTarget implements IWindow { /** * The read-only Window property pageYOffset is an alias for scrollY. * - * @returns number + * @returns Scroll Y. */ public get pageYOffset(): number { return this.scrollY; diff --git a/packages/happy-dom/src/window/WindowPageOpenUtility.ts b/packages/happy-dom/src/window/WindowPageOpenUtility.ts index 9989b6343..b78d871f0 100644 --- a/packages/happy-dom/src/window/WindowPageOpenUtility.ts +++ b/packages/happy-dom/src/window/WindowPageOpenUtility.ts @@ -44,7 +44,7 @@ export default class WindowPageOpenUtility { break; case '_blank': default: - const newPage = browserFrame.page.context.newPage(); + const newPage = browserFrame.page.context.newPage(browserFrame); targetFrame = newPage.mainFrame; break; } @@ -60,7 +60,7 @@ export default class WindowPageOpenUtility { return null; } - if (targetURL.protocol === 'javascript') { + if (targetURL.protocol === 'javascript:') { return targetFrame.window; } diff --git a/packages/happy-dom/test/browser/Browser.test.ts b/packages/happy-dom/test/browser/Browser.test.ts index f2acf213f..69a46474b 100644 --- a/packages/happy-dom/test/browser/Browser.test.ts +++ b/packages/happy-dom/test/browser/Browser.test.ts @@ -163,5 +163,12 @@ describe('Browser', () => { browser.close(); expect(() => browser.newPage()).toThrow('No default context. The browser has been closed.'); }); + + it('Supports opener as parameter.', () => { + const browser = new Browser(); + const page1 = browser.newPage(); + const page2 = browser.newPage(page1.mainFrame); + expect(page2.mainFrame.opener).toBe(page1.mainFrame); + }); }); }); diff --git a/packages/happy-dom/test/browser/BrowserContext.test.ts b/packages/happy-dom/test/browser/BrowserContext.test.ts index 62dfe3fdd..7610a99bb 100644 --- a/packages/happy-dom/test/browser/BrowserContext.test.ts +++ b/packages/happy-dom/test/browser/BrowserContext.test.ts @@ -83,5 +83,12 @@ describe('BrowserContext', () => { expect(browser.defaultContext.pages.length).toBe(1); expect(browser.defaultContext.pages[0]).toBe(page); }); + + it('Supports opener as parameter.', () => { + const browser = new Browser(); + const page1 = browser.defaultContext.newPage(); + const page2 = browser.defaultContext.newPage(page1.mainFrame); + expect(page2.mainFrame.opener).toBe(page1.mainFrame); + }); }); }); diff --git a/packages/happy-dom/test/browser/BrowserFrame.test.ts b/packages/happy-dom/test/browser/BrowserFrame.test.ts index 66d85caf8..6453dde5f 100644 --- a/packages/happy-dom/test/browser/BrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/BrowserFrame.test.ts @@ -9,6 +9,7 @@ import { describe, it, expect, afterEach, vi } from 'vitest'; import Fetch from '../../src/fetch/Fetch'; import DOMException from '../../src/exception/DOMException'; import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum'; +import BrowserNavigationEnum from '../../src/browser/types/BrowserNavigationEnum'; describe('BrowserFrame', () => { afterEach(() => { @@ -203,10 +204,6 @@ describe('BrowserFrame', () => { expect(page.mainFrame.url).toBe('about:blank'); expect(page.mainFrame.window).toBe(oldWindow); - expect(page.mainFrame.window.document.body.innerHTML).toBe(''); - - await new Promise((resolve) => setTimeout(resolve, 2)); - expect(page.mainFrame.window.document.body.innerHTML).toBe('test'); }); @@ -298,5 +295,249 @@ describe('BrowserFrame', () => { expect(page.mainFrame.window).not.toBe(oldWindow); expect(page.mainFrame.window.document.body.innerHTML).toBe(''); }); + + it(`Doesn't navigate if the setting "browserNavigation" is set to "['deny']"`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new Browser({ + settings: { + browserNavigation: [BrowserNavigationEnum.deny] + } + }); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + + await page.mainFrame.goto('http://localhost:9999'); + + expect(page.mainFrame.url).toBe('about:blank'); + expect(page.mainFrame.window === oldWindow).toBe(true); + }); + + it(`Navigates if the setting "browserNavigation" is set to "['allow']"`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const browser = new Browser({ + settings: { + browserNavigation: [BrowserNavigationEnum.allow] + } + }); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + + await page.mainFrame.goto('http://localhost:9999'); + + expect(page.mainFrame.url).toBe('http://localhost:9999/'); + expect(page.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate if the setting "browserNavigation" is set to "['allow-same-origin']" and the parent is of a different origin.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new Browser({ + settings: { + browserNavigation: [BrowserNavigationEnum.allowSameOrigin] + } + }); + const page = browser.newPage(); + const childFrame = BrowserFrameUtility.newFrame(page.mainFrame); + const oldWindow = childFrame.window; + + page.mainFrame.url = 'https://github.com'; + + const response = await childFrame.goto('http://localhost:9999'); + + expect(response).toBeNull(); + expect(childFrame.url).toBe('about:blank'); + expect(childFrame.window === oldWindow).toBe(true); + }); + + it(`Navigates if the setting "browserNavigation" is set to "['allow-same-origin']" and the parent is the same origin.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new Browser({ + settings: { + browserNavigation: [BrowserNavigationEnum.allowSameOrigin] + } + }); + const page = browser.newPage(); + const childFrame = BrowserFrameUtility.newFrame(page.mainFrame); + const oldWindow = childFrame.window; + + page.mainFrame.url = 'https://github.com'; + + await childFrame.goto('https://github.com/capricorn86/happy-dom'); + + expect(childFrame.url).toBe('https://github.com/capricorn86/happy-dom'); + expect(childFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate if the setting "browserNavigation" is set to "['allow-same-origin']" and the opener is of a different origin.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new Browser({ + settings: { + browserNavigation: [BrowserNavigationEnum.allowSameOrigin] + } + }); + const page = browser.newPage(); + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + page.mainFrame.url = 'https://github.com'; + + const response = await childPage.mainFrame.goto('http://localhost:9999'); + + expect(response).toBeNull(); + expect(childPage.mainFrame.url).toBe('about:blank'); + expect(childPage.mainFrame.window === oldWindow).toBe(true); + }); + + it(`Navigate if the setting "browserNavigation" is set to "['allow-same-origin']" and the opener has the same origin.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new Browser({ + settings: { + browserNavigation: [BrowserNavigationEnum.allowSameOrigin] + } + }); + const page = browser.newPage(); + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + page.mainFrame.url = 'https://github.com'; + + await childPage.mainFrame.goto('https://github.com/capricorn86/happy-dom'); + + expect(childPage.mainFrame.url).toBe('https://github.com/capricorn86/happy-dom'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate if the setting "browserNavigation" is set to "['allow-same-origin']" when navigating from one origin to another.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new Browser({ + settings: { + browserNavigation: [BrowserNavigationEnum.allowSameOrigin] + } + }); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + + page.mainFrame.url = 'https://github.com'; + + const response = await page.mainFrame.goto('http://localhost:9999'); + + expect(response).toBeNull(); + expect(page.mainFrame.url).toBe('https://github.com/'); + expect(page.mainFrame.window === oldWindow).toBe(true); + }); + + it(`Navigates if the setting "browserNavigation" is set to "['allowChildFrames']" inside a child frame.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new Browser({ + settings: { + browserNavigation: [BrowserNavigationEnum.allowChildFrames] + } + }); + const page = browser.newPage(); + const childFrame = BrowserFrameUtility.newFrame(page.mainFrame); + const oldWindow = childFrame.window; + + await childFrame.goto('http://localhost:9999'); + + expect(childFrame.url).toBe('http://localhost:9999/'); + expect(childFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate if the setting "browserNavigation" is set to "['allowChildFrames']" inside a main frame.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new Browser({ + settings: { + browserNavigation: [BrowserNavigationEnum.allowChildFrames] + } + }); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + + const response = await page.mainFrame.goto('http://localhost:9999'); + + expect(response).toBeNull(); + expect(page.mainFrame.url).toBe('about:blank'); + expect(page.mainFrame.window === oldWindow).toBe(true); + }); + + it(`Navigates if the setting "browserNavigation" is set to "['allowChildPages']" inside a child page.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new Browser({ + settings: { + browserNavigation: [BrowserNavigationEnum.allowChildPages] + } + }); + const page = browser.newPage(); + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + await childPage.mainFrame.goto('http://localhost:9999'); + + expect(childPage.mainFrame.url).toBe('http://localhost:9999/'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate if the setting "browserNavigation" is set to "['allowChildPages']" inside a main page.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new Browser({ + settings: { + browserNavigation: [BrowserNavigationEnum.allowChildPages] + } + }); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + + const response = await page.mainFrame.goto('http://localhost:9999'); + + expect(response).toBeNull(); + expect(page.mainFrame.url).toBe('about:blank'); + expect(page.mainFrame.window === oldWindow).toBe(true); + }); }); }); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts index af8118afc..c53211341 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts @@ -145,5 +145,13 @@ describe('DetachedBrowser', () => { browser.close(); expect(() => browser.newPage()).toThrow('No default context. The browser has been closed.'); }); + + it('Supports opener as parameter.', () => { + const window = new Window(); + const browser = new DetachedBrowser(Window, window); + const page1 = browser.newPage(); + const page2 = browser.newPage(page1.mainFrame); + expect(page2.mainFrame.opener).toBe(page1.mainFrame); + }); }); }); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts index a68a9b83a..8cfa59031 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts @@ -89,5 +89,13 @@ describe('DetachedBrowserContext', () => { expect(browser.defaultContext.pages[0].mainFrame.window).toBe(window); expect(browser.defaultContext.pages[1]).toBe(page); }); + + it('Supports opener as parameter.', () => { + const window = new Window(); + const browser = new DetachedBrowser(Window, window); + const page1 = browser.defaultContext.newPage(); + const page2 = browser.defaultContext.newPage(page1.mainFrame); + expect(page2.mainFrame.opener).toBe(page1.mainFrame); + }); }); }); diff --git a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts index 1b0445c7d..f686d95e7 100644 --- a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts +++ b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts @@ -8,6 +8,7 @@ import IRequest from '../../../src/fetch/types/IRequest.js'; import IResponse from '../../../src/fetch/types/IResponse.js'; import Fetch from '../../../src/fetch/Fetch.js'; import Browser from '../../../src/browser/Browser.js'; +import BrowserNavigationEnum from '../../../src/browser/types/BrowserNavigationEnum.js'; const BLOB_URL = 'blob:https://mozilla.org'; @@ -505,7 +506,7 @@ describe('HTMLAnchorElement', () => { it(`Doesn't navigate or change the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "browserNavigation" is set to ["deny"].`, () => { const window = new Window({ settings: { - browserNavigation: ['deny'] + browserNavigation: [BrowserNavigationEnum.deny] } }); document = window.document; @@ -524,7 +525,7 @@ describe('HTMLAnchorElement', () => { it(`Doesn't navigate, but changes the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "browserNavigation" is set to ["deny", "url-set-fallback"].`, () => { const window = new Window({ settings: { - browserNavigation: ['deny', 'url-set-fallback'] + browserNavigation: [BrowserNavigationEnum.deny, BrowserNavigationEnum.setURLFallback] } }); document = window.document; @@ -546,7 +547,7 @@ describe('HTMLAnchorElement', () => { it('Changes the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "browserNavigation" is set to ["allow", "url-set-fallback"].', () => { const window = new Window({ settings: { - browserNavigation: ['allow', 'url-set-fallback'] + browserNavigation: [BrowserNavigationEnum.allow, BrowserNavigationEnum.setURLFallback] } }); document = window.document; diff --git a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts index 8f8a7bf0c..3d1fe6c20 100644 --- a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts +++ b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts @@ -60,12 +60,18 @@ describe('HTMLIFrameElement', () => { expect(element.contentDocument?.documentElement.innerHTML).toBe(''); }); - it('Returns content window for "javascript:scroll(10, 20)".', () => { - element.src = 'javascript:scroll(10, 20)'; - document.body.appendChild(element); - expect(element.contentWindow === element.contentDocument?.defaultView).toBe(true); - expect(element.contentDocument?.documentElement.scrollLeft).toBe(10); - expect(element.contentDocument?.documentElement.scrollTop).toBe(20); + it('Returns content window for "javascript:scroll(10, 20)".', async () => { + await new Promise((resolve) => { + element.src = 'javascript:scroll(10, 20)'; + document.body.appendChild(element); + expect(element.contentWindow === element.contentDocument?.defaultView).toBe(true); + + element.addEventListener('load', () => { + expect(element.contentDocument?.documentElement.scrollLeft).toBe(10); + expect(element.contentDocument?.documentElement.scrollTop).toBe(20); + resolve(null); + }); + }); }); it(`Does'nt load anything if the Happy DOM setting "disableIframePageLoading" is set to true.`, () => { @@ -88,11 +94,15 @@ describe('HTMLIFrameElement', () => { vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { fetchedURL = url; - return Promise.resolve(({ - text: () => Promise.resolve(responseHTML), - ok: true, - headers: new Headers({ 'x-frame-options': 'deny' }) - })); + return new Promise((resolve) => { + setTimeout(() => { + resolve(({ + text: () => Promise.resolve(responseHTML), + ok: true, + headers: new Headers({ 'x-frame-options': 'deny' }) + })); + }, 1); + }); }); window.happyDOM.setURL('https://localhost:8080'); @@ -115,11 +125,15 @@ describe('HTMLIFrameElement', () => { vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { fetchedURL = url; - return Promise.resolve(({ - text: () => Promise.resolve(responseHTML), - ok: true, - headers: new Headers({ 'x-frame-options': 'sameorigin' }) - })); + return new Promise((resolve) => { + setTimeout(() => { + resolve(({ + text: () => Promise.resolve(responseHTML), + ok: true, + headers: new Headers({ 'x-frame-options': 'sameorigin' }) + })); + }, 1); + }); }); window.happyDOM.setURL('https://localhost:3000'); @@ -142,11 +156,15 @@ describe('HTMLIFrameElement', () => { vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { fetchedURL = url; - return Promise.resolve(({ - text: () => Promise.resolve(responseHTML), - ok: true, - headers: new Headers({ 'x-frame-options': 'sameorigin' }) - })); + return new Promise((resolve) => { + setTimeout(() => { + resolve(({ + text: () => Promise.resolve(responseHTML), + ok: true, + headers: new Headers({ 'x-frame-options': 'sameorigin' }) + })); + }, 1); + }); }); window.happyDOM.setURL('https://localhost:8080'); @@ -160,6 +178,7 @@ describe('HTMLIFrameElement', () => { ); resolve(null); }); + document.body.appendChild(element); }); }); @@ -265,11 +284,15 @@ describe('HTMLIFrameElement', () => { vi.spyOn(Window.prototype, 'fetch').mockImplementation( (url: IRequestInfo): Promise => { fetchedURL = url; - return Promise.resolve(({ - text: () => Promise.resolve('Test'), - ok: true, - headers: new Headers() - })); + return new Promise((resolve) => { + setTimeout(() => { + resolve(({ + text: () => Promise.resolve('Test'), + ok: true, + headers: new Headers() + })); + }, 1); + }); } ); diff --git a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts new file mode 100644 index 000000000..8b420e8e3 --- /dev/null +++ b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts @@ -0,0 +1,279 @@ +import Window from '../../src/window/Window.js'; +import IWindow from '../../src/window/IWindow.js'; +import HTTP from 'http'; +import Stream from 'stream'; +import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; +import DetachedWindowAPI from '../../src/window/DetachedWindowAPI.js'; +import VirtualConsolePrinter from '../../src/console/VirtualConsolePrinter.js'; +import DefaultBrowserSettings from '../../src/browser/DefaultBrowserSettings.js'; + +describe('DetachedWindowAPI', () => { + let window: IWindow; + + beforeEach(() => { + window = new Window(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('get settings()', () => { + it('Returns browser settings.', () => { + const window = new Window({ settings: { disableJavaScriptEvaluation: true } }); + expect(window.happyDOM?.settings).toEqual({ + ...DefaultBrowserSettings, + disableJavaScriptEvaluation: true + }); + }); + + it('Supports editing setting properties.', () => { + const window = new Window({ settings: { disableJavaScriptEvaluation: true } }); + (window.happyDOM).settings.disableJavaScriptEvaluation = false; + expect(window.happyDOM?.settings).toEqual(DefaultBrowserSettings); + }); + }); + + describe('get virtualConsolePrinter()', () => { + it('Returns an instance of VirtualConsolePrinter.', () => { + window.console.log('Test 1', { key1: 'value1' }); + window.console.info('Test 2', { key2: 'value2' }); + + expect(window.happyDOM?.virtualConsolePrinter).toBeInstanceOf(VirtualConsolePrinter); + expect(window.happyDOM?.virtualConsolePrinter.readAsString()).toBe( + `Test 1 {"key1":"value1"}\nTest 2 {"key2":"value2"}\n` + ); + }); + }); + + describe('whenComplete()', () => { + it('Resolves the Promise when all async tasks has been completed.', async () => { + const responseText = '{ "test": "test" }'; + mockModule('https', { + request: () => { + return { + end: () => {}, + on: (event: string, callback: (response: HTTP.IncomingMessage) => void) => { + if (event === 'response') { + async function* generate(): AsyncGenerator { + yield responseText; + } + + const response = Stream.Readable.from(generate()); + + response.statusCode = 200; + response.statusMessage = ''; + response.headers = { + 'content-length': '0' + }; + response.rawHeaders = ['content-length', '0']; + + setTimeout(() => callback(response)); + } + }, + setTimeout: () => {} + }; + } + }); + + window.location.href = 'https://localhost:8080'; + let isFirstWhenAsyncCompleteCalled = false; + window.happyDOM?.whenComplete().then(() => { + isFirstWhenAsyncCompleteCalled = true; + }); + let tasksDone = 0; + const intervalID = window.setInterval(() => { + tasksDone++; + }); + window.clearInterval(intervalID); + window.setTimeout(() => { + tasksDone++; + }); + window.setTimeout(() => { + tasksDone++; + }); + window.requestAnimationFrame(() => { + tasksDone++; + }); + window.requestAnimationFrame(() => { + tasksDone++; + }); + window.fetch('/url/1/').then((response) => { + response.json().then(() => { + tasksDone++; + }); + }); + window.fetch('/url/2/').then((response) => { + response.text().then(() => { + tasksDone++; + }); + }); + await window.happyDOM?.whenComplete(); + expect(tasksDone).toBe(6); + expect(isFirstWhenAsyncCompleteCalled).toBe(true); + }); + }); + + describe('whenAsyncComplete()', () => { + it('Calls whenComplete().', async () => { + let isCalled = false; + vi.spyOn(window.happyDOM, 'whenComplete').mockImplementation(() => { + isCalled = true; + return Promise.resolve(); + }); + await window.happyDOM?.whenAsyncComplete(); + expect(isCalled).toBe(true); + }); + }); + + describe('abort()', () => { + it('Cancels all ongoing asynchrounous tasks.', async () => { + await new Promise((resolve) => { + window.location.href = 'https://localhost:8080'; + let isFirstWhenAsyncCompleteCalled = false; + window.happyDOM?.whenAsyncComplete().then(() => { + isFirstWhenAsyncCompleteCalled = true; + }); + let tasksDone = 0; + const intervalID = window.setInterval(() => { + tasksDone++; + }); + window.clearInterval(intervalID); + window.setTimeout(() => { + tasksDone++; + }); + window.setTimeout(() => { + tasksDone++; + }); + window.requestAnimationFrame(() => { + tasksDone++; + }); + window.requestAnimationFrame(() => { + tasksDone++; + }); + + window + .fetch('/url/') + .then((response) => + response + .json() + .then(() => { + tasksDone++; + }) + .catch(() => {}) + ) + .catch(() => {}); + + window + .fetch('/url/') + .then((response) => + response + .json() + .then(() => { + tasksDone++; + }) + .catch(() => {}) + ) + .catch(() => {}); + + let isSecondWhenAsyncCompleteCalled = false; + window.happyDOM?.whenComplete().then(() => { + isSecondWhenAsyncCompleteCalled = true; + }); + + window.happyDOM?.abort(); + + expect(tasksDone).toBe(0); + + window.setTimeout(() => { + expect(isFirstWhenAsyncCompleteCalled).toBe(true); + expect(isSecondWhenAsyncCompleteCalled).toBe(true); + resolve(null); + }, 1); + }); + }); + }); + + describe('cancelAsync', () => { + it('Calls abort().', () => { + let isCalled = false; + vi.spyOn(window.happyDOM, 'abort').mockImplementation(() => { + isCalled = true; + }); + window.happyDOM?.cancelAsync(); + expect(isCalled).toBe(true); + }); + }); + + describe('setURL()', () => { + it('Sets URL.', () => { + window.happyDOM?.setURL('https://localhost:8080'); + expect(window.location.href).toBe('https://localhost:8080/'); + }); + }); + + describe('setViewport()', () => { + it('Sets the viewport width.', () => { + window.happyDOM?.setViewport({ width: 100 }); + expect(window.innerWidth).toBe(100); + expect(window.outerWidth).toBe(100); + }); + + it('Sets the viewport height.', () => { + window.happyDOM?.setViewport({ height: 100 }); + expect(window.innerHeight).toBe(100); + expect(window.outerHeight).toBe(100); + }); + + it('Sets the viewport width and height.', () => { + window.happyDOM?.setViewport({ width: 100, height: 100 }); + expect(window.innerWidth).toBe(100); + expect(window.outerWidth).toBe(100); + expect(window.innerHeight).toBe(100); + expect(window.outerHeight).toBe(100); + }); + + it('Sets the viewport device scale factor.', () => { + window.happyDOM?.setViewport({ deviceScaleFactor: 2 }); + expect(window.devicePixelRatio).toBe(2); + }); + }); + + describe('setWindowSize()', () => { + it('Sets window width.', () => { + window.happyDOM?.setWindowSize({ width: 1920 }); + expect(window.innerWidth).toBe(1920); + expect(window.outerWidth).toBe(1920); + }); + + it('Sets window height.', () => { + window.happyDOM?.setWindowSize({ height: 1080 }); + expect(window.innerHeight).toBe(1080); + expect(window.outerHeight).toBe(1080); + }); + + it('Sets window width and height.', () => { + window.happyDOM?.setWindowSize({ width: 1920, height: 1080 }); + expect(window.innerWidth).toBe(1920); + expect(window.innerHeight).toBe(1080); + expect(window.outerWidth).toBe(1920); + expect(window.outerHeight).toBe(1080); + }); + }); + + describe('setInnerWidth()', () => { + it('Sets window width.', () => { + window.happyDOM?.setInnerWidth(1920); + expect(window.innerWidth).toBe(1920); + expect(window.outerWidth).toBe(1920); + }); + }); + + describe('setInnerHeight()', () => { + it('Sets window height.', () => { + window.happyDOM?.setInnerHeight(1080); + expect(window.innerHeight).toBe(1080); + expect(window.outerHeight).toBe(1080); + }); + }); +}); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 45809b5b3..73a549893 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -35,6 +35,7 @@ import ICrossOriginWindow from '../../src/window/ICrossOriginWindow.js'; import CrossOriginWindow from '../../src/window/CrossOriginWindow.js'; import BrowserFrameUtility from '../../src/browser/BrowserFrameUtility.js'; import IHTMLIFrameElement from '../../src/nodes/html-iframe-element/IHTMLIFrameElement.js'; +import DetachedWindowAPI from '../../src/window/DetachedWindowAPI.js'; const GET_NAVIGATOR_PLATFORM = (): string => { return ( @@ -139,18 +140,18 @@ describe('Window', () => { expect(windowWithOptions.outerHeight).toBe(1080); expect(windowWithOptions.console).toBe(globalThis.console); expect(windowWithOptions.location.href).toBe('http://localhost:8080/'); - expect(windowWithOptions.happyDOM.virtualConsolePrinter).toBeInstanceOf( + expect(windowWithOptions.happyDOM?.virtualConsolePrinter).toBeInstanceOf( VirtualConsolePrinter ); - 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.disableErrorCapturing).toBe(false); - expect(windowWithOptions.happyDOM.settings.enableFileSystemHttpRequests).toBe(false); - expect(windowWithOptions.happyDOM.settings.navigator.userAgent).toBe('test'); - expect(windowWithOptions.happyDOM.settings.device.prefersColorScheme).toBe('dark'); - expect(windowWithOptions.happyDOM.settings.device.mediaType).toBe('screen'); + 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.disableErrorCapturing).toBe(false); + expect(windowWithOptions.happyDOM?.settings.enableFileSystemHttpRequests).toBe(false); + expect(windowWithOptions.happyDOM?.settings.navigator.userAgent).toBe('test'); + 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); @@ -158,22 +159,22 @@ describe('Window', () => { expect(windowWithoutOptions.outerHeight).toBe(768); expect(windowWithoutOptions.console).toBeInstanceOf(VirtualConsole); expect(windowWithoutOptions.location.href).toBe('about:blank'); - expect(windowWithoutOptions.happyDOM.virtualConsolePrinter).toBeInstanceOf( + expect(windowWithoutOptions.happyDOM?.virtualConsolePrinter).toBeInstanceOf( VirtualConsolePrinter ); - 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.disableErrorCapturing).toBe(false); - expect(windowWithoutOptions.happyDOM.settings.enableFileSystemHttpRequests).toBe(false); - expect(windowWithoutOptions.happyDOM.settings.navigator.userAgent).toBe( + 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.disableErrorCapturing).toBe(false); + expect(windowWithoutOptions.happyDOM?.settings.enableFileSystemHttpRequests).toBe(false); + expect(windowWithoutOptions.happyDOM?.settings.navigator.userAgent).toBe( `Mozilla/5.0 (${GET_NAVIGATOR_PLATFORM()}) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${ PackageVersion.version }` ); - expect(windowWithoutOptions.happyDOM.settings.device.prefersColorScheme).toBe('light'); - expect(windowWithoutOptions.happyDOM.settings.device.mediaType).toBe('screen'); + expect(windowWithoutOptions.happyDOM?.settings.device.prefersColorScheme).toBe('light'); + expect(windowWithoutOptions.happyDOM?.settings.device.mediaType).toBe('screen'); }); it('Supports deprecated "innerWidth" and "innerHeight".', () => { @@ -189,195 +190,58 @@ describe('Window', () => { }); }); - describe('happyDOM.whenAsyncComplete()', () => { - it('Resolves the Promise returned by whenAsyncComplete() when all async tasks has been completed.', async () => { - const responseText = '{ "test": "test" }'; - mockModule('https', { - request: () => { - return { - end: () => {}, - on: (event: string, callback: (response: HTTP.IncomingMessage) => void) => { - if (event === 'response') { - async function* generate(): AsyncGenerator { - yield responseText; - } - - const response = Stream.Readable.from(generate()); - - response.statusCode = 200; - response.statusMessage = ''; - response.headers = { - 'content-length': '0' - }; - response.rawHeaders = ['content-length', '0']; - - setTimeout(() => callback(response)); - } - }, - setTimeout: () => {} - }; - } - }); + describe('get happyDOM()', () => { + it('Returns an instance of DetachedWindowAPI.', () => { + expect(window.happyDOM).toBeInstanceOf(DetachedWindowAPI); + }); - window.location.href = 'https://localhost:8080'; - let isFirstWhenAsyncCompleteCalled = false; - window.happyDOM.whenAsyncComplete().then(() => { - isFirstWhenAsyncCompleteCalled = true; - }); - let tasksDone = 0; - const intervalID = window.setInterval(() => { - tasksDone++; - }); - window.clearInterval(intervalID); - window.setTimeout(() => { - tasksDone++; - }); - window.setTimeout(() => { - tasksDone++; - }); - window.requestAnimationFrame(() => { - tasksDone++; - }); - window.requestAnimationFrame(() => { - tasksDone++; - }); - window.fetch('/url/1/').then((response) => { - response.json().then(() => { - tasksDone++; - }); - }); - window.fetch('/url/2/').then((response) => { - response.text().then(() => { - tasksDone++; - }); - }); - await window.happyDOM.whenAsyncComplete(); - expect(tasksDone).toBe(6); - expect(isFirstWhenAsyncCompleteCalled).toBe(true); + it('Returns "undefined" if the Window is not detached.', () => { + const browser = new Browser(); + const page = browser.newPage(); + expect(page.mainFrame.window.happyDOM).toBeUndefined(); }); - }); - describe('happyDOM.cancelAsync()', () => { - it('Cancels all ongoing asynchrounous tasks.', async () => { + it('Returns "undefined" when navigating an iframe for security reasons. The page loaded can potentially contain malicious code.', async () => { await new Promise((resolve) => { - window.location.href = 'https://localhost:8080'; - let isFirstWhenAsyncCompleteCalled = false; - window.happyDOM.whenAsyncComplete().then(() => { - isFirstWhenAsyncCompleteCalled = true; - }); - let tasksDone = 0; - const intervalID = window.setInterval(() => { - tasksDone++; - }); - window.clearInterval(intervalID); - window.setTimeout(() => { - tasksDone++; - }); - window.setTimeout(() => { - tasksDone++; - }); - window.requestAnimationFrame(() => { - tasksDone++; - }); - window.requestAnimationFrame(() => { - tasksDone++; - }); - - window - .fetch('/url/') - .then((response) => - response - .json() - .then(() => { - tasksDone++; - }) - .catch(() => {}) - ) - .catch(() => {}); - - window - .fetch('/url/') - .then((response) => - response - .json() - .then(() => { - tasksDone++; - }) - .catch(() => {}) - ) - .catch(() => {}); - - let isSecondWhenAsyncCompleteCalled = false; - window.happyDOM.whenAsyncComplete().then(() => { - isSecondWhenAsyncCompleteCalled = true; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); }); - window.happyDOM.cancelAsync(); + window.happyDOM?.setURL('https://localhost:8080'); - expect(tasksDone).toBe(0); + const iframe = document.createElement('iframe'); - window.setTimeout(() => { - expect(isFirstWhenAsyncCompleteCalled).toBe(true); - expect(isSecondWhenAsyncCompleteCalled).toBe(true); + iframe.src = 'https://localhost:8080/test/page/'; + iframe.addEventListener('load', () => { + expect((iframe.contentWindow).happyDOM).toBeUndefined(); resolve(null); - }, 1); - }); - }); - }); - - describe('happyDOM.setURL()', () => { - it('Sets URL.', () => { - window.happyDOM.setURL('https://localhost:8080'); - expect(window.location.href).toBe('https://localhost:8080/'); - }); - }); - - describe('happyDOM.virtualConsolePrinter.readAsString()', () => { - it('Returns the buffered console output.', () => { - window.console.log('Test 1', { key1: 'value1' }); - window.console.info('Test 2', { key2: 'value2' }); - - expect(window.happyDOM.virtualConsolePrinter?.readAsString()).toBe( - `Test 1 {"key1":"value1"}\nTest 2 {"key2":"value2"}\n` - ); - }); - }); + }); - describe('happyDOM.setWindowSize()', () => { - it('Sets window width.', () => { - window.happyDOM.setWindowSize({ width: 1920 }); - expect(window.innerWidth).toBe(1920); - expect(window.outerWidth).toBe(1920); + document.body.appendChild(iframe); + }); }); - it('Sets window height.', () => { - window.happyDOM.setWindowSize({ height: 1080 }); - expect(window.innerHeight).toBe(1080); - expect(window.outerHeight).toBe(1080); - }); + it('Returns "undefined" when opening a new page. The page loaded can potentially contain malicious code.', async () => { + await new Promise((resolve) => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); - it('Sets window width and height.', () => { - window.happyDOM.setWindowSize({ width: 1920, height: 1080 }); - expect(window.innerWidth).toBe(1920); - expect(window.innerHeight).toBe(1080); - expect(window.outerWidth).toBe(1920); - expect(window.outerHeight).toBe(1080); - }); - }); + window.happyDOM?.setURL('https://localhost:8080'); - describe('happyDOM.setInnerWidth()', () => { - it('Sets window width.', () => { - window.happyDOM.setInnerWidth(1920); - expect(window.innerWidth).toBe(1920); - expect(window.outerWidth).toBe(1920); - }); - }); + const newWindow = window.open('https://localhost:8080/test/page/'); - describe('happyDOM.setInnerHeight()', () => { - it('Sets window height.', () => { - window.happyDOM.setInnerHeight(1080); - expect(window.innerHeight).toBe(1080); - expect(window.outerHeight).toBe(1080); + newWindow.addEventListener('load', () => { + expect(newWindow.happyDOM).toBeUndefined(); + resolve(null); + }); + }); }); }); @@ -601,7 +465,7 @@ describe('Window', () => { const parentStyle = document.createElement('style'); const elementStyle = document.createElement('style'); - window.happyDOM.setWindowSize({ width: 1024 }); + window.happyDOM?.setViewport({ width: 1024 }); parentStyle.innerHTML = ` div { @@ -695,7 +559,7 @@ describe('Window', () => { const parentStyle = document.createElement('style'); const elementStyle = document.createElement('style'); - window.happyDOM.setWindowSize({ width: 1024 }); + window.happyDOM?.setViewport({ width: 1024 }); parentStyle.innerHTML = ` html { @@ -735,7 +599,7 @@ describe('Window', () => { const parentStyle = document.createElement('style'); const elementStyle = document.createElement('style'); - window.happyDOM.setWindowSize({ width: 1024 }); + window.happyDOM?.setViewport({ width: 1024 }); parentStyle.innerHTML = ` html { @@ -771,7 +635,7 @@ describe('Window', () => { const parentStyle = document.createElement('style'); const elementStyle = document.createElement('style'); - window.happyDOM.setWindowSize({ width: 1024 }); + window.happyDOM?.setViewport({ width: 1024 }); parentStyle.innerHTML = ` html { @@ -962,7 +826,7 @@ describe('Window', () => { isCallbackCalled = true; resolve(null); }); - window.happyDOM.abort(); + window.happyDOM?.abort(); setTimeout(() => { expect(isCallbackCalled).toBe(false); resolve(null); @@ -1172,7 +1036,7 @@ describe('Window', () => { describe('matchMedia()', () => { it('Returns a new MediaQueryList object that can then be used to determine if the document matches the media query string.', () => { - window.happyDOM.setWindowSize({ width: 1024 }); + window.happyDOM?.setViewport({ width: 1024 }); const mediaQueryString = '(max-width: 512px)'; const mediaQueryList = window.matchMedia(mediaQueryString); @@ -1264,7 +1128,7 @@ describe('Window', () => { expect(window.pageYOffset).toBe(0); expect(window.scrollX).toBe(0); expect(window.scrollY).toBe(0); - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(window.document.documentElement.scrollLeft).toBe(50); expect(window.document.documentElement.scrollTop).toBe(60); expect(window.pageXOffset).toBe(50); @@ -1518,7 +1382,7 @@ describe('Window', () => { const targetOrigin = 'https://localhost:8081'; const documentOrigin = 'https://localhost:8080'; - window.happyDOM.setURL(documentOrigin); + window.happyDOM?.setURL(documentOrigin); expect(() => window.postMessage(message, targetOrigin)).toThrowError( new DOMException( @@ -1567,7 +1431,7 @@ describe('Window', () => { }); }); - window.happyDOM.setURL('https://localhost:8080/test/'); + window.happyDOM?.setURL('https://localhost:8080/test/'); const newWindow = window.open('/path/to/file.html'); expect(newWindow).toBeInstanceOf(Window); @@ -1640,7 +1504,7 @@ describe('Window', () => { throw new Error('This should not be called.'); }); - window.happyDOM.setURL('https://www.github.com/'); + window.happyDOM?.setURL('https://www.github.com/'); expect(window.open('/capricorn86/happy-dom/', '_self')).toBe(null); expect(window.location.href).toBe('https://www.github.com/capricorn86/happy-dom/'); @@ -1659,15 +1523,15 @@ describe('Window', () => { }); }); - window.happyDOM.setURL('https://localhost:8080'); + window.happyDOM?.setURL('https://localhost:8080'); const newWindow = window.open('/test/1/', '_blank'); expect(newWindow.name).toBe(''); - newWindow.document.write(''); + newWindow.document.write(''); const iframe = newWindow.document.querySelector('iframe'); - (iframe.contentWindow).happyDOM.setURL('https://localhost:8080'); + expect((iframe.contentWindow).happyDOM).toBeUndefined(); const newWindow2 = ( (iframe.contentWindow).open('https://localhost:8080/test/2/', '_top') ); @@ -1687,15 +1551,15 @@ describe('Window', () => { }); }); - window.happyDOM.setURL('https://localhost:8080'); + window.happyDOM?.setURL('https://localhost:8080'); const newWindow = window.open('/test/1/', '_blank'); expect(newWindow.name).toBe(''); - newWindow.document.write(''); + newWindow.document.write(''); const iframe = newWindow.document.querySelector('iframe'); - (iframe.contentWindow).happyDOM.setURL('https://localhost:8080'); + expect((iframe.contentWindow).happyDOM).toBeUndefined(); const newWindow2 = ( (iframe.contentWindow).open('https://localhost:8080/test/2/', '_parent') ); From 286b30e002e93bebcdf88071f17f47c7bbed1987 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 21 Nov 2023 01:17:45 +0100 Subject: [PATCH 27/63] #466@trivial: Continues on implementation. --- .../happy-dom/src/browser/BrowserFrame.ts | 10 +- .../src/browser/BrowserFrameUtility.ts | 327 ------------------ packages/happy-dom/src/browser/BrowserPage.ts | 5 +- .../src/browser/BrowserSettingsFactory.ts | 4 + .../src/browser/DefaultBrowserSettings.ts | 11 +- .../detached-browser/DetachedBrowserFrame.ts | 10 +- .../detached-browser/DetachedBrowserPage.ts | 5 +- .../BrowserNavigationCrossOriginPolicyEnum.ts | 10 + .../browser/types/BrowserNavigationEnum.ts | 10 - .../src/browser/types/IBrowserSettings.ts | 49 ++- .../browser/types/IOptionalBrowserSettings.ts | 52 ++- .../browser/utilities/BrowserFrameFactory.ts | 53 +++ .../utilities/BrowserFrameNavigator.ts | 153 ++++++++ .../utilities/BrowserFrameScriptEvaluator.ts | 19 + .../src/browser/utilities/BrowserFrameURL.ts | 40 +++ .../utilities/BrowserFrameValidator.ts | 83 +++++ .../{ => utilities}/BrowserPageUtility.ts | 40 ++- .../HTMLIFrameElementPageLoader.ts | 14 +- packages/happy-dom/src/window/Window.ts | 3 - .../src/window/WindowPageOpenUtility.ts | 15 +- .../test/browser/BrowserFrame.test.ts | 97 ++++-- .../test/browser/BrowserPage.test.ts | 20 +- .../DetachedBrowserPage.test.ts | 20 +- .../HTMLAnchorElement.test.ts | 29 +- packages/happy-dom/test/window/Window.test.ts | 12 +- 25 files changed, 620 insertions(+), 471 deletions(-) delete mode 100644 packages/happy-dom/src/browser/BrowserFrameUtility.ts create mode 100644 packages/happy-dom/src/browser/types/BrowserNavigationCrossOriginPolicyEnum.ts delete mode 100644 packages/happy-dom/src/browser/types/BrowserNavigationEnum.ts create mode 100644 packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts create mode 100644 packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts create mode 100644 packages/happy-dom/src/browser/utilities/BrowserFrameScriptEvaluator.ts create mode 100644 packages/happy-dom/src/browser/utilities/BrowserFrameURL.ts create mode 100644 packages/happy-dom/src/browser/utilities/BrowserFrameValidator.ts rename packages/happy-dom/src/browser/{ => utilities}/BrowserPageUtility.ts (62%) diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 7c9951e83..0489fd96c 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -4,9 +4,11 @@ import IBrowserFrame from './types/IBrowserFrame.js'; import Window from '../window/Window.js'; import Location from '../location/Location.js'; import IResponse from '../fetch/types/IResponse.js'; -import BrowserFrameUtility from './BrowserFrameUtility.js'; import IGoToOptions from './types/IGoToOptions.js'; import { Script } from 'vm'; +import BrowserFrameURL from './utilities/BrowserFrameURL.js'; +import BrowserFrameScriptEvaluator from './utilities/BrowserFrameScriptEvaluator.js'; +import BrowserFrameNavigator from './utilities/BrowserFrameNavigator.js'; /** * Browser frame. @@ -70,7 +72,7 @@ export default class BrowserFrame implements IBrowserFrame { public set url(url) { (this.window.location) = new Location( this, - BrowserFrameUtility.getRelativeURL(this, url).href + BrowserFrameURL.getRelativeURL(this, url).href ); } @@ -105,7 +107,7 @@ export default class BrowserFrame implements IBrowserFrame { * @returns Result. */ public evaluate(script: string | Script): any { - return BrowserFrameUtility.evaluate(this, script); + return BrowserFrameScriptEvaluator.evaluate(this, script); } /** @@ -116,6 +118,6 @@ export default class BrowserFrame implements IBrowserFrame { * @returns Response. */ public goto(url: string, options?: IGoToOptions): Promise { - return BrowserFrameUtility.goto(Window, this, url, options); + return BrowserFrameNavigator.goto(Window, this, url, options); } } diff --git a/packages/happy-dom/src/browser/BrowserFrameUtility.ts b/packages/happy-dom/src/browser/BrowserFrameUtility.ts deleted file mode 100644 index b8c29c0e0..000000000 --- a/packages/happy-dom/src/browser/BrowserFrameUtility.ts +++ /dev/null @@ -1,327 +0,0 @@ -import BrowserPage from './BrowserPage.js'; -import IBrowserFrame from './types/IBrowserFrame.js'; -import Window from '../window/Window.js'; -import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; -import IBrowserPage from './types/IBrowserPage.js'; -import IGoToOptions from './types/IGoToOptions.js'; -import IResponse from '../fetch/types/IResponse.js'; -import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; -import IWindow from '../window/IWindow.js'; -import WindowErrorUtility from '../window/WindowErrorUtility.js'; -import DetachedBrowserFrame from './detached-browser/DetachedBrowserFrame.js'; -import { URL } from 'url'; -import DOMException from '../exception/DOMException.js'; -import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; -import Location from '../location/Location.js'; -import AbortController from '../fetch/AbortController.js'; -import { Script } from 'vm'; -import BrowserNavigationEnum from './types/BrowserNavigationEnum.js'; - -/** - * Browser frame utility. - */ -export default class BrowserFrameUtility { - /** - * Aborts all ongoing operations and destroys the frame. - */ - public static closeFrame(frame: IBrowserFrame): void { - if (!frame.window) { - return; - } - - if (frame.parentFrame) { - const index = frame.parentFrame.childFrames.indexOf(frame); - if (index !== -1) { - frame.parentFrame.childFrames.splice(index, 1); - } - } - - for (const childFrame of frame.childFrames.slice()) { - this.closeFrame(childFrame); - } - - (frame.window.closed) = true; - frame._asyncTaskManager.destroy(); - WindowBrowserSettingsReader.removeSettings(frame.window); - (frame.page) = null; - (frame.window) = null; - (frame.opener) = null; - } - - /** - * Creates a new frame. - * - * @param parentFrame Parent frame. - * @returns Frame. - */ - public static newFrame(parentFrame: IBrowserFrame): IBrowserFrame { - const frame = new ( IBrowserFrame>parentFrame.constructor)( - parentFrame.page - ); - (frame.parentFrame) = parentFrame; - parentFrame.childFrames.push(frame); - return frame; - } - - /** - * Returns frames. - * - * @param parentFrame Parent frame. - * @returns Frames, including the parent. - */ - public static getFrames(parentFrame: IBrowserFrame): IBrowserFrame[] { - let frames = [parentFrame]; - for (const frame of parentFrame.childFrames) { - frames = frames.concat(this.getFrames(frame)); - } - return frames; - } - - /** - * Go to a page. - * - * @throws Error if the request can't be resolved (because of SSL error or similar). It will not throw if the response is not ok. - * @param windowClass Window class. - * @param frame Frame. - * @param url URL. - * @param [options] Options. - * @returns Response. - */ - public static async goto( - windowClass: new (options: { - browserFrame: IBrowserFrame; - console: Console; - url?: string; - }) => IWindow, - frame: IBrowserFrame, - url: string, - options?: IGoToOptions - ): Promise { - const targetURL = this.getRelativeURL(frame, url); - - if (targetURL.protocol === 'javascript:') { - if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { - const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( - (frame.window) - ))._readyStateManager; - - readyStateManager.startTask(); - - // The browser will wait for the next tick before executing the script. - await new Promise((resolve) => frame.page.mainFrame.window.setTimeout(resolve)); - - const code = - '//# sourceURL=' + frame.url + '\n' + targetURL.href.replace('javascript:', ''); - - if (frame.page.context.browser.settings.disableErrorCapturing) { - frame.window.eval(code); - } else { - WindowErrorUtility.captureError(frame.window, () => frame.window.eval(code)); - } - - readyStateManager.endTask(); - } - - return null; - } - - if (this.isDetachedMainFrame(frame) || !this.isBrowserNavigationAllowed(frame, targetURL)) { - if ( - frame.page.context.browser.settings.browserNavigation.includes( - BrowserNavigationEnum.setURLFallback - ) - ) { - (frame.window.location) = new Location(frame, targetURL.href); - } - - return null; - } - - for (const childFrame of frame.childFrames) { - BrowserFrameUtility.closeFrame(childFrame); - } - - (frame.childFrames) = []; - (frame.window.closed) = true; - frame._asyncTaskManager.destroy(); - WindowBrowserSettingsReader.removeSettings(frame.window); - - (frame.window) = new windowClass({ - browserFrame: frame, - console: frame.page.console, - url: targetURL.href - }); - - if (options?.referrer) { - (frame.window.document.referrer) = options.referrer; - } - - if (targetURL.protocol === 'about:') { - return null; - } - - const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( - (frame.window) - ))._readyStateManager; - - readyStateManager.startTask(); - - let abortController = new AbortController(); - let response: IResponse; - let responseText: string; - - const timeout = frame.window.setTimeout( - () => abortController.abort('Request timed out.'), - options?.timeout ?? 30000 - ); - - try { - response = await frame.window.fetch(targetURL.href, { - referrer: options?.referrer, - referrerPolicy: options?.referrerPolicy, - signal: abortController.signal - }); - - // Handles the "X-Frame-Options" header for child frames. - if (frame.parentFrame) { - const originURL = frame.parentFrame.window.location; - const xFrameOptions = response.headers.get('X-Frame-Options')?.toLowerCase(); - const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; - - if (xFrameOptions === 'deny' || (xFrameOptions === 'sameorigin' && !isSameOrigin)) { - throw new Error( - `Refused to display '${url}' in a frame because it set 'X-Frame-Options' to '${xFrameOptions}'.` - ); - } - } - - responseText = await response.text(); - } catch (error) { - frame.window.clearTimeout(timeout); - readyStateManager.endTask(); - throw error; - } - - frame.window.clearTimeout(timeout); - frame.content = responseText; - readyStateManager.endTask(); - - if (!response.ok) { - frame.page.console.error(`GET ${targetURL.href} ${response.status} (${response.statusText})`); - } - - return response; - } - - /** - * Returns true if the frame is a detached main frame. - * - * @param frame Frame. - * @returns True if the frame is a detached main frame. - */ - public static isBrowserNavigationAllowed(frame: IBrowserFrame, toURL: URL): boolean { - const settings = frame.page.context.browser.settings; - let fromURL = frame.page.mainFrame.window.location; - if (frame.opener) { - fromURL = frame.opener.window.location; - } - if (frame.parentFrame) { - fromURL = frame.parentFrame.window.location; - } - - if (settings.browserNavigation.includes(BrowserNavigationEnum.all)) { - return true; - } - - if ( - settings.browserNavigation.includes(BrowserNavigationEnum.sameOrigin) && - fromURL.protocol !== 'about:' && - toURL.protocol !== 'about:' && - toURL.protocol !== 'javascript:' && - fromURL.origin !== toURL.origin - ) { - return false; - } - - if ( - settings.browserNavigation.includes(BrowserNavigationEnum.mainFrame) && - frame.page.mainFrame !== frame - ) { - return false; - } - - if ( - settings.browserNavigation.includes(BrowserNavigationEnum.childFrame) && - frame.page.mainFrame === frame - ) { - return false; - } - - if ( - settings.browserNavigation.includes(BrowserNavigationEnum.allowChildPages) && - !!frame.opener - ) { - return true; - } - - return false; - } - - /** - * Returns true if the frame is a detached main frame. - * - * @param frame Frame. - * @returns True if the frame is a detached main frame. - */ - public static isDetachedMainFrame(frame: IBrowserFrame): boolean { - return ( - frame instanceof DetachedBrowserFrame && - frame.page.context === frame.page.context.browser.defaultContext && - frame.page.context.pages[0] === frame.page && - frame.page.mainFrame === frame - ); - } - - /** - * Returns relative URL. - * - * @param frame Frame. - * @param url URL. - * @returns Relative URL. - */ - public static getRelativeURL(frame: IBrowserFrame, url: string): URL { - url = url || 'about:blank'; - - if (url.startsWith('about:') || url.startsWith('javascript:')) { - return new URL(url); - } - - try { - return new URL(url, frame.window.location); - } catch (e) { - if (frame.window.location.hostname) { - throw new DOMException( - `Failed to construct URL from string "${url}".`, - DOMExceptionNameEnum.uriMismatchError - ); - } else { - throw new DOMException( - `Failed to construct URL from string "${url}" relative to URL "${frame.window.location.href}".`, - DOMExceptionNameEnum.uriMismatchError - ); - } - } - } - - /** - * Evaluates code or a VM Script in the frame's context. - * - * @param frame Frame. - * @param script Script. - * @returns Result. - */ - public static evaluate(frame: IBrowserFrame, script: string | Script): any { - script = typeof script === 'string' ? new Script(script) : script; - return script.runInContext(frame.window); - } -} diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index cebbc1f17..c843d6775 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -4,11 +4,10 @@ import BrowserFrame from './BrowserFrame.js'; import BrowserContext from './BrowserContext.js'; import VirtualConsole from '../console/VirtualConsole.js'; import IBrowserPage from './types/IBrowserPage.js'; -import BrowserFrameUtility from './BrowserFrameUtility.js'; +import BrowserPageUtility from './utilities/BrowserPageUtility.js'; import { Script } from 'vm'; import IGoToOptions from './types/IGoToOptions.js'; import IResponse from '../fetch/types/IResponse.js'; -import BrowserPageUtility from './BrowserPageUtility.js'; /** * Browser page. @@ -34,7 +33,7 @@ export default class BrowserPage implements IBrowserPage { * Returns frames. */ public get frames(): BrowserFrame[] { - return BrowserFrameUtility.getFrames(this.mainFrame); + return BrowserPageUtility.getFrames(this); } /** diff --git a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts index 9296cff7c..7ed799ab2 100644 --- a/packages/happy-dom/src/browser/BrowserSettingsFactory.ts +++ b/packages/happy-dom/src/browser/BrowserSettingsFactory.ts @@ -17,6 +17,10 @@ export default class BrowserSettingsFactory { return { ...DefaultBrowserSettings, ...settings, + navigation: { + ...DefaultBrowserSettings.navigation, + ...settings?.navigation + }, navigator: { ...DefaultBrowserSettings.navigator, ...settings?.navigator diff --git a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts index 7265d3af5..ea0d5b925 100644 --- a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts +++ b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts @@ -1,5 +1,5 @@ import PackageVersion from '../version.js'; -import BrowserNavigationEnum from './types/BrowserNavigationEnum.js'; +import BrowserNavigationCrossOriginPolicyEnum from './types/BrowserNavigationCrossOriginPolicyEnum.js'; import IBrowserSettings from './types/IBrowserSettings.js'; export default { @@ -7,11 +7,16 @@ export default { disableJavaScriptFileLoading: false, disableCSSFileLoading: false, disableIframePageLoading: false, - disableWindowOpenPageLoading: false, disableComputedStyleRendering: false, disableErrorCapturing: false, enableFileSystemHttpRequests: false, - browserNavigation: [BrowserNavigationEnum.allow, BrowserNavigationEnum.setURLFallback], + navigation: { + disableMainFrameNavigation: false, + disableChildFrameNavigation: false, + disableChildPageNavigation: false, + disableFallbackToSetURL: false, + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.anyOrigin + }, navigator: { userAgent: `Mozilla/5.0 (X11; ${ process.platform.charAt(0).toUpperCase() + process.platform.slice(1) + ' ' + process.arch diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index 9d956f3e8..78818b210 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -4,9 +4,11 @@ import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; import IBrowserFrame from '../types/IBrowserFrame.js'; import Location from '../../location/Location.js'; import IResponse from '../../fetch/types/IResponse.js'; -import BrowserFrameUtility from '../BrowserFrameUtility.js'; import IGoToOptions from '../types/IGoToOptions.js'; import { Script } from 'vm'; +import BrowserFrameURL from '../utilities/BrowserFrameURL.js'; +import BrowserFrameScriptEvaluator from '../utilities/BrowserFrameScriptEvaluator.js'; +import BrowserFrameNavigator from '../utilities/BrowserFrameNavigator.js'; /** * Browser frame used when constructing a Window instance without a browser. @@ -72,7 +74,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public set url(url) { (this.window.location) = new Location( this, - BrowserFrameUtility.getRelativeURL(this, url).href + BrowserFrameURL.getRelativeURL(this, url).href ); } @@ -105,7 +107,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @returns Result. */ public evaluate(script: string | Script): any { - return BrowserFrameUtility.evaluate(this, script); + return BrowserFrameScriptEvaluator.evaluate(this, script); } /** @@ -116,7 +118,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @returns Response. */ public goto(url: string, options?: IGoToOptions): Promise { - return BrowserFrameUtility.goto( + return BrowserFrameNavigator.goto( this.page.context.browser.detachedWindowClass, this, url, diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts index 0bb7a5b6a..91d50b3d9 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts @@ -4,11 +4,10 @@ import DetachedBrowserFrame from './DetachedBrowserFrame.js'; import DetachedBrowserContext from './DetachedBrowserContext.js'; import VirtualConsole from '../../console/VirtualConsole.js'; import IBrowserPage from '../types/IBrowserPage.js'; -import BrowserFrameUtility from '../BrowserFrameUtility.js'; import { Script } from 'vm'; import IGoToOptions from '../types/IGoToOptions.js'; import IResponse from '../../fetch/types/IResponse.js'; -import BrowserPageUtility from '../BrowserPageUtility.js'; +import BrowserPageUtility from '../utilities/BrowserPageUtility.js'; /** * Detached browser page used when constructing a Window instance without a browser. @@ -34,7 +33,7 @@ export default class DetachedBrowserPage implements IBrowserPage { * Returns frames. */ public get frames(): DetachedBrowserFrame[] { - return BrowserFrameUtility.getFrames(this.mainFrame); + return BrowserPageUtility.getFrames(this); } /** diff --git a/packages/happy-dom/src/browser/types/BrowserNavigationCrossOriginPolicyEnum.ts b/packages/happy-dom/src/browser/types/BrowserNavigationCrossOriginPolicyEnum.ts new file mode 100644 index 000000000..b16116ddb --- /dev/null +++ b/packages/happy-dom/src/browser/types/BrowserNavigationCrossOriginPolicyEnum.ts @@ -0,0 +1,10 @@ +enum BrowserNavigationCrossOriginPolicyEnum { + // The browser can navigate to any origin. + anyOrigin = 'any-origin', + // The browser can only navigate to the same origin as the current page or its parent. + sameOrigin = 'same-origin', + // The browser can never navigate from a secure protocol (https) to an unsecure protocol (http), but it can always navigate to a secure (https). + strictOrigin = 'strict-origin' +} + +export default BrowserNavigationCrossOriginPolicyEnum; diff --git a/packages/happy-dom/src/browser/types/BrowserNavigationEnum.ts b/packages/happy-dom/src/browser/types/BrowserNavigationEnum.ts deleted file mode 100644 index 20a251ce1..000000000 --- a/packages/happy-dom/src/browser/types/BrowserNavigationEnum.ts +++ /dev/null @@ -1,10 +0,0 @@ -enum BrowserNavigationEnum { - all = 'all', - sameOrigin = 'sameorigin', - mainFrame = 'main-frame', - childFrame = 'child-frame', - childPage = 'child-page', - urlFallback = 'set-url-fallback' -} - -export default BrowserNavigationEnum; diff --git a/packages/happy-dom/src/browser/types/IBrowserSettings.ts b/packages/happy-dom/src/browser/types/IBrowserSettings.ts index 097a6ceef..da7f32e69 100644 --- a/packages/happy-dom/src/browser/types/IBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IBrowserSettings.ts @@ -1,22 +1,63 @@ -import BrowserNavigationEnum from './BrowserNavigationEnum.js'; +import BrowserNavigationCrossOriginPolicyEnum from './BrowserNavigationCrossOriginPolicyEnum.js'; /** * Browser settings. */ export default interface IBrowserSettings { + /** Disables JavaScript evaluation. */ disableJavaScriptEvaluation: boolean; + + /** Disables JavaScript file loading. */ disableJavaScriptFileLoading: boolean; + + /** Disables CSS file loading. */ disableCSSFileLoading: boolean; - disableIframePageLoading: boolean; - disableWindowOpenPageLoading: boolean; + + /** Disables computed style rendering. */ disableComputedStyleRendering: boolean; + + /** Disables error capturing. */ disableErrorCapturing: boolean; + + /** Enables file system HTTP requests. */ enableFileSystemHttpRequests: boolean; - browserNavigation: BrowserNavigationEnum[]; + + /** + * @deprecated Use navigation.disableChildFrameNavigation instead. + */ + disableIframePageLoading: boolean; + + /** + * Settings for the browser's navigation (when following links or opening windows). + */ + navigation: { + /** Disables navigation to other pages in the main frame or a page. */ + disableMainFrameNavigation: boolean; + + /** Disables navigation to other pages in child frames (such as iframes). */ + disableChildFrameNavigation: boolean; + + /** Disables navigation to other pages in child pages (such as popup windows). */ + disableChildPageNavigation: boolean; + + /** Disables the fallback to setting the URL when navigating to a page is disabled or when inside a detached browser frame. */ + disableFallbackToSetURL: boolean; + + /** Sets the policy for cross-origin navigation. */ + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum; + }; + + /** + * Settings for the browser's navigator. + */ navigator: { userAgent: string; maxTouchPoints: number; }; + + /** + * Settings for the browser's device. + */ device: { prefersColorScheme: string; mediaType: string; diff --git a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts index 4fd64d9c0..5ce8d8136 100644 --- a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts @@ -1,22 +1,60 @@ -import BrowserNavigationEnum from './BrowserNavigationEnum.js'; +import BrowserNavigationCrossOriginPolicyEnum from './BrowserNavigationCrossOriginPolicyEnum.js'; -/** - * Browser settings. - */ export default interface IOptionalBrowserSettings { + /** Disables JavaScript evaluation. */ disableJavaScriptEvaluation?: boolean; + + /** Disables JavaScript file loading. */ disableJavaScriptFileLoading?: boolean; + + /** Disables CSS file loading. */ disableCSSFileLoading?: boolean; - disableIframePageLoading?: boolean; - disableWindowOpenPageLoading?: boolean; + + /** Disables computed style rendering. */ disableComputedStyleRendering?: boolean; + + /** Disables error capturing. */ disableErrorCapturing?: boolean; + + /** Enables file system HTTP requests. */ enableFileSystemHttpRequests?: boolean; - browserNavigation?: BrowserNavigationEnum[]; + + /** + * @deprecated Use navigation.disableChildFrameNavigation instead. + */ + disableIframePageLoading?: boolean; + + /** + * Settings for the browser's navigation (when following links or opening windows). + */ + navigation?: { + /** Disables navigation to other pages in the main frame or a page. */ + disableMainFrameNavigation?: boolean; + + /** Disables navigation to other pages in child frames (such as iframes). */ + disableChildFrameNavigation?: boolean; + + /** Disables navigation to other pages in child pages (such as popup windows). */ + disableChildPageNavigation?: boolean; + + /** Disables the fallback to setting the URL when navigating to a page is disabled or when inside a detached browser frame. */ + disableFallbackToSetURL?: boolean; + + /** Sets the policy for cross-origin navigation. */ + crossOriginPolicy?: BrowserNavigationCrossOriginPolicyEnum; + }; + + /** + * Settings for the browser's navigator. + */ navigator?: { userAgent?: string; maxTouchPoints?: number; }; + + /** + * Settings for the browser's device. + */ device?: { prefersColorScheme?: string; mediaType?: string; diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts new file mode 100644 index 000000000..677868a3a --- /dev/null +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts @@ -0,0 +1,53 @@ +import BrowserPage from '../BrowserPage.js'; +import IBrowserFrame from '../types/IBrowserFrame.js'; +import Window from '../../window/Window.js'; +import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; +import IBrowserPage from '../types/IBrowserPage.js'; +/** + * Browser frame factory. + */ +export default class BrowserFrameFactory { + /** + * Creates a new frame. + * + * @param parentFrame Parent frame. + * @returns Frame. + */ + public static newChildFrame(parentFrame: IBrowserFrame): IBrowserFrame { + const frame = new ( IBrowserFrame>parentFrame.constructor)( + parentFrame.page + ); + (frame.parentFrame) = parentFrame; + parentFrame.childFrames.push(frame); + return frame; + } + + /** + * Aborts all ongoing operations and destroys the frame. + * + * @param frame Frame. + */ + public static destroyFrame(frame: IBrowserFrame): void { + if (!frame.window) { + return; + } + + if (frame.parentFrame) { + const index = frame.parentFrame.childFrames.indexOf(frame); + if (index !== -1) { + frame.parentFrame.childFrames.splice(index, 1); + } + } + + for (const childFrame of frame.childFrames.slice()) { + this.destroyFrame(childFrame); + } + + (frame.window.closed) = true; + frame._asyncTaskManager.destroy(); + WindowBrowserSettingsReader.removeSettings(frame.window); + (frame.page) = null; + (frame.window) = null; + (frame.opener) = null; + } +} diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts new file mode 100644 index 000000000..60a26baa5 --- /dev/null +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts @@ -0,0 +1,153 @@ +import IBrowserFrame from '../types/IBrowserFrame.js'; +import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; +import IGoToOptions from '../types/IGoToOptions.js'; +import IResponse from '../../fetch/types/IResponse.js'; +import DocumentReadyStateManager from '../../nodes/document/DocumentReadyStateManager.js'; +import IWindow from '../../window/IWindow.js'; +import WindowErrorUtility from '../../window/WindowErrorUtility.js'; +import Location from '../../location/Location.js'; +import AbortController from '../../fetch/AbortController.js'; +import BrowserFrameFactory from './BrowserFrameFactory.js'; +import BrowserFrameURL from './BrowserFrameURL.js'; +import BrowserFrameValidator from './BrowserFrameValidator.js'; + +/** + * Browser frame navigation utility. + */ +export default class BrowserFrameNavigator { + /** + * Go to a page. + * + * @throws Error if the request can't be resolved (because of SSL error or similar). It will not throw if the response is not ok. + * @param windowClass Window class. + * @param frame Frame. + * @param url URL. + * @param [options] Options. + * @returns Response. + */ + public static async goto( + windowClass: new (options: { + browserFrame: IBrowserFrame; + console: Console; + url?: string; + }) => IWindow, + frame: IBrowserFrame, + url: string, + options?: IGoToOptions + ): Promise { + const targetURL = BrowserFrameURL.getRelativeURL(frame, url); + + if (!BrowserFrameValidator.validateCrossOriginPolicy(frame, targetURL)) { + return null; + } + + if (!BrowserFrameValidator.validateFrameNavigation(frame)) { + if (!frame.page.context.browser.settings.navigation.disableFallbackToSetURL) { + (frame.window.location) = new Location(frame, targetURL.href); + } + + return null; + } + + if (targetURL.protocol === 'javascript:') { + if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { + const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( + (frame.window) + ))._readyStateManager; + + readyStateManager.startTask(); + + // The browser will wait for the next tick before executing the script. + await new Promise((resolve) => frame.page.mainFrame.window.setTimeout(resolve)); + + const code = + '//# sourceURL=' + frame.url + '\n' + targetURL.href.replace('javascript:', ''); + + if (frame.page.context.browser.settings.disableErrorCapturing) { + frame.window.eval(code); + } else { + WindowErrorUtility.captureError(frame.window, () => frame.window.eval(code)); + } + + readyStateManager.endTask(); + } + + return null; + } + + for (const childFrame of frame.childFrames) { + BrowserFrameFactory.destroyFrame(childFrame); + } + + (frame.childFrames) = []; + (frame.window.closed) = true; + frame._asyncTaskManager.destroy(); + WindowBrowserSettingsReader.removeSettings(frame.window); + + (frame.window) = new windowClass({ + browserFrame: frame, + console: frame.page.console, + url: targetURL.href + }); + + if (options?.referrer) { + (frame.window.document.referrer) = options.referrer; + } + + if (targetURL.protocol === 'about:') { + return null; + } + + const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( + (frame.window) + ))._readyStateManager; + + readyStateManager.startTask(); + + let abortController = new AbortController(); + let response: IResponse; + let responseText: string; + + const timeout = frame.window.setTimeout( + () => abortController.abort('Request timed out.'), + options?.timeout ?? 30000 + ); + + try { + response = await frame.window.fetch(targetURL.href, { + referrer: options?.referrer, + referrerPolicy: options?.referrerPolicy, + signal: abortController.signal + }); + + // Handles the "X-Frame-Options" header for child frames. + if (frame.parentFrame) { + const originURL = frame.parentFrame.window.location; + const xFrameOptions = response.headers.get('X-Frame-Options')?.toLowerCase(); + const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; + + if (xFrameOptions === 'deny' || (xFrameOptions === 'sameorigin' && !isSameOrigin)) { + throw new Error( + `Refused to display '${url}' in a frame because it set 'X-Frame-Options' to '${xFrameOptions}'.` + ); + } + } + + responseText = await response.text(); + } catch (error) { + frame.window.clearTimeout(timeout); + readyStateManager.endTask(); + throw error; + } + + frame.window.clearTimeout(timeout); + frame.content = responseText; + readyStateManager.endTask(); + + if (!response.ok) { + frame.page.console.error(`GET ${targetURL.href} ${response.status} (${response.statusText})`); + } + + return response; + } +} diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameScriptEvaluator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameScriptEvaluator.ts new file mode 100644 index 000000000..d23d6d739 --- /dev/null +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameScriptEvaluator.ts @@ -0,0 +1,19 @@ +import IBrowserFrame from '../types/IBrowserFrame.js'; +import { Script } from 'vm'; + +/** + * Browser frame script evaluator. + */ +export default class BrowserFrameScriptEvaluator { + /** + * Evaluates code or a VM Script in the frame's context. + * + * @param frame Frame. + * @param script Script. + * @returns Result. + */ + public static evaluate(frame: IBrowserFrame, script: string | Script): any { + script = typeof script === 'string' ? new Script(script) : script; + return script.runInContext(frame.window); + } +} diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameURL.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameURL.ts new file mode 100644 index 000000000..e4edf48d6 --- /dev/null +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameURL.ts @@ -0,0 +1,40 @@ +import IBrowserFrame from '../types/IBrowserFrame.js'; +import { URL } from 'url'; +import DOMException from '../../exception/DOMException.js'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; + +/** + * Browser frame URL utility. + */ +export default class BrowserFrameURL { + /** + * Returns relative URL. + * + * @param frame Frame. + * @param url URL. + * @returns Relative URL. + */ + public static getRelativeURL(frame: IBrowserFrame, url: string): URL { + url = url || 'about:blank'; + + if (url.startsWith('about:') || url.startsWith('javascript:')) { + return new URL(url); + } + + try { + return new URL(url, frame.window.location); + } catch (e) { + if (frame.window.location.hostname) { + throw new DOMException( + `Failed to construct URL from string "${url}".`, + DOMExceptionNameEnum.uriMismatchError + ); + } else { + throw new DOMException( + `Failed to construct URL from string "${url}" relative to URL "${frame.window.location.href}".`, + DOMExceptionNameEnum.uriMismatchError + ); + } + } + } +} diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameValidator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameValidator.ts new file mode 100644 index 000000000..6aaf35327 --- /dev/null +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameValidator.ts @@ -0,0 +1,83 @@ +import IBrowserFrame from '../types/IBrowserFrame.js'; +import { URL } from 'url'; +import BrowserNavigationCrossOriginPolicyEnum from '../types/BrowserNavigationCrossOriginPolicyEnum.js'; +import DetachedBrowserFrame from '../detached-browser/DetachedBrowserFrame.js'; + +/** + * Browser frame validator. + */ +export default class BrowserFrameValidator { + /** + * Returns true if the frame navigation complies with the cross origin policy. + * + * @param frame Frame. + * @param toURL URL. + * @returns True if the frame navigation complies with the cross origin policy. + */ + public static validateCrossOriginPolicy(frame: IBrowserFrame, toURL: URL): boolean { + const settings = frame.page.context.browser.settings; + let fromURL = frame.page.mainFrame.window.location; + + if (frame.opener) { + fromURL = frame.opener.window.location; + } + if (frame.parentFrame) { + fromURL = frame.parentFrame.window.location; + } + + if ( + settings.navigation.crossOriginPolicy === BrowserNavigationCrossOriginPolicyEnum.sameOrigin && + fromURL.protocol !== 'about:' && + toURL.protocol !== 'about:' && + toURL.protocol !== 'javascript:' && + fromURL.origin !== toURL.origin + ) { + return false; + } + + if ( + settings.navigation.crossOriginPolicy === + BrowserNavigationCrossOriginPolicyEnum.strictOrigin && + fromURL.protocol === 'http:' && + toURL.protocol === 'https:' + ) { + return false; + } + + return true; + } + + /** + * Returns true if navigation is allowed for the frame. + * + * @param frame Frame. + * @returns True if navigation is allowed for the frame. + */ + public static validateFrameNavigation(frame: IBrowserFrame): boolean { + const settings = frame.page.context.browser.settings; + + // When using the Window instance directly and not via the Browser API we should not navigate the browser frame. + if ( + frame instanceof DetachedBrowserFrame && + frame.page.context === frame.page.context.browser.defaultContext && + frame.page.context.pages[0] === frame.page && + frame.page.mainFrame === frame + ) { + return false; + } + + if (settings.navigation.disableMainFrameNavigation && frame.page.mainFrame === frame) { + return false; + } + + if (settings.navigation.disableChildFrameNavigation && frame.page.mainFrame !== frame) { + return false; + } + + if (settings.navigation.disableChildPageNavigation && !!frame.opener) { + return false; + } + + return true; + } +} diff --git a/packages/happy-dom/src/browser/BrowserPageUtility.ts b/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts similarity index 62% rename from packages/happy-dom/src/browser/BrowserPageUtility.ts rename to packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts index d07e983c0..0f48ae3bd 100644 --- a/packages/happy-dom/src/browser/BrowserPageUtility.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts @@ -1,15 +1,25 @@ -import IBrowserPage from './types/IBrowserPage.js'; -import IBrowserPageViewport from './types/IBrowserPageViewport.js'; -import Event from '../event/Event.js'; -import BrowserFrameUtility from './BrowserFrameUtility.js'; -import IVirtualConsolePrinter from '../console/types/IVirtualConsolePrinter.js'; -import IBrowserFrame from './types/IBrowserFrame.js'; -import IBrowserContext from './types/IBrowserContext.js'; +import IBrowserFrame from '../types/IBrowserFrame.js'; +import IBrowserPage from '../types/IBrowserPage.js'; +import IBrowserPageViewport from '../types/IBrowserPageViewport.js'; +import Event from '../../event/Event.js'; +import IVirtualConsolePrinter from '../../console/types/IVirtualConsolePrinter.js'; +import IBrowserContext from '../types/IBrowserContext.js'; +import BrowserFrameFactory from './BrowserFrameFactory.js'; /** * Browser page utility. */ export default class BrowserPageUtility { + /** + * Returns frames for a page. + * + * @param page Page. + * @returns Frames. + */ + public static getFrames(page: IBrowserPage): IBrowserFrame[] { + return this.findFrames(page.mainFrame); + } + /** * Sets the viewport. * @@ -49,7 +59,7 @@ export default class BrowserPageUtility { return; } - BrowserFrameUtility.closeFrame(page.mainFrame); + BrowserFrameFactory.destroyFrame(page.mainFrame); const index = page.context.pages.indexOf(page); if (index !== -1) { @@ -66,4 +76,18 @@ export default class BrowserPageUtility { context.close(); } } + + /** + * Returns all frames. + * + * @param parentFrame Parent frame. + * @returns Frames, including the parent. + */ + private static findFrames(parentFrame: IBrowserFrame): IBrowserFrame[] { + let frames = [parentFrame]; + for (const frame of parentFrame.childFrames) { + frames = frames.concat(this.findFrames(frame)); + } + return frames; + } } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts index f52854507..cac96a562 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts @@ -7,7 +7,8 @@ import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; import IHTMLIFrameElement from './IHTMLIFrameElement.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; -import BrowserFrameUtility from '../../browser/BrowserFrameUtility.js'; +import BrowserFrameURL from '../../browser/utilities/BrowserFrameURL.js'; +import BrowserFrameFactory from '../../browser/utilities/BrowserFrameFactory.js'; /** * HTML Iframe page loader. @@ -43,7 +44,7 @@ export default class HTMLIFrameElementPageLoader { public loadPage(): void { if (!this.#element.isConnected) { if (this.#browserIFrame) { - BrowserFrameUtility.closeFrame(this.#browserIFrame); + BrowserFrameFactory.destroyFrame(this.#browserIFrame); this.#browserIFrame = null; } this.#contentWindowContainer.window = null; @@ -52,10 +53,7 @@ export default class HTMLIFrameElementPageLoader { const window = this.#element.ownerDocument._defaultView; const originURL = this.#browserParentFrame.window.location; - const targetURL = BrowserFrameUtility.getRelativeURL( - this.#browserParentFrame, - this.#element.src - ); + const targetURL = BrowserFrameURL.getRelativeURL(this.#browserParentFrame, this.#element.src); if (this.#browserIFrame && this.#browserIFrame.window.location.href === targetURL.href) { return; @@ -77,7 +75,7 @@ export default class HTMLIFrameElementPageLoader { const parentWindow = isSameOrigin ? window : new CrossOriginWindow(window); this.#browserIFrame = - this.#browserIFrame ?? BrowserFrameUtility.newFrame(this.#browserParentFrame); + this.#browserIFrame ?? BrowserFrameFactory.newChildFrame(this.#browserParentFrame); ((this.#browserIFrame.window.top)) = parentWindow; ((this.#browserIFrame.window.parent)) = parentWindow; @@ -97,7 +95,7 @@ export default class HTMLIFrameElementPageLoader { */ public unloadPage(): void { if (this.#browserIFrame) { - BrowserFrameUtility.closeFrame(this.#browserIFrame); + BrowserFrameFactory.destroyFrame(this.#browserIFrame); this.#browserIFrame = null; } this.#contentWindowContainer.window = null; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 46cc467b2..dff05675d 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -863,9 +863,6 @@ export default class Window extends EventTarget implements IWindow { target?: string, features?: string ): IWindow | ICrossOriginWindow | null { - if (this.#browserFrame.page.context.browser.settings.disableWindowOpenPageLoading) { - return null; - } return WindowPageOpenUtility.openPage(this.#browserFrame, { url, target, diff --git a/packages/happy-dom/src/window/WindowPageOpenUtility.ts b/packages/happy-dom/src/window/WindowPageOpenUtility.ts index b78d871f0..5ca080b1f 100644 --- a/packages/happy-dom/src/window/WindowPageOpenUtility.ts +++ b/packages/happy-dom/src/window/WindowPageOpenUtility.ts @@ -3,7 +3,7 @@ import CrossOriginWindow from './CrossOriginWindow.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; import FetchCORSUtility from '../fetch/utilities/FetchCORSUtility.js'; import ICrossOriginWindow from './ICrossOriginWindow.js'; -import BrowserFrameUtility from '../browser/BrowserFrameUtility.js'; +import BrowserFrameURL from '../browser/utilities/BrowserFrameURL.js'; /** * Window page open handler. @@ -29,7 +29,8 @@ export default class WindowPageOpenUtility { const features = this.getWindowFeatures(options?.features || ''); const target = options?.target !== undefined ? String(options.target) : null; const originURL = browserFrame.window.location; - const targetURL = BrowserFrameUtility.getRelativeURL(browserFrame, options.url); + const targetURL = BrowserFrameURL.getRelativeURL(browserFrame, options.url); + const oldWindow = browserFrame.window; let targetFrame: IBrowserFrame; switch (target) { @@ -55,15 +56,15 @@ export default class WindowPageOpenUtility { }) .catch((error) => targetFrame.page.console.error(error)); - // When using the Window instance directly and not via the Browser API we should not navigate the browser frame. - if (BrowserFrameUtility.isDetachedMainFrame(targetFrame)) { - return null; - } - if (targetURL.protocol === 'javascript:') { return targetFrame.window; } + // When using a detached Window instance directly and not via the Browser API we will not navigate and the window for the frame will not have changed. + if (targetFrame === browserFrame && browserFrame.window === oldWindow) { + return null; + } + if (features.popup && target !== '_self' && target !== '_top' && target !== '_parent') { if (features?.width || features?.height) { targetFrame.page.setViewport({ diff --git a/packages/happy-dom/test/browser/BrowserFrame.test.ts b/packages/happy-dom/test/browser/BrowserFrame.test.ts index 6453dde5f..a6f668172 100644 --- a/packages/happy-dom/test/browser/BrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/BrowserFrame.test.ts @@ -1,6 +1,5 @@ import { Script } from 'vm'; import Browser from '../../src/browser/Browser'; -import BrowserFrameUtility from '../../src/browser/BrowserFrameUtility'; import Event from '../../src/event/Event'; import Window from '../../src/window/Window'; import IRequest from '../../src/fetch/types/IRequest'; @@ -9,7 +8,8 @@ import { describe, it, expect, afterEach, vi } from 'vitest'; import Fetch from '../../src/fetch/Fetch'; import DOMException from '../../src/exception/DOMException'; import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum'; -import BrowserNavigationEnum from '../../src/browser/types/BrowserNavigationEnum'; +import BrowserNavigationCrossOriginPolicyEnum from '../../src/browser/types/BrowserNavigationCrossOriginPolicyEnum'; +import BrowserFrameFactory from '../../src/browser/utilities/BrowserFrameFactory'; describe('BrowserFrame', () => { afterEach(() => { @@ -21,8 +21,8 @@ describe('BrowserFrame', () => { const browser = new Browser(); const page = browser.defaultContext.newPage(); expect(page.mainFrame.childFrames).toEqual([]); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); expect(page.mainFrame.childFrames).toEqual([frame1, frame2]); }); }); @@ -32,8 +32,8 @@ describe('BrowserFrame', () => { const browser = new Browser(); const page = browser.defaultContext.newPage(); expect(page.mainFrame.parentFrame).toBe(null); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(frame1); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(frame1); expect(frame2.parentFrame).toBe(frame1); expect(frame1.parentFrame).toBe(page.mainFrame); expect(page.mainFrame.parentFrame).toBe(null); @@ -119,8 +119,8 @@ describe('BrowserFrame', () => { it('Waits for all pages to complete.', async () => { const browser = new Browser(); const page = browser.newPage(); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); page.mainFrame.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame1.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); @@ -135,8 +135,8 @@ describe('BrowserFrame', () => { it('Aborts all ongoing operations.', async () => { const browser = new Browser(); const page = browser.newPage(); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); page.mainFrame.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); @@ -296,14 +296,16 @@ describe('BrowserFrame', () => { expect(page.mainFrame.window.document.body.innerHTML).toBe(''); }); - it(`Doesn't navigate if the setting "browserNavigation" is set to "['deny']"`, async () => { + it(`Doesn't navigate the main frame if the setting "navigation.disableMainFrameNavigation" is set to "true"`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); const browser = new Browser({ settings: { - browserNavigation: [BrowserNavigationEnum.deny] + navigation: { + disableMainFrameNavigation: true + } } }); const page = browser.newPage(); @@ -315,7 +317,7 @@ describe('BrowserFrame', () => { expect(page.mainFrame.window === oldWindow).toBe(true); }); - it(`Navigates if the setting "browserNavigation" is set to "['allow']"`, async () => { + it(`Navigates the main frame if the setting "navigation.disableMainFrameNavigation" is set to "false"`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.resolve({ text: () => Promise.resolve('Test') @@ -324,7 +326,9 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { - browserNavigation: [BrowserNavigationEnum.allow] + navigation: { + disableMainFrameNavigation: false + } } }); const page = browser.newPage(); @@ -336,18 +340,20 @@ describe('BrowserFrame', () => { expect(page.mainFrame.window === oldWindow).toBe(false); }); - it(`Doesn't navigate if the setting "browserNavigation" is set to "['allow-same-origin']" and the parent is of a different origin.`, async () => { + it(`Doesn't navigate if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}" and the parent is of a different origin.`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); const browser = new Browser({ settings: { - browserNavigation: [BrowserNavigationEnum.allowSameOrigin] + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + } } }); const page = browser.newPage(); - const childFrame = BrowserFrameUtility.newFrame(page.mainFrame); + const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); const oldWindow = childFrame.window; page.mainFrame.url = 'https://github.com'; @@ -359,7 +365,7 @@ describe('BrowserFrame', () => { expect(childFrame.window === oldWindow).toBe(true); }); - it(`Navigates if the setting "browserNavigation" is set to "['allow-same-origin']" and the parent is the same origin.`, async () => { + it(`Navigates if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}" and the parent is the same origin.`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.resolve(({ text: () => Promise.resolve('Test'), @@ -369,11 +375,13 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { - browserNavigation: [BrowserNavigationEnum.allowSameOrigin] + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + } } }); const page = browser.newPage(); - const childFrame = BrowserFrameUtility.newFrame(page.mainFrame); + const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); const oldWindow = childFrame.window; page.mainFrame.url = 'https://github.com'; @@ -384,14 +392,16 @@ describe('BrowserFrame', () => { expect(childFrame.window === oldWindow).toBe(false); }); - it(`Doesn't navigate if the setting "browserNavigation" is set to "['allow-same-origin']" and the opener is of a different origin.`, async () => { + it(`Doesn't navigate if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}" and the opener is of a different origin.`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); const browser = new Browser({ settings: { - browserNavigation: [BrowserNavigationEnum.allowSameOrigin] + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + } } }); const page = browser.newPage(); @@ -407,7 +417,7 @@ describe('BrowserFrame', () => { expect(childPage.mainFrame.window === oldWindow).toBe(true); }); - it(`Navigate if the setting "browserNavigation" is set to "['allow-same-origin']" and the opener has the same origin.`, async () => { + it(`Navigate if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}" and the opener has the same origin.`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.resolve(({ text: () => Promise.resolve('Test'), @@ -417,7 +427,9 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { - browserNavigation: [BrowserNavigationEnum.allowSameOrigin] + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + } } }); const page = browser.newPage(); @@ -432,14 +444,16 @@ describe('BrowserFrame', () => { expect(childPage.mainFrame.window === oldWindow).toBe(false); }); - it(`Doesn't navigate if the setting "browserNavigation" is set to "['allow-same-origin']" when navigating from one origin to another.`, async () => { + it(`Doesn't navigate if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}" when navigating from one origin to another.`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); const browser = new Browser({ settings: { - browserNavigation: [BrowserNavigationEnum.allowSameOrigin] + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + } } }); const page = browser.newPage(); @@ -454,7 +468,7 @@ describe('BrowserFrame', () => { expect(page.mainFrame.window === oldWindow).toBe(true); }); - it(`Navigates if the setting "browserNavigation" is set to "['allowChildFrames']" inside a child frame.`, async () => { + it(`Navigates if the setting "navigation.disableChildFrameNavigation" is set to "false" inside a child frame.`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.resolve(({ text: () => Promise.resolve('Test'), @@ -464,11 +478,13 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { - browserNavigation: [BrowserNavigationEnum.allowChildFrames] + navigation: { + disableChildFrameNavigation: false + } } }); const page = browser.newPage(); - const childFrame = BrowserFrameUtility.newFrame(page.mainFrame); + const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); const oldWindow = childFrame.window; await childFrame.goto('http://localhost:9999'); @@ -477,27 +493,30 @@ describe('BrowserFrame', () => { expect(childFrame.window === oldWindow).toBe(false); }); - it(`Doesn't navigate if the setting "browserNavigation" is set to "['allowChildFrames']" inside a main frame.`, async () => { + it(`Doesn't navigate if the setting "navigation.disableChildFrameNavigation" is set to "true" inside a child frame.`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); const browser = new Browser({ settings: { - browserNavigation: [BrowserNavigationEnum.allowChildFrames] + navigation: { + disableChildFrameNavigation: true + } } }); const page = browser.newPage(); - const oldWindow = page.mainFrame.window; + const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); + const oldWindow = childFrame.window; - const response = await page.mainFrame.goto('http://localhost:9999'); + const response = await childFrame.goto('http://localhost:9999'); expect(response).toBeNull(); expect(page.mainFrame.url).toBe('about:blank'); expect(page.mainFrame.window === oldWindow).toBe(true); }); - it(`Navigates if the setting "browserNavigation" is set to "['allowChildPages']" inside a child page.`, async () => { + it(`Navigates if the setting "navigation.disableChildPageNavigation" is set to "false" inside a child page.`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.resolve(({ text: () => Promise.resolve('Test'), @@ -507,7 +526,9 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { - browserNavigation: [BrowserNavigationEnum.allowChildPages] + navigation: { + disableChildPageNavigation: false + } } }); const page = browser.newPage(); @@ -520,14 +541,16 @@ describe('BrowserFrame', () => { expect(childPage.mainFrame.window === oldWindow).toBe(false); }); - it(`Doesn't navigate if the setting "browserNavigation" is set to "['allowChildPages']" inside a main page.`, async () => { + it(`Doesn't navigate if the setting "navigation.disableChildPageNavigation" is set to "true" inside a main page.`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); const browser = new Browser({ settings: { - browserNavigation: [BrowserNavigationEnum.allowChildPages] + navigation: { + disableChildPageNavigation: true + } } }); const page = browser.newPage(); diff --git a/packages/happy-dom/test/browser/BrowserPage.test.ts b/packages/happy-dom/test/browser/BrowserPage.test.ts index b3f924564..b6015f62b 100644 --- a/packages/happy-dom/test/browser/BrowserPage.test.ts +++ b/packages/happy-dom/test/browser/BrowserPage.test.ts @@ -3,10 +3,10 @@ import BrowserFrame from '../../src/browser/BrowserFrame'; import Window from '../../src/window/Window'; import VirtualConsolePrinter from '../../src/console/VirtualConsolePrinter'; import VirtualConsole from '../../src/console/VirtualConsole'; -import BrowserFrameUtility from '../../src/browser/BrowserFrameUtility'; import IResponse from '../../src/fetch/types/IResponse'; import { describe, it, expect, afterEach, vi } from 'vitest'; import IGoToOptions from '../../src/browser/types/IGoToOptions'; +import BrowserFrameFactory from '../../src/browser/utilities/BrowserFrameFactory'; describe('BrowserPage', () => { afterEach(() => { @@ -58,8 +58,8 @@ describe('BrowserPage', () => { it('Returns the frames.', () => { const browser = new Browser(); const page = browser.defaultContext.newPage(); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); expect(page.frames).toEqual([page.mainFrame, frame1, frame2]); }); }); @@ -106,9 +106,9 @@ describe('BrowserPage', () => { it('Closes the page.', () => { const browser = new Browser(); const page = browser.defaultContext.newPage(); - const mainFrame = BrowserFrameUtility.newFrame(page.mainFrame); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + const mainFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); page.close(); @@ -127,8 +127,8 @@ describe('BrowserPage', () => { it('Waits for all pages to complete.', async () => { const browser = new Browser(); const page = browser.newPage(); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); await page.whenComplete(); @@ -141,8 +141,8 @@ describe('BrowserPage', () => { it('Aborts all ongoing operations.', async () => { const browser = new Browser(); const page = browser.newPage(); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); page.abort(); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts index 3f8ec36d0..14399c8ff 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts @@ -3,10 +3,10 @@ import DetachedBrowserFrame from '../../../src/browser/detached-browser/Detached import Window from '../../../src/window/Window'; import VirtualConsolePrinter from '../../../src/console/VirtualConsolePrinter'; import VirtualConsole from '../../../src/console/VirtualConsole'; -import BrowserFrameUtility from '../../../src/browser/BrowserFrameUtility'; import IResponse from '../../../src/fetch/types/IResponse'; import { describe, it, expect, afterEach, vi } from 'vitest'; import IGoToOptions from '../../../src/browser/types/IGoToOptions'; +import BrowserFrameFactory from '../../../src/browser/utilities/BrowserFrameFactory'; describe('DetachedBrowserPage', () => { afterEach(() => { @@ -58,8 +58,8 @@ describe('DetachedBrowserPage', () => { it('Returns the frames.', () => { const browser = new DetachedBrowser(Window, new Window()); const page = browser.defaultContext.newPage(); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); expect(page.frames).toEqual([page.mainFrame, frame1, frame2]); }); }); @@ -106,9 +106,9 @@ describe('DetachedBrowserPage', () => { it('Closes the page.', () => { const browser = new DetachedBrowser(Window, new Window()); const page = browser.defaultContext.newPage(); - const mainFrame = BrowserFrameUtility.newFrame(page.mainFrame); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + const mainFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); page.close(); @@ -128,8 +128,8 @@ describe('DetachedBrowserPage', () => { it('Waits for all pages to complete.', async () => { const browser = new DetachedBrowser(Window, new Window()); const page = browser.newPage(); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); await page.whenComplete(); @@ -142,8 +142,8 @@ describe('DetachedBrowserPage', () => { it('Aborts all ongoing operations.', async () => { const browser = new DetachedBrowser(Window, new Window()); const page = browser.newPage(); - const frame1 = BrowserFrameUtility.newFrame(page.mainFrame); - const frame2 = BrowserFrameUtility.newFrame(page.mainFrame); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); page.abort(); diff --git a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts index f686d95e7..8895759d5 100644 --- a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts +++ b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts @@ -8,7 +8,6 @@ import IRequest from '../../../src/fetch/types/IRequest.js'; import IResponse from '../../../src/fetch/types/IResponse.js'; import Fetch from '../../../src/fetch/Fetch.js'; import Browser from '../../../src/browser/Browser.js'; -import BrowserNavigationEnum from '../../../src/browser/types/BrowserNavigationEnum.js'; const BLOB_URL = 'blob:https://mozilla.org'; @@ -413,10 +412,7 @@ describe('HTMLAnchorElement', () => { const page = browser.newPage(); const window = page.mainFrame.window; - let request: IRequest | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { - request = this.request; return Promise.resolve({ text: () => Promise.resolve('Test') }); @@ -446,10 +442,7 @@ describe('HTMLAnchorElement', () => { const page = browser.newPage(); const window = page.mainFrame.window; - let request: IRequest | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { - request = this.request; return Promise.resolve({ text: () => Promise.resolve('Test') }); @@ -480,10 +473,7 @@ describe('HTMLAnchorElement', () => { const page = browser.newPage(); const window = page.mainFrame.window; - let request: IRequest | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { - request = this.request; return Promise.resolve({ text: () => Promise.resolve('Test') }); @@ -503,10 +493,12 @@ describe('HTMLAnchorElement', () => { expect(newWindow.document.body.innerHTML).toBe('Test'); }); - it(`Doesn't navigate or change the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "browserNavigation" is set to ["deny"].`, () => { + it(`Doesn't navigate or change the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "navigation.disableFallbackToSetURL" is set to "true".`, () => { const window = new Window({ settings: { - browserNavigation: [BrowserNavigationEnum.deny] + navigation: { + disableFallbackToSetURL: true + } } }); document = window.document; @@ -522,10 +514,13 @@ describe('HTMLAnchorElement', () => { expect(window.location.href).toBe('about:blank'); }); - it(`Doesn't navigate, but changes the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "browserNavigation" is set to ["deny", "url-set-fallback"].`, () => { + it(`Doesn't navigate, but changes the location of a new window when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "navigation.disableFallbackToSetURL" is set to "false" and "navigation.disableChildPageNavigation" is set to "true".`, () => { const window = new Window({ settings: { - browserNavigation: [BrowserNavigationEnum.deny, BrowserNavigationEnum.setURLFallback] + navigation: { + disableFallbackToSetURL: false, + disableChildPageNavigation: true + } } }); document = window.document; @@ -544,10 +539,12 @@ describe('HTMLAnchorElement', () => { expect(newWindow.location.href).toBe('https://www.example.com/'); }); - it('Changes the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "browserNavigation" is set to ["allow", "url-set-fallback"].', () => { + it('Changes the location when a "click" event is dispatched inside the main frame of a detached browser when the Happy DOM setting "navigation.disableFallbackToSetURL" is set to "false".', () => { const window = new Window({ settings: { - browserNavigation: [BrowserNavigationEnum.allow, BrowserNavigationEnum.setURLFallback] + navigation: { + disableFallbackToSetURL: false + } } }); document = window.document; diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 73a549893..f17f9512e 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -17,8 +17,6 @@ import Response from '../../src/fetch/Response.js'; import IRequest from '../../src/fetch/types/IRequest.js'; import IResponse from '../../src/fetch/types/IResponse.js'; import Fetch from '../../src/fetch/Fetch.js'; -import HTTP from 'http'; -import Stream from 'stream'; import MessageEvent from '../../src/event/events/MessageEvent.js'; import Event from '../../src/event/Event.js'; import ErrorEvent from '../../src/event/events/ErrorEvent.js'; @@ -33,9 +31,9 @@ import IHTMLDialogElement from '../../src/nodes/html-dialog-element/IHTMLDialogE import Browser from '../../src/browser/Browser.js'; import ICrossOriginWindow from '../../src/window/ICrossOriginWindow.js'; import CrossOriginWindow from '../../src/window/CrossOriginWindow.js'; -import BrowserFrameUtility from '../../src/browser/BrowserFrameUtility.js'; import IHTMLIFrameElement from '../../src/nodes/html-iframe-element/IHTMLIFrameElement.js'; import DetachedWindowAPI from '../../src/window/DetachedWindowAPI.js'; +import BrowserFrameFactory from '../../src/browser/utilities/BrowserFrameFactory.js'; const GET_NAVIGATOR_PLATFORM = (): string => { return ( @@ -1315,7 +1313,7 @@ describe('Window', () => { await new Promise((resolve) => { const browser = new Browser(); const page = browser.newPage(); - const frame = BrowserFrameUtility.newFrame(page.mainFrame); + const frame = BrowserFrameFactory.newChildFrame(page.mainFrame); const message = 'test'; let triggeredEvent: MessageEvent | null = null; @@ -1506,13 +1504,13 @@ describe('Window', () => { window.happyDOM?.setURL('https://www.github.com/'); - expect(window.open('/capricorn86/happy-dom/', '_self')).toBe(null); + expect(window.open('/capricorn86/happy-dom/', '_self') === null).toBe(true); expect(window.location.href).toBe('https://www.github.com/capricorn86/happy-dom/'); - expect(window.open('/capricorn86/happy-dom/2/', '_top')).toBe(null); + expect(window.open('/capricorn86/happy-dom/2/', '_top') === null).toBe(true); expect(window.location.href).toBe('https://www.github.com/capricorn86/happy-dom/2/'); - expect(window.open('/capricorn86/happy-dom/3/', '_parent')).toBe(null); + expect(window.open('/capricorn86/happy-dom/3/', '_parent') === null).toBe(true); expect(window.location.href).toBe('https://www.github.com/capricorn86/happy-dom/3/'); }); From b2c6e6a1ba47b7879b2ba38364606a0dec39a8d6 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 21 Nov 2023 22:36:06 +0100 Subject: [PATCH 28/63] #466@trivial: Continues on implementation. --- .../src/browser/DefaultBrowserSettings.ts | 2 +- .../detached-browser/DetachedBrowser.ts | 2 +- .../detached-browser/DetachedBrowserFrame.ts | 5 +- .../BrowserNavigationCrossOriginPolicyEnum.ts | 0 .../src/browser/types/IBrowserSettings.ts | 2 +- .../browser/types/IOptionalBrowserSettings.ts | 2 +- .../utilities/BrowserFrameNavigator.ts | 24 +- .../utilities/BrowserFrameValidator.ts | 6 +- .../test/browser/BrowserFrame.test.ts | 239 ++++-- .../DetachedBrowserFrame.test.ts | 747 ++++++++++++++++++ 10 files changed, 955 insertions(+), 74 deletions(-) rename packages/happy-dom/src/browser/{types => enums}/BrowserNavigationCrossOriginPolicyEnum.ts (100%) create mode 100644 packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts diff --git a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts index ea0d5b925..3533c381b 100644 --- a/packages/happy-dom/src/browser/DefaultBrowserSettings.ts +++ b/packages/happy-dom/src/browser/DefaultBrowserSettings.ts @@ -1,5 +1,5 @@ import PackageVersion from '../version.js'; -import BrowserNavigationCrossOriginPolicyEnum from './types/BrowserNavigationCrossOriginPolicyEnum.js'; +import BrowserNavigationCrossOriginPolicyEnum from './enums/BrowserNavigationCrossOriginPolicyEnum.js'; import IBrowserSettings from './types/IBrowserSettings.js'; export default { diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts index 76047effb..acb7d14aa 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts @@ -19,7 +19,7 @@ export default class DetachedBrowser implements IBrowser { public readonly console: Console | null; public readonly detachedWindowClass: new (options: { browserFrame: IBrowserFrame; - console: Console; + console?: Console; url?: string; }) => IWindow; public readonly detachedWindow: IWindow; diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index 78818b210..590ec72f7 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -29,10 +29,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { constructor(page: DetachedBrowserPage) { this.page = page; this.window = page.context.browser.contexts[0]?.pages[0]?.mainFrame - ? new page.context.browser.detachedWindowClass({ - browserFrame: this, - console: page.console - }) + ? new page.context.browser.detachedWindowClass({ browserFrame: this }) : page.context.browser.detachedWindow; } diff --git a/packages/happy-dom/src/browser/types/BrowserNavigationCrossOriginPolicyEnum.ts b/packages/happy-dom/src/browser/enums/BrowserNavigationCrossOriginPolicyEnum.ts similarity index 100% rename from packages/happy-dom/src/browser/types/BrowserNavigationCrossOriginPolicyEnum.ts rename to packages/happy-dom/src/browser/enums/BrowserNavigationCrossOriginPolicyEnum.ts diff --git a/packages/happy-dom/src/browser/types/IBrowserSettings.ts b/packages/happy-dom/src/browser/types/IBrowserSettings.ts index da7f32e69..a4b061031 100644 --- a/packages/happy-dom/src/browser/types/IBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IBrowserSettings.ts @@ -1,4 +1,4 @@ -import BrowserNavigationCrossOriginPolicyEnum from './BrowserNavigationCrossOriginPolicyEnum.js'; +import BrowserNavigationCrossOriginPolicyEnum from '../enums/BrowserNavigationCrossOriginPolicyEnum.js'; /** * Browser settings. diff --git a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts index 5ce8d8136..6c4d2c5f4 100644 --- a/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts +++ b/packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts @@ -1,4 +1,4 @@ -import BrowserNavigationCrossOriginPolicyEnum from './BrowserNavigationCrossOriginPolicyEnum.js'; +import BrowserNavigationCrossOriginPolicyEnum from '../enums/BrowserNavigationCrossOriginPolicyEnum.js'; export default interface IOptionalBrowserSettings { /** Disables JavaScript evaluation. */ diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts index 60a26baa5..3b45e76c0 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts @@ -37,18 +37,6 @@ export default class BrowserFrameNavigator { ): Promise { const targetURL = BrowserFrameURL.getRelativeURL(frame, url); - if (!BrowserFrameValidator.validateCrossOriginPolicy(frame, targetURL)) { - return null; - } - - if (!BrowserFrameValidator.validateFrameNavigation(frame)) { - if (!frame.page.context.browser.settings.navigation.disableFallbackToSetURL) { - (frame.window.location) = new Location(frame, targetURL.href); - } - - return null; - } - if (targetURL.protocol === 'javascript:') { if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( @@ -75,6 +63,18 @@ export default class BrowserFrameNavigator { return null; } + if (!BrowserFrameValidator.validateCrossOriginPolicy(frame, targetURL)) { + return null; + } + + if (!BrowserFrameValidator.validateFrameNavigation(frame)) { + if (!frame.page.context.browser.settings.navigation.disableFallbackToSetURL) { + (frame.window.location) = new Location(frame, targetURL.href); + } + + return null; + } + for (const childFrame of frame.childFrames) { BrowserFrameFactory.destroyFrame(childFrame); } diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameValidator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameValidator.ts index 6aaf35327..0a51da7e8 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameValidator.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameValidator.ts @@ -1,6 +1,6 @@ import IBrowserFrame from '../types/IBrowserFrame.js'; import { URL } from 'url'; -import BrowserNavigationCrossOriginPolicyEnum from '../types/BrowserNavigationCrossOriginPolicyEnum.js'; +import BrowserNavigationCrossOriginPolicyEnum from '../enums/BrowserNavigationCrossOriginPolicyEnum.js'; import DetachedBrowserFrame from '../detached-browser/DetachedBrowserFrame.js'; /** @@ -38,8 +38,8 @@ export default class BrowserFrameValidator { if ( settings.navigation.crossOriginPolicy === BrowserNavigationCrossOriginPolicyEnum.strictOrigin && - fromURL.protocol === 'http:' && - toURL.protocol === 'https:' + fromURL.protocol === 'https:' && + toURL.protocol === 'http:' ) { return false; } diff --git a/packages/happy-dom/test/browser/BrowserFrame.test.ts b/packages/happy-dom/test/browser/BrowserFrame.test.ts index a6f668172..eb02a0bd5 100644 --- a/packages/happy-dom/test/browser/BrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/BrowserFrame.test.ts @@ -8,7 +8,7 @@ import { describe, it, expect, afterEach, vi } from 'vitest'; import Fetch from '../../src/fetch/Fetch'; import DOMException from '../../src/exception/DOMException'; import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum'; -import BrowserNavigationCrossOriginPolicyEnum from '../../src/browser/types/BrowserNavigationCrossOriginPolicyEnum'; +import BrowserNavigationCrossOriginPolicyEnum from '../../src/browser/enums/BrowserNavigationCrossOriginPolicyEnum'; import BrowserFrameFactory from '../../src/browser/utilities/BrowserFrameFactory'; describe('BrowserFrame', () => { @@ -296,7 +296,7 @@ describe('BrowserFrame', () => { expect(page.mainFrame.window.document.body.innerHTML).toBe(''); }); - it(`Doesn't navigate the main frame if the setting "navigation.disableMainFrameNavigation" is set to "true"`, async () => { + it(`Doesn't navigate a child frame with a different origin from its parent if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}".`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); @@ -304,43 +304,51 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { navigation: { - disableMainFrameNavigation: true + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin } } }); const page = browser.newPage(); - const oldWindow = page.mainFrame.window; + const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); + const oldWindow = childFrame.window; - await page.mainFrame.goto('http://localhost:9999'); + page.mainFrame.url = 'https://github.com'; - expect(page.mainFrame.url).toBe('about:blank'); - expect(page.mainFrame.window === oldWindow).toBe(true); + const response = await childFrame.goto('http://localhost:9999'); + + expect(response).toBeNull(); + expect(childFrame.url).toBe('about:blank'); + expect(childFrame.window === oldWindow).toBe(true); }); - it(`Navigates the main frame if the setting "navigation.disableMainFrameNavigation" is set to "false"`, async () => { + it(`Navigates a child frame with the same origin as its parent if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}".`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { - return Promise.resolve({ - text: () => Promise.resolve('Test') - }); + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); }); const browser = new Browser({ settings: { navigation: { - disableMainFrameNavigation: false + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin } } }); const page = browser.newPage(); - const oldWindow = page.mainFrame.window; + const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); + const oldWindow = childFrame.window; - await page.mainFrame.goto('http://localhost:9999'); + page.mainFrame.url = 'https://github.com'; - expect(page.mainFrame.url).toBe('http://localhost:9999/'); - expect(page.mainFrame.window === oldWindow).toBe(false); + await childFrame.goto('https://github.com/capricorn86/happy-dom'); + + expect(childFrame.url).toBe('https://github.com/capricorn86/happy-dom'); + expect(childFrame.window === oldWindow).toBe(false); }); - it(`Doesn't navigate if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}" and the parent is of a different origin.`, async () => { + it(`Doesn't navigate a popup with a different origin from its parent if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}".`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); @@ -353,19 +361,19 @@ describe('BrowserFrame', () => { } }); const page = browser.newPage(); - const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); - const oldWindow = childFrame.window; + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; page.mainFrame.url = 'https://github.com'; - const response = await childFrame.goto('http://localhost:9999'); + const response = await childPage.mainFrame.goto('http://localhost:9999'); expect(response).toBeNull(); - expect(childFrame.url).toBe('about:blank'); - expect(childFrame.window === oldWindow).toBe(true); + expect(childPage.mainFrame.url).toBe('about:blank'); + expect(childPage.mainFrame.window === oldWindow).toBe(true); }); - it(`Navigates if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}" and the parent is the same origin.`, async () => { + it(`Navigates a popup with the same origin as its parent if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}".`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.resolve(({ text: () => Promise.resolve('Test'), @@ -381,18 +389,18 @@ describe('BrowserFrame', () => { } }); const page = browser.newPage(); - const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); - const oldWindow = childFrame.window; + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; page.mainFrame.url = 'https://github.com'; - await childFrame.goto('https://github.com/capricorn86/happy-dom'); + await childPage.mainFrame.goto('https://github.com/capricorn86/happy-dom'); - expect(childFrame.url).toBe('https://github.com/capricorn86/happy-dom'); - expect(childFrame.window === oldWindow).toBe(false); + expect(childPage.mainFrame.url).toBe('https://github.com/capricorn86/happy-dom'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); }); - it(`Doesn't navigate if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}" and the opener is of a different origin.`, async () => { + it(`Doesn't navigate from one origin to another if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}".`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); @@ -405,19 +413,18 @@ describe('BrowserFrame', () => { } }); const page = browser.newPage(); - const childPage = browser.newPage(page.mainFrame); - const oldWindow = childPage.mainFrame.window; + const oldWindow = page.mainFrame.window; page.mainFrame.url = 'https://github.com'; - const response = await childPage.mainFrame.goto('http://localhost:9999'); + const response = await page.mainFrame.goto('http://localhost:9999'); expect(response).toBeNull(); - expect(childPage.mainFrame.url).toBe('about:blank'); - expect(childPage.mainFrame.window === oldWindow).toBe(true); + expect(page.mainFrame.url).toBe('https://github.com/'); + expect(page.mainFrame.window === oldWindow).toBe(true); }); - it(`Navigate if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}" and the opener has the same origin.`, async () => { + it(`Navigates from "http" to "https" if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.strictOrigin}".`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.resolve(({ text: () => Promise.resolve('Test'), @@ -428,7 +435,34 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { navigation: { - crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin + } + } + }); + const page = browser.newPage(); + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + page.mainFrame.url = 'http://github.com'; + + await childPage.mainFrame.goto('https://github.com/capricorn86/happy-dom'); + + expect(childPage.mainFrame.url).toBe('https://github.com/capricorn86/happy-dom'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Navigates from "https" to "https" if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.strictOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new Browser({ + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin } } }); @@ -444,7 +478,59 @@ describe('BrowserFrame', () => { expect(childPage.mainFrame.window === oldWindow).toBe(false); }); - it(`Doesn't navigate if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}" when navigating from one origin to another.`, async () => { + it(`Navigates from "about" to "http" if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.strictOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new Browser({ + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin + } + } + }); + const page = browser.newPage(); + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + await childPage.mainFrame.goto('http://github.com/capricorn86/happy-dom'); + + expect(childPage.mainFrame.url).toBe('http://github.com/capricorn86/happy-dom'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Navigates from "https" to "about" if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.strictOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new Browser({ + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin + } + } + }); + const page = browser.newPage(); + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + page.mainFrame.url = 'https://github.com'; + + await childPage.mainFrame.goto('about:blank'); + + expect(childPage.mainFrame.url).toBe('about:blank'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate from "https" to "http" if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.strictOrigin}".`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); @@ -452,7 +538,7 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { navigation: { - crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin } } }); @@ -461,14 +547,14 @@ describe('BrowserFrame', () => { page.mainFrame.url = 'https://github.com'; - const response = await page.mainFrame.goto('http://localhost:9999'); + const response = await page.mainFrame.goto('http://github.com/capricorn86/happy-dom'); expect(response).toBeNull(); expect(page.mainFrame.url).toBe('https://github.com/'); expect(page.mainFrame.window === oldWindow).toBe(true); }); - it(`Navigates if the setting "navigation.disableChildFrameNavigation" is set to "false" inside a child frame.`, async () => { + it(`Navigates child frames if the setting "navigation.disableChildFrameNavigation" is set to "false".`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.resolve(({ text: () => Promise.resolve('Test'), @@ -479,7 +565,8 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { navigation: { - disableChildFrameNavigation: false + disableChildFrameNavigation: false, + disableFallbackToSetURL: true } } }); @@ -493,7 +580,7 @@ describe('BrowserFrame', () => { expect(childFrame.window === oldWindow).toBe(false); }); - it(`Doesn't navigate if the setting "navigation.disableChildFrameNavigation" is set to "true" inside a child frame.`, async () => { + it(`Doesn't navigate child frames if the setting "navigation.disableChildFrameNavigation" is set to "true".`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); @@ -501,7 +588,8 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { navigation: { - disableChildFrameNavigation: true + disableChildFrameNavigation: true, + disableFallbackToSetURL: true } } }); @@ -512,11 +600,11 @@ describe('BrowserFrame', () => { const response = await childFrame.goto('http://localhost:9999'); expect(response).toBeNull(); - expect(page.mainFrame.url).toBe('about:blank'); - expect(page.mainFrame.window === oldWindow).toBe(true); + expect(childFrame.url).toBe('about:blank'); + expect(childFrame.window === oldWindow).toBe(true); }); - it(`Navigates if the setting "navigation.disableChildPageNavigation" is set to "false" inside a child page.`, async () => { + it(`Navigates child pages if the setting "navigation.disableChildPageNavigation" is set to "false".`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.resolve(({ text: () => Promise.resolve('Test'), @@ -527,7 +615,8 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { navigation: { - disableChildPageNavigation: false + disableChildPageNavigation: false, + disableFallbackToSetURL: true } } }); @@ -541,7 +630,7 @@ describe('BrowserFrame', () => { expect(childPage.mainFrame.window === oldWindow).toBe(false); }); - it(`Doesn't navigate if the setting "navigation.disableChildPageNavigation" is set to "true" inside a main page.`, async () => { + it(`Doesn't navigate child pages if the setting "navigation.disableChildPageNavigation" is set to "true".`, async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { return Promise.reject(new Error('Should not be called.')); }); @@ -549,18 +638,66 @@ describe('BrowserFrame', () => { const browser = new Browser({ settings: { navigation: { - disableChildPageNavigation: true + disableChildPageNavigation: true, + disableFallbackToSetURL: true } } }); const page = browser.newPage(); - const oldWindow = page.mainFrame.window; + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; - const response = await page.mainFrame.goto('http://localhost:9999'); + const response = await childPage.mainFrame.goto('http://localhost:9999'); expect(response).toBeNull(); + expect(childPage.mainFrame.url).toBe('about:blank'); + expect(childPage.mainFrame.window === oldWindow).toBe(true); + }); + + it(`Doesn't navigate the main frame if the setting "navigation.disableMainFrameNavigation" is set to "true".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new Browser({ + settings: { + navigation: { + disableMainFrameNavigation: true, + disableFallbackToSetURL: true + } + } + }); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + + await page.mainFrame.goto('http://localhost:9999'); + expect(page.mainFrame.url).toBe('about:blank'); expect(page.mainFrame.window === oldWindow).toBe(true); }); + + it(`Navigates the main frame if the setting "navigation.disableMainFrameNavigation" is set to "false".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const browser = new Browser({ + settings: { + navigation: { + disableMainFrameNavigation: false, + disableFallbackToSetURL: true + } + } + }); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + + await page.mainFrame.goto('http://localhost:9999'); + + expect(page.mainFrame.url).toBe('http://localhost:9999/'); + expect(page.mainFrame.window === oldWindow).toBe(false); + }); }); }); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts new file mode 100644 index 000000000..158613b1b --- /dev/null +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts @@ -0,0 +1,747 @@ +import { Script } from 'vm'; +import DetachedBrowser from '../../../src/browser/detached-browser/DetachedBrowser'; +import Event from '../../../src/event/Event'; +import Window from '../../../src/window/Window'; +import IRequest from '../../../src/fetch/types/IRequest'; +import IResponse from '../../../src/fetch/types/IResponse'; +import { describe, it, expect, afterEach, vi } from 'vitest'; +import Fetch from '../../../src/fetch/Fetch'; +import DOMException from '../../../src/exception/DOMException'; +import DOMExceptionNameEnum from '../../../src/exception/DOMExceptionNameEnum'; +import BrowserNavigationCrossOriginPolicyEnum from '../../../src/browser/enums/BrowserNavigationCrossOriginPolicyEnum'; +import BrowserFrameFactory from '../../../src/browser/utilities/BrowserFrameFactory'; + +describe('DetachedBrowserFrame', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('get childFrames()', () => { + it('Returns child frames.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.pages[0]; + expect(page.mainFrame.childFrames).toEqual([]); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); + expect(page.mainFrame.childFrames).toEqual([frame1, frame2]); + }); + }); + + describe('get parentFrame()', () => { + it('Returns the parent frame.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.pages[0]; + expect(page.mainFrame.parentFrame).toBe(null); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(frame1); + expect(frame2.parentFrame).toBe(frame1); + expect(frame1.parentFrame).toBe(page.mainFrame); + expect(page.mainFrame.parentFrame).toBe(null); + }); + }); + + describe('get page()', () => { + it('Returns the page.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.pages[0]; + expect(page.mainFrame.page).toBe(page); + }); + }); + + describe('get window()', () => { + it('Returns the window.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + expect(page.mainFrame.window).toBeInstanceOf(Window); + expect(page.mainFrame.window.console).toBe(page.console); + }); + }); + + describe('get content()', () => { + it('Returns the document HTML content.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.pages[0]; + page.mainFrame.window.document.write('
test
'); + expect(page.content).toBe('
test
'); + }); + }); + + describe('set content()', () => { + it('Sets the document HTML content.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.pages[0]; + page.mainFrame.content = '
test
'; + expect(page.mainFrame.window.document.documentElement.outerHTML).toBe( + '
test
' + ); + }); + + it('Removes listeners and child nodes before setting the document HTML content.', () => { + const browser = new DetachedBrowser(Window, new Window(), { + settings: { disableErrorCapturing: true } + }); + const page = browser.defaultContext.pages[0]; + page.mainFrame.content = '
test
'; + page.mainFrame.window.document.addEventListener('load', () => { + throw new Error('Should not be called'); + }); + page.mainFrame.window.document.addEventListener('error', () => { + throw new Error('Should not be called'); + }); + page.mainFrame.content = '
test
'; + page.mainFrame.window.document.dispatchEvent(new Event('load')); + page.mainFrame.window.document.dispatchEvent(new Event('error')); + expect(page.mainFrame.window.document.documentElement.outerHTML).toBe( + '
test
' + ); + }); + }); + + describe('get url()', () => { + it('Returns the document URL.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.pages[0]; + page.mainFrame.url = 'http://localhost:3000'; + expect(page.mainFrame.url).toBe('http://localhost:3000/'); + }); + }); + + describe('set url()', () => { + it('Sets the document URL.', () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.pages[0]; + const location = page.mainFrame.window.location; + page.mainFrame.url = 'http://localhost:3000'; + expect(page.mainFrame.window.location.href).toBe('http://localhost:3000/'); + expect(page.mainFrame.window.location).not.toBe(location); + }); + }); + + describe('whenComplete()', () => { + it('Waits for all pages to complete.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.pages[0]; + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); + page.mainFrame.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + frame1.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + frame2.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); + await page.whenComplete(); + expect(page.mainFrame.window['test']).toBe(1); + expect(frame1.window['test']).toBe(2); + expect(frame2.window['test']).toBe(3); + }); + }); + + describe('abort()', () => { + it('Aborts all ongoing operations.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); + const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); + page.mainFrame.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); + frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); + page.abort(); + await new Promise((resolve) => setTimeout(resolve, 50)); + expect(page.mainFrame.window['test']).toBeUndefined(); + expect(frame1.window['test']).toBeUndefined(); + expect(frame2.window['test']).toBeUndefined(); + }); + }); + + describe('evaluate()', () => { + it("Evaluates a code string in the frame's context.", () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.pages[0]; + expect(page.mainFrame.evaluate('globalThis.test = 1')).toBe(1); + expect(page.mainFrame.window['test']).toBe(1); + }); + + it("Evaluates a VM script in the frame's context.", () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.pages[0]; + expect(page.mainFrame.evaluate(new Script('globalThis.test = 1'))).toBe(1); + expect(page.mainFrame.window['test']).toBe(1); + }); + }); + + describe('goto()', () => { + it('Navigates to a URL.', async () => { + let request: IRequest | null = null; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + url: request?.url, + text: () => + new Promise((resolve) => setTimeout(() => resolve('Test'), 1)) + }); + }); + + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.newPage(); + const oldWindow = page.mainFrame.window; + const response = await page.mainFrame.goto('http://localhost:3000', { + referrer: 'http://localhost:3000/referrer', + referrerPolicy: 'no-referrer-when-downgrade' + }); + + expect((response).url).toBe('http://localhost:3000/'); + expect(((request)).referrer).toBe('http://localhost:3000/referrer'); + expect(((request)).referrerPolicy).toBe('no-referrer-when-downgrade'); + expect(page.mainFrame.url).toBe('http://localhost:3000/'); + expect(page.mainFrame.window).not.toBe(oldWindow); + expect(oldWindow.location.href).toBe('about:blank'); + expect(page.mainFrame.window.location.href).toBe('http://localhost:3000/'); + expect(page.mainFrame.window.document.body.innerHTML).toBe('Test'); + }); + + it('Navigates to a URL with "javascript:" as protocol.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.defaultContext.pages[0]; + const oldWindow = page.mainFrame.window; + const response = await page.mainFrame.goto('javascript:document.write("test");'); + + expect(response).toBeNull(); + expect(page.mainFrame.url).toBe('about:blank'); + expect(page.mainFrame.window).toBe(oldWindow); + + expect(page.mainFrame.window.document.body.innerHTML).toBe('test'); + }); + + it('Navigates to a URL with "about:" as protocol.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + const response = await page.mainFrame.goto('about:blank'); + + expect(response).toBeNull(); + expect(page.mainFrame.url).toBe('about:blank'); + expect(page.mainFrame.window).not.toBe(oldWindow); + }); + + it('Aborts request if it times out.', async () => { + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + let error: Error | null = null; + try { + await page.mainFrame.goto('http://localhost:9999', { + timeout: 1 + }); + } catch (e) { + error = e; + } + + expect(error).toEqual( + new DOMException( + 'The operation was aborted. Request timed out.', + DOMExceptionNameEnum.abortError + ) + ); + + expect(page.mainFrame.url).toBe('http://localhost:9999/'); + expect(page.mainFrame.window).not.toBe(oldWindow); + expect(page.mainFrame.window.document.body.innerHTML).toBe(''); + }); + + it('Handles error status code in response.', async () => { + let request: IRequest | null = null; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + url: request?.url, + status: 404, + statusText: 'Not Found', + text: () => + new Promise((resolve) => + setTimeout(() => resolve('404 error'), 1) + ) + }); + }); + + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + const response = await page.mainFrame.goto('http://localhost:3000'); + + expect(page.mainFrame.url).toBe('http://localhost:3000/'); + expect(page.mainFrame.window).not.toBe(oldWindow); + expect(page.mainFrame.window.location.href).toBe('http://localhost:3000/'); + expect(page.mainFrame.window.document.body.innerHTML).toBe('404 error'); + + expect(((response)).status).toBe(404); + expect(page.virtualConsolePrinter.readAsString()).toBe( + 'GET http://localhost:3000/ 404 (Not Found)\n' + ); + }); + + it('Handles reject when performing fetch.', async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Error')); + }); + + const browser = new DetachedBrowser(Window, new Window()); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + let error: Error | null = null; + try { + await page.mainFrame.goto('http://localhost:9999'); + } catch (e) { + error = e; + } + + expect(error).toEqual(new Error('Error')); + + expect(page.mainFrame.url).toBe('http://localhost:9999/'); + expect(page.mainFrame.window).not.toBe(oldWindow); + expect(page.mainFrame.window.document.body.innerHTML).toBe(''); + }); + + it(`Doesn't navigate a child frame with a different origin from its parent if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + } + } + }); + const page = browser.defaultContext.pages[0]; + const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); + const oldWindow = childFrame.window; + + page.mainFrame.url = 'https://github.com'; + + const response = await childFrame.goto('http://localhost:9999'); + + expect(response).toBeNull(); + expect(childFrame.url).toBe('about:blank'); + expect(childFrame.window === oldWindow).toBe(true); + }); + + it(`Navigates a child frame with the same origin as its parent if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + } + } + }); + const page = browser.defaultContext.pages[0]; + const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); + const oldWindow = childFrame.window; + + page.mainFrame.url = 'https://github.com'; + + await childFrame.goto('https://github.com/capricorn86/happy-dom'); + + expect(childFrame.url).toBe('https://github.com/capricorn86/happy-dom'); + expect(childFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate a popup with a different origin from its parent if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + } + } + }); + const page = browser.defaultContext.pages[0]; + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + page.mainFrame.url = 'https://github.com'; + + const response = await childPage.mainFrame.goto('http://localhost:9999'); + + expect(response).toBeNull(); + expect(childPage.mainFrame.url).toBe('about:blank'); + expect(childPage.mainFrame.window === oldWindow).toBe(true); + }); + + it(`Navigates a popup with the same origin as its parent if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + } + } + }); + const page = browser.defaultContext.pages[0]; + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + page.mainFrame.url = 'https://github.com'; + + await childPage.mainFrame.goto('https://github.com/capricorn86/happy-dom'); + + expect(childPage.mainFrame.url).toBe('https://github.com/capricorn86/happy-dom'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate from one origin to another if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.sameOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin + } + } + }); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + + page.mainFrame.url = 'https://github.com'; + + const response = await page.mainFrame.goto('http://localhost:9999'); + + expect(response).toBeNull(); + expect(page.mainFrame.url).toBe('https://github.com/'); + expect(page.mainFrame.window === oldWindow).toBe(true); + }); + + it(`Navigates from "http" to "https" if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.strictOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin + } + } + }); + const page = browser.defaultContext.pages[0]; + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + page.mainFrame.url = 'http://github.com'; + + await childPage.mainFrame.goto('https://github.com/capricorn86/happy-dom'); + + expect(childPage.mainFrame.url).toBe('https://github.com/capricorn86/happy-dom'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Navigates from "https" to "https" if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.strictOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin + } + } + }); + const page = browser.defaultContext.pages[0]; + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + page.mainFrame.url = 'https://github.com'; + + await childPage.mainFrame.goto('https://github.com/capricorn86/happy-dom'); + + expect(childPage.mainFrame.url).toBe('https://github.com/capricorn86/happy-dom'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Navigates from "about" to "http" if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.strictOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin + } + } + }); + const page = browser.defaultContext.pages[0]; + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + await childPage.mainFrame.goto('http://github.com/capricorn86/happy-dom'); + + expect(childPage.mainFrame.url).toBe('http://github.com/capricorn86/happy-dom'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Navigates from "https" to "about" if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.strictOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin + } + } + }); + const page = browser.defaultContext.pages[0]; + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + page.mainFrame.url = 'https://github.com'; + + await childPage.mainFrame.goto('about:blank'); + + expect(childPage.mainFrame.url).toBe('about:blank'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate from "https" to "http" if the setting "navigation.crossOriginPolicy" is set to "${BrowserNavigationCrossOriginPolicyEnum.strictOrigin}".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin + } + } + }); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + + page.mainFrame.url = 'https://github.com'; + + const response = await page.mainFrame.goto('http://github.com/capricorn86/happy-dom'); + + expect(response).toBeNull(); + expect(page.mainFrame.url).toBe('https://github.com/'); + expect(page.mainFrame.window === oldWindow).toBe(true); + }); + + it(`Navigates child frames if the setting "navigation.disableChildFrameNavigation" is set to "false".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + disableChildFrameNavigation: false, + disableFallbackToSetURL: true + } + } + }); + const page = browser.defaultContext.pages[0]; + const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); + const oldWindow = childFrame.window; + + await childFrame.goto('http://localhost:9999'); + + expect(childFrame.url).toBe('http://localhost:9999/'); + expect(childFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate child frames if the setting "navigation.disableChildFrameNavigation" is set to "true".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + disableChildFrameNavigation: true, + disableFallbackToSetURL: true + } + } + }); + const page = browser.defaultContext.pages[0]; + const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); + const oldWindow = childFrame.window; + + const response = await childFrame.goto('http://localhost:9999'); + + expect(response).toBeNull(); + expect(childFrame.url).toBe('about:blank'); + expect(childFrame.window === oldWindow).toBe(true); + }); + + it(`Navigates child pages if the setting "navigation.disableChildPageNavigation" is set to "false".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve(({ + text: () => Promise.resolve('Test'), + headers: new Headers() + })); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + disableChildPageNavigation: false, + disableFallbackToSetURL: true + } + } + }); + const page = browser.defaultContext.pages[0]; + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + await childPage.mainFrame.goto('http://localhost:9999'); + + expect(childPage.mainFrame.url).toBe('http://localhost:9999/'); + expect(childPage.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate child pages if the setting "navigation.disableChildPageNavigation" is set to "true".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + disableChildPageNavigation: true, + disableFallbackToSetURL: true + } + } + }); + const page = browser.defaultContext.pages[0]; + const childPage = browser.newPage(page.mainFrame); + const oldWindow = childPage.mainFrame.window; + + const response = await childPage.mainFrame.goto('http://localhost:9999'); + + expect(response).toBeNull(); + expect(childPage.mainFrame.url).toBe('about:blank'); + expect(childPage.mainFrame.window === oldWindow).toBe(true); + }); + + it(`Doesn't navigate the main frame if the setting "navigation.disableMainFrameNavigation" is set to "true".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + disableMainFrameNavigation: true, + disableFallbackToSetURL: true + } + } + }); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + + await page.mainFrame.goto('http://localhost:9999'); + + expect(page.mainFrame.url).toBe('about:blank'); + expect(page.mainFrame.window === oldWindow).toBe(true); + }); + + it(`Navigates the main frame if the setting "navigation.disableMainFrameNavigation" is set to "false".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + disableMainFrameNavigation: false, + disableFallbackToSetURL: true + } + } + }); + const page = browser.newPage(); + const oldWindow = page.mainFrame.window; + + await page.mainFrame.goto('http://localhost:9999'); + + expect(page.mainFrame.url).toBe('http://localhost:9999/'); + expect(page.mainFrame.window === oldWindow).toBe(false); + }); + + it(`Doesn't navigate the main page frame of a detached browser.`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + disableFallbackToSetURL: true + } + } + }); + const page = browser.defaultContext.pages[0]; + const oldWindow = page.mainFrame.window; + + await page.mainFrame.goto('http://localhost:9999'); + + expect(page.mainFrame.url).toBe('about:blank'); + expect(page.mainFrame.window === oldWindow).toBe(true); + }); + + it(`Sets URL when the main page frame of a detached browser if "disableFallbackToSetURL" is set to "false".`, async () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Should not be called.')); + }); + + const browser = new DetachedBrowser(Window, new Window(), { + settings: { + navigation: { + disableFallbackToSetURL: false + } + } + }); + const page = browser.defaultContext.pages[0]; + const oldWindow = page.mainFrame.window; + + await page.mainFrame.goto('http://localhost:9999'); + + expect(page.mainFrame.url).toBe('http://localhost:9999/'); + expect(page.mainFrame.window === oldWindow).toBe(true); + }); + }); +}); From 791099688c610ccc60da7c6bcf9e1fc2cd76755e Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 23 Nov 2023 00:50:21 +0100 Subject: [PATCH 29/63] #466@trivial: Continues on implementation. --- packages/happy-dom/src/browser/Browser.ts | 9 +- .../happy-dom/src/browser/BrowserContext.ts | 6 +- .../DetachedBrowserContext.ts | 3 + .../src/browser/types/IBrowserContext.ts | 2 + packages/happy-dom/src/cookie/Cookie.ts | 125 ---------- .../happy-dom/src/cookie/CookieContainer.ts | 70 ++++++ packages/happy-dom/src/cookie/CookieJar.ts | 81 ------- .../cookie/{ => enums}/CookieSameSiteEnum.ts | 0 .../happy-dom/src/cookie/types/ICookie.ts | 16 ++ .../src/cookie/types/ICookieContainer.ts | 26 +++ .../cookie/urilities/CookieExpireUtility.ts | 16 ++ .../cookie/urilities/CookieStringUtility.ts | 116 ++++++++++ .../src/cookie/urilities/CookieURLUtility.ts | 26 +++ packages/happy-dom/src/fetch/Fetch.ts | 29 ++- .../happy-dom/src/nodes/document/Document.ts | 21 +- packages/happy-dom/src/window/Window.ts | 1 + .../src/window/WindowClassFactory.ts | 62 +++++ .../test/cookie/CookieContainer.test.ts | 217 ++++++++++++++++++ .../happy-dom/test/cookie/CookieJar.test.ts | 117 ---------- 19 files changed, 601 insertions(+), 342 deletions(-) delete mode 100644 packages/happy-dom/src/cookie/Cookie.ts create mode 100644 packages/happy-dom/src/cookie/CookieContainer.ts delete mode 100644 packages/happy-dom/src/cookie/CookieJar.ts rename packages/happy-dom/src/cookie/{ => enums}/CookieSameSiteEnum.ts (100%) create mode 100644 packages/happy-dom/src/cookie/types/ICookie.ts create mode 100644 packages/happy-dom/src/cookie/types/ICookieContainer.ts create mode 100644 packages/happy-dom/src/cookie/urilities/CookieExpireUtility.ts create mode 100644 packages/happy-dom/src/cookie/urilities/CookieStringUtility.ts create mode 100644 packages/happy-dom/src/cookie/urilities/CookieURLUtility.ts create mode 100644 packages/happy-dom/test/cookie/CookieContainer.test.ts delete mode 100644 packages/happy-dom/test/cookie/CookieJar.test.ts diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index e31131b22..6b17c337a 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -5,6 +5,7 @@ import BrowserSettingsFactory from './BrowserSettingsFactory.js'; import BrowserPage from './BrowserPage.js'; import IBrowser from './types/IBrowser.js'; import BrowserFrame from './BrowserFrame.js'; +import ICookieContainer from '../cookie/types/ICookieContainer.js'; /** * Browser. @@ -23,10 +24,14 @@ export default class Browser implements IBrowser { * @param [options.settings] Browser settings. * @param [options.console] Console. */ - constructor(options?: { settings?: IOptionalBrowserSettings; console?: Console }) { + constructor(options?: { + settings?: IOptionalBrowserSettings; + console?: Console; + cookieContainer?: ICookieContainer; + }) { this.console = options?.console || null; this.settings = BrowserSettingsFactory.getSettings(options?.settings); - this.contexts = [new BrowserContext(this)]; + this.contexts = [new BrowserContext(this, { cookieContainer: options?.cookieContainer })]; } /** diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index 73cbfae76..57f4648a6 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -1,3 +1,5 @@ +import CookieContainer from '../cookie/CookieContainer.js'; +import ICookieContainer from '../cookie/types/ICookieContainer.js'; import Browser from './Browser.js'; import BrowserFrame from './BrowserFrame.js'; import BrowserPage from './BrowserPage.js'; @@ -9,14 +11,16 @@ import IBrowserContext from './types/IBrowserContext.js'; export default class BrowserContext implements IBrowserContext { public readonly pages: BrowserPage[] = []; public readonly browser: Browser; + public readonly cookieContainer: ICookieContainer; /** * Constructor. * * @param browser */ - constructor(browser: Browser) { + constructor(browser: Browser, options?: { cookieContainer?: ICookieContainer }) { this.browser = browser; + this.cookieContainer = options?.cookieContainer ?? new CookieContainer(); } /** diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts index 6fd76b94d..cc3bb0eae 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts @@ -2,6 +2,8 @@ import DetachedBrowser from './DetachedBrowser.js'; import DetachedBrowserPage from './DetachedBrowserPage.js'; import IBrowserContext from '../types/IBrowserContext.js'; import DetachedBrowserFrame from './DetachedBrowserFrame.js'; +import ICookieContainer from '../../cookie/types/ICookieContainer.js'; +import CookieContainer from '../../cookie/CookieContainer.js'; /** * Detached browser context used when constructing a Window instance without a browser. @@ -9,6 +11,7 @@ import DetachedBrowserFrame from './DetachedBrowserFrame.js'; export default class DetachedBrowserContext implements IBrowserContext { public readonly pages: DetachedBrowserPage[]; public readonly browser: DetachedBrowser; + public readonly cookieContainer: ICookieContainer = new CookieContainer(); /** * Constructor. diff --git a/packages/happy-dom/src/browser/types/IBrowserContext.ts b/packages/happy-dom/src/browser/types/IBrowserContext.ts index a0a1669dd..b3027978d 100644 --- a/packages/happy-dom/src/browser/types/IBrowserContext.ts +++ b/packages/happy-dom/src/browser/types/IBrowserContext.ts @@ -1,3 +1,4 @@ +import ICookieContainer from '../../cookie/types/ICookieContainer.js'; import IBrowser from './IBrowser.js'; import IBrowserFrame from './IBrowserFrame.js'; import IBrowserPage from './IBrowserPage.js'; @@ -8,6 +9,7 @@ import IBrowserPage from './IBrowserPage.js'; export default interface IBrowserContext { readonly pages: IBrowserPage[]; readonly browser: IBrowser; + readonly cookieContainer: ICookieContainer; /** * Aborts all ongoing operations and destroys the context. diff --git a/packages/happy-dom/src/cookie/Cookie.ts b/packages/happy-dom/src/cookie/Cookie.ts deleted file mode 100644 index c06d5b23c..000000000 --- a/packages/happy-dom/src/cookie/Cookie.ts +++ /dev/null @@ -1,125 +0,0 @@ -import DOMException from '../exception/DOMException.js'; -import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; -import CookieSameSiteEnum from './CookieSameSiteEnum.js'; -import URL from '../url/URL.js'; - -/** - * Cookie. - */ -export default class Cookie { - // Required - public key = ''; - public value: string | null = null; - public originURL: URL; - - // Optional - public domain = ''; - public path = ''; - public expires: Date | null = null; - public httpOnly = false; - public secure = false; - public sameSite: CookieSameSiteEnum = CookieSameSiteEnum.lax; - - /** - * Constructor. - * - * @param originURL Origin URL. - * @param cookie Cookie. - */ - constructor(originURL, cookie: string) { - const parts = cookie.split(';'); - const [key, value] = parts.shift().split('='); - - this.originURL = originURL; - this.key = key.trim(); - this.value = value !== undefined ? value : null; - - if (!this.key) { - throw new DOMException(`Invalid cookie: ${cookie}.`, DOMExceptionNameEnum.syntaxError); - } - - for (const part of parts) { - const keyAndValue = part.split('='); - const key = keyAndValue[0].trim().toLowerCase(); - const value = keyAndValue[1]; - - switch (key) { - case 'expires': - this.expires = new Date(value); - break; - case 'max-age': - this.expires = new Date(parseInt(value, 10) * 1000 + Date.now()); - break; - case 'domain': - this.domain = value; - break; - case 'path': - this.path = value.startsWith('/') ? value : `/${value}`; - break; - case 'httponly': - this.httpOnly = true; - break; - case 'secure': - this.secure = true; - break; - case 'samesite': - switch (value.toLowerCase()) { - case 'strict': - this.sameSite = CookieSameSiteEnum.strict; - break; - case 'lax': - this.sameSite = CookieSameSiteEnum.lax; - break; - case 'none': - this.sameSite = CookieSameSiteEnum.none; - } - break; - } - } - } - - /** - * Returns cookie string. - * - * @returns Cookie string. - */ - public toString(): string { - if (this.value !== null) { - return `${this.key}=${this.value}`; - } - - return this.key; - } - - /** - * Returns "true" if expired. - * - * @returns "true" if expired. - */ - public isExpired(): boolean { - // If the expries/maxage is set, then determine whether it is expired. - if (this.expires && this.expires.getTime() < Date.now()) { - return true; - } - // If the expries/maxage is not set, it's a session-level cookie that will expire when the browser is closed. - // (it's never expired in happy-dom) - return false; - } - - /** - * Validate cookie. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#cookie_prefixes - * @returns "true" if valid. - */ - public validate(): boolean { - const lowerKey = this.key.toLowerCase(); - if (lowerKey.startsWith('__secure-') && !this.secure) { - return false; - } - if (lowerKey.startsWith('__host-') && (!this.secure || this.path !== '/' || this.domain)) { - return false; - } - return true; - } -} diff --git a/packages/happy-dom/src/cookie/CookieContainer.ts b/packages/happy-dom/src/cookie/CookieContainer.ts new file mode 100644 index 000000000..428c48514 --- /dev/null +++ b/packages/happy-dom/src/cookie/CookieContainer.ts @@ -0,0 +1,70 @@ +import URL from '../url/URL.js'; +import ICookie from './types/ICookie.js'; +import ICookieContainer from './types/ICookieContainer.js'; +import CookieExpireUtility from './urilities/CookieExpireUtility.js'; +import CookieURLUtility from './urilities/CookieURLUtility.js'; + +/** + * Cookie Container. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie. + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie. + */ +export default class CookieContainer implements ICookieContainer { + #cookies: ICookie[] = []; + + /** + * Adds cookies. + * + * @param cookies Cookies. + */ + public addCookies(cookies: ICookie[]): void { + const indexMap: { [k: string]: number } = {}; + const getKey = (cookie: ICookie) => + `${cookie.key}-${cookie.originURL.hostname}-${ + cookie.originURL.pathname + }-${typeof cookie.value}`; + + // Creates a map of cookie key, domain, path and value to index. + for (let i = 0, max = this.#cookies.length; i < max; i++) { + indexMap[getKey(this.#cookies[i])] = i; + } + + for (const cookie of cookies) { + // Remove existing cookie with same name, domain and path. + const index = indexMap[getKey(cookie)]; + + if (index !== undefined) { + this.#cookies.splice(index, 1); + } + + if (!CookieExpireUtility.hasExpired(cookie)) { + indexMap[getKey(cookie)] = this.#cookies.length; + this.#cookies.push(cookie); + } + } + } + + /** + * Returns cookies. + * + * @param [url] URL. + * @param [httpOnly] "true" if only http cookies should be returned. + * @returns Cookies. + */ + public getCookies(url: URL | null = null, httpOnly = false): ICookie[] { + const cookies = []; + + for (const cookie of this.#cookies) { + if ( + !CookieExpireUtility.hasExpired(cookie) && + (!httpOnly || !cookie.httpOnly) && + (!url || CookieURLUtility.cookieMatchesURL(cookie, url || cookie.originURL)) + ) { + cookies.push(cookie); + } + } + + return cookies; + } +} diff --git a/packages/happy-dom/src/cookie/CookieJar.ts b/packages/happy-dom/src/cookie/CookieJar.ts deleted file mode 100644 index 389e93d41..000000000 --- a/packages/happy-dom/src/cookie/CookieJar.ts +++ /dev/null @@ -1,81 +0,0 @@ -import Cookie from './Cookie.js'; -import CookieSameSiteEnum from './CookieSameSiteEnum.js'; -import URL from '../url/URL.js'; - -/** - * CookieJar. - * - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie. - * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie. - */ -export default class CookieJar { - private cookies: Cookie[] = []; - - /** - * Adds cookie string. - * - * @param originURL Origin URL. - * @param cookieString Cookie string. - */ - public addCookieString(originURL: URL, cookieString: string): void { - if (!cookieString) { - return; - } - - const newCookie = new Cookie(originURL, cookieString); - - if (!newCookie.validate()) { - return; - } - - for (let i = 0, max = this.cookies.length; i < max; i++) { - if ( - this.cookies[i].key === newCookie.key && - this.cookies[i].originURL.hostname === newCookie.originURL.hostname && - // Cookies with or without values are treated differently in the browser. - // Therefore, the cookie should only be replaced if either both has a value or if both has no value. - // The cookie value is null if it has no value set. - // This is a bit unlogical, so it would be nice with a link to the spec here. - typeof this.cookies[i].value === typeof newCookie.value - ) { - this.cookies.splice(i, 1); - break; - } - } - - if (!newCookie.isExpired()) { - this.cookies.push(newCookie); - } - } - - /** - * Get cookie string. - * - * @param targetURL Target URL. - * @param fromDocument If true, the caller is a document. - * @returns Cookie string. - */ - public getCookieString(targetURL: URL, fromDocument: boolean): string { - let cookieString = ''; - - for (const cookie of this.cookies) { - if ( - (!fromDocument || !cookie.httpOnly) && - !cookie.isExpired() && - (!cookie.secure || targetURL.protocol === 'https:') && - (!cookie.domain || targetURL.hostname.endsWith(cookie.domain)) && - (!cookie.path || targetURL.pathname.startsWith(cookie.path)) && - // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value - ((cookie.sameSite === CookieSameSiteEnum.none && cookie.secure) || - cookie.originURL.hostname === targetURL.hostname) - ) { - if (cookieString) { - cookieString += '; '; - } - cookieString += cookie.toString(); - } - } - - return cookieString; - } -} diff --git a/packages/happy-dom/src/cookie/CookieSameSiteEnum.ts b/packages/happy-dom/src/cookie/enums/CookieSameSiteEnum.ts similarity index 100% rename from packages/happy-dom/src/cookie/CookieSameSiteEnum.ts rename to packages/happy-dom/src/cookie/enums/CookieSameSiteEnum.ts diff --git a/packages/happy-dom/src/cookie/types/ICookie.ts b/packages/happy-dom/src/cookie/types/ICookie.ts new file mode 100644 index 000000000..35bbea971 --- /dev/null +++ b/packages/happy-dom/src/cookie/types/ICookie.ts @@ -0,0 +1,16 @@ +import CookieSameSiteEnum from '../enums/CookieSameSiteEnum.js'; + +export default interface ICookie { + // Required + key: string; + value: string | null; + originURL: URL; + + // Optional + domain: string; + path: string; + expires: Date | null; + httpOnly: boolean; + secure: boolean; + sameSite: CookieSameSiteEnum; +} diff --git a/packages/happy-dom/src/cookie/types/ICookieContainer.ts b/packages/happy-dom/src/cookie/types/ICookieContainer.ts new file mode 100644 index 000000000..3fb79eefa --- /dev/null +++ b/packages/happy-dom/src/cookie/types/ICookieContainer.ts @@ -0,0 +1,26 @@ +import URL from '../../url/URL.js'; +import ICookie from '../types/ICookie.js'; + +/** + * Cookie Container. + * + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie. + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie. + */ +export default interface ICookieContainer { + /** + * Adds cookies. + * + * @param cookies Cookies. + */ + addCookies(cookies: ICookie[]): void; + + /** + * Returns cookies. + * + * @param [url] URL. + * @param [httpOnly] "true" if only http cookies should be returned. + * @returns Cookies. + */ + getCookies(url: URL | null, httpOnly: boolean): ICookie[]; +} diff --git a/packages/happy-dom/src/cookie/urilities/CookieExpireUtility.ts b/packages/happy-dom/src/cookie/urilities/CookieExpireUtility.ts new file mode 100644 index 000000000..835d5f48d --- /dev/null +++ b/packages/happy-dom/src/cookie/urilities/CookieExpireUtility.ts @@ -0,0 +1,16 @@ +import ICookie from '../types/ICookie.js'; + +/** + * Cookie expire utility. + */ +export default class CookieExpireUtility { + /** + * Returns "true" if cookie has expired. + * + * @param cookie Cookie. + * @returns "true" if cookie has expired. + */ + public static hasExpired(cookie: ICookie): boolean { + return cookie.expires && cookie.expires.getTime() < Date.now(); + } +} diff --git a/packages/happy-dom/src/cookie/urilities/CookieStringUtility.ts b/packages/happy-dom/src/cookie/urilities/CookieStringUtility.ts new file mode 100644 index 000000000..2d6fe0c7e --- /dev/null +++ b/packages/happy-dom/src/cookie/urilities/CookieStringUtility.ts @@ -0,0 +1,116 @@ +import CookieSameSiteEnum from '../enums/CookieSameSiteEnum.js'; +import URL from '../../url/URL.js'; +import ICookie from '../types/ICookie.js'; + +/** + * Cookie string. + */ +export default class CookieStringUtility { + /** + * Returns cookie. + * + * @param originURL Origin URL. + * @param cookieString Cookie string. + * @returns Cookie. + */ + public static stringToCookie(originURL: URL, cookieString: string): ICookie | null { + const parts = cookieString.split(';'); + const [key, value] = parts.shift().split('='); + + const cookie: ICookie = { + // Required + key: key.trim(), + value: value || null, + originURL, + + // Optional + domain: '', + path: '', + expires: null, + httpOnly: false, + secure: false, + sameSite: CookieSameSiteEnum.lax + }; + + // Invalid if key is empty. + if (!cookie.key) { + return null; + } + + for (const part of parts) { + const keyAndValue = part.split('='); + const key = keyAndValue[0].trim().toLowerCase(); + const value = keyAndValue[1]; + + switch (key) { + case 'expires': + cookie.expires = new Date(value); + break; + case 'max-age': + cookie.expires = new Date(parseInt(value, 10) * 1000 + Date.now()); + break; + case 'domain': + cookie.domain = value; + break; + case 'path': + cookie.path = value.startsWith('/') ? value : `/${value}`; + break; + case 'httponly': + cookie.httpOnly = true; + break; + case 'secure': + cookie.secure = true; + break; + case 'samesite': + switch (value.toLowerCase()) { + case 'strict': + cookie.sameSite = CookieSameSiteEnum.strict; + break; + case 'lax': + cookie.sameSite = CookieSameSiteEnum.lax; + break; + case 'none': + cookie.sameSite = CookieSameSiteEnum.none; + } + break; + } + } + + const lowerKey = cookie.key.toLowerCase(); + + // Invalid if __secure- prefix is used and cookie is not secure. + if (lowerKey.startsWith('__secure-') && !cookie.secure) { + return null; + } + + // Invalid if __host- prefix is used and cookie is not secure, not on root path or has a domain. + if ( + lowerKey.startsWith('__host-') && + (!cookie.secure || cookie.path !== '/' || cookie.domain) + ) { + return null; + } + + return cookie; + } + + /** + * Returns cookie string with key and value. + * + * @param cookies Cookies. + * @returns Cookie string. + */ + public static cookiesToString(cookies: ICookie[]): string { + const cookieString: string[] = []; + + for (const cookie of cookies) { + if (cookie.value !== null) { + cookieString.push(`${cookie.key}=${cookie.value}`); + } else { + cookieString.push(cookie.key); + } + } + + return cookieString.join('; '); + } +} diff --git a/packages/happy-dom/src/cookie/urilities/CookieURLUtility.ts b/packages/happy-dom/src/cookie/urilities/CookieURLUtility.ts new file mode 100644 index 000000000..efc4dda8f --- /dev/null +++ b/packages/happy-dom/src/cookie/urilities/CookieURLUtility.ts @@ -0,0 +1,26 @@ +import CookieSameSiteEnum from '../enums/CookieSameSiteEnum.js'; +import URL from '../../url/URL.js'; +import ICookie from '../types/ICookie.js'; + +/** + * Cookie string. + */ +export default class CookieURLUtility { + /** + * Returns "true" if cookie matches URL. + * + * @param cookie Cookie. + * @param url URL. + * @returns "true" if cookie matches URL. + */ + public static cookieMatchesURL(cookie: ICookie, url: URL): boolean { + return ( + (!cookie.secure || url.protocol === 'https:') && + (!cookie.domain || url.hostname.endsWith(cookie.domain)) && + (!cookie.path || url.pathname.startsWith(cookie.path)) && + // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + ((cookie.sameSite === CookieSameSiteEnum.none && cookie.secure) || + cookie.originURL.hostname === url.hostname) + ); + } +} diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index 24aaac02e..07c7aad99 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -17,8 +17,10 @@ import FetchCORSUtility from './utilities/FetchCORSUtility.js'; import Request from './Request.js'; import Response from './Response.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; -import CookieJar from '../cookie/CookieJar.js'; +import Event from '../event/Event.js'; import AbortSignal from './AbortSignal.js'; +import IBrowserFrame from '../browser/types/IBrowserFrame.js'; +import CookieStringUtility from '../cookie/urilities/CookieStringUtility.js'; const SUPPORTED_SCHEMAS = ['data:', 'http:', 'https:']; const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; @@ -48,20 +50,22 @@ export default class Fetch { private asyncTaskManager: AsyncTaskManager; private request: Request; private redirectCount = 0; + #browserFrame: IBrowserFrame; /** * Constructor. * * @param options Options. - * @param options.document + * @param options.browserFrame Browser frame. + * @param options.ownerDocument Owner document. * @param options.asyncTaskManager Async task manager. * @param options.url URL. * @param [options.init] Init. - * @param [options.ownerDocument] Owner document. * @param [options.redirectCount] Redirect count. * @param [options.contentType] Content Type. */ constructor(options: { + browserFrame: IBrowserFrame; ownerDocument: IDocument; asyncTaskManager: AsyncTaskManager; url: IRequestInfo; @@ -71,6 +75,7 @@ export default class Fetch { }) { const url = options.url; + this.#browserFrame = options.browserFrame; this.ownerDocument = options.ownerDocument; this.asyncTaskManager = options.asyncTaskManager; this.request = @@ -494,6 +499,7 @@ export default class Fetch { } const fetch = new (this.constructor)({ + browserFrame: this.#browserFrame, ownerDocument: this.ownerDocument, asyncTaskManager: this.asyncTaskManager, url: locationURL, @@ -584,11 +590,12 @@ export default class Fetch { this.request.credentials === 'include' || (this.request.credentials === 'same-origin' && !isCORS) ) { - const cookie = (<{ _cookie: CookieJar }>( - (document._defaultView.document) - ))._cookie.getCookieString(document._defaultView.location, false); - if (cookie) { - headers.set('Cookie', cookie); + const cookies = this.#browserFrame.page.context.cookieContainer.getCookies( + document._defaultView.location, + false + ); + if (cookies.length > 0) { + headers.set('Cookie', CookieStringUtility.cookiesToString(cookies)); } } @@ -643,9 +650,9 @@ export default class Fetch { // Handles setting cookie headers to the document. // "set-cookie" and "set-cookie2" are not allowed in response headers according to spec. if (lowerKey === 'set-cookie' || lowerKey === 'set-cookie2') { - (<{ _cookie: CookieJar }>( - (this.ownerDocument._defaultView.document) - ))._cookie.addCookieString(this.request._url, header); + this.#browserFrame.page.context.cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(this.request._url, header) + ]); } else { headers.append(key, header); } diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 1784f883f..54c306441 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -17,7 +17,6 @@ import QuerySelector from '../../query-selector/QuerySelector.js'; import IDocument from './IDocument.js'; import CSSStyleSheet from '../../css/CSSStyleSheet.js'; import DOMException from '../../exception/DOMException.js'; -import CookieJar from '../../cookie/CookieJar.js'; import IElement from '../element/IElement.js'; import IHTMLScriptElement from '../html-script-element/IHTMLScriptElement.js'; import IHTMLElement from '../html-element/IHTMLElement.js'; @@ -43,6 +42,9 @@ import ElementUtility from '../element/ElementUtility.js'; import HTMLCollection from '../element/HTMLCollection.js'; import VisibilityStateEnum from './VisibilityStateEnum.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; +import ICookieContainer from '../../cookie/types/ICookieContainer.js'; +import CookieContainer from '../../cookie/CookieContainer.js'; +import CookieStringUtility from '../../cookie/urilities/CookieStringUtility.js'; const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; @@ -68,13 +70,12 @@ export default class Document extends Node implements IDocument { // Used as an unique identifier which is updated whenever the DOM gets modified. public _cacheID = 0; - // Public in order to be accessible by the fetch and xhr. - public _cookie = new CookieJar(); protected _isFirstWrite = true; protected _isFirstWriteAfterOpen = false; private _selection: Selection = null; + #cookieContainer: ICookieContainer; // Events public onreadystatechange: (event: Event) => void = null; @@ -291,7 +292,12 @@ export default class Document extends Node implements IDocument { * @returns Cookie. */ public get cookie(): string { - return this._cookie.getCookieString(this._defaultView.location, true); + if (!this.#cookieContainer) { + this.#cookieContainer = new CookieContainer(); + } + return CookieStringUtility.cookiesToString( + this.#cookieContainer.getCookies(this._defaultView.location, true) + ); } /** @@ -300,7 +306,12 @@ export default class Document extends Node implements IDocument { * @param cookie Cookie string. */ public set cookie(cookie: string) { - this._cookie.addCookieString(this._defaultView.location, cookie); + if (!this.#cookieContainer) { + this.#cookieContainer = new CookieContainer(); + } + this.#cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(this._defaultView.location, cookie) + ]); } /** diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index dff05675d..dab74adfa 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -1012,6 +1012,7 @@ export default class Window extends EventTarget implements IWindow { */ public async fetch(url: RequestInfo, init?: IRequestInit): Promise { return await new Fetch({ + browserFrame: this.#browserFrame, ownerDocument: this.document, asyncTaskManager: this.#browserFrame._asyncTaskManager, url, diff --git a/packages/happy-dom/src/window/WindowClassFactory.ts b/packages/happy-dom/src/window/WindowClassFactory.ts index a6e6140de..290e6d839 100644 --- a/packages/happy-dom/src/window/WindowClassFactory.ts +++ b/packages/happy-dom/src/window/WindowClassFactory.ts @@ -51,6 +51,7 @@ import HTMLButtonElementImplementation from '../nodes/html-button-element/HTMLBu import HTMLOptGroupElementImplementation from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; import HTMLOptionElementImplementation from '../nodes/html-option-element/HTMLOptionElement.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; +import CookieStringUtility from '../cookie/urilities/CookieStringUtility.js'; /** * Some classes need to get access to the window object without having a reference to the window in the constructor. @@ -184,15 +185,76 @@ export default class WindowClassFactory { } class Document extends DocumentImplementation { public readonly _defaultView: IWindow = window; + + public get cookie(): string { + return CookieStringUtility.cookiesToString( + properties.browserFrame.page.context.cookieContainer.getCookies( + this._defaultView.location, + true + ) + ); + } + + public set cookie(cookie: string) { + properties.browserFrame.page.context.cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(this._defaultView.location, cookie) + ]); + } } + class HTMLDocument extends HTMLDocumentImplementation { public readonly _defaultView: IWindow = window; + + public get cookie(): string { + return CookieStringUtility.cookiesToString( + properties.browserFrame.page.context.cookieContainer.getCookies( + this._defaultView.location, + true + ) + ); + } + + public set cookie(cookie: string) { + properties.browserFrame.page.context.cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(this._defaultView.location, cookie) + ]); + } } class XMLDocument extends XMLDocumentImplementation { public readonly _defaultView: IWindow = window; + + public get cookie(): string { + return CookieStringUtility.cookiesToString( + properties.browserFrame.page.context.cookieContainer.getCookies( + this._defaultView.location, + true + ) + ); + } + + public set cookie(cookie: string) { + properties.browserFrame.page.context.cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(this._defaultView.location, cookie) + ]); + } } class SVGDocument extends SVGDocumentImplementation { public readonly _defaultView: IWindow = window; + + public get cookie(): string { + return CookieStringUtility.cookiesToString( + properties.browserFrame.page.context.cookieContainer.getCookies( + this._defaultView.location, + true + ) + ); + } + + public set cookie(cookie: string) { + properties.browserFrame.page.context.cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(this._defaultView.location, cookie) + ]); + } } class DocumentType extends DocumentTypeImplementation { public get ownerDocument(): IDocument { diff --git a/packages/happy-dom/test/cookie/CookieContainer.test.ts b/packages/happy-dom/test/cookie/CookieContainer.test.ts new file mode 100644 index 000000000..c3d042640 --- /dev/null +++ b/packages/happy-dom/test/cookie/CookieContainer.test.ts @@ -0,0 +1,217 @@ +import CookieContainer from '../../src/cookie/CookieContainer.js'; +import ICookie from '../../src/cookie/types/ICookie.js'; +import ICookieContainer from '../../src/cookie/types/ICookieContainer.js'; +import CookieStringUtility from '../../src/cookie/urilities/CookieStringUtility.js'; +import URL from '../../src/url/URL.js'; +import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; + +describe('CookieContainer', () => { + let cookieContainer: ICookieContainer; + + beforeEach(() => { + cookieContainer = new CookieContainer(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('addCookies()', () => { + it('Adds cookie string.', () => { + const expires = 60 * 1000 + Date.now(); + const originURL = new URL('https://example.com/path/to/page/'); + const maxAge = 60; + + cookieContainer.addCookies([ + ( + CookieStringUtility.stringToCookie( + originURL, + `key1=value1; Expires=${new Date(expires).toString()};` + ) + ), + CookieStringUtility.stringToCookie(originURL, `key2=value2; Max-Age=${maxAge};`), + CookieStringUtility.stringToCookie(originURL, `key3=value3; Domain=example.com;`), + CookieStringUtility.stringToCookie(originURL, `key4=value4; Domain=other.com;`), + ( + CookieStringUtility.stringToCookie( + originURL, + `key5=value5; Domain=other.com; SameSite=None;` + ) + ), + ( + CookieStringUtility.stringToCookie( + originURL, + `key6=value6; Domain=other.com; SameSite=None; Secure;` + ) + ), + CookieStringUtility.stringToCookie(originURL, `key7=value7; Path=path/to/page/;`), + CookieStringUtility.stringToCookie(originURL, `key8=value8; HttpOnly;`), + CookieStringUtility.stringToCookie(originURL, `key9=value9; Secure;`), + ( + CookieStringUtility.stringToCookie(originURL, `key10=value10; SameSite=None; Secure;`) + ), + CookieStringUtility.stringToCookie(originURL, `key10;`) + ]); + + expect( + CookieStringUtility.cookiesToString( + cookieContainer.getCookies(new URL('https://example.com/path/to/page/'), false) + ) + ).toBe( + 'key1=value1; key2=value2; key3=value3; key7=value7; key8=value8; key9=value9; key10=value10; key10' + ); + + expect( + CookieStringUtility.cookiesToString( + cookieContainer.getCookies(new URL('https://example.com/path/to/page/'), true) + ) + ).toBe( + 'key1=value1; key2=value2; key3=value3; key7=value7; key9=value9; key10=value10; key10' + ); + + expect( + CookieStringUtility.cookiesToString( + cookieContainer.getCookies(new URL('http://example.com/path/to/page/'), false) + ) + ).toBe('key1=value1; key2=value2; key3=value3; key7=value7; key8=value8; key10'); + + expect( + CookieStringUtility.cookiesToString( + cookieContainer.getCookies(new URL('https://other.com/path/to/page/'), false) + ) + ).toBe('key6=value6; key10=value10'); + + cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(originURL, `key10=newValue10`) + ]); + + expect( + CookieStringUtility.cookiesToString( + cookieContainer.getCookies(new URL('https://example.com/path/to/page/'), false) + ) + ).toBe( + 'key1=value1; key2=value2; key3=value3; key7=value7; key8=value8; key9=value9; key10; key10=newValue10' + ); + + expect( + CookieStringUtility.cookiesToString( + cookieContainer.getCookies(new URL('https://other.com/path/to/page/'), false) + ) + ).toBe('key6=value6'); + + vi.spyOn(Date, 'now').mockImplementation(() => expires + 1000); + + expect( + CookieStringUtility.cookiesToString( + cookieContainer.getCookies(new URL('https://example.com/path/to/page/'), false) + ) + ).toBe('key3=value3; key7=value7; key8=value8; key9=value9; key10; key10=newValue10'); + + cookieContainer.addCookies([ + ( + CookieStringUtility.stringToCookie( + originURL, + `key10; Expires=${new Date(expires).toString()};` + ) + ) + ]); + + expect( + CookieStringUtility.cookiesToString( + cookieContainer.getCookies(new URL('https://example.com/path/to/page/'), false) + ) + ).toBe('key3=value3; key7=value7; key8=value8; key9=value9; key10=newValue10'); + + cookieContainer.addCookies([ + ( + CookieStringUtility.stringToCookie( + originURL, + `key10=; Expires=${new Date(expires).toString()};` + ) + ) + ]); + + expect( + CookieStringUtility.cookiesToString( + cookieContainer.getCookies(new URL('https://example.com/path/to/page/'), false) + ) + ).toBe('key3=value3; key7=value7; key8=value8; key9=value9'); + }); + + it('Validates secure cookie keys.', () => { + const originURL = new URL('https://example.com/path/to/page/'); + const targetURL = new URL('https://example.com/path/to/page/'); + + cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(originURL, `__secure-key=value`) + ]); + + expect( + CookieStringUtility.cookiesToString(cookieContainer.getCookies(targetURL, false)) + ).toBe(''); + + cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(originURL, `__secure-key=value; Secure;`) + ]); + + expect( + CookieStringUtility.cookiesToString(cookieContainer.getCookies(targetURL, false)) + ).toBe('__secure-key=value'); + }); + + it('Validates host cookie keys.', () => { + const originURL = new URL('https://example.com/path/to/page/'); + const targetURL = new URL('https://example.com/path/to/page/'); + + cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(originURL, `__host-key=value`) + ]); + + expect( + CookieStringUtility.cookiesToString(cookieContainer.getCookies(targetURL, false)) + ).toBe(''); + + cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(originURL, `__host-key=value; Secure;`) + ]); + + expect( + CookieStringUtility.cookiesToString(cookieContainer.getCookies(targetURL, false)) + ).toBe(''); + + cookieContainer.addCookies([ + ( + CookieStringUtility.stringToCookie( + originURL, + `__host-key=value; Secure; Path=/path/to/page/;` + ) + ) + ]); + + expect( + CookieStringUtility.cookiesToString(cookieContainer.getCookies(targetURL, false)) + ).toBe(''); + + cookieContainer.addCookies([ + ( + CookieStringUtility.stringToCookie( + originURL, + `__host-key=value; Secure; Domain=example.com; Path=/;` + ) + ) + ]); + + expect( + CookieStringUtility.cookiesToString(cookieContainer.getCookies(targetURL, false)) + ).toBe(''); + + cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(originURL, `__host-key=value; Secure; Path=/;`) + ]); + + expect( + CookieStringUtility.cookiesToString(cookieContainer.getCookies(targetURL, false)) + ).toBe('__host-key=value'); + }); + }); +}); diff --git a/packages/happy-dom/test/cookie/CookieJar.test.ts b/packages/happy-dom/test/cookie/CookieJar.test.ts deleted file mode 100644 index e00a7502e..000000000 --- a/packages/happy-dom/test/cookie/CookieJar.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import CookieJar from '../../src/cookie/CookieJar.js'; -import URL from '../../src/url/URL.js'; -import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; - -describe('CookieJar', () => { - let cookieJar: CookieJar; - - beforeEach(() => { - cookieJar = new CookieJar(); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('addCookieString()', () => { - it('Adds cookie string.', () => { - const expires = 60 * 1000 + Date.now(); - const originURL = new URL('https://example.com/path/to/page/'); - const maxAge = 60; - - cookieJar.addCookieString(originURL, `key1=value1; Expires=${new Date(expires).toString()};`); - cookieJar.addCookieString(originURL, `key2=value2; Max-Age=${maxAge};`); - cookieJar.addCookieString(originURL, `key3=value3; Domain=example.com;`); - cookieJar.addCookieString(originURL, `key4=value4; Domain=other.com;`); - cookieJar.addCookieString(originURL, `key5=value5; Domain=other.com; SameSite=None;`); - cookieJar.addCookieString(originURL, `key6=value6; Domain=other.com; SameSite=None; Secure;`); - cookieJar.addCookieString(originURL, `key7=value7; Path=path/to/page/;`); - cookieJar.addCookieString(originURL, `key8=value8; HttpOnly;`); - cookieJar.addCookieString(originURL, `key9=value9; Secure;`); - cookieJar.addCookieString(originURL, `key10=value10; SameSite=None; Secure;`); - cookieJar.addCookieString(originURL, `key10;`); - - expect(cookieJar.getCookieString(new URL('https://example.com/path/to/page/'), false)).toBe( - 'key1=value1; key2=value2; key3=value3; key7=value7; key8=value8; key9=value9; key10=value10; key10' - ); - - expect(cookieJar.getCookieString(new URL('https://example.com/path/to/page/'), true)).toBe( - 'key1=value1; key2=value2; key3=value3; key7=value7; key9=value9; key10=value10; key10' - ); - - expect(cookieJar.getCookieString(new URL('http://example.com/path/to/page/'), false)).toBe( - 'key1=value1; key2=value2; key3=value3; key7=value7; key8=value8; key10' - ); - - expect(cookieJar.getCookieString(new URL('https://other.com/path/to/page/'), false)).toBe( - 'key6=value6; key10=value10' - ); - - cookieJar.addCookieString(originURL, `key10=newValue10`); - - expect(cookieJar.getCookieString(new URL('https://example.com/path/to/page/'), false)).toBe( - 'key1=value1; key2=value2; key3=value3; key7=value7; key8=value8; key9=value9; key10; key10=newValue10' - ); - - expect(cookieJar.getCookieString(new URL('https://other.com/path/to/page/'), false)).toBe( - 'key6=value6' - ); - - vi.spyOn(Date, 'now').mockImplementation(() => expires + 1000); - - expect(cookieJar.getCookieString(new URL('https://example.com/path/to/page/'), false)).toBe( - 'key3=value3; key7=value7; key8=value8; key9=value9; key10; key10=newValue10' - ); - - cookieJar.addCookieString(originURL, `key10; Expires=${new Date(expires).toString()};`); - - expect(cookieJar.getCookieString(new URL('https://example.com/path/to/page/'), false)).toBe( - 'key3=value3; key7=value7; key8=value8; key9=value9; key10=newValue10' - ); - - cookieJar.addCookieString(originURL, `key10=; Expires=${new Date(expires).toString()};`); - - expect(cookieJar.getCookieString(new URL('https://example.com/path/to/page/'), false)).toBe( - 'key3=value3; key7=value7; key8=value8; key9=value9' - ); - }); - - it('Validates secure cookie keys.', () => { - const originURL = new URL('https://example.com/path/to/page/'); - const targetURL = new URL('https://example.com/path/to/page/'); - - cookieJar.addCookieString(originURL, `__secure-key=value`); - - expect(cookieJar.getCookieString(targetURL, false)).toBe(''); - - cookieJar.addCookieString(originURL, `__secure-key=value; Secure;`); - - expect(cookieJar.getCookieString(targetURL, false)).toBe('__secure-key=value'); - }); - - it('Validates host cookie keys.', () => { - const originURL = new URL('https://example.com/path/to/page/'); - const targetURL = new URL('https://example.com/path/to/page/'); - - cookieJar.addCookieString(originURL, `__host-key=value`); - - expect(cookieJar.getCookieString(targetURL, false)).toBe(''); - - cookieJar.addCookieString(originURL, `__host-key=value; Secure;`); - - expect(cookieJar.getCookieString(targetURL, false)).toBe(''); - - cookieJar.addCookieString(originURL, `__host-key=value; Secure; Path=/path/to/page/;`); - - expect(cookieJar.getCookieString(targetURL, false)).toBe(''); - - cookieJar.addCookieString(originURL, `__host-key=value; Secure; Domain=example.com; Path=/;`); - - expect(cookieJar.getCookieString(targetURL, false)).toBe(''); - - cookieJar.addCookieString(originURL, `__host-key=value; Secure; Path=/;`); - - expect(cookieJar.getCookieString(targetURL, false)).toBe('__host-key=value'); - }); - }); -}); From 3f686e6c7e5985a5ed9552f395a1e894ecdc5327 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 28 Nov 2023 01:22:51 +0100 Subject: [PATCH 30/63] #466@trivial: Continues on implementation. --- packages/happy-dom/src/browser/Browser.ts | 11 +- .../happy-dom/src/browser/BrowserContext.ts | 8 +- .../happy-dom/src/browser/BrowserFrame.ts | 11 +- .../DetachedBrowserFrame.ts | 39 +- .../DetachedBrowserPage.ts | 27 +- .../detached-browser/DetachedBrowser.ts | 111 -- .../DetachedBrowserContext.ts | 75 -- .../src/browser/types/IBrowserFrame.ts | 4 +- .../utilities/BrowserFrameNavigator.ts | 14 +- packages/happy-dom/src/clipboard/Clipboard.ts | 6 +- .../CSSMeasurementConverter.ts | 4 +- .../dom-implementation/DOMImplementation.ts | 15 +- .../happy-dom/src/dom-parser/DOMParser.ts | 19 +- packages/happy-dom/src/event/Event.ts | 6 +- packages/happy-dom/src/event/EventTarget.ts | 8 +- packages/happy-dom/src/event/IUIEventInit.ts | 4 +- packages/happy-dom/src/event/UIEvent.ts | 4 +- .../src/event/events/IMessageEventInit.ts | 4 +- .../src/event/events/MessageEvent.ts | 4 +- packages/happy-dom/src/fetch/Fetch.ts | 55 +- packages/happy-dom/src/fetch/Request.ts | 54 +- packages/happy-dom/src/fetch/ResourceFetch.ts | 16 +- packages/happy-dom/src/fetch/Response.ts | 67 +- .../utilities/FetchRequestReferrerUtility.ts | 18 +- packages/happy-dom/src/file/FileReader.ts | 44 +- packages/happy-dom/src/index.ts | 4 + .../src/match-media/MediaQueryItem.ts | 6 +- .../src/match-media/MediaQueryList.ts | 10 +- .../src/match-media/MediaQueryParser.ts | 4 +- packages/happy-dom/src/navigator/Navigator.ts | 6 +- .../happy-dom/src/nodes/document/Document.ts | 38 +- .../document/DocumentReadyStateManager.ts | 6 +- .../happy-dom/src/nodes/document/IDocument.ts | 6 +- .../happy-dom/src/nodes/element/Element.ts | 10 +- .../html-iframe-element/HTMLIFrameElement.ts | 10 +- .../HTMLIFrameElementPageLoader.ts | 20 +- .../html-iframe-element/IHTMLIFrameElement.ts | 6 +- packages/happy-dom/src/range/Range.ts | 334 ++--- .../happy-dom/src/window/BrowserWindow.ts | 1053 ++++++++++++++++ ...nWindow.ts => CrossOriginBrowserWindow.ts} | 19 +- .../happy-dom/src/window/IBrowserWindow.ts | 566 +++++++++ ...Window.ts => ICrossOriginBrowserWindow.ts} | 14 +- packages/happy-dom/src/window/IWindow.ts | 562 +-------- packages/happy-dom/src/window/Window.ts | 1076 +---------------- .../src/window/WindowBrowserSettingsReader.ts | 8 +- .../src/window/WindowClassFactory.ts | 125 +- .../src/window/WindowErrorUtility.ts | 10 +- .../src/window/WindowPageOpenUtility.ts | 14 +- .../src/xml-http-request/XMLHttpRequest.ts | 108 +- packages/happy-dom/test/fetch/Fetch.test.ts | 18 +- packages/happy-dom/test/fetch/Request.test.ts | 34 +- .../happy-dom/test/fetch/Response.test.ts | 28 +- .../happy-dom/test/file/FileReader.test.ts | 2 +- .../test/match-media/MediaQueryList.test.ts | 8 +- .../test/nodes/document/Document.test.ts | 2 +- .../test/nodes/element/Element.test.ts | 2 +- .../HTMLIFrameElement.test.ts | 16 +- .../html-link-element/HTMLLinkElement.test.ts | 8 +- .../HTMLScriptElement.test.ts | 29 +- .../happy-dom/test/nodes/node/Node.test.ts | 4 +- .../test/window/DetachedWindowAPI.test.ts | 6 +- packages/happy-dom/test/window/Window.test.ts | 8 +- .../xml-http-request/XMLHttpRequest.test.ts | 8 +- packages/jest-environment/src/index.ts | 6 +- .../test/UncaughtExceptionObserver.test.ts | 2 +- 65 files changed, 2340 insertions(+), 2484 deletions(-) rename packages/happy-dom/src/browser/{detached-browser => }/DetachedBrowserFrame.ts (68%) rename packages/happy-dom/src/browser/{detached-browser => }/DetachedBrowserPage.ts (75%) delete mode 100644 packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts delete mode 100644 packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts create mode 100644 packages/happy-dom/src/window/BrowserWindow.ts rename packages/happy-dom/src/window/{CrossOriginWindow.ts => CrossOriginBrowserWindow.ts} (79%) create mode 100644 packages/happy-dom/src/window/IBrowserWindow.ts rename packages/happy-dom/src/window/{ICrossOriginWindow.ts => ICrossOriginBrowserWindow.ts} (66%) diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index 6b17c337a..0f140beec 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -2,10 +2,11 @@ import IBrowserSettings from './types/IBrowserSettings.js'; import BrowserContext from './BrowserContext.js'; import IOptionalBrowserSettings from './types/IOptionalBrowserSettings.js'; import BrowserSettingsFactory from './BrowserSettingsFactory.js'; -import BrowserPage from './BrowserPage.js'; +import IBrowserPage from './types/IBrowserPage.js'; import IBrowser from './types/IBrowser.js'; -import BrowserFrame from './BrowserFrame.js'; +import IBrowserFrame from './types/IBrowserFrame.js'; import ICookieContainer from '../cookie/types/ICookieContainer.js'; +import IBrowserContext from './types/IBrowserContext.js'; /** * Browser. @@ -13,7 +14,7 @@ import ICookieContainer from '../cookie/types/ICookieContainer.js'; * Much of the interface for the browser has been taken from Puppeteer and Playwright, so that the API is familiar. */ export default class Browser implements IBrowser { - public readonly contexts: BrowserContext[]; + public readonly contexts: IBrowserContext[]; public readonly settings: IBrowserSettings; public readonly console: Console | null; @@ -39,7 +40,7 @@ export default class Browser implements IBrowser { * * @returns Default context. */ - public get defaultContext(): BrowserContext { + public get defaultContext(): IBrowserContext { if (this.contexts.length === 0) { throw new Error('No default context. The browser has been closed.'); } @@ -97,7 +98,7 @@ export default class Browser implements IBrowser { * @param [opener] Opener. * @returns Page. */ - public newPage(opener?: BrowserFrame): BrowserPage { + public newPage(opener?: IBrowserFrame): IBrowserPage { if (this.contexts.length === 0) { throw new Error('No default context. The browser has been closed.'); } diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index 57f4648a6..60f1ebe56 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -1,15 +1,15 @@ import CookieContainer from '../cookie/CookieContainer.js'; import ICookieContainer from '../cookie/types/ICookieContainer.js'; import Browser from './Browser.js'; -import BrowserFrame from './BrowserFrame.js'; -import BrowserPage from './BrowserPage.js'; +import IBrowserFrame from './types/IBrowserFrame.js'; +import IBrowserPage from './types/IBrowserPage.js'; import IBrowserContext from './types/IBrowserContext.js'; /** * Browser context. */ export default class BrowserContext implements IBrowserContext { - public readonly pages: BrowserPage[] = []; + public readonly pages: IBrowserPage[] = []; public readonly browser: Browser; public readonly cookieContainer: ICookieContainer; @@ -61,7 +61,7 @@ export default class BrowserContext implements IBrowserContext { * @param [opener] Opener. * @returns Page. */ - public newPage(opener?: BrowserFrame): BrowserPage { + public newPage(opener?: IBrowserFrame): IBrowserPage { const page = new BrowserPage(this); ((page.mainFrame.opener)) = opener || null; this.pages.push(page); diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 0489fd96c..015f95c6d 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -1,7 +1,7 @@ import BrowserPage from './BrowserPage.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import IBrowserFrame from './types/IBrowserFrame.js'; -import Window from '../window/Window.js'; +import BrowserWindow from '../window/BrowserWindow.js'; import Location from '../location/Location.js'; import IResponse from '../fetch/types/IResponse.js'; import IGoToOptions from './types/IGoToOptions.js'; @@ -18,7 +18,7 @@ export default class BrowserFrame implements IBrowserFrame { public readonly parentFrame: BrowserFrame | null = null; public readonly opener: BrowserFrame | null = null; public readonly page: BrowserPage; - public readonly window: Window; + public readonly window: BrowserWindow; public _asyncTaskManager = new AsyncTaskManager(); /** @@ -28,10 +28,7 @@ export default class BrowserFrame implements IBrowserFrame { */ constructor(page: BrowserPage) { this.page = page; - this.window = new Window({ - browserFrame: this, - console: page.console - }); + this.window = new BrowserWindow(this); } /** @@ -118,6 +115,6 @@ export default class BrowserFrame implements IBrowserFrame { * @returns Response. */ public goto(url: string, options?: IGoToOptions): Promise { - return BrowserFrameNavigator.goto(Window, this, url, options); + return BrowserFrameNavigator.goto(BrowserWindow, this, url, options); } } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/DetachedBrowserFrame.ts similarity index 68% rename from packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts rename to packages/happy-dom/src/browser/DetachedBrowserFrame.ts index 590ec72f7..aa5dc269e 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/DetachedBrowserFrame.ts @@ -1,14 +1,15 @@ -import IWindow from '../../window/IWindow.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; import DetachedBrowserPage from './DetachedBrowserPage.js'; -import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; -import IBrowserFrame from '../types/IBrowserFrame.js'; -import Location from '../../location/Location.js'; -import IResponse from '../../fetch/types/IResponse.js'; -import IGoToOptions from '../types/IGoToOptions.js'; +import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import IBrowserFrame from './types/IBrowserFrame.js'; +import Location from '../location/Location.js'; +import IResponse from '../fetch/types/IResponse.js'; +import IGoToOptions from './types/IGoToOptions.js'; import { Script } from 'vm'; -import BrowserFrameURL from '../utilities/BrowserFrameURL.js'; -import BrowserFrameScriptEvaluator from '../utilities/BrowserFrameScriptEvaluator.js'; -import BrowserFrameNavigator from '../utilities/BrowserFrameNavigator.js'; +import BrowserFrameURL from './utilities/BrowserFrameURL.js'; +import BrowserFrameScriptEvaluator from './utilities/BrowserFrameScriptEvaluator.js'; +import BrowserFrameNavigator from './utilities/BrowserFrameNavigator.js'; +import IWindow from '../window/IWindow.js'; /** * Browser frame used when constructing a Window instance without a browser. @@ -18,19 +19,22 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public readonly parentFrame: DetachedBrowserFrame | null = null; public readonly opener: DetachedBrowserFrame | null = null; public readonly page: DetachedBrowserPage; - public readonly window: IWindow; public _asyncTaskManager = new AsyncTaskManager(); + // Needs to be injected when constructing the browser frame in Window.ts. + public window: IWindow; + readonly #windowClass: new (browserFrame: IBrowserFrame) => IBrowserWindow; /** * Constructor. * * @param page Page. */ - constructor(page: DetachedBrowserPage) { + constructor( + page: DetachedBrowserPage, + windowClass: new (browserFrame: IBrowserFrame) => IBrowserWindow + ) { this.page = page; - this.window = page.context.browser.contexts[0]?.pages[0]?.mainFrame - ? new page.context.browser.detachedWindowClass({ browserFrame: this }) - : page.context.browser.detachedWindow; + this.#windowClass = windowClass; } /** @@ -115,11 +119,6 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @returns Response. */ public goto(url: string, options?: IGoToOptions): Promise { - return BrowserFrameNavigator.goto( - this.page.context.browser.detachedWindowClass, - this, - url, - options - ); + return BrowserFrameNavigator.goto(this.#windowClass, this, url, options); } } diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts b/packages/happy-dom/src/browser/DetachedBrowserPage.ts similarity index 75% rename from packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts rename to packages/happy-dom/src/browser/DetachedBrowserPage.ts index 91d50b3d9..61a64e7a8 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts +++ b/packages/happy-dom/src/browser/DetachedBrowserPage.ts @@ -1,13 +1,15 @@ -import VirtualConsolePrinter from '../../console/VirtualConsolePrinter.js'; -import IBrowserPageViewport from '../types/IBrowserPageViewport.js'; +import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import IBrowserPageViewport from './types/IBrowserPageViewport.js'; import DetachedBrowserFrame from './DetachedBrowserFrame.js'; -import DetachedBrowserContext from './DetachedBrowserContext.js'; -import VirtualConsole from '../../console/VirtualConsole.js'; -import IBrowserPage from '../types/IBrowserPage.js'; +import IBrowserContext from './types/IBrowserContext.js'; +import VirtualConsole from '../console/VirtualConsole.js'; +import IBrowserPage from './types/IBrowserPage.js'; import { Script } from 'vm'; -import IGoToOptions from '../types/IGoToOptions.js'; -import IResponse from '../../fetch/types/IResponse.js'; -import BrowserPageUtility from '../utilities/BrowserPageUtility.js'; +import IGoToOptions from './types/IGoToOptions.js'; +import IResponse from '../fetch/types/IResponse.js'; +import BrowserPageUtility from './utilities/BrowserPageUtility.js'; +import IBrowserFrame from './types/IBrowserFrame.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; /** * Detached browser page used when constructing a Window instance without a browser. @@ -15,7 +17,7 @@ import BrowserPageUtility from '../utilities/BrowserPageUtility.js'; export default class DetachedBrowserPage implements IBrowserPage { public readonly virtualConsolePrinter = new VirtualConsolePrinter(); public readonly mainFrame: DetachedBrowserFrame; - public readonly context: DetachedBrowserContext; + public readonly context: IBrowserContext; public readonly console: Console; /** @@ -23,10 +25,13 @@ export default class DetachedBrowserPage implements IBrowserPage { * * @param context Browser context. */ - constructor(context: DetachedBrowserContext) { + constructor( + context: IBrowserContext, + windowClass: new (browserFrame: IBrowserFrame) => IBrowserWindow + ) { this.context = context; this.console = context.browser.console ?? new VirtualConsole(this.virtualConsolePrinter); - this.mainFrame = new DetachedBrowserFrame(this); + this.mainFrame = new DetachedBrowserFrame(this, windowClass); } /** diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts deleted file mode 100644 index acb7d14aa..000000000 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts +++ /dev/null @@ -1,111 +0,0 @@ -import IBrowserSettings from '../types/IBrowserSettings.js'; -import DetachedBrowserContext from './DetachedBrowserContext.js'; -import IOptionalBrowserSettings from '../types/IOptionalBrowserSettings.js'; -import BrowserSettingsFactory from '../BrowserSettingsFactory.js'; -import DetachedBrowserPage from './DetachedBrowserPage.js'; -import IBrowser from '../types/IBrowser.js'; -import IWindow from '../../window/IWindow.js'; -import IBrowserFrame from '../types/IBrowserFrame.js'; -import DetachedBrowserFrame from './DetachedBrowserFrame.js'; - -/** - * Detached browser used when constructing a Window instance without a browser. - * - * Much of the interface for the browser has been taken from Puppeteer and Playwright, so that the API is familiar. - */ -export default class DetachedBrowser implements IBrowser { - public readonly contexts: DetachedBrowserContext[]; - public readonly settings: IBrowserSettings; - public readonly console: Console | null; - public readonly detachedWindowClass: new (options: { - browserFrame: IBrowserFrame; - console?: Console; - url?: string; - }) => IWindow; - public readonly detachedWindow: IWindow; - - /** - * Constructor. - * - * @param windowClass Window class. - * @param window Window. - * @param [options] Options. - * @param [options.settings] Browser settings. - * @param [options.console] Console. - */ - constructor( - windowClass: new () => IWindow, - window: IWindow, - options?: { settings?: IOptionalBrowserSettings; console?: Console } - ) { - this.detachedWindowClass = windowClass; - this.detachedWindow = window; - this.console = options?.console || null; - this.settings = BrowserSettingsFactory.getSettings(options?.settings); - this.contexts = []; - this.contexts.push(new DetachedBrowserContext(this)); - } - - /** - * Returns the default context. - * - * @returns Default context. - */ - public get defaultContext(): DetachedBrowserContext { - if (this.contexts.length === 0) { - throw new Error('No default context. The browser has been closed.'); - } - return this.contexts[0]; - } - - /** - * Aborts all ongoing operations and destroys the browser. - */ - public close(): void { - for (const context of this.contexts.slice()) { - context.close(); - } - (this.contexts) = []; - (this.console) = null; - (this.detachedWindow) = null; - ( IWindow | null>this.detachedWindowClass) = null; - } - - /** - * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. - * - * @returns Promise. - */ - public async whenComplete(): Promise { - await Promise.all(this.contexts.map((page) => page.whenComplete())); - } - - /** - * Aborts all ongoing operations. - */ - public abort(): void { - for (const context of this.contexts) { - context.abort(); - } - } - - /** - * Creates a new incognito context. - */ - public newIncognitoContext(): DetachedBrowserContext { - throw new Error('Not possible to create a new context on a detached browser.'); - } - - /** - * Creates a new page. - * - * @param [opener] Opener. - * @returns Page. - */ - public newPage(opener?: DetachedBrowserFrame): DetachedBrowserPage { - if (this.contexts.length === 0) { - throw new Error('No default context. The browser has been closed.'); - } - return this.contexts[0].newPage(opener); - } -} diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts deleted file mode 100644 index cc3bb0eae..000000000 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts +++ /dev/null @@ -1,75 +0,0 @@ -import DetachedBrowser from './DetachedBrowser.js'; -import DetachedBrowserPage from './DetachedBrowserPage.js'; -import IBrowserContext from '../types/IBrowserContext.js'; -import DetachedBrowserFrame from './DetachedBrowserFrame.js'; -import ICookieContainer from '../../cookie/types/ICookieContainer.js'; -import CookieContainer from '../../cookie/CookieContainer.js'; - -/** - * Detached browser context used when constructing a Window instance without a browser. - */ -export default class DetachedBrowserContext implements IBrowserContext { - public readonly pages: DetachedBrowserPage[]; - public readonly browser: DetachedBrowser; - public readonly cookieContainer: ICookieContainer = new CookieContainer(); - - /** - * Constructor. - * - * @param browser Browser. - */ - constructor(browser: DetachedBrowser) { - this.browser = browser; - this.pages = []; - this.pages.push(new DetachedBrowserPage(this)); - } - - /** - * Aborts all ongoing operations and destroys the context. - */ - public close(): void { - if (!this.browser) { - return; - } - for (const page of this.pages.slice()) { - page.close(); - } - const browser = this.browser; - (this.pages) = []; - (this.browser) = null; - if (browser.defaultContext === this) { - browser.close(); - } - } - - /** - * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. - * - * @returns Promise. - */ - public async whenComplete(): Promise { - await Promise.all(this.pages.map((page) => page.whenComplete())); - } - - /** - * Aborts all ongoing operations. - */ - public abort(): void { - for (const page of this.pages) { - page.abort(); - } - } - - /** - * Creates a new page. - * - * @param [opener] Opener. - * @returns Page. - */ - public newPage(opener?: DetachedBrowserFrame): DetachedBrowserPage { - const page = new DetachedBrowserPage(this); - ((page.mainFrame.opener)) = opener || null; - this.pages.push(page); - return page; - } -} diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index aa0b1cf32..948dc262b 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -1,5 +1,5 @@ import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; -import IWindow from '../../window/IWindow.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; import IBrowserPage from './IBrowserPage.js'; import IResponse from '../../fetch/types/IResponse.js'; import IGoToOptions from './IGoToOptions.js'; @@ -10,7 +10,7 @@ import { Script } from 'vm'; */ export default interface IBrowserFrame { readonly childFrames: IBrowserFrame[]; - readonly window: IWindow; + readonly window: IBrowserWindow; content: string; url: string; readonly parentFrame: IBrowserFrame | null; diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts index 3b45e76c0..f6506e046 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts @@ -3,7 +3,7 @@ import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReade import IGoToOptions from '../types/IGoToOptions.js'; import IResponse from '../../fetch/types/IResponse.js'; import DocumentReadyStateManager from '../../nodes/document/DocumentReadyStateManager.js'; -import IWindow from '../../window/IWindow.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import Location from '../../location/Location.js'; import AbortController from '../../fetch/AbortController.js'; @@ -26,11 +26,7 @@ export default class BrowserFrameNavigator { * @returns Response. */ public static async goto( - windowClass: new (options: { - browserFrame: IBrowserFrame; - console: Console; - url?: string; - }) => IWindow, + windowClass: new (browserFrame: IBrowserFrame) => IBrowserWindow, frame: IBrowserFrame, url: string, options?: IGoToOptions @@ -84,11 +80,7 @@ export default class BrowserFrameNavigator { frame._asyncTaskManager.destroy(); WindowBrowserSettingsReader.removeSettings(frame.window); - (frame.window) = new windowClass({ - browserFrame: frame, - console: frame.page.console, - url: targetURL.href - }); + (frame.window) = new windowClass(frame); if (options?.referrer) { (frame.window.document.referrer) = options.referrer; diff --git a/packages/happy-dom/src/clipboard/Clipboard.ts b/packages/happy-dom/src/clipboard/Clipboard.ts index 3b1980b71..097a679b8 100644 --- a/packages/happy-dom/src/clipboard/Clipboard.ts +++ b/packages/happy-dom/src/clipboard/Clipboard.ts @@ -1,5 +1,5 @@ import DOMException from '../exception/DOMException.js'; -import IWindow from '../window/IWindow.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; import ClipboardItem from './ClipboardItem.js'; import Blob from '../file/Blob.js'; @@ -10,7 +10,7 @@ import Blob from '../file/Blob.js'; * https://developer.mozilla.org/en-US/docs/Web/API/Clipboard. */ export default class Clipboard { - #ownerWindow: IWindow; + #ownerWindow: IBrowserWindow; #data: ClipboardItem[] = []; /** @@ -18,7 +18,7 @@ export default class Clipboard { * * @param ownerWindow Owner window. */ - constructor(ownerWindow: IWindow) { + constructor(ownerWindow: IBrowserWindow) { this.#ownerWindow = ownerWindow; } diff --git a/packages/happy-dom/src/css/declaration/measurement-converter/CSSMeasurementConverter.ts b/packages/happy-dom/src/css/declaration/measurement-converter/CSSMeasurementConverter.ts index bc5b8bca6..30566ac65 100644 --- a/packages/happy-dom/src/css/declaration/measurement-converter/CSSMeasurementConverter.ts +++ b/packages/happy-dom/src/css/declaration/measurement-converter/CSSMeasurementConverter.ts @@ -1,4 +1,4 @@ -import IWindow from '../../../window/IWindow.js'; +import IBrowserWindow from '../../../window/IBrowserWindow.js'; /** * CSS Measurement Converter. @@ -16,7 +16,7 @@ export default class CSSMeasurementConverter { * @returns Measurement in pixels. */ public static toPixels(options: { - ownerWindow: IWindow; + ownerWindow: IBrowserWindow; value: string; rootFontSize: string | number; parentFontSize: string | number; diff --git a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts index 59cf8b173..610206d7f 100644 --- a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts +++ b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts @@ -1,3 +1,4 @@ +import { IBrowserWindow } from '../index.js'; import DocumentType from '../nodes/document-type/DocumentType.js'; import IDocument from '../nodes/document/IDocument.js'; @@ -5,15 +6,15 @@ import IDocument from '../nodes/document/IDocument.js'; * The DOMImplementation interface represents an object providing methods which are not dependent on any particular document. Such an object is returned by the. */ export default class DOMImplementation { - protected _ownerDocument: IDocument = null; + #window: IBrowserWindow; /** * Constructor. * - * @param ownerDocument + * @param window Window. */ - constructor(ownerDocument: IDocument) { - this._ownerDocument = ownerDocument; + constructor(window: IBrowserWindow) { + this.#window = window; } /** @@ -22,14 +23,14 @@ export default class DOMImplementation { * TODO: Not fully implemented. */ public createDocument(): IDocument { - return new this._ownerDocument._defaultView.XMLDocument(); + return new this.#window.XMLDocument(); } /** * Creates and returns an HTML Document. */ public createHTMLDocument(): IDocument { - return new this._ownerDocument._defaultView.HTMLDocument(); + return new this.#window.HTMLDocument(); } /** @@ -44,7 +45,7 @@ export default class DOMImplementation { publicId: string, systemId: string ): DocumentType { - const documentType = new this._ownerDocument._defaultView.DocumentType(); + const documentType = new this.#window.DocumentType(); documentType.name = qualifiedName; documentType.publicId = publicId; documentType.systemId = systemId; diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index 49d1ebc00..4cd35d2e9 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -3,6 +3,7 @@ import XMLParser from '../xml-parser/XMLParser.js'; import Node from '../nodes/node/Node.js'; import DOMException from '../exception/DOMException.js'; import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; /** * DOM parser. @@ -11,8 +12,16 @@ import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; * https://developer.mozilla.org/en-US/docs/Web/API/DOMParser. */ export default class DOMParser { - // Will be populated by a sub-class in Window. - public readonly _ownerDocument: IDocument; + readonly #window: IBrowserWindow; + + /** + * Constructor. + * + * @param window Window. + */ + constructor(window: IBrowserWindow) { + this.#window = window; + } /** * Parses HTML and returns a root element. @@ -94,13 +103,13 @@ export default class DOMParser { private _createDocument(mimeType: string): IDocument { switch (mimeType) { case 'text/html': - return new this._ownerDocument._defaultView.HTMLDocument(); + return new this.#window.HTMLDocument(); case 'image/svg+xml': - return new this._ownerDocument._defaultView.SVGDocument(); + return new this.#window.SVGDocument(); case 'text/xml': case 'application/xml': case 'application/xhtml+xml': - return new this._ownerDocument._defaultView.XMLDocument(); + return new this.#window.XMLDocument(); default: throw new DOMException(`Unknown mime type "${mimeType}".`); } diff --git a/packages/happy-dom/src/event/Event.ts b/packages/happy-dom/src/event/Event.ts index 143104596..26f6b18d3 100644 --- a/packages/happy-dom/src/event/Event.ts +++ b/packages/happy-dom/src/event/Event.ts @@ -1,6 +1,6 @@ import IEventInit from './IEventInit.js'; import INode from '../nodes/node/INode.js'; -import IWindow from '../window/IWindow.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; import IShadowRoot from '../nodes/shadow-root/IShadowRoot.js'; import IEventTarget from './IEventTarget.js'; import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js'; @@ -81,7 +81,9 @@ export default class Event { } const composedPath = []; - let eventTarget: INode | IShadowRoot | IWindow = (this._target); + let eventTarget: INode | IShadowRoot | IBrowserWindow = ( + (this._target) + ); while (eventTarget) { composedPath.push(eventTarget); diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts index aae2d1fd2..4ecc2ff86 100644 --- a/packages/happy-dom/src/event/EventTarget.ts +++ b/packages/happy-dom/src/event/EventTarget.ts @@ -5,7 +5,7 @@ import IEventListenerOptions from './IEventListenerOptions.js'; import EventPhaseEnum from './EventPhaseEnum.js'; import INode from '../nodes/node/INode.js'; import IDocument from '../nodes/document/IDocument.js'; -import IWindow from '../window/IWindow.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; import WindowErrorUtility from '../window/WindowErrorUtility.js'; import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; @@ -259,15 +259,15 @@ export default abstract class EventTarget implements IEventTarget { * * @returns Window. */ - public _getWindow(): IWindow | null { + public _getWindow(): IBrowserWindow | null { if (((this)).ownerDocument) { return ((this)).ownerDocument._defaultView; } if (((this))._defaultView) { return ((this))._defaultView; } - if (((this)).document) { - return (this); + if (((this)).document) { + return (this); } return null; } diff --git a/packages/happy-dom/src/event/IUIEventInit.ts b/packages/happy-dom/src/event/IUIEventInit.ts index e1f1c9093..d6047c74d 100644 --- a/packages/happy-dom/src/event/IUIEventInit.ts +++ b/packages/happy-dom/src/event/IUIEventInit.ts @@ -1,7 +1,7 @@ -import IWindow from '../window/IWindow.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; import IEventInit from './IEventInit.js'; export default interface IUIEventInit extends IEventInit { detail?: number; - view?: IWindow; + view?: IBrowserWindow; } diff --git a/packages/happy-dom/src/event/UIEvent.ts b/packages/happy-dom/src/event/UIEvent.ts index 1fc859f16..42cc4e1ba 100644 --- a/packages/happy-dom/src/event/UIEvent.ts +++ b/packages/happy-dom/src/event/UIEvent.ts @@ -1,4 +1,4 @@ -import IWindow from '../window/IWindow.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; import Event from './Event.js'; import IUIEventInit from './IUIEventInit.js'; @@ -15,7 +15,7 @@ export default class UIEvent extends Event { public readonly layerY: number = 0; public readonly pageX: number = 0; public readonly pageY: number = 0; - public readonly view: IWindow | null; + public readonly view: IBrowserWindow | null; /** * Constructor. diff --git a/packages/happy-dom/src/event/events/IMessageEventInit.ts b/packages/happy-dom/src/event/events/IMessageEventInit.ts index 59571f94e..99852dcde 100644 --- a/packages/happy-dom/src/event/events/IMessageEventInit.ts +++ b/packages/happy-dom/src/event/events/IMessageEventInit.ts @@ -1,11 +1,11 @@ import IEventInit from '../IEventInit.js'; -import IWindow from '../../window/IWindow.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; import IMessagePort from '../IMessagePort.js'; export default interface IMessageEventInit extends IEventInit { data?: unknown | null; origin?: string; lastEventId?: string; - source?: IWindow | null; + source?: IBrowserWindow | null; ports?: IMessagePort[]; } diff --git a/packages/happy-dom/src/event/events/MessageEvent.ts b/packages/happy-dom/src/event/events/MessageEvent.ts index a2cd12f8e..25e001497 100644 --- a/packages/happy-dom/src/event/events/MessageEvent.ts +++ b/packages/happy-dom/src/event/events/MessageEvent.ts @@ -1,4 +1,4 @@ -import IWindow from '../../window/IWindow.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; import Event from '../Event.js'; import IMessagePort from '../IMessagePort.js'; import IMessageEventInit from './IMessageEventInit.js'; @@ -12,7 +12,7 @@ export default class MessageEvent extends Event { public readonly data: unknown | null; public readonly origin: string; public readonly lastEventId: string; - public readonly source: IWindow | null; + public readonly source: IBrowserWindow | null; public readonly ports: IMessagePort[]; /** diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index 07c7aad99..e87107ce6 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -1,5 +1,4 @@ import IRequestInit from './types/IRequestInit.js'; -import IDocument from '../nodes/document/IDocument.js'; import IResponse from './types/IResponse.js'; import IRequestInfo from './types/IRequestInfo.js'; import Headers from './Headers.js'; @@ -16,11 +15,11 @@ import DataURIParser from './data-uri/DataURIParser.js'; import FetchCORSUtility from './utilities/FetchCORSUtility.js'; import Request from './Request.js'; import Response from './Response.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import Event from '../event/Event.js'; import AbortSignal from './AbortSignal.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; import CookieStringUtility from '../cookie/urilities/CookieStringUtility.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; const SUPPORTED_SCHEMAS = ['data:', 'http:', 'https:']; const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; @@ -46,19 +45,17 @@ export default class Fetch { private previousChunk: Buffer | null = null; private nodeRequest: HTTP.ClientRequest | null = null; private response: Response | null = null; - private ownerDocument: IDocument; - private asyncTaskManager: AsyncTaskManager; private request: Request; private redirectCount = 0; #browserFrame: IBrowserFrame; + #window: IBrowserWindow; /** * Constructor. * * @param options Options. * @param options.browserFrame Browser frame. - * @param options.ownerDocument Owner document. - * @param options.asyncTaskManager Async task manager. + * @param options.window Window. * @param options.url URL. * @param [options.init] Init. * @param [options.redirectCount] Redirect count. @@ -66,8 +63,7 @@ export default class Fetch { */ constructor(options: { browserFrame: IBrowserFrame; - ownerDocument: IDocument; - asyncTaskManager: AsyncTaskManager; + window: IBrowserWindow; url: IRequestInfo; init?: IRequestInit; redirectCount?: number; @@ -76,11 +72,10 @@ export default class Fetch { const url = options.url; this.#browserFrame = options.browserFrame; - this.ownerDocument = options.ownerDocument; - this.asyncTaskManager = options.asyncTaskManager; + this.#window = options.window; this.request = typeof options.url === 'string' || options.url instanceof URL - ? new options.ownerDocument._defaultView.Request(options.url, options.init) + ? new options.browserFrame.window.Request(options.url, options.init) : url; if (options.contentType) { (this.request._contentType) = options.contentType; @@ -95,18 +90,20 @@ export default class Fetch { */ public send(): Promise { return new Promise((resolve, reject) => { - const taskID = this.asyncTaskManager.startTask(() => this.onAsyncTaskManagerAbort()); + const taskID = this.#browserFrame._asyncTaskManager.startTask(() => + this.onAsyncTaskManagerAbort() + ); if (this.resolve) { throw new Error('Fetch already sent.'); } this.resolve = (response: IResponse | Promise): void => { - this.asyncTaskManager.endTask(taskID); + this.#browserFrame._asyncTaskManager.endTask(taskID); resolve(response); }; this.reject = (error: Error): void => { - this.asyncTaskManager.endTask(taskID); + this.#browserFrame._asyncTaskManager.endTask(taskID); reject(error); }; @@ -115,7 +112,7 @@ export default class Fetch { if (this.request._url.protocol === 'data:') { const result = DataURIParser.parse(this.request.url); - this.response = new this.ownerDocument._defaultView.Response(result.buffer, { + this.response = new this.#window.Response(result.buffer, { headers: { 'Content-Type': result.type } }); resolve(this.response); @@ -207,7 +204,7 @@ export default class Fetch { */ private onError(error: Error): void { this.finalizeRequest(); - this.ownerDocument._defaultView.console.error(error); + this.#window.console.error(error); this.reject( new DOMException( `Fetch to "${this.request.url}" failed. Error: ${error.message}`, @@ -276,7 +273,7 @@ export default class Fetch { nodeResponse.statusCode === 204 || nodeResponse.statusCode === 304 ) { - this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); + this.response = new this.#window.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -298,7 +295,7 @@ export default class Fetch { // Ignore error as it is forwarded to the response body. } }); - this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); + this.response = new this.#window.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -330,7 +327,7 @@ export default class Fetch { }); } - this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); + this.response = new this.#window.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -338,7 +335,7 @@ export default class Fetch { raw.on('end', () => { // Some old IIS servers return zero-length OK deflate responses, so 'data' is never emitted. if (!this.response) { - this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); + this.response = new this.#window.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -354,7 +351,7 @@ export default class Fetch { // Ignore error as it is forwarded to the response body. } }); - this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); + this.response = new this.#window.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -362,7 +359,7 @@ export default class Fetch { } // Otherwise, use response as is - this.response = new this.ownerDocument._defaultView.Response(body, responseOptions); + this.response = new this.#window.Response(body, responseOptions); (this.response.redirected) = this.redirectCount > 0; (this.response.url) = this.request.url; this.resolve(this.response); @@ -461,7 +458,7 @@ export default class Fetch { if ( this.request.credentials === 'omit' || (this.request.credentials === 'same-origin' && - FetchCORSUtility.isCORS(this.ownerDocument.location, locationURL)) + FetchCORSUtility.isCORS(this.#window.location, locationURL)) ) { headers.delete('authorization'); headers.delete('www-authenticate'); @@ -500,8 +497,7 @@ export default class Fetch { const fetch = new (this.constructor)({ browserFrame: this.#browserFrame, - ownerDocument: this.ownerDocument, - asyncTaskManager: this.asyncTaskManager, + window: this.#window, url: locationURL, init: requestInit, redirectCount: this.redirectCount + 1, @@ -532,7 +528,7 @@ export default class Fetch { if (this.request.referrer && this.request.referrer !== 'no-referrer') { this.request._referrer = FetchRequestReferrerUtility.getSentReferrer( - this.ownerDocument, + this.#window, this.request ); } else { @@ -563,8 +559,7 @@ export default class Fetch { */ private getRequestHeaders(): { [key: string]: string } { const headers = new Headers(this.request.headers); - const document = this.ownerDocument; - const isCORS = FetchCORSUtility.isCORS(document.location, this.request._url); + const isCORS = FetchCORSUtility.isCORS(this.#window.location, this.request._url); // TODO: Maybe we need to add support for OPTIONS request with 'Access-Control-Allow-*' headers? if ( @@ -579,7 +574,7 @@ export default class Fetch { headers.set('Connection', 'close'); if (!headers.has('User-Agent')) { - headers.set('User-Agent', document._defaultView.navigator.userAgent); + headers.set('User-Agent', this.#window.navigator.userAgent); } if (this.request._referrer instanceof URL) { @@ -591,7 +586,7 @@ export default class Fetch { (this.request.credentials === 'same-origin' && !isCORS) ) { const cookies = this.#browserFrame.page.context.cookieContainer.getCookies( - document._defaultView.location, + this.#window.location, false ); if (cookies.length > 0) { diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts index b700334ad..77d59e2ea 100644 --- a/packages/happy-dom/src/fetch/Request.ts +++ b/packages/happy-dom/src/fetch/Request.ts @@ -21,6 +21,7 @@ import IRequestCredentials from './types/IRequestCredentials.js'; import FormData from '../form-data/FormData.js'; import MultipartFormDataParser from './multipart/MultipartFormDataParser.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; /** * Fetch request. @@ -31,9 +32,6 @@ import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; * @see https://fetch.spec.whatwg.org/#request-class */ export default class Request implements IRequest { - // Needs to be injected by a sub-class. - protected readonly _asyncTaskManager: AsyncTaskManager; - // Public properties public readonly method: string; public readonly body: Stream.Readable | null; @@ -50,14 +48,24 @@ export default class Request implements IRequest { public _referrer: '' | 'no-referrer' | 'client' | URL = 'client'; public readonly _url: URL; public readonly _bodyBuffer: Buffer | null; + readonly #window: IBrowserWindow; + readonly #asyncTaskManager: AsyncTaskManager; /** * Constructor. * + * @param injected Injected properties. * @param input Input. * @param [init] Init. */ - constructor(input: IRequestInfo, init?: IRequestInit) { + constructor( + injected: { window: IBrowserWindow; asyncTaskManager: AsyncTaskManager }, + input: IRequestInfo, + init?: IRequestInit + ) { + this.#window = injected.window; + this.#asyncTaskManager = injected.asyncTaskManager; + if (!input) { throw new TypeError(`Failed to contruct 'Request': 1 argument required, only 0 present.`); } @@ -98,7 +106,7 @@ export default class Request implements IRequest { ); this.signal = init?.signal || (input).signal || new AbortSignal(); this._referrer = FetchRequestReferrerUtility.getInitialReferrer( - this._ownerDocument, + injected.window, init?.referrer !== null && init?.referrer !== undefined ? init?.referrer : (input).referrer @@ -109,16 +117,16 @@ export default class Request implements IRequest { } else { try { if (input instanceof Request && input.url) { - this._url = new URL(input.url, this._ownerDocument.location); + this._url = new URL(input.url, injected.window.location); } else { - this._url = new URL(input, this._ownerDocument.location); + this._url = new URL(input, injected.window.location); } } catch (error) { throw new DOMException( `Failed to construct 'Request. Invalid URL "${input}" on document location '${ - this._ownerDocument.location + injected.window.location }'.${ - this._ownerDocument.location.origin === 'null' + injected.window.location.origin === 'null' ? ' Relative URLs are not permitted on current document location.' : '' }`, @@ -190,17 +198,17 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this._asyncTaskManager.startTask(() => this.signal._abort()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal._abort()); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); throw error; } - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); } @@ -232,17 +240,17 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this._asyncTaskManager.startTask(() => this.signal._abort()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal._abort()); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); throw error; } - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); return buffer; } @@ -262,17 +270,17 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this._asyncTaskManager.startTask(() => this.signal._abort()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal._abort()); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); throw error; } - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); return new TextDecoder().decode(buffer); } @@ -302,18 +310,18 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this._asyncTaskManager.startTask(() => this.signal._abort()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal._abort()); let formData: FormData; try { const type = this._contentType; formData = await MultipartFormDataParser.streamToFormData(this.body, type); } catch (error) { - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); throw error; } - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); return formData; } @@ -323,7 +331,7 @@ export default class Request implements IRequest { * * @returns Clone. */ - public clone(): IRequest { - return new (this.constructor)(this); + public clone(): Request { + return new this.#window.Request(this); } } diff --git a/packages/happy-dom/src/fetch/ResourceFetch.ts b/packages/happy-dom/src/fetch/ResourceFetch.ts index c4feebd9e..b03789732 100644 --- a/packages/happy-dom/src/fetch/ResourceFetch.ts +++ b/packages/happy-dom/src/fetch/ResourceFetch.ts @@ -1,5 +1,5 @@ import DOMException from '../exception/DOMException.js'; -import IDocument from '../nodes/document/IDocument.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; import URL from '../url/URL.js'; /** @@ -9,12 +9,12 @@ export default class ResourceFetch { /** * Returns resource data asynchronously. * - * @param document Document. + * @param window Window. * @param url URL. * @returns Response. */ - public static async fetch(document: IDocument, url: string): Promise { - const response = await document._defaultView.fetch(url); + public static async fetch(window: IBrowserWindow, url: string): Promise { + const response = await window.fetch(url); if (!response.ok) { throw new DOMException( `Failed to perform request to "${url}". Status code: ${response.status}` @@ -26,15 +26,15 @@ export default class ResourceFetch { /** * Returns resource data synchronously. * - * @param document Document. + * @param window Window. * @param url URL. * @returns Response. */ - public static fetchSync(document: IDocument, url: string): string { + public static fetchSync(window: IBrowserWindow, url: string): string { // We want to only load SyncRequest when it is needed to improve performance and not have direct dependencies to server side packages. - const absoluteURL = new URL(url, document._defaultView.location).href; + const absoluteURL = new URL(url, window.location).href; - const xhr = new document._defaultView.XMLHttpRequest(); + const xhr = new window.XMLHttpRequest(); xhr.open('GET', absoluteURL, false); xhr.send(); diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts index 1e963dfb0..1037da3e8 100644 --- a/packages/happy-dom/src/fetch/Response.ts +++ b/packages/happy-dom/src/fetch/Response.ts @@ -15,6 +15,7 @@ import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import { TextDecoder } from 'util'; import MultipartFormDataParser from './multipart/MultipartFormDataParser.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; @@ -27,8 +28,8 @@ const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; * @see https://developer.mozilla.org/en-US/docs/Web/API/Response/Response */ export default class Response implements IResponse { - // Needs to be injected by a sub-class. - protected readonly _asyncTaskManager: AsyncTaskManager; + // Needs to be injected by sub-class. + protected static _window: IBrowserWindow; // Public properties public readonly body: Stream.Readable | null = null; @@ -41,15 +42,24 @@ export default class Response implements IResponse { public readonly statusText: string; public readonly ok: boolean; public readonly headers: IHeaders; + readonly #window: IBrowserWindow; + readonly #asyncTaskManager: AsyncTaskManager; /** * Constructor. * + * @param injected Injected properties. * @param input Input. * @param body * @param [init] Init. */ - constructor(body?: IResponseBody, init?: IResponseInit) { + constructor( + injected: { window: IBrowserWindow; asyncTaskManager: AsyncTaskManager }, + body?: IResponseBody, + init?: IResponseInit + ) { + this.#window = injected.window; + this.#asyncTaskManager = injected.asyncTaskManager; this.status = init?.status !== undefined ? init.status : 200; this.statusText = init?.statusText || ''; this.ok = this.status >= 200 && this.status < 300; @@ -89,17 +99,17 @@ export default class Response implements IResponse { (this.bodyUsed) = true; - const taskID = this._asyncTaskManager.startTask(); + const taskID = this.#asyncTaskManager.startTask(); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); throw error; } - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); } @@ -131,17 +141,17 @@ export default class Response implements IResponse { (this.bodyUsed) = true; - const taskID = this._asyncTaskManager.startTask(); + const taskID = this.#asyncTaskManager.startTask(); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); throw error; } - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); return buffer; } @@ -161,20 +171,20 @@ export default class Response implements IResponse { (this.bodyUsed) = true; - if (!this._asyncTaskManager) { + if (!this.#asyncTaskManager) { debugger; } - const taskID = this._asyncTaskManager.startTask(); + const taskID = this.#asyncTaskManager.startTask(); let buffer: Buffer; try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); throw error; } - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); return new TextDecoder().decode(buffer); } @@ -196,7 +206,7 @@ export default class Response implements IResponse { */ public async formData(): Promise { const contentType = this.headers.get('content-type'); - const taskID = this._asyncTaskManager.startTask(); + const taskID = this.#asyncTaskManager.startTask(); if (contentType.startsWith('application/x-www-form-urlencoded')) { const formData = new FormData(); @@ -205,7 +215,7 @@ export default class Response implements IResponse { try { text = await this.text(); } catch (error) { - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); throw error; } @@ -215,7 +225,7 @@ export default class Response implements IResponse { formData.append(name, value); } - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); return formData; } @@ -225,11 +235,11 @@ export default class Response implements IResponse { try { formData = await MultipartFormDataParser.streamToFormData(this.body, contentType); } catch (error) { - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); throw error; } - this._asyncTaskManager.endTask(taskID); + this.#asyncTaskManager.endTask(taskID); return formData; } @@ -239,8 +249,8 @@ export default class Response implements IResponse { * * @returns Clone. */ - public clone(): IResponse { - const response = new (this.constructor)(); + public clone(): Response { + const response = new this.#window.Response(); (response.status) = this.status; (response.statusText) = this.statusText; @@ -252,7 +262,7 @@ export default class Response implements IResponse { (response.type) = this.type; (response.url) = this.url; - return response; + return response; } /** * Returns a redirect response. @@ -261,7 +271,7 @@ export default class Response implements IResponse { * @param status Status code. * @returns Response. */ - public static redirect(url: string, status = 302): IResponse { + public static redirect(url: string, status = 302): Response { if (!REDIRECT_STATUS_CODES.includes(status)) { throw new DOMException( 'Failed to create redirect response: Invalid redirect status code.', @@ -269,7 +279,7 @@ export default class Response implements IResponse { ); } - return new (this)(null, { + return new this._window.Response(null, { headers: { location: new URL(url).toString() }, @@ -284,8 +294,8 @@ export default class Response implements IResponse { * @param status Status code. * @returns Response. */ - public static error(): IResponse { - const response = new (this)(null, { status: 0, statusText: '' }); + public static error(): Response { + const response = new this._window.Response(null, { status: 0, statusText: '' }); (response.type) = 'error'; return response; } @@ -293,24 +303,25 @@ export default class Response implements IResponse { /** * Returns an JSON response. * + * @param injected Injected properties. * @param data Data. * @param [init] Init. * @returns Response. */ - public static json(data: object, init?: IResponseInit): IResponse { + public static json(data: object, init?: IResponseInit): Response { const body = JSON.stringify(data); if (body === undefined) { throw new TypeError('data is not JSON serializable'); } - const headers = new Headers(init && init.headers); + const headers = new this._window.Headers(init && init.headers); if (!headers.has('content-type')) { headers.set('content-type', 'application/json'); } - return new (this)(body, { + return new this._window.Response(body, { status: 200, ...init, headers diff --git a/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts index 6c4e7cb2c..4dd3b3466 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts @@ -1,6 +1,6 @@ import URL from '../../url/URL.js'; import IRequest from '../types/IRequest.js'; -import IDocument from '../../nodes/document/IDocument.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; import { isIP } from 'net'; import Headers from '../Headers.js'; import IRequestReferrerPolicy from '../types/IRequestReferrerPolicy.js'; @@ -29,22 +29,22 @@ export default class FetchRequestReferrerUtility { * https://github.com/node-fetch/node-fetch/blob/main/src/utils/referrer.js (MIT) * * @see https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer - * @param document Document. + * @param window Window. * @param request Request. * @returns Request referrer. */ public static getSentReferrer( - document: IDocument, + window: IBrowserWindow, request: IRequest ): '' | 'no-referrer' | 'client' | URL { - if (request.referrer === 'about:client' && document._defaultView.location.origin === 'null') { + if (request.referrer === 'about:client' && window.location.origin === 'null') { return 'no-referrer'; } const requestURL = new URL(request.url); const referrerURL = request.referrer === 'about:client' - ? new URL(document._defaultView.location.href) + ? new URL(window.location.href) : new URL(request.referrer); if (REQUEST_REFERRER_UNSUPPORTED_PROTOCOL_REGEXP.test(referrerURL.protocol)) { @@ -113,19 +113,19 @@ export default class FetchRequestReferrerUtility { /** * Returns initial referrer. * - * @param document Document. + * @param window Window. * @param referrer Referrer. * @returns Initial referrer. */ public static getInitialReferrer( - document: IDocument, + window: IBrowserWindow, referrer: '' | 'no-referrer' | 'client' | string | URL ): '' | 'no-referrer' | 'client' | URL { if (referrer === '' || referrer === 'no-referrer' || referrer === 'client') { return referrer; } else if (referrer) { - const referrerURL = referrer instanceof URL ? referrer : new URL(referrer, document.location); - return referrerURL.origin === document.location.origin ? referrerURL : 'client'; + const referrerURL = referrer instanceof URL ? referrer : new URL(referrer, window.location); + return referrerURL.origin === window.location.origin ? referrerURL : 'client'; } return 'client'; diff --git a/packages/happy-dom/src/file/FileReader.ts b/packages/happy-dom/src/file/FileReader.ts index de9057ef8..9cdeccbef 100644 --- a/packages/happy-dom/src/file/FileReader.ts +++ b/packages/happy-dom/src/file/FileReader.ts @@ -1,6 +1,6 @@ import WhatwgMIMEType from 'whatwg-mimetype'; import WhatwgEncoding from 'whatwg-encoding'; -import IDocument from '../nodes/document/IDocument.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; import ProgressEvent from '../event/events/ProgressEvent.js'; import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; @@ -18,9 +18,6 @@ import FileReaderEventTypeEnum from './FileReaderEventTypeEnum.js'; * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/file-api/FileReader-impl.js (MIT licensed). */ export default class FileReader extends EventTarget { - // Will be populated by a sub-class in Window. - public readonly _ownerDocument: IDocument; - public readonly error: Error = null; public readonly result: Buffer | ArrayBuffer | string = null; public readonly readyState: number = FileReaderReadyStateEnum.empty; @@ -30,9 +27,20 @@ export default class FileReader extends EventTarget { public readonly onloadstart: (event: ProgressEvent) => void = null; public readonly onloadend: (event: ProgressEvent) => void = null; public readonly onprogress: (event: ProgressEvent) => void = null; - private _isTerminated = false; - private _loadTimeout: NodeJS.Timeout = null; - private _parseTimeout: NodeJS.Timeout = null; + #isTerminated = false; + #loadTimeout: NodeJS.Timeout | null = null; + #parseTimeout: NodeJS.Timeout | null = null; + readonly #window: IBrowserWindow; + + /** + * Constructor. + * + * @param window Window. + */ + constructor(window: IBrowserWindow) { + super(); + this.#window = window; + } /** * Reads as ArrayBuffer. @@ -79,10 +87,8 @@ export default class FileReader extends EventTarget { * Aborts the file reader. */ public abort(): void { - const window = this._ownerDocument._defaultView; - - window.clearTimeout(this._loadTimeout); - window.clearTimeout(this._parseTimeout); + this.#window.clearTimeout(this.#loadTimeout); + this.#window.clearTimeout(this.#parseTimeout); if ( this.readyState === FileReaderReadyStateEnum.empty || @@ -97,7 +103,7 @@ export default class FileReader extends EventTarget { (this.result) = null; } - this._isTerminated = true; + this.#isTerminated = true; this.dispatchEvent(new ProgressEvent(FileReaderEventTypeEnum.abort)); this.dispatchEvent(new ProgressEvent(FileReaderEventTypeEnum.loadend)); } @@ -110,8 +116,6 @@ export default class FileReader extends EventTarget { * @param [encoding] Encoding. */ private _readFile(blob: Blob, format: FileReaderFormatEnum, encoding: string = null): void { - const window = this._ownerDocument._defaultView; - if (this.readyState === FileReaderReadyStateEnum.loading) { throw new DOMException( 'The object is in an invalid state.', @@ -121,9 +125,9 @@ export default class FileReader extends EventTarget { (this.readyState) = FileReaderReadyStateEnum.loading; - this._loadTimeout = window.setTimeout(() => { - if (this._isTerminated) { - this._isTerminated = false; + this.#loadTimeout = this.#window.setTimeout(() => { + if (this.#isTerminated) { + this.#isTerminated = false; return; } @@ -142,9 +146,9 @@ export default class FileReader extends EventTarget { }) ); - this._parseTimeout = window.setTimeout(() => { - if (this._isTerminated) { - this._isTerminated = false; + this.#parseTimeout = this.#window.setTimeout(() => { + if (this.#isTerminated) { + this.#isTerminated = false; return; } diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index c926ed59f..c1056b478 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -1,6 +1,8 @@ import GlobalWindow from './window/GlobalWindow.js'; import IWindow from './window/IWindow.js'; import Window from './window/Window.js'; +import IBrowserWindow from './window/IBrowserWindow.js'; +import BrowserWindow from './window/BrowserWindow.js'; import DataTransfer from './event/DataTransfer.js'; import DataTransferItem from './event/DataTransferItem.js'; import DataTransferItemList from './event/DataTransferItemList.js'; @@ -163,6 +165,8 @@ export { GlobalWindow, Window, IWindow, + BrowserWindow, + IBrowserWindow, DataTransfer, DataTransferItem, DataTransferItemList, diff --git a/packages/happy-dom/src/match-media/MediaQueryItem.ts b/packages/happy-dom/src/match-media/MediaQueryItem.ts index 6b0c10ccd..421e271a3 100644 --- a/packages/happy-dom/src/match-media/MediaQueryItem.ts +++ b/packages/happy-dom/src/match-media/MediaQueryItem.ts @@ -1,5 +1,5 @@ import CSSMeasurementConverter from '../css/declaration/measurement-converter/CSSMeasurementConverter.js'; -import IWindow from '../window/IWindow.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; import IMediaQueryRange from './IMediaQueryRange.js'; import IMediaQueryRule from './IMediaQueryRule.js'; @@ -14,7 +14,7 @@ export default class MediaQueryItem { public rules: IMediaQueryRule[]; public ranges: IMediaQueryRange[]; private rootFontSize: string | number | null = null; - private ownerWindow: IWindow; + private ownerWindow: IBrowserWindow; /** * Constructor. @@ -28,7 +28,7 @@ export default class MediaQueryItem { * @param [options.ranges] Ranges. */ constructor(options: { - ownerWindow: IWindow; + ownerWindow: IBrowserWindow; rootFontSize?: string | number | null; mediaTypes?: MediaQueryTypeEnum[]; not?: boolean; diff --git a/packages/happy-dom/src/match-media/MediaQueryList.ts b/packages/happy-dom/src/match-media/MediaQueryList.ts index 2b51e0e67..4e089350a 100644 --- a/packages/happy-dom/src/match-media/MediaQueryList.ts +++ b/packages/happy-dom/src/match-media/MediaQueryList.ts @@ -1,6 +1,6 @@ import EventTarget from '../event/EventTarget.js'; import Event from '../event/Event.js'; -import IWindow from '../window/IWindow.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; import IEventListener from '../event/IEventListener.js'; import MediaQueryListEvent from '../event/events/MediaQueryListEvent.js'; import IMediaQueryItem from './MediaQueryItem.js'; @@ -14,7 +14,7 @@ import MediaQueryParser from './MediaQueryParser.js'; */ export default class MediaQueryList extends EventTarget { public onchange: (event: Event) => void = null; - private _ownerWindow: IWindow; + private _ownerWindow: IBrowserWindow; private _items: IMediaQueryItem[] | null = null; private _media: string; private _rootFontSize: string | number | null = null; @@ -27,7 +27,11 @@ export default class MediaQueryList extends EventTarget { * @param options.media Media. * @param [options.rootFontSize] Root font size. */ - constructor(options: { ownerWindow: IWindow; media: string; rootFontSize?: string | number }) { + constructor(options: { + ownerWindow: IBrowserWindow; + media: string; + rootFontSize?: string | number; + }) { super(); this._ownerWindow = options.ownerWindow; this._media = options.media; diff --git a/packages/happy-dom/src/match-media/MediaQueryParser.ts b/packages/happy-dom/src/match-media/MediaQueryParser.ts index fd4566b97..3b9913070 100644 --- a/packages/happy-dom/src/match-media/MediaQueryParser.ts +++ b/packages/happy-dom/src/match-media/MediaQueryParser.ts @@ -1,6 +1,6 @@ import MediaQueryItem from './MediaQueryItem.js'; import MediaQueryTypeEnum from './MediaQueryTypeEnum.js'; -import IWindow from '../window/IWindow.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; /** * Media query RegExp. @@ -44,7 +44,7 @@ export default class MediaQueryParser { * @returns Media query items. */ public static parse(options: { - ownerWindow: IWindow; + ownerWindow: IBrowserWindow; mediaQuery: string; rootFontSize?: string | number | null; }): MediaQueryItem[] { diff --git a/packages/happy-dom/src/navigator/Navigator.ts b/packages/happy-dom/src/navigator/Navigator.ts index b3bb9dade..a8dd4b593 100644 --- a/packages/happy-dom/src/navigator/Navigator.ts +++ b/packages/happy-dom/src/navigator/Navigator.ts @@ -1,6 +1,6 @@ import MimeTypeArray from './MimeTypeArray.js'; import PluginArray from './PluginArray.js'; -import IWindow from '../window/IWindow.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; import Permissions from '../permissions/Permissions.js'; import Clipboard from '../clipboard/Clipboard.js'; import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; @@ -14,7 +14,7 @@ import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.j * https://html.spec.whatwg.org/multipage/system-state.html#dom-navigator. */ export default class Navigator { - #ownerWindow: IWindow; + #ownerWindow: IBrowserWindow; #clipboard: Clipboard; #permissions: Permissions; @@ -23,7 +23,7 @@ export default class Navigator { * * @param ownerWindow Owner window. */ - constructor(ownerWindow: IWindow) { + constructor(ownerWindow: IBrowserWindow) { this.#ownerWindow = ownerWindow; this.#clipboard = new Clipboard(ownerWindow); this.#permissions = new Permissions(); diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 54c306441..2992b652a 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -1,6 +1,6 @@ import Element from '../element/Element.js'; import HTMLUnknownElement from '../html-unknown-element/HTMLUnknownElement.js'; -import IWindow from '../../window/IWindow.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; import Node from '../node/Node.js'; import NodeIterator from '../../tree-walker/NodeIterator.js'; import TreeWalker from '../../tree-walker/TreeWalker.js'; @@ -42,9 +42,8 @@ import ElementUtility from '../element/ElementUtility.js'; import HTMLCollection from '../element/HTMLCollection.js'; import VisibilityStateEnum from './VisibilityStateEnum.js'; import NodeTypeEnum from '../node/NodeTypeEnum.js'; -import ICookieContainer from '../../cookie/types/ICookieContainer.js'; -import CookieContainer from '../../cookie/CookieContainer.js'; import CookieStringUtility from '../../cookie/urilities/CookieStringUtility.js'; +import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; @@ -52,14 +51,13 @@ const PROCESSING_INSTRUCTION_TARGET_REGEXP = /^[a-z][a-z0-9-]+$/; * Document. */ export default class Document extends Node implements IDocument { - // Needs to be injected by sub-class. - public readonly _defaultView: IWindow; public nodeType = Node.DOCUMENT_NODE; public adoptedStyleSheets: CSSStyleSheet[] = []; - public implementation = new DOMImplementation(this); + public readonly implementation: DOMImplementation; public readonly readyState = DocumentReadyStateEnum.interactive; public readonly isConnected: boolean = true; - public readonly defaultView: IWindow | null = null; + public readonly defaultView: IBrowserWindow | null = null; + public readonly _defaultView: IBrowserWindow; public readonly referrer = ''; public readonly _windowClass: {} | null = null; public readonly _children: IHTMLCollection = new HTMLCollection(); @@ -75,7 +73,7 @@ export default class Document extends Node implements IDocument { protected _isFirstWriteAfterOpen = false; private _selection: Selection = null; - #cookieContainer: ICookieContainer; + #browserFrame: IBrowserFrame; // Events public onreadystatechange: (event: Event) => void = null; @@ -188,6 +186,20 @@ export default class Document extends Node implements IDocument { public onpaste: (event: Event) => void = null; public onbeforematch: (event: Event) => void = null; + /** + * Constructor. + * + * @param injected Injected properties. + * @param injected.browserFrame Browser frame. + * @param injected.window Window. + */ + constructor(injected: { browserFrame: IBrowserFrame; window: IBrowserWindow }) { + super(); + this.#browserFrame = injected.browserFrame; + this.implementation = new DOMImplementation(injected.window); + this._defaultView = injected.window; + } + /** * Returns owner document. * @@ -292,11 +304,8 @@ export default class Document extends Node implements IDocument { * @returns Cookie. */ public get cookie(): string { - if (!this.#cookieContainer) { - this.#cookieContainer = new CookieContainer(); - } return CookieStringUtility.cookiesToString( - this.#cookieContainer.getCookies(this._defaultView.location, true) + this.#browserFrame.page.context.cookieContainer.getCookies(this._defaultView.location, true) ); } @@ -306,10 +315,7 @@ export default class Document extends Node implements IDocument { * @param cookie Cookie string. */ public set cookie(cookie: string) { - if (!this.#cookieContainer) { - this.#cookieContainer = new CookieContainer(); - } - this.#cookieContainer.addCookies([ + this.#browserFrame.page.context.cookieContainer.addCookies([ CookieStringUtility.stringToCookie(this._defaultView.location, cookie) ]); } diff --git a/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts b/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts index ee526daa9..0ba7269a1 100644 --- a/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts +++ b/packages/happy-dom/src/nodes/document/DocumentReadyStateManager.ts @@ -1,4 +1,4 @@ -import IWindow from '../../window/IWindow.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; /** * Document ready state manager. @@ -6,7 +6,7 @@ import IWindow from '../../window/IWindow.js'; export default class DocumentReadyStateManager { private totalTasks = 0; private readyStateCallbacks: (() => void)[] = []; - private window: IWindow = null; + private window: IBrowserWindow = null; private immediate: NodeJS.Immediate | null = null; private isComplete = false; @@ -15,7 +15,7 @@ export default class DocumentReadyStateManager { * * @param window */ - constructor(window: IWindow) { + constructor(window: IBrowserWindow) { this.window = window; } diff --git a/packages/happy-dom/src/nodes/document/IDocument.ts b/packages/happy-dom/src/nodes/document/IDocument.ts index 6b4417902..3a4948b3d 100644 --- a/packages/happy-dom/src/nodes/document/IDocument.ts +++ b/packages/happy-dom/src/nodes/document/IDocument.ts @@ -1,6 +1,6 @@ import IElement from '../element/IElement.js'; import IHTMLElement from '../html-element/IHTMLElement.js'; -import IWindow from '../../window/IWindow.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; import NodeIterator from '../../tree-walker/NodeIterator.js'; import TreeWalker from '../../tree-walker/TreeWalker.js'; import Event from '../../event/Event.js'; @@ -27,8 +27,8 @@ import VisibilityStateEnum from './VisibilityStateEnum.js'; * Document. */ export default interface IDocument extends IParentNode { - readonly defaultView: IWindow | null; - readonly _defaultView: IWindow; + readonly defaultView: IBrowserWindow | null; + readonly _defaultView: IBrowserWindow; readonly implementation: DOMImplementation; readonly documentElement: IHTMLElement; readonly doctype: IDocumentType; diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 49d0a84cc..a33afad5b 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -365,7 +365,10 @@ export default class Element extends Node implements IElement { for (let i = 0, max = this.attributes.length; i < max; i++) { const attribute = this.attributes[i]; clone.attributes.setNamedItem( - Object.assign(new this.ownerDocument._defaultView.Attr(), attribute) + Object.assign( + this.ownerDocument.createAttributeNS(attribute.namespaceURI, attribute.name), + attribute + ) ); } @@ -680,9 +683,8 @@ export default class Element extends Node implements IElement { /** * Attaches a shadow root. * - * @param _shadowRootInit Shadow root init. - * @param shadowRootInit - * @param shadowRootInit.mode + * @param shadowRootInit Shadow root init. + * @param shadowRootInit.mode Shadow root mode. * @returns Shadow root. */ public attachShadow(shadowRootInit: { mode: string }): IShadowRoot { diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index d3ab9b280..0ce7c18f2 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -1,12 +1,12 @@ import Event from '../../event/Event.js'; -import IWindow from '../../window/IWindow.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; import IDocument from '../document/IDocument.js'; import HTMLElement from '../html-element/HTMLElement.js'; import INode from '../node/INode.js'; import IHTMLIFrameElement from './IHTMLIFrameElement.js'; import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import HTMLIFrameElementNamedNodeMap from './HTMLIFrameElementNamedNodeMap.js'; -import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; +import ICrossOriginBrowserWindow from '../../window/ICrossOriginBrowserWindow.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import HTMLIFrameElementPageLoader from './HTMLIFrameElementPageLoader.js'; @@ -24,7 +24,7 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram public onerror: (event: Event) => void | null = null; // Internal properties - #contentWindowContainer: { window: IWindow | ICrossOriginWindow | null } = { + #contentWindowContainer: { window: IBrowserWindow | ICrossOriginBrowserWindow | null } = { window: null }; #pageLoader: HTMLIFrameElementPageLoader; @@ -176,7 +176,7 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram * @returns Content document. */ public get contentDocument(): IDocument | null { - return (this.#contentWindowContainer.window)?.document ?? null; + return (this.#contentWindowContainer.window)?.document ?? null; } /** @@ -184,7 +184,7 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram * * @returns Content window. */ - public get contentWindow(): IWindow | ICrossOriginWindow | null { + public get contentWindow(): IBrowserWindow | ICrossOriginBrowserWindow | null { return this.#contentWindowContainer.window; } diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts index cac96a562..4cb757234 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts @@ -1,9 +1,9 @@ import Event from '../../event/Event.js'; -import IWindow from '../../window/IWindow.js'; -import CrossOriginWindow from '../../window/CrossOriginWindow.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; +import CrossOriginBrowserWindow from '../../window/CrossOriginBrowserWindow.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; -import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; +import ICrossOriginBrowserWindow from '../../window/ICrossOriginBrowserWindow.js'; import IHTMLIFrameElement from './IHTMLIFrameElement.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; @@ -15,7 +15,7 @@ import BrowserFrameFactory from '../../browser/utilities/BrowserFrameFactory.js' */ export default class HTMLIFrameElementPageLoader { #element: IHTMLIFrameElement; - #contentWindowContainer: { window: IWindow | ICrossOriginWindow | null }; + #contentWindowContainer: { window: IBrowserWindow | ICrossOriginBrowserWindow | null }; #browserParentFrame: IBrowserFrame; #browserIFrame: IBrowserFrame; @@ -31,7 +31,7 @@ export default class HTMLIFrameElementPageLoader { constructor(options: { element: IHTMLIFrameElement; browserParentFrame: IBrowserFrame; - contentWindowContainer: { window: IWindow | ICrossOriginWindow | null }; + contentWindowContainer: { window: IBrowserWindow | ICrossOriginBrowserWindow | null }; }) { this.#element = options.element; this.#contentWindowContainer = options.contentWindowContainer; @@ -72,13 +72,15 @@ export default class HTMLIFrameElementPageLoader { // Iframes has a special rule for CORS and doesn't allow access between frames when the origin is different. const isSameOrigin = originURL.origin === targetURL.origin || targetURL.origin === 'null'; - const parentWindow = isSameOrigin ? window : new CrossOriginWindow(window); + const parentWindow = isSameOrigin ? window : new CrossOriginBrowserWindow(window); this.#browserIFrame = this.#browserIFrame ?? BrowserFrameFactory.newChildFrame(this.#browserParentFrame); - ((this.#browserIFrame.window.top)) = parentWindow; - ((this.#browserIFrame.window.parent)) = parentWindow; + ((this.#browserIFrame.window.top)) = + parentWindow; + ((this.#browserIFrame.window.parent)) = + parentWindow; this.#browserIFrame .goto(targetURL.href) @@ -87,7 +89,7 @@ export default class HTMLIFrameElementPageLoader { this.#contentWindowContainer.window = isSameOrigin ? this.#browserIFrame.window - : new CrossOriginWindow(this.#browserIFrame.window, window); + : new CrossOriginBrowserWindow(this.#browserIFrame.window, window); } /** diff --git a/packages/happy-dom/src/nodes/html-iframe-element/IHTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/IHTMLIFrameElement.ts index dc441ab88..a430b1ff8 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/IHTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/IHTMLIFrameElement.ts @@ -1,8 +1,8 @@ import Event from '../../event/Event.js'; -import IWindow from '../../window/IWindow.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; import IDocument from '../document/IDocument.js'; import IHTMLElement from '../html-element/IHTMLElement.js'; -import ICrossOriginWindow from '../../window/ICrossOriginWindow.js'; +import ICrossOriginBrowserWindow from '../../window/ICrossOriginBrowserWindow.js'; /** * HTML Iframe Element. @@ -19,7 +19,7 @@ export default interface IHTMLIFrameElement extends IHTMLElement { sandbox: string | null; srcdoc: string | null; readonly contentDocument: IDocument | null; - readonly contentWindow: IWindow | ICrossOriginWindow | null; + readonly contentWindow: IBrowserWindow | ICrossOriginBrowserWindow | null; // Events onload: (event: Event) => void | null; diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts index 2f2b74e4b..457ab2e2d 100644 --- a/packages/happy-dom/src/range/Range.ts +++ b/packages/happy-dom/src/range/Range.ts @@ -15,6 +15,7 @@ import IText from '../nodes/text/IText.js'; import DOMRectListFactory from '../nodes/element/DOMRectListFactory.js'; import IDOMRectList from '../nodes/element/IDOMRectList.js'; import IRangeBoundaryPoint from './IRangeBoundaryPoint.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; /** * Range. @@ -34,22 +35,21 @@ export default class Range { public readonly END_TO_START: number = RangeHowEnum.endToStart; public readonly START_TO_END: number = RangeHowEnum.startToEnd; public readonly START_TO_START: number = RangeHowEnum.startToStart; - public _start: IRangeBoundaryPoint = null; - public _end: IRangeBoundaryPoint = null; + #start: IRangeBoundaryPoint | null = null; + #end: IRangeBoundaryPoint | null = null; + #window: IBrowserWindow; + public readonly _ownerDocument: IDocument; /** * Constructor. + * + * @param window Window. */ - constructor() { - this._start = { node: this._ownerDocument, offset: 0 }; - this._end = { node: this._ownerDocument, offset: 0 }; - } - - /** - * Returns owner document. - */ - public get _ownerDocument(): IDocument { - throw new Error('_ownerDocument needs to be implemented by sub-class.'); + constructor(window: IBrowserWindow) { + this.#window = window; + this._ownerDocument = window.document; + this.#start = { node: window.document, offset: 0 }; + this.#end = { node: window.document, offset: 0 }; } /** @@ -59,7 +59,7 @@ export default class Range { * @returns Start container. */ public get startContainer(): INode { - return this._start.node; + return this.#start.node; } /** @@ -69,7 +69,7 @@ export default class Range { * @returns End container. */ public get endContainer(): INode { - return this._end.node; + return this.#end.node; } /** @@ -79,14 +79,14 @@ export default class Range { * @returns Start offset. */ public get startOffset(): number { - if (this._start.offset > 0) { - const length = NodeUtility.getNodeLength(this._start.node); - if (this._start.offset > length) { - this._start.offset = length; + if (this.#start.offset > 0) { + const length = NodeUtility.getNodeLength(this.#start.node); + if (this.#start.offset > length) { + this.#start.offset = length; } } - return this._start.offset; + return this.#start.offset; } /** @@ -96,14 +96,14 @@ export default class Range { * @returns End offset. */ public get endOffset(): number { - if (this._end.offset > 0) { - const length = NodeUtility.getNodeLength(this._end.node); - if (this._end.offset > length) { - this._end.offset = length; + if (this.#end.offset > 0) { + const length = NodeUtility.getNodeLength(this.#end.node); + if (this.#end.offset > length) { + this.#end.offset = length; } } - return this._end.offset; + return this.#end.offset; } /** @@ -113,7 +113,7 @@ export default class Range { * @returns Collapsed. */ public get collapsed(): boolean { - return this._start.node === this._end.node && this.startOffset === this.endOffset; + return this.#start.node === this.#end.node && this.startOffset === this.endOffset; } /** @@ -123,10 +123,10 @@ export default class Range { * @returns Node. */ public get commonAncestorContainer(): INode { - let container = this._start.node; + let container = this.#start.node; while (container) { - if (NodeUtility.isInclusiveAncestor(container, this._end.node)) { + if (NodeUtility.isInclusiveAncestor(container, this.#end.node)) { return container; } container = container.parentNode; @@ -143,9 +143,9 @@ export default class Range { */ public collapse(toStart = false): void { if (toStart) { - this._end = Object.assign({}, this._start); + this.#end = Object.assign({}, this.#start); } else { - this._start = Object.assign({}, this._end); + this.#start = Object.assign({}, this.#end); } } @@ -188,27 +188,27 @@ export default class Range { switch (how) { case RangeHowEnum.startToStart: - thisPoint.node = this._start.node; + thisPoint.node = this.#start.node; thisPoint.offset = this.startOffset; - sourcePoint.node = sourceRange._start.node; + sourcePoint.node = sourceRange.#start.node; sourcePoint.offset = sourceRange.startOffset; break; case RangeHowEnum.startToEnd: - thisPoint.node = this._end.node; + thisPoint.node = this.#end.node; thisPoint.offset = this.endOffset; - sourcePoint.node = sourceRange._start.node; + sourcePoint.node = sourceRange.#start.node; sourcePoint.offset = sourceRange.startOffset; break; case RangeHowEnum.endToEnd: - thisPoint.node = this._end.node; + thisPoint.node = this.#end.node; thisPoint.offset = this.endOffset; - sourcePoint.node = sourceRange._end.node; + sourcePoint.node = sourceRange.#end.node; sourcePoint.offset = sourceRange.endOffset; break; case RangeHowEnum.endToStart: - thisPoint.node = this._start.node; + thisPoint.node = this.#start.node; thisPoint.offset = this.startOffset; - sourcePoint.node = sourceRange._end.node; + sourcePoint.node = sourceRange.#end.node; sourcePoint.offset = sourceRange.endOffset; break; } @@ -238,14 +238,14 @@ export default class Range { if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._start.node, + node: this.#start.node, offset: this.startOffset }) === -1 ) { return -1; } else if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._end.node, + node: this.#end.node, offset: this.endOffset }) === 1 ) { @@ -271,24 +271,24 @@ export default class Range { } if ( - this._start.node === this._end.node && - (this._start.node.nodeType === NodeTypeEnum.textNode || - this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._start.node.nodeType === NodeTypeEnum.commentNode) + this.#start.node === this.#end.node && + (this.#start.node.nodeType === NodeTypeEnum.textNode || + this.#start.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.#start.node.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this._start.node).cloneNode(false); + const clone = (this.#start.node).cloneNode(false); clone['_data'] = clone.substringData(startOffset, endOffset - startOffset); fragment.appendChild(clone); return fragment; } - let commonAncestor = this._start.node; - while (!NodeUtility.isInclusiveAncestor(commonAncestor, this._end.node)) { + let commonAncestor = this.#start.node; + while (!NodeUtility.isInclusiveAncestor(commonAncestor, this.#end.node)) { commonAncestor = commonAncestor.parentNode; } let firstPartialContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { + if (!NodeUtility.isInclusiveAncestor(this.#start.node, this.#end.node)) { let candidate = commonAncestor.firstChild; while (!firstPartialContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -300,7 +300,7 @@ export default class Range { } let lastPartiallyContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this._end.node, this._start.node)) { + if (!NodeUtility.isInclusiveAncestor(this.#end.node, this.#start.node)) { let candidate = commonAncestor.lastChild; while (!lastPartiallyContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -331,10 +331,10 @@ export default class Range { firstPartialContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || firstPartialContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this._start.node).cloneNode(false); + const clone = (this.#start.node).cloneNode(false); clone['_data'] = clone.substringData( startOffset, - NodeUtility.getNodeLength(this._start.node) - startOffset + NodeUtility.getNodeLength(this.#start.node) - startOffset ); fragment.appendChild(clone); @@ -342,11 +342,11 @@ export default class Range { const clone = firstPartialContainedChild.cloneNode(); fragment.appendChild(clone); - const subRange = new (this.constructor)(); - subRange._start.node = this._start.node; - subRange._start.offset = startOffset; - subRange._end.node = firstPartialContainedChild; - subRange._end.offset = NodeUtility.getNodeLength(firstPartialContainedChild); + const subRange = new this.#window.Range(); + subRange.#start.node = this.#start.node; + subRange.#start.offset = startOffset; + subRange.#end.node = firstPartialContainedChild; + subRange.#end.offset = NodeUtility.getNodeLength(firstPartialContainedChild); const subDocumentFragment = subRange.cloneContents(); clone.appendChild(subDocumentFragment); @@ -363,7 +363,7 @@ export default class Range { lastPartiallyContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || lastPartiallyContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this._end.node).cloneNode(false); + const clone = (this.#end.node).cloneNode(false); clone['_data'] = clone.substringData(0, endOffset); fragment.appendChild(clone); @@ -371,11 +371,11 @@ export default class Range { const clone = lastPartiallyContainedChild.cloneNode(false); fragment.appendChild(clone); - const subRange = new (this.constructor)(); - subRange._start.node = lastPartiallyContainedChild; - subRange._start.offset = 0; - subRange._end.node = this._end.node; - subRange._end.offset = endOffset; + const subRange = new this.#window.Range(); + subRange.#start.node = lastPartiallyContainedChild; + subRange.#start.offset = 0; + subRange.#end.node = this.#end.node; + subRange.#end.offset = endOffset; const subFragment = subRange.cloneContents(); clone.appendChild(subFragment); @@ -391,12 +391,12 @@ export default class Range { * @returns Range. */ public cloneRange(): Range { - const clone = new (this.constructor)(); + const clone = new this.#window.Range(); - clone._start.node = this._start.node; - clone._start.offset = this._start.offset; - clone._end.node = this._end.node; - clone._end.offset = this._end.offset; + clone.#start.node = this.#start.node; + clone.#start.offset = this.#start.offset; + clone.#end.node = this.#end.node; + clone.#end.offset = this.#end.offset; return clone; } @@ -427,18 +427,18 @@ export default class Range { } if ( - this._start.node === this._end.node && - (this._start.node.nodeType === NodeTypeEnum.textNode || - this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._start.node.nodeType === NodeTypeEnum.commentNode) + this.#start.node === this.#end.node && + (this.#start.node.nodeType === NodeTypeEnum.textNode || + this.#start.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.#start.node.nodeType === NodeTypeEnum.commentNode) ) { - (this._start.node).replaceData(startOffset, endOffset - startOffset, ''); + (this.#start.node).replaceData(startOffset, endOffset - startOffset, ''); return; } const nodesToRemove = []; - let currentNode = this._start.node; - const endNode = NodeUtility.nextDescendantNode(this._end.node); + let currentNode = this.#start.node; + const endNode = NodeUtility.nextDescendantNode(this.#end.node); while (currentNode && currentNode !== endNode) { if ( RangeUtility.isContained(currentNode, this) && @@ -452,15 +452,15 @@ export default class Range { let newNode; let newOffset; - if (NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { - newNode = this._start.node; + if (NodeUtility.isInclusiveAncestor(this.#start.node, this.#end.node)) { + newNode = this.#start.node; newOffset = startOffset; } else { - let referenceNode = this._start.node; + let referenceNode = this.#start.node; while ( referenceNode && - !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this._end.node) + !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this.#end.node) ) { referenceNode = referenceNode.parentNode; } @@ -470,13 +470,13 @@ export default class Range { } if ( - this._start.node.nodeType === NodeTypeEnum.textNode || - this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._start.node.nodeType === NodeTypeEnum.commentNode + this.#start.node.nodeType === NodeTypeEnum.textNode || + this.#start.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.#start.node.nodeType === NodeTypeEnum.commentNode ) { - (this._start.node).replaceData( + (this.#start.node).replaceData( this.startOffset, - NodeUtility.getNodeLength(this._start.node) - this.startOffset, + NodeUtility.getNodeLength(this.#start.node) - this.startOffset, '' ); } @@ -487,17 +487,17 @@ export default class Range { } if ( - this._end.node.nodeType === NodeTypeEnum.textNode || - this._end.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._end.node.nodeType === NodeTypeEnum.commentNode + this.#end.node.nodeType === NodeTypeEnum.textNode || + this.#end.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.#end.node.nodeType === NodeTypeEnum.commentNode ) { - (this._end.node).replaceData(0, endOffset, ''); + (this.#end.node).replaceData(0, endOffset, ''); } - this._start.node = newNode; - this._start.offset = newOffset; - this._end.node = newNode; - this._end.offset = newOffset; + this.#start.node = newNode; + this.#start.offset = newOffset; + this.#end.node = newNode; + this.#end.offset = newOffset; } /** @@ -525,28 +525,28 @@ export default class Range { } if ( - this._start.node === this._end.node && - (this._start.node.nodeType === NodeTypeEnum.textNode || - this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._start.node.nodeType === NodeTypeEnum.commentNode) + this.#start.node === this.#end.node && + (this.#start.node.nodeType === NodeTypeEnum.textNode || + this.#start.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.#start.node.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this._start.node.cloneNode(false); + const clone = this.#start.node.cloneNode(false); clone['_data'] = clone.substringData(startOffset, endOffset - startOffset); fragment.appendChild(clone); - (this._start.node).replaceData(startOffset, endOffset - startOffset, ''); + (this.#start.node).replaceData(startOffset, endOffset - startOffset, ''); return fragment; } - let commonAncestor = this._start.node; - while (!NodeUtility.isInclusiveAncestor(commonAncestor, this._end.node)) { + let commonAncestor = this.#start.node; + while (!NodeUtility.isInclusiveAncestor(commonAncestor, this.#end.node)) { commonAncestor = commonAncestor.parentNode; } let firstPartialContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { + if (!NodeUtility.isInclusiveAncestor(this.#start.node, this.#end.node)) { let candidate = commonAncestor.firstChild; while (!firstPartialContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -558,7 +558,7 @@ export default class Range { } let lastPartiallyContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this._end.node, this._start.node)) { + if (!NodeUtility.isInclusiveAncestor(this.#end.node, this.#start.node)) { let candidate = commonAncestor.lastChild; while (!lastPartiallyContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -585,15 +585,15 @@ export default class Range { let newNode; let newOffset; - if (NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { - newNode = this._start.node; + if (NodeUtility.isInclusiveAncestor(this.#start.node, this.#end.node)) { + newNode = this.#start.node; newOffset = startOffset; } else { - let referenceNode = this._start.node; + let referenceNode = this.#start.node; while ( referenceNode && - !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this._end.node) + !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this.#end.node) ) { referenceNode = referenceNode.parentNode; } @@ -608,28 +608,28 @@ export default class Range { firstPartialContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || firstPartialContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this._start.node.cloneNode(false); + const clone = this.#start.node.cloneNode(false); clone['_data'] = clone.substringData( startOffset, - NodeUtility.getNodeLength(this._start.node) - startOffset + NodeUtility.getNodeLength(this.#start.node) - startOffset ); fragment.appendChild(clone); - (this._start.node).replaceData( + (this.#start.node).replaceData( startOffset, - NodeUtility.getNodeLength(this._start.node) - startOffset, + NodeUtility.getNodeLength(this.#start.node) - startOffset, '' ); } else if (firstPartialContainedChild !== null) { const clone = firstPartialContainedChild.cloneNode(false); fragment.appendChild(clone); - const subRange = new (this.constructor)(); - subRange._start.node = this._start.node; - subRange._start.offset = startOffset; - subRange._end.node = firstPartialContainedChild; - subRange._end.offset = NodeUtility.getNodeLength(firstPartialContainedChild); + const subRange = new this.#window.Range(); + subRange.#start.node = this.#start.node; + subRange.#start.offset = startOffset; + subRange.#end.node = firstPartialContainedChild; + subRange.#end.offset = NodeUtility.getNodeLength(firstPartialContainedChild); const subFragment = subRange.extractContents(); clone.appendChild(subFragment); @@ -645,30 +645,30 @@ export default class Range { lastPartiallyContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || lastPartiallyContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this._end.node.cloneNode(false); + const clone = this.#end.node.cloneNode(false); clone['_data'] = clone.substringData(0, endOffset); fragment.appendChild(clone); - (this._end.node).replaceData(0, endOffset, ''); + (this.#end.node).replaceData(0, endOffset, ''); } else if (lastPartiallyContainedChild !== null) { const clone = lastPartiallyContainedChild.cloneNode(false); fragment.appendChild(clone); - const subRange = new (this.constructor)(); - subRange._start.node = lastPartiallyContainedChild; - subRange._start.offset = 0; - subRange._end.node = this._end.node; - subRange._end.offset = endOffset; + const subRange = new this.#window.Range(); + subRange.#start.node = lastPartiallyContainedChild; + subRange.#start.offset = 0; + subRange.#end.node = this.#end.node; + subRange.#end.offset = endOffset; const subFragment = subRange.extractContents(); clone.appendChild(subFragment); } - this._start.node = newNode; - this._start.offset = newOffset; - this._end.node = newNode; - this._end.offset = newOffset; + this.#start.node = newNode; + this.#start.offset = newOffset; + this.#end.node = newNode; + this.#end.offset = newOffset; return fragment; } @@ -712,11 +712,11 @@ export default class Range { if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._start.node, + node: this.#start.node, offset: this.startOffset }) === -1 || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._end.node, + node: this.#end.node, offset: this.endOffset }) === 1 ) { @@ -734,22 +734,22 @@ export default class Range { */ public insertNode(newNode: INode): void { if ( - this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._start.node.nodeType === NodeTypeEnum.commentNode || - (this._start.node.nodeType === NodeTypeEnum.textNode && !this._start.node.parentNode) || - newNode === this._start.node + this.#start.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.#start.node.nodeType === NodeTypeEnum.commentNode || + (this.#start.node.nodeType === NodeTypeEnum.textNode && !this.#start.node.parentNode) || + newNode === this.#start.node ) { throw new DOMException('Invalid start node.', DOMExceptionNameEnum.hierarchyRequestError); } let referenceNode = - this._start.node.nodeType === NodeTypeEnum.textNode - ? this._start.node - : (this._start.node)._childNodes[this.startOffset] || null; - const parent = !referenceNode ? this._start.node : referenceNode.parentNode; + this.#start.node.nodeType === NodeTypeEnum.textNode + ? this.#start.node + : (this.#start.node)._childNodes[this.startOffset] || null; + const parent = !referenceNode ? this.#start.node : referenceNode.parentNode; - if (this._start.node.nodeType === NodeTypeEnum.textNode) { - referenceNode = (this._start.node).splitText(this.startOffset); + if (this.#start.node.nodeType === NodeTypeEnum.textNode) { + referenceNode = (this.#start.node).splitText(this.startOffset); } if (newNode === referenceNode) { @@ -772,8 +772,8 @@ export default class Range { parent.insertBefore(newNode, referenceNode); if (this.collapsed) { - this._end.node = parent; - this._end.offset = newOffset; + this.#end.node = parent; + this.#end.offset = newOffset; } } @@ -800,11 +800,11 @@ export default class Range { return ( RangeUtility.compareBoundaryPointsPosition( { node: parent, offset }, - { node: this._end.node, offset: this.endOffset } + { node: this.#end.node, offset: this.endOffset } ) === -1 && RangeUtility.compareBoundaryPointsPosition( { node: parent, offset: offset + 1 }, - { node: this._start.node, offset: this.startOffset } + { node: this.#start.node, offset: this.startOffset } ) === 1 ); } @@ -825,10 +825,10 @@ export default class Range { const index = (node.parentNode)._childNodes.indexOf(node); - this._start.node = node.parentNode; - this._start.offset = index; - this._end.node = node.parentNode; - this._end.offset = index + 1; + this.#start.node = node.parentNode; + this.#start.offset = index; + this.#end.node = node.parentNode; + this.#end.offset = index + 1; } /** @@ -845,10 +845,10 @@ export default class Range { ); } - this._start.node = node; - this._start.offset = 0; - this._end.node = node; - this._end.offset = NodeUtility.getNodeLength(node); + this.#start.node = node; + this.#start.offset = 0; + this.#end.node = node; + this.#end.offset = NodeUtility.getNodeLength(node); } /** @@ -866,16 +866,16 @@ export default class Range { if ( node.ownerDocument !== this._ownerDocument || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._start.node, + node: this.#start.node, offset: this.startOffset }) === -1 ) { - this._start.node = node; - this._start.offset = offset; + this.#start.node = node; + this.#start.offset = offset; } - this._end.node = node; - this._end.offset = offset; + this.#end.node = node; + this.#end.offset = offset; } /** @@ -893,16 +893,16 @@ export default class Range { if ( node.ownerDocument !== this._ownerDocument || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._end.node, + node: this.#end.node, offset: this.endOffset }) === 1 ) { - this._end.node = node; - this._end.offset = offset; + this.#end.node = node; + this.#end.offset = offset; } - this._start.node = node; - this._start.offset = offset; + this.#start.node = node; + this.#start.offset = offset; } /** @@ -1024,18 +1024,18 @@ export default class Range { let string = ''; if ( - this._start.node === this._end.node && - this._start.node.nodeType === NodeTypeEnum.textNode + this.#start.node === this.#end.node && + this.#start.node.nodeType === NodeTypeEnum.textNode ) { - return (this._start.node).data.slice(startOffset, endOffset); + return (this.#start.node).data.slice(startOffset, endOffset); } - if (this._start.node.nodeType === NodeTypeEnum.textNode) { - string += (this._start.node).data.slice(startOffset); + if (this.#start.node.nodeType === NodeTypeEnum.textNode) { + string += (this.#start.node).data.slice(startOffset); } - const endNode = NodeUtility.nextDescendantNode(this._end.node); - let currentNode = this._start.node; + const endNode = NodeUtility.nextDescendantNode(this.#end.node); + let currentNode = this.#start.node; while (currentNode && currentNode !== endNode) { if ( @@ -1048,8 +1048,8 @@ export default class Range { currentNode = NodeUtility.following(currentNode); } - if (this._end.node.nodeType === NodeTypeEnum.textNode) { - string += (this._end.node).data.slice(0, endOffset); + if (this.#end.node.nodeType === NodeTypeEnum.textNode) { + string += (this.#end.node).data.slice(0, endOffset); } return string; diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts new file mode 100644 index 000000000..b20f5d212 --- /dev/null +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -0,0 +1,1053 @@ +import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; +import Document from '../nodes/document/Document.js'; +import HTMLDocument from '../nodes/html-document/HTMLDocument.js'; +import XMLDocument from '../nodes/xml-document/XMLDocument.js'; +import SVGDocument from '../nodes/svg-document/SVGDocument.js'; +import Node from '../nodes/node/Node.js'; +import NodeFilter from '../tree-walker/NodeFilter.js'; +import Text from '../nodes/text/Text.js'; +import Comment from '../nodes/comment/Comment.js'; +import ShadowRoot from '../nodes/shadow-root/ShadowRoot.js'; +import Element from '../nodes/element/Element.js'; +import HTMLTemplateElement from '../nodes/html-template-element/HTMLTemplateElement.js'; +import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js'; +import HTMLElement from '../nodes/html-element/HTMLElement.js'; +import HTMLUnknownElement from '../nodes/html-unknown-element/HTMLUnknownElement.js'; +import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; +import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement.js'; +import HTMLTextAreaElement from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; +import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement.js'; +import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement.js'; +import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js'; +import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js'; +import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement.js'; +import HTMLMediaElement from '../nodes/html-media-element/HTMLMediaElement.js'; +import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; +import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement.js'; +import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js'; +import HTMLIFrameElement from '../nodes/html-iframe-element/HTMLIFrameElement.js'; +import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; +import SVGSVGElement from '../nodes/svg-element/SVGSVGElement.js'; +import SVGElement from '../nodes/svg-element/SVGElement.js'; +import SVGGraphicsElement from '../nodes/svg-element/SVGGraphicsElement.js'; +import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js'; +import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; +import CharacterData from '../nodes/character-data/CharacterData.js'; +import DocumentType from '../nodes/document-type/DocumentType.js'; +import NodeIterator from '../tree-walker/NodeIterator.js'; +import TreeWalker from '../tree-walker/TreeWalker.js'; +import Event from '../event/Event.js'; +import CustomEvent from '../event/events/CustomEvent.js'; +import AnimationEvent from '../event/events/AnimationEvent.js'; +import KeyboardEvent from '../event/events/KeyboardEvent.js'; +import MessageEvent from '../event/events/MessageEvent.js'; +import ProgressEvent from '../event/events/ProgressEvent.js'; +import MediaQueryListEvent from '../event/events/MediaQueryListEvent.js'; +import EventTarget from '../event/EventTarget.js'; +import MessagePort from '../event/MessagePort.js'; +import { URLSearchParams } from 'url'; +import URL from '../url/URL.js'; +import Location from '../location/Location.js'; +import MutationObserver from '../mutation-observer/MutationObserver.js'; +import MutationRecord from '../mutation-observer/MutationRecord.js'; +import XMLSerializer from '../xml-serializer/XMLSerializer.js'; +import ResizeObserver from '../resize-observer/ResizeObserver.js'; +import Blob from '../file/Blob.js'; +import File from '../file/File.js'; +import DOMException from '../exception/DOMException.js'; +import History from '../history/History.js'; +import CSSStyleSheet from '../css/CSSStyleSheet.js'; +import CSSStyleDeclaration from '../css/declaration/CSSStyleDeclaration.js'; +import CSS from '../css/CSS.js'; +import CSSUnitValue from '../css/CSSUnitValue.js'; +import CSSRule from '../css/CSSRule.js'; +import CSSContainerRule from '../css/rules/CSSContainerRule.js'; +import CSSFontFaceRule from '../css/rules/CSSFontFaceRule.js'; +import CSSKeyframeRule from '../css/rules/CSSKeyframeRule.js'; +import CSSKeyframesRule from '../css/rules/CSSKeyframesRule.js'; +import CSSMediaRule from '../css/rules/CSSMediaRule.js'; +import CSSStyleRule from '../css/rules/CSSStyleRule.js'; +import CSSSupportsRule from '../css/rules/CSSSupportsRule.js'; +import MouseEvent from '../event/events/MouseEvent.js'; +import PointerEvent from '../event/events/PointerEvent.js'; +import FocusEvent from '../event/events/FocusEvent.js'; +import WheelEvent from '../event/events/WheelEvent.js'; +import DataTransfer from '../event/DataTransfer.js'; +import DataTransferItem from '../event/DataTransferItem.js'; +import DataTransferItemList from '../event/DataTransferItemList.js'; +import InputEvent from '../event/events/InputEvent.js'; +import UIEvent from '../event/UIEvent.js'; +import ErrorEvent from '../event/events/ErrorEvent.js'; +import StorageEvent from '../event/events/StorageEvent.js'; +import SubmitEvent from '../event/events/SubmitEvent.js'; +import Screen from '../screen/Screen.js'; +import Response from '../fetch/Response.js'; +import IResponse from '../fetch/types/IResponse.js'; +import IRequestInit from '../fetch/types/IRequestInit.js'; +import Storage from '../storage/Storage.js'; +import HTMLCollection from '../nodes/element/HTMLCollection.js'; +import HTMLFormControlsCollection from '../nodes/html-form-element/HTMLFormControlsCollection.js'; +import NodeList from '../nodes/node/NodeList.js'; +import MediaQueryList from '../match-media/MediaQueryList.js'; +import Selection from '../selection/Selection.js'; +import Navigator from '../navigator/Navigator.js'; +import MimeType from '../navigator/MimeType.js'; +import MimeTypeArray from '../navigator/MimeTypeArray.js'; +import Plugin from '../navigator/Plugin.js'; +import PluginArray from '../navigator/PluginArray.js'; +import Fetch from '../fetch/Fetch.js'; +import DOMRect from '../nodes/element/DOMRect.js'; +import VMGlobalPropertyScript from './VMGlobalPropertyScript.js'; +import * as PerfHooks from 'perf_hooks'; +import VM from 'vm'; +import { Buffer } from 'buffer'; +import { webcrypto } from 'crypto'; +import XMLHttpRequestUpload from '../xml-http-request/XMLHttpRequestUpload.js'; +import XMLHttpRequestEventTarget from '../xml-http-request/XMLHttpRequestEventTarget.js'; +import Base64 from '../base64/Base64.js'; +import Attr from '../nodes/attr/Attr.js'; +import NamedNodeMap from '../named-node-map/NamedNodeMap.js'; +import IElement from '../nodes/element/IElement.js'; +import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction.js'; +import RequestInfo from '../fetch/types/IRequestInfo.js'; +import FileList from '../nodes/html-input-element/FileList.js'; +import Stream from 'stream'; +import FormData from '../form-data/FormData.js'; +import AbortController from '../fetch/AbortController.js'; +import AbortSignal from '../fetch/AbortSignal.js'; +import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; +import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; +import ValidityState from '../validity-state/ValidityState.js'; +import WindowErrorUtility from './WindowErrorUtility.js'; +import ICrossOriginBrowserWindow from './ICrossOriginBrowserWindow.js'; +import Permissions from '../permissions/Permissions.js'; +import PermissionStatus from '../permissions/PermissionStatus.js'; +import Clipboard from '../clipboard/Clipboard.js'; +import ClipboardItem from '../clipboard/ClipboardItem.js'; +import ClipboardEvent from '../event/events/ClipboardEvent.js'; +import Headers from '../fetch/Headers.js'; +import WindowClassFactory from './WindowClassFactory.js'; +import Audio from '../nodes/html-audio-element/Audio.js'; +import Image from '../nodes/html-image-element/Image.js'; +import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; +import DOMParser from '../dom-parser/DOMParser.js'; +import FileReader from '../file/FileReader.js'; +import Request from '../fetch/Request.js'; +import Range from '../range/Range.js'; +import XMLHttpRequest from '../xml-http-request/XMLHttpRequest.js'; +import WindowBrowserSettingsReader from './WindowBrowserSettingsReader.js'; +import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; +import DocumentReadyStateEnum from '../nodes/document/DocumentReadyStateEnum.js'; +import IBrowserFrame from '../browser/types/IBrowserFrame.js'; +import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; +import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; +import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; +import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; +import WindowPageOpenUtility from './WindowPageOpenUtility.js'; +import IResponseBody from '../fetch/types/IResponseBody.js'; +import IResponseInit from '../fetch/types/IResponseInit.js'; +import IRequestInfo from '../fetch/types/IRequestInfo.js'; +import IBrowserWindow from './IBrowserWindow.js'; + +const ORIGINAL_SET_TIMEOUT = setTimeout; +const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; +const ORIGINAL_SET_INTERVAL = setInterval; +const ORIGINAL_CLEAR_INTERVAL = clearInterval; +const ORIGINAL_QUEUE_MICROTASK = queueMicrotask; + +/** + * Browser window. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/Window. + */ +export default class BrowserWindow extends EventTarget implements IBrowserWindow { + // Nodes + public readonly Node: typeof Node; + public readonly Attr: typeof Attr; + public readonly SVGSVGElement: typeof SVGSVGElement; + public readonly SVGElement: typeof SVGElement; + public readonly SVGGraphicsElement: typeof SVGGraphicsElement; + public readonly Text: typeof Text; + public readonly Comment: typeof Comment; + public readonly ShadowRoot: typeof ShadowRoot; + public readonly ProcessingInstruction: typeof ProcessingInstruction; + public readonly Element: typeof Element; + public readonly CharacterData: typeof CharacterData; + public readonly Document: new () => Document; + public readonly HTMLDocument: new () => HTMLDocument; + public readonly XMLDocument: new () => XMLDocument; + public readonly SVGDocument: new () => SVGDocument; + public readonly DocumentType: typeof DocumentType; + + // Element classes + public readonly HTMLAnchorElement: typeof HTMLAnchorElement; + public readonly HTMLButtonElement: typeof HTMLButtonElement; + public readonly HTMLOptGroupElement: typeof HTMLOptGroupElement; + public readonly HTMLOptionElement: typeof HTMLOptionElement; + public readonly HTMLElement: typeof HTMLElement; + public readonly HTMLUnknownElement: typeof HTMLUnknownElement; + public readonly HTMLTemplateElement: typeof HTMLTemplateElement; + public readonly HTMLFormElement: typeof HTMLFormElement; + public readonly HTMLInputElement: typeof HTMLInputElement; + public readonly HTMLSelectElement: typeof HTMLSelectElement; + public readonly HTMLTextAreaElement: typeof HTMLTextAreaElement; + public readonly HTMLImageElement: typeof HTMLImageElement; + public readonly HTMLScriptElement: typeof HTMLScriptElement; + public readonly HTMLLinkElement: typeof HTMLLinkElement; + public readonly HTMLStyleElement: typeof HTMLStyleElement; + public readonly HTMLLabelElement: typeof HTMLLabelElement; + public readonly HTMLSlotElement: typeof HTMLSlotElement; + public readonly HTMLMetaElement: typeof HTMLMetaElement; + public readonly HTMLMediaElement: typeof HTMLMediaElement; + public readonly HTMLAudioElement: typeof HTMLAudioElement; + public readonly HTMLVideoElement: typeof HTMLVideoElement; + public readonly HTMLBaseElement: typeof HTMLBaseElement; + public readonly HTMLIFrameElement: typeof HTMLIFrameElement; + public readonly HTMLDialogElement: typeof HTMLDialogElement; + + // Non-implemented element classes + public readonly HTMLHeadElement: typeof HTMLElement; + public readonly HTMLTitleElement: typeof HTMLElement; + public readonly HTMLBodyElement: typeof HTMLElement; + public readonly HTMLHeadingElement: typeof HTMLElement; + public readonly HTMLParagraphElement: typeof HTMLElement; + public readonly HTMLHRElement: typeof HTMLElement; + public readonly HTMLPreElement: typeof HTMLElement; + public readonly HTMLUListElement: typeof HTMLElement; + public readonly HTMLOListElement: typeof HTMLElement; + public readonly HTMLLIElement: typeof HTMLElement; + public readonly HTMLMenuElement: typeof HTMLElement; + public readonly HTMLDListElement: typeof HTMLElement; + public readonly HTMLDivElement: typeof HTMLElement; + public readonly HTMLAreaElement: typeof HTMLElement; + public readonly HTMLBRElement: typeof HTMLElement; + public readonly HTMLCanvasElement: typeof HTMLElement; + public readonly HTMLDataElement: typeof HTMLElement; + public readonly HTMLDataListElement: typeof HTMLElement; + public readonly HTMLDetailsElement: typeof HTMLElement; + public readonly HTMLDirectoryElement: typeof HTMLElement; + public readonly HTMLFieldSetElement: typeof HTMLElement; + public readonly HTMLFontElement: typeof HTMLElement; + public readonly HTMLHtmlElement: typeof HTMLElement; + public readonly HTMLLegendElement: typeof HTMLElement; + public readonly HTMLMapElement: typeof HTMLElement; + public readonly HTMLMarqueeElement: typeof HTMLElement; + public readonly HTMLMeterElement: typeof HTMLElement; + public readonly HTMLModElement: typeof HTMLElement; + public readonly HTMLOutputElement: typeof HTMLElement; + public readonly HTMLPictureElement: typeof HTMLElement; + public readonly HTMLProgressElement: typeof HTMLElement; + public readonly HTMLQuoteElement: typeof HTMLElement; + public readonly HTMLSourceElement: typeof HTMLElement; + public readonly HTMLSpanElement: typeof HTMLElement; + public readonly HTMLTableCaptionElement: typeof HTMLElement; + public readonly HTMLTableCellElement: typeof HTMLElement; + public readonly HTMLTableColElement: typeof HTMLElement; + public readonly HTMLTableElement: typeof HTMLElement; + public readonly HTMLTimeElement: typeof HTMLElement; + public readonly HTMLTableRowElement: typeof HTMLElement; + public readonly HTMLTableSectionElement: typeof HTMLElement; + public readonly HTMLFrameElement: typeof HTMLElement; + public readonly HTMLFrameSetElement: typeof HTMLElement; + public readonly HTMLEmbedElement: typeof HTMLElement; + public readonly HTMLObjectElement: typeof HTMLElement; + public readonly HTMLParamElement: typeof HTMLElement; + public readonly HTMLTrackElement: typeof HTMLElement; + + // Events classes + public readonly Event = Event; + public readonly UIEvent = UIEvent; + public readonly CustomEvent = CustomEvent; + public readonly AnimationEvent = AnimationEvent; + public readonly KeyboardEvent = KeyboardEvent; + public readonly MessageEvent = MessageEvent; + public readonly MouseEvent = MouseEvent; + public readonly PointerEvent = PointerEvent; + public readonly FocusEvent = FocusEvent; + public readonly WheelEvent = WheelEvent; + public readonly InputEvent = InputEvent; + public readonly ErrorEvent = ErrorEvent; + public readonly StorageEvent = StorageEvent; + public readonly SubmitEvent = SubmitEvent; + public readonly ProgressEvent = ProgressEvent; + public readonly MediaQueryListEvent = MediaQueryListEvent; + public readonly ClipboardEvent = ClipboardEvent; + + // Non-implemented event classes + public readonly AudioProcessingEvent = Event; + public readonly BeforeInputEvent = Event; + public readonly BeforeUnloadEvent = Event; + public readonly BlobEvent = Event; + public readonly CloseEvent = Event; + public readonly CompositionEvent = Event; + public readonly CSSFontFaceLoadEvent = Event; + public readonly DeviceLightEvent = Event; + public readonly DeviceMotionEvent = Event; + public readonly DeviceOrientationEvent = Event; + public readonly DeviceProximityEvent = Event; + public readonly DOMTransactionEvent = Event; + public readonly DragEvent = Event; + public readonly EditingBeforeInputEvent = Event; + public readonly FetchEvent = Event; + public readonly GamepadEvent = Event; + public readonly HashChangeEvent = Event; + public readonly IDBVersionChangeEvent = Event; + public readonly MediaStreamEvent = Event; + public readonly MutationEvent = Event; + public readonly OfflineAudioCompletionEvent = Event; + public readonly OverconstrainedError = Event; + public readonly PageTransitionEvent = Event; + public readonly PaymentRequestUpdateEvent = Event; + public readonly PopStateEvent = Event; + public readonly RelatedEvent = Event; + public readonly RTCDataChannelEvent = Event; + public readonly RTCIdentityErrorEvent = Event; + public readonly RTCIdentityEvent = Event; + public readonly RTCPeerConnectionIceEvent = Event; + public readonly SensorEvent = Event; + public readonly SVGEvent = Event; + public readonly SVGZoomEvent = Event; + public readonly TimeEvent = Event; + public readonly TouchEvent = Event; + public readonly TrackEvent = Event; + public readonly TransitionEvent = Event; + public readonly UserProximityEvent = Event; + public readonly WebGLContextEvent = Event; + public readonly TextEvent = Event; + + // Other classes + public readonly NamedNodeMap = NamedNodeMap; + public readonly NodeFilter = NodeFilter; + public readonly NodeIterator = NodeIterator; + public readonly TreeWalker = TreeWalker; + public readonly MutationObserver = MutationObserver; + public readonly MutationRecord = MutationRecord; + public readonly EventTarget = EventTarget; + public readonly MessagePort = MessagePort; + public readonly DataTransfer = DataTransfer; + public readonly DataTransferItem = DataTransferItem; + public readonly DataTransferItemList = DataTransferItemList; + public readonly URL = URL; + public readonly Location = Location; + public readonly CustomElementRegistry = CustomElementRegistry; + public readonly Window = this.constructor; + public readonly XMLSerializer = XMLSerializer; + public readonly ResizeObserver = ResizeObserver; + public readonly CSSStyleSheet = CSSStyleSheet; + public readonly Blob = Blob; + public readonly File = File; + public readonly DOMException = DOMException; + public readonly History = History; + public readonly Screen = Screen; + public readonly Storage = Storage; + public readonly URLSearchParams = URLSearchParams; + public readonly HTMLCollection = HTMLCollection; + public readonly HTMLFormControlsCollection = HTMLFormControlsCollection; + public readonly NodeList = NodeList; + public readonly CSSUnitValue = CSSUnitValue; + public readonly CSSRule = CSSRule; + public readonly CSSContainerRule = CSSContainerRule; + public readonly CSSFontFaceRule = CSSFontFaceRule; + public readonly CSSKeyframeRule = CSSKeyframeRule; + public readonly CSSKeyframesRule = CSSKeyframesRule; + public readonly CSSMediaRule = CSSMediaRule; + public readonly CSSStyleRule = CSSStyleRule; + public readonly CSSSupportsRule = CSSSupportsRule; + public readonly Selection = Selection; + public readonly Navigator = Navigator; + public readonly MimeType = MimeType; + public readonly MimeTypeArray = MimeTypeArray; + public readonly Plugin = Plugin; + public readonly PluginArray = PluginArray; + public readonly FileList = FileList; + public readonly DOMRect = DOMRect; + public readonly RadioNodeList = RadioNodeList; + public readonly ValidityState = ValidityState; + public readonly Headers = Headers; + public readonly Request: new (input: IRequestInfo, init?: IRequestInit) => Request; + public readonly Response: new (body?: IResponseBody, init?: IResponseInit) => Response; + public readonly XMLHttpRequestUpload = XMLHttpRequestUpload; + public readonly XMLHttpRequestEventTarget = XMLHttpRequestEventTarget; + public readonly ReadableStream = Stream.Readable; + public readonly WritableStream = Stream.Writable; + public readonly TransformStream = Stream.Transform; + public readonly AbortController = AbortController; + public readonly AbortSignal = AbortSignal; + public readonly FormData = FormData; + public readonly Permissions = Permissions; + public readonly PermissionStatus = PermissionStatus; + public readonly Clipboard = Clipboard; + public readonly ClipboardItem = ClipboardItem; + public readonly XMLHttpRequest: new () => XMLHttpRequest; + public readonly DOMParser: new () => DOMParser; + public readonly Range: new () => Range; + public readonly FileReader: new () => FileReader; + public readonly Image: typeof Image; + public readonly DocumentFragment: typeof DocumentFragment; + public readonly Audio: typeof Audio; + + // Events + public onload: ((event: Event) => void) | null = null; + public onerror: ((event: ErrorEvent) => void) | null = null; + + // Public properties. + public readonly document: Document; + public readonly customElements: CustomElementRegistry; + public readonly location: Location; + public readonly history: History; + public readonly navigator: Navigator; + public readonly opener: IBrowserWindow | null = null; + public readonly self: IBrowserWindow = this; + public readonly top: IBrowserWindow = this; + public readonly parent: IBrowserWindow = this; + public readonly window: IBrowserWindow = this; + public readonly globalThis: IBrowserWindow = this; + public readonly screen: Screen; + public readonly devicePixelRatio = 1; + public readonly sessionStorage: Storage; + public readonly localStorage: Storage; + public readonly performance = PerfHooks.performance; + public readonly innerWidth: number = 1024; + public readonly innerHeight: number = 768; + public readonly outerWidth: number = 1024; + public readonly outerHeight: number = 768; + public readonly screenLeft: number = 0; + public readonly screenTop: number = 0; + public readonly screenX: number = 0; + public readonly screenY: number = 0; + public readonly crypto = webcrypto; + public readonly closed = false; + public name: string = ''; + + // Node.js Globals + public Array: typeof Array; + public ArrayBuffer: typeof ArrayBuffer; + public Boolean: typeof Boolean; + public Buffer = Buffer; + public DataView: typeof DataView; + public Date: typeof Date; + public Error: typeof Error; + public EvalError: typeof EvalError; + public Float32Array: typeof Float32Array; + public Float64Array: typeof Float64Array; + public Function: typeof Function; + public Infinity: typeof Infinity; + public Int16Array: typeof Int16Array; + public Int32Array: typeof Int32Array; + public Int8Array: typeof Int8Array; + public Intl: typeof Intl; + public JSON: typeof JSON; + public Map: MapConstructor; + public Math: typeof Math; + public NaN: typeof NaN; + public Number: typeof Number; + public Object: typeof Object; + public Promise: typeof Promise; + public RangeError: typeof RangeError; + public ReferenceError: typeof ReferenceError; + public RegExp: typeof RegExp; + public Set: SetConstructor; + public String: typeof String; + public Symbol: Function; + public SyntaxError: typeof SyntaxError; + public TypeError: typeof TypeError; + public URIError: typeof URIError; + public Uint16Array: typeof Uint16Array; + public Uint32Array: typeof Uint32Array; + public Uint8Array: typeof Uint8Array; + public Uint8ClampedArray: typeof Uint8ClampedArray; + public WeakMap: WeakMapConstructor; + public WeakSet: WeakSetConstructor; + public decodeURI: typeof decodeURI; + public decodeURIComponent: typeof decodeURIComponent; + public encodeURI: typeof encodeURI; + public encodeURIComponent: typeof encodeURIComponent; + public eval: typeof eval; + /** + * @deprecated + */ + public escape: (str: string) => string; + public global: typeof globalThis; + public isFinite: typeof isFinite; + public isNaN: typeof isNaN; + public parseFloat: typeof parseFloat; + public parseInt: typeof parseInt; + public undefined: typeof undefined; + /** + * @deprecated + */ + public unescape: (str: string) => string; + public gc: () => void; + public v8debug?: unknown; + + // Public internal properties + + // Used for tracking capture event listeners to improve performance when they are not used. + // See EventTarget class. + public _captureEventListenerCount: { [eventType: string]: number } = {}; + public readonly _readyStateManager = new DocumentReadyStateManager(this); + + // Private properties + #setTimeout: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; + #clearTimeout: (id: NodeJS.Timeout) => void; + #setInterval: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; + #clearInterval: (id: NodeJS.Timeout) => void; + #queueMicrotask: (callback: Function) => void; + #browserFrame: IBrowserFrame; + + /** + * Constructor. + * + * @param browserFrame Browser frame. + */ + constructor(browserFrame: IBrowserFrame) { + super(); + + this.#browserFrame = browserFrame; + this.#setTimeout = ORIGINAL_SET_TIMEOUT; + this.#clearTimeout = ORIGINAL_CLEAR_TIMEOUT; + this.#setInterval = ORIGINAL_SET_INTERVAL; + this.#clearInterval = ORIGINAL_CLEAR_INTERVAL; + this.#queueMicrotask = ORIGINAL_QUEUE_MICROTASK; + + this.customElements = new CustomElementRegistry(); + this.navigator = new Navigator(this); + this.history = new History(); + this.screen = new Screen(); + this.sessionStorage = new Storage(); + this.localStorage = new Storage(); + + WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); + + this.location = new Location(this.#browserFrame, 'about:blank'); + + // Binds all methods to "this", so that it will use the correct context when called globally. + for (const key of Object.getOwnPropertyNames(BrowserWindow.prototype).concat( + Object.getOwnPropertyNames(EventTarget.prototype) + )) { + if ( + key !== 'constructor' && + key[0] !== '_' && + key[0] === key[0].toLowerCase() && + typeof this[key] === 'function' + ) { + this[key] = this[key].bind(this); + } + } + + this._setupVMContext(); + + const classes = WindowClassFactory.getClasses({ + window: this, + browserFrame: this.#browserFrame + }); + + // Classes that require the window to be injected + this.Response = classes.Response; + this.Request = classes.Request; + this.Image = classes.Image; + this.DocumentFragment = classes.DocumentFragment; + this.FileReader = classes.FileReader; + this.DOMParser = classes.DOMParser; + this.XMLHttpRequest = classes.XMLHttpRequest; + this.Range = classes.Range; + this.Audio = classes.Audio; + + // Nodes + this.Node = classes.Node; + this.Attr = classes.Attr; + this.SVGSVGElement = classes.SVGSVGElement; + this.SVGElement = classes.SVGElement; + this.SVGGraphicsElement = classes.SVGGraphicsElement; + this.Text = classes.Text; + this.Comment = classes.Comment; + this.ShadowRoot = classes.ShadowRoot; + this.ProcessingInstruction = classes.ProcessingInstruction; + this.Element = classes.Element; + this.CharacterData = classes.CharacterData; + this.Document = classes.Document; + this.HTMLDocument = classes.HTMLDocument; + this.XMLDocument = classes.XMLDocument; + this.SVGDocument = classes.SVGDocument; + this.DocumentType = classes.DocumentType; + + // HTML Element classes + this.HTMLAnchorElement = classes.HTMLAnchorElement; + this.HTMLButtonElement = classes.HTMLButtonElement; + this.HTMLOptGroupElement = classes.HTMLOptGroupElement; + this.HTMLOptionElement = classes.HTMLOptionElement; + this.HTMLElement = classes.HTMLElement; + this.HTMLUnknownElement = classes.HTMLUnknownElement; + this.HTMLTemplateElement = classes.HTMLTemplateElement; + this.HTMLFormElement = classes.HTMLFormElement; + this.HTMLInputElement = classes.HTMLInputElement; + this.HTMLSelectElement = classes.HTMLSelectElement; + this.HTMLTextAreaElement = classes.HTMLTextAreaElement; + this.HTMLImageElement = classes.HTMLImageElement; + this.HTMLScriptElement = classes.HTMLScriptElement; + this.HTMLLinkElement = classes.HTMLLinkElement; + this.HTMLStyleElement = classes.HTMLStyleElement; + this.HTMLLabelElement = classes.HTMLLabelElement; + this.HTMLSlotElement = classes.HTMLSlotElement; + this.HTMLMetaElement = classes.HTMLMetaElement; + this.HTMLMediaElement = classes.HTMLMediaElement; + this.HTMLAudioElement = classes.HTMLAudioElement; + this.HTMLVideoElement = classes.HTMLVideoElement; + this.HTMLBaseElement = classes.HTMLBaseElement; + this.HTMLIFrameElement = classes.HTMLIFrameElement; + this.HTMLDialogElement = classes.HTMLDialogElement; + + // Non-implemented HTML element classes + this.HTMLHeadElement = classes.HTMLElement; + this.HTMLTitleElement = classes.HTMLElement; + this.HTMLBodyElement = classes.HTMLElement; + this.HTMLHeadingElement = classes.HTMLElement; + this.HTMLParagraphElement = classes.HTMLElement; + this.HTMLHRElement = classes.HTMLElement; + this.HTMLPreElement = classes.HTMLElement; + this.HTMLUListElement = classes.HTMLElement; + this.HTMLOListElement = classes.HTMLElement; + this.HTMLLIElement = classes.HTMLElement; + this.HTMLMenuElement = classes.HTMLElement; + this.HTMLDListElement = classes.HTMLElement; + this.HTMLDivElement = classes.HTMLElement; + this.HTMLAreaElement = classes.HTMLElement; + this.HTMLBRElement = classes.HTMLElement; + this.HTMLCanvasElement = classes.HTMLElement; + this.HTMLDataElement = classes.HTMLElement; + this.HTMLDataListElement = classes.HTMLElement; + this.HTMLDetailsElement = classes.HTMLElement; + this.HTMLDirectoryElement = classes.HTMLElement; + this.HTMLFieldSetElement = classes.HTMLElement; + this.HTMLFontElement = classes.HTMLElement; + this.HTMLHtmlElement = classes.HTMLElement; + this.HTMLLegendElement = classes.HTMLElement; + this.HTMLMapElement = classes.HTMLElement; + this.HTMLMarqueeElement = classes.HTMLElement; + this.HTMLMeterElement = classes.HTMLElement; + this.HTMLModElement = classes.HTMLElement; + this.HTMLOutputElement = classes.HTMLElement; + this.HTMLPictureElement = classes.HTMLElement; + this.HTMLProgressElement = classes.HTMLElement; + this.HTMLQuoteElement = classes.HTMLElement; + this.HTMLSourceElement = classes.HTMLElement; + this.HTMLSpanElement = classes.HTMLElement; + this.HTMLTableCaptionElement = classes.HTMLElement; + this.HTMLTableCellElement = classes.HTMLElement; + this.HTMLTableColElement = classes.HTMLElement; + this.HTMLTableElement = classes.HTMLElement; + this.HTMLTimeElement = classes.HTMLElement; + this.HTMLTableRowElement = classes.HTMLElement; + this.HTMLTableSectionElement = classes.HTMLElement; + this.HTMLFrameElement = classes.HTMLElement; + this.HTMLFrameSetElement = classes.HTMLElement; + this.HTMLEmbedElement = classes.HTMLElement; + this.HTMLObjectElement = classes.HTMLElement; + this.HTMLParamElement = classes.HTMLElement; + this.HTMLTrackElement = classes.HTMLElement; + + // Document + this.document = new this.HTMLDocument(); + (this.document.defaultView) = this; + + // Default document elements + const doctype = this.document.implementation.createDocumentType('html', '', ''); + const documentElement = this.document.createElement('html'); + const bodyElement = this.document.createElement('body'); + const headElement = this.document.createElement('head'); + + this.document.appendChild(doctype); + this.document.appendChild(documentElement); + + documentElement.appendChild(headElement); + documentElement.appendChild(bodyElement); + + // Ready state manager + this._readyStateManager.whenComplete().then(() => { + (this.document.readyState) = DocumentReadyStateEnum.complete; + this.document.dispatchEvent(new Event('readystatechange')); + this.document.dispatchEvent(new Event('load', { bubbles: true })); + }); + } + + /** + * Returns the console. + * + * @returns Console. + */ + public get console(): Console { + return this.#browserFrame.page.console; + } + + /** + * The number of pixels that the document is currently scrolled horizontally. + * + * @returns Scroll X. + */ + public get scrollX(): number { + return this.document?.documentElement?.scrollLeft ?? 0; + } + + /** + * The read-only Window property pageXOffset is an alias for scrollX. + * + * @returns Scroll X. + */ + public get pageXOffset(): number { + return this.scrollX; + } + + /** + * The number of pixels that the document is currently scrolled vertically. + * + * @returns Scroll Y. + */ + public get scrollY(): number { + return this.document?.documentElement?.scrollTop ?? 0; + } + + /** + * The read-only Window property pageYOffset is an alias for scrollY. + * + * @returns Scroll Y. + */ + public get pageYOffset(): number { + return this.scrollY; + } + + /** + * The CSS interface holds useful CSS-related methods. + * + * @returns CSS interface. + */ + public get CSS(): CSS { + return new CSS(); + } + + /** + * Returns an object containing the values of all CSS properties of an element. + * + * @param element Element. + * @returns CSS style declaration. + */ + public getComputedStyle(element: IElement): CSSStyleDeclaration { + element['_computedStyle'] = element['_computedStyle'] || new CSSStyleDeclaration(element, true); + return element['_computedStyle']; + } + + /** + * Returns selection. + * + * @returns Selection. + */ + public getSelection(): Selection { + return this.document.getSelection(); + } + + /** + * Scrolls to a particular set of coordinates. + * + * @param x X position or options object. + * @param y Y position. + */ + public scroll(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void { + if (typeof x === 'object') { + if (x.behavior === 'smooth') { + this.setTimeout(() => { + if (x.top !== undefined) { + (this.document.documentElement.scrollTop) = x.top; + } + if (x.left !== undefined) { + (this.document.documentElement.scrollLeft) = x.left; + } + }); + } else { + if (x.top !== undefined) { + (this.document.documentElement.scrollTop) = x.top; + } + if (x.left !== undefined) { + (this.document.documentElement.scrollLeft) = x.left; + } + } + } else if (x !== undefined && y !== undefined) { + (this.document.documentElement.scrollLeft) = x; + (this.document.documentElement.scrollTop) = y; + } + } + + /** + * Scrolls to a particular set of coordinates. + * + * @param x X position or options object. + * @param y Y position. + */ + public scrollTo( + x: { top?: number; left?: number; behavior?: string } | number, + y?: number + ): void { + this.scroll(x, y); + } + + /** + * Shifts focus away from the window. + */ + public blur(): void { + // TODO: Implement. + } + + /** + * Gives focus to the window. + */ + public focus(): void { + // TODO: Implement. + } + + /** + * Loads a specified resource into a new or existing browsing context (that is, a tab, a window, or an iframe) under a specified name. + * + * @param [url] URL. + * @param [target] Target. + * @param [features] Window features. + * @returns Window. + */ + public open( + url?: string, + target?: string, + features?: string + ): IBrowserWindow | ICrossOriginBrowserWindow | null { + return WindowPageOpenUtility.openPage(this.#browserFrame, { + url, + target, + features + }); + } + + /** + * Closes the window. + */ + public close(): void { + if (this.#browserFrame.page.mainFrame === this.#browserFrame) { + this.#browserFrame.page.close(); + } + } + + /** + * Returns a new MediaQueryList object that can then be used to determine if the document matches the media query string. + * + * @param mediaQueryString A string specifying the media query to parse into a MediaQueryList. + * @returns A new MediaQueryList. + */ + public matchMedia(mediaQueryString: string): MediaQueryList { + return new MediaQueryList({ ownerWindow: this, media: mediaQueryString }); + } + + /** + * Sets a timer which executes a function once the timer expires. + * + * @param callback Function to be executed. + * @param [delay=0] Delay in ms. + * @param args Arguments passed to the callback function. + * @returns Timeout ID. + */ + public setTimeout(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { + const id = this.#setTimeout(() => { + if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { + callback(...args); + } else { + WindowErrorUtility.captureError(this, () => callback(...args)); + } + this.#browserFrame._asyncTaskManager.endTimer(id); + }, delay); + this.#browserFrame._asyncTaskManager.startTimer(id); + return id; + } + + /** + * Cancels a timeout previously established by calling setTimeout(). + * + * @param id ID of the timeout. + */ + public clearTimeout(id: NodeJS.Timeout): void { + this.#clearTimeout(id); + this.#browserFrame._asyncTaskManager.endTimer(id); + } + + /** + * Calls a function with a fixed time delay between each call. + * + * @param callback Function to be executed. + * @param [delay=0] Delay in ms. + * @param args Arguments passed to the callback function. + * @returns Interval ID. + */ + public setInterval(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { + const id = this.#setInterval(() => { + if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { + callback(...args); + } else { + WindowErrorUtility.captureError( + this, + () => callback(...args), + () => this.clearInterval(id) + ); + } + }, delay); + this.#browserFrame._asyncTaskManager.startTimer(id); + return id; + } + + /** + * Cancels a timed repeating action which was previously established by a call to setInterval(). + * + * @param id ID of the interval. + */ + public clearInterval(id: NodeJS.Timeout): void { + this.#clearInterval(id); + this.#browserFrame._asyncTaskManager.endTimer(id); + } + + /** + * Mock animation frames with timeouts. + * + * @param callback Callback. + * @returns ID. + */ + public requestAnimationFrame(callback: (timestamp: number) => void): NodeJS.Immediate { + const id = global.setImmediate(() => { + if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { + callback(this.performance.now()); + } else { + WindowErrorUtility.captureError(this, () => callback(this.performance.now())); + } + this.#browserFrame._asyncTaskManager.endImmediate(id); + }); + this.#browserFrame._asyncTaskManager.startImmediate(id); + return id; + } + + /** + * Mock animation frames with timeouts. + * + * @param id ID. + */ + public cancelAnimationFrame(id: NodeJS.Immediate): void { + global.clearImmediate(id); + this.#browserFrame._asyncTaskManager.endImmediate(id); + } + + /** + * Queues a microtask to be executed at a safe time prior to control returning to the browser's event loop. + * + * @param callback Function to be executed. + */ + public queueMicrotask(callback: Function): void { + let isAborted = false; + const taskId = this.#browserFrame._asyncTaskManager.startTask(() => (isAborted = true)); + this.#queueMicrotask(() => { + if (!isAborted) { + if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { + callback(); + } else { + WindowErrorUtility.captureError(this, <() => unknown>callback); + } + this.#browserFrame._asyncTaskManager.endTask(taskId); + } + }); + } + + /** + * This method provides an easy, logical way to fetch resources asynchronously across the network. + * + * @param url URL. + * @param [init] Init. + * @returns Promise. + */ + public async fetch(url: RequestInfo, init?: IRequestInit): Promise { + return await new Fetch({ + browserFrame: this.#browserFrame, + window: this, + url, + init + }).send(); + } + + /** + * Creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data). + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/btoa + * @param data Binay data. + * @returns Base64-encoded string. + */ + public btoa(data: unknown): string { + return Base64.btoa(data); + } + + /** + * Decodes a string of data which has been encoded using Base64 encoding. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/atob + * @see https://infra.spec.whatwg.org/#forgiving-base64-encode. + * @see Https://html.spec.whatwg.org/multipage/webappapis.html#btoa. + * @param data Binay string. + * @returns An ASCII string containing decoded data from encodedData. + */ + public atob(data: unknown): string { + return Base64.atob(data); + } + + /** + * Safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it. + * + * @param message Message. + * @param [targetOrigin=*] Target origin. + * @param _transfer Transfer. Not implemented. + */ + public postMessage(message: unknown, targetOrigin = '*', _transfer?: unknown[]): void { + // TODO: Implement transfer. + + if (targetOrigin && targetOrigin !== '*' && this.location.origin !== targetOrigin) { + throw new DOMException( + `Failed to execute 'postMessage' on 'Window': The target origin provided ('${targetOrigin}') does not match the recipient window\'s origin ('${this.location.origin}').`, + DOMExceptionNameEnum.securityError + ); + } + + try { + JSON.stringify(message); + } catch (error) { + throw new DOMException( + `Failed to execute 'postMessage' on 'Window': The provided message cannot be serialized.`, + DOMExceptionNameEnum.invalidStateError + ); + } + + this.setTimeout(() => + this.dispatchEvent( + new MessageEvent('message', { + data: message, + origin: this.#browserFrame.parentFrame + ? this.#browserFrame.parentFrame.window.location.origin + : this.#browserFrame.window.location.origin, + source: this.#browserFrame.parentFrame + ? this.#browserFrame.parentFrame.window + : this.#browserFrame.window, + lastEventId: '' + }) + ) + ); + } + + /** + * Setup of VM context. + */ + protected _setupVMContext(): void { + if (!VM.isContext(this)) { + VM.createContext(this); + + // Sets global properties from the VM to the Window object. + // Otherwise "this.Array" will be undefined for example. + VMGlobalPropertyScript.runInContext(this); + } + } +} diff --git a/packages/happy-dom/src/window/CrossOriginWindow.ts b/packages/happy-dom/src/window/CrossOriginBrowserWindow.ts similarity index 79% rename from packages/happy-dom/src/window/CrossOriginWindow.ts rename to packages/happy-dom/src/window/CrossOriginBrowserWindow.ts index 4ac016290..1f24c8683 100644 --- a/packages/happy-dom/src/window/CrossOriginWindow.ts +++ b/packages/happy-dom/src/window/CrossOriginBrowserWindow.ts @@ -1,20 +1,23 @@ import EventTarget from '../event/EventTarget.js'; -import IWindow from './IWindow.js'; +import IBrowserWindow from './IBrowserWindow.js'; import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import Location from '../location/Location.js'; -import ICrossOriginWindow from './ICrossOriginWindow.js'; +import ICrossOriginBrowserWindow from './ICrossOriginBrowserWindow.js'; /** * Browser window with limited access due to CORS restrictions in iframes. */ -export default class CrossOriginWindow extends EventTarget implements ICrossOriginWindow { +export default class CrossOriginBrowserWindow + extends EventTarget + implements ICrossOriginBrowserWindow +{ public readonly self = this; public readonly window = this; - public readonly parent: IWindow | ICrossOriginWindow; - public readonly top: IWindow | ICrossOriginWindow; + public readonly parent: IBrowserWindow | ICrossOriginBrowserWindow; + public readonly top: IBrowserWindow | ICrossOriginBrowserWindow; public readonly location: Location; - #targetWindow: IWindow; + #targetWindow: IBrowserWindow; /** * Constructor. @@ -22,7 +25,7 @@ export default class CrossOriginWindow extends EventTarget implements ICrossOrig * @param target Target window. * @param [parent] Parent window. */ - constructor(target: IWindow, parent?: IWindow) { + constructor(target: IBrowserWindow, parent?: IBrowserWindow) { super(); this.parent = parent ?? this; @@ -52,7 +55,7 @@ export default class CrossOriginWindow extends EventTarget implements ICrossOrig * * @returns Opener. */ - public get opener(): IWindow | ICrossOriginWindow | null { + public get opener(): IBrowserWindow | ICrossOriginBrowserWindow | null { return this.#targetWindow.opener; } diff --git a/packages/happy-dom/src/window/IBrowserWindow.ts b/packages/happy-dom/src/window/IBrowserWindow.ts new file mode 100644 index 000000000..44013123f --- /dev/null +++ b/packages/happy-dom/src/window/IBrowserWindow.ts @@ -0,0 +1,566 @@ +import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; +import Document from '../nodes/document/Document.js'; +import IDocument from '../nodes/document/IDocument.js'; +import HTMLDocument from '../nodes/html-document/HTMLDocument.js'; +import XMLDocument from '../nodes/xml-document/XMLDocument.js'; +import SVGDocument from '../nodes/svg-document/SVGDocument.js'; +import DocumentType from '../nodes/document-type/DocumentType.js'; +import Node from '../nodes/node/Node.js'; +import Text from '../nodes/text/Text.js'; +import Comment from '../nodes/comment/Comment.js'; +import ShadowRoot from '../nodes/shadow-root/ShadowRoot.js'; +import Element from '../nodes/element/Element.js'; +import HTMLTemplateElement from '../nodes/html-template-element/HTMLTemplateElement.js'; +import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js'; +import HTMLElement from '../nodes/html-element/HTMLElement.js'; +import HTMLUnknownElement from '../nodes/html-unknown-element/HTMLUnknownElement.js'; +import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; +import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement.js'; +import HTMLTextAreaElement from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; +import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement.js'; +import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement.js'; +import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js'; +import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js'; +import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement.js'; +import HTMLMediaElement from '../nodes/html-media-element/HTMLMediaElement.js'; +import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; +import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement.js'; +import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js'; +import HTMLIFrameElement from '../nodes/html-iframe-element/HTMLIFrameElement.js'; +import SVGSVGElement from '../nodes/svg-element/SVGSVGElement.js'; +import SVGElement from '../nodes/svg-element/SVGElement.js'; +import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js'; +import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; +import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; +import Image from '../nodes/html-image-element/Image.js'; +import Audio from '../nodes/html-audio-element/Audio.js'; +import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; +import CharacterData from '../nodes/character-data/CharacterData.js'; +import NodeIterator from '../tree-walker/NodeIterator.js'; +import TreeWalker from '../tree-walker/TreeWalker.js'; +import Event from '../event/Event.js'; +import CustomEvent from '../event/events/CustomEvent.js'; +import AnimationEvent from '../event/events/AnimationEvent.js'; +import KeyboardEvent from '../event/events/KeyboardEvent.js'; +import ProgressEvent from '../event/events/ProgressEvent.js'; +import MediaQueryListEvent from '../event/events/MediaQueryListEvent.js'; +import EventTarget from '../event/EventTarget.js'; +import { URLSearchParams } from 'url'; +import URL from '../url/URL.js'; +import Location from '../location/Location.js'; +import MutationObserver from '../mutation-observer/MutationObserver.js'; +import MutationRecord from '../mutation-observer/MutationRecord.js'; +import DOMParser from '../dom-parser/DOMParser.js'; +import XMLSerializer from '../xml-serializer/XMLSerializer.js'; +import ResizeObserver from '../resize-observer/ResizeObserver.js'; +import Blob from '../file/Blob.js'; +import File from '../file/File.js'; +import DOMException from '../exception/DOMException.js'; +import FileReader from '../file/FileReader.js'; +import History from '../history/History.js'; +import CSSStyleSheet from '../css/CSSStyleSheet.js'; +import CSSStyleDeclaration from '../css/declaration/CSSStyleDeclaration.js'; +import CSS from '../css/CSS.js'; +import CSSUnitValue from '../css/CSSUnitValue.js'; +import CSSRule from '../css/CSSRule.js'; +import CSSContainerRule from '../css/rules/CSSContainerRule.js'; +import CSSFontFaceRule from '../css/rules/CSSFontFaceRule.js'; +import CSSKeyframeRule from '../css/rules/CSSKeyframeRule.js'; +import CSSKeyframesRule from '../css/rules/CSSKeyframesRule.js'; +import CSSMediaRule from '../css/rules/CSSMediaRule.js'; +import CSSStyleRule from '../css/rules/CSSStyleRule.js'; +import CSSSupportsRule from '../css/rules/CSSSupportsRule.js'; +import PointerEvent from '../event/events/PointerEvent.js'; +import MouseEvent from '../event/events/MouseEvent.js'; +import FocusEvent from '../event/events/FocusEvent.js'; +import WheelEvent from '../event/events/WheelEvent.js'; +import DataTransfer from '../event/DataTransfer.js'; +import DataTransferItem from '../event/DataTransferItem.js'; +import DataTransferItemList from '../event/DataTransferItemList.js'; +import InputEvent from '../event/events/InputEvent.js'; +import UIEvent from '../event/UIEvent.js'; +import ErrorEvent from '../event/events/ErrorEvent.js'; +import StorageEvent from '../event/events/StorageEvent.js'; +import SubmitEvent from '../event/events/SubmitEvent.js'; +import MessageEvent from '../event/events/MessageEvent.js'; +import MessagePort from '../event/MessagePort.js'; +import Screen from '../screen/Screen.js'; +import Storage from '../storage/Storage.js'; +import NodeFilter from '../tree-walker/NodeFilter.js'; +import HTMLCollection from '../nodes/element/HTMLCollection.js'; +import HTMLFormControlsCollection from '../nodes/html-form-element/HTMLFormControlsCollection.js'; +import NodeList from '../nodes/node/NodeList.js'; +import Selection from '../selection/Selection.js'; +import IEventTarget from '../event/IEventTarget.js'; +import Navigator from '../navigator/Navigator.js'; +import MimeType from '../navigator/MimeType.js'; +import MimeTypeArray from '../navigator/MimeTypeArray.js'; +import Plugin from '../navigator/Plugin.js'; +import PluginArray from '../navigator/PluginArray.js'; +import IRequestInit from '../fetch/types/IRequestInit.js'; +import IResponse from '../fetch/types/IResponse.js'; +import Range from '../range/Range.js'; +import MediaQueryList from '../match-media/MediaQueryList.js'; +import XMLHttpRequest from '../xml-http-request/XMLHttpRequest.js'; +import XMLHttpRequestUpload from '../xml-http-request/XMLHttpRequestUpload.js'; +import XMLHttpRequestEventTarget from '../xml-http-request/XMLHttpRequestEventTarget.js'; +import DOMRect from '../nodes/element/DOMRect.js'; +import Attr from '../nodes/attr/Attr.js'; +import NamedNodeMap from '../named-node-map/NamedNodeMap.js'; +import { Performance } from 'perf_hooks'; +import IElement from '../nodes/element/IElement.js'; +import SVGGraphicsElement from '../nodes/svg-element/SVGGraphicsElement.js'; +import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction.js'; +import RequestInfo from '../fetch/types/IRequestInfo.js'; +import FileList from '../nodes/html-input-element/FileList.js'; +import Stream from 'stream'; +import { webcrypto } from 'crypto'; +import FormData from '../form-data/FormData.js'; +import AbortController from '../fetch/AbortController.js'; +import AbortSignal from '../fetch/AbortSignal.js'; +import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; +import ValidityState from '../validity-state/ValidityState.js'; +import INodeJSGlobal from './INodeJSGlobal.js'; +import ICrossOriginBrowserWindow from './ICrossOriginBrowserWindow.js'; +import Permissions from '../permissions/Permissions.js'; +import PermissionStatus from '../permissions/PermissionStatus.js'; +import Clipboard from '../clipboard/Clipboard.js'; +import ClipboardItem from '../clipboard/ClipboardItem.js'; +import ClipboardEvent from '../event/events/ClipboardEvent.js'; +import Headers from '../fetch/Headers.js'; +import Request from '../fetch/Request.js'; +import Response from '../fetch/Response.js'; +import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; +import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; +import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; +import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; +import IResponseBody from '../fetch/types/IResponseBody.js'; +import IResponseInit from '../fetch/types/IResponseInit.js'; +import BrowserWindow from './BrowserWindow.js'; + +/** + * Window without dependencies to server side specific packages. + */ +export default interface IBrowserWindow extends IEventTarget, INodeJSGlobal { + // Nodes + readonly Node: typeof Node; + readonly Attr: typeof Attr; + readonly SVGSVGElement: typeof SVGSVGElement; + readonly SVGElement: typeof SVGElement; + readonly SVGGraphicsElement: typeof SVGGraphicsElement; + readonly Text: typeof Text; + readonly Comment: typeof Comment; + readonly ShadowRoot: typeof ShadowRoot; + readonly Element: typeof Element; + readonly DocumentFragment: typeof DocumentFragment; + readonly CharacterData: typeof CharacterData; + readonly ProcessingInstruction: typeof ProcessingInstruction; + readonly Document: new () => Document; + readonly HTMLDocument: new () => HTMLDocument; + readonly XMLDocument: new () => XMLDocument; + readonly SVGDocument: new () => SVGDocument; + readonly DocumentType: typeof DocumentType; + + // Element classes + readonly HTMLAnchorElement: typeof HTMLAnchorElement; + readonly HTMLButtonElement: typeof HTMLButtonElement; + readonly HTMLOptGroupElement: typeof HTMLOptGroupElement; + readonly HTMLOptionElement: typeof HTMLOptionElement; + readonly HTMLElement: typeof HTMLElement; + readonly HTMLUnknownElement: typeof HTMLUnknownElement; + readonly HTMLTemplateElement: typeof HTMLTemplateElement; + readonly HTMLFormElement: typeof HTMLFormElement; + readonly HTMLInputElement: typeof HTMLInputElement; + readonly HTMLSelectElement: typeof HTMLSelectElement; + readonly HTMLTextAreaElement: typeof HTMLTextAreaElement; + readonly HTMLImageElement: typeof HTMLImageElement; + readonly HTMLScriptElement: typeof HTMLScriptElement; + readonly HTMLLinkElement: typeof HTMLLinkElement; + readonly HTMLStyleElement: typeof HTMLStyleElement; + readonly HTMLSlotElement: typeof HTMLSlotElement; + readonly HTMLLabelElement: typeof HTMLLabelElement; + readonly HTMLMetaElement: typeof HTMLMetaElement; + readonly HTMLMediaElement: typeof HTMLMediaElement; + readonly HTMLAudioElement: typeof HTMLAudioElement; + readonly HTMLVideoElement: typeof HTMLVideoElement; + readonly HTMLBaseElement: typeof HTMLBaseElement; + readonly HTMLIFrameElement: typeof HTMLIFrameElement; + readonly HTMLDialogElement: typeof HTMLDialogElement; + + /** + * Non-implemented element classes + */ + readonly HTMLHeadElement: typeof HTMLElement; + readonly HTMLTitleElement: typeof HTMLElement; + readonly HTMLBodyElement: typeof HTMLElement; + readonly HTMLHeadingElement: typeof HTMLElement; + readonly HTMLParagraphElement: typeof HTMLElement; + readonly HTMLHRElement: typeof HTMLElement; + readonly HTMLPreElement: typeof HTMLElement; + readonly HTMLUListElement: typeof HTMLElement; + readonly HTMLOListElement: typeof HTMLElement; + readonly HTMLLIElement: typeof HTMLElement; + readonly HTMLMenuElement: typeof HTMLElement; + readonly HTMLDListElement: typeof HTMLElement; + readonly HTMLDivElement: typeof HTMLElement; + readonly HTMLAreaElement: typeof HTMLElement; + readonly HTMLBRElement: typeof HTMLElement; + readonly HTMLCanvasElement: typeof HTMLElement; + readonly HTMLDataElement: typeof HTMLElement; + readonly HTMLDataListElement: typeof HTMLElement; + readonly HTMLDetailsElement: typeof HTMLElement; + readonly HTMLDirectoryElement: typeof HTMLElement; + readonly HTMLFieldSetElement: typeof HTMLElement; + readonly HTMLFontElement: typeof HTMLElement; + readonly HTMLHtmlElement: typeof HTMLElement; + readonly HTMLLegendElement: typeof HTMLElement; + readonly HTMLMapElement: typeof HTMLElement; + readonly HTMLMarqueeElement: typeof HTMLElement; + readonly HTMLMeterElement: typeof HTMLElement; + readonly HTMLModElement: typeof HTMLElement; + readonly HTMLOutputElement: typeof HTMLElement; + readonly HTMLPictureElement: typeof HTMLElement; + readonly HTMLProgressElement: typeof HTMLElement; + readonly HTMLQuoteElement: typeof HTMLElement; + readonly HTMLSourceElement: typeof HTMLElement; + readonly HTMLSpanElement: typeof HTMLElement; + readonly HTMLTableCaptionElement: typeof HTMLElement; + readonly HTMLTableCellElement: typeof HTMLElement; + readonly HTMLTableColElement: typeof HTMLElement; + readonly HTMLTableElement: typeof HTMLElement; + readonly HTMLTimeElement: typeof HTMLElement; + readonly HTMLTableRowElement: typeof HTMLElement; + readonly HTMLTableSectionElement: typeof HTMLElement; + readonly HTMLFrameElement: typeof HTMLElement; + readonly HTMLFrameSetElement: typeof HTMLElement; + readonly HTMLEmbedElement: typeof HTMLElement; + readonly HTMLObjectElement: typeof HTMLElement; + readonly HTMLParamElement: typeof HTMLElement; + readonly HTMLTrackElement: typeof HTMLElement; + + // Event classes + readonly Event: typeof Event; + readonly UIEvent: typeof UIEvent; + readonly CustomEvent: typeof CustomEvent; + readonly AnimationEvent: typeof AnimationEvent; + readonly KeyboardEvent: typeof KeyboardEvent; + readonly PointerEvent: typeof PointerEvent; + readonly MouseEvent: typeof MouseEvent; + readonly FocusEvent: typeof FocusEvent; + readonly WheelEvent: typeof WheelEvent; + readonly InputEvent: typeof InputEvent; + readonly ErrorEvent: typeof ErrorEvent; + readonly StorageEvent: typeof StorageEvent; + readonly SubmitEvent: typeof SubmitEvent; + readonly MessageEvent: typeof MessageEvent; + readonly MessagePort: typeof MessagePort; + readonly ProgressEvent: typeof ProgressEvent; + readonly MediaQueryListEvent: typeof MediaQueryListEvent; + readonly ClipboardEvent: typeof ClipboardEvent; + + /** + * Non-implemented event classes + */ + readonly AudioProcessingEvent: typeof Event; + readonly BeforeInputEvent: typeof Event; + readonly BeforeUnloadEvent: typeof Event; + readonly BlobEvent: typeof Event; + readonly CloseEvent: typeof Event; + readonly CompositionEvent: typeof Event; + readonly CSSFontFaceLoadEvent: typeof Event; + readonly DeviceLightEvent: typeof Event; + readonly DeviceMotionEvent: typeof Event; + readonly DeviceOrientationEvent: typeof Event; + readonly DeviceProximityEvent: typeof Event; + readonly DOMTransactionEvent: typeof Event; + readonly DragEvent: typeof Event; + readonly EditingBeforeInputEvent: typeof Event; + readonly FetchEvent: typeof Event; + readonly GamepadEvent: typeof Event; + readonly HashChangeEvent: typeof Event; + readonly IDBVersionChangeEvent: typeof Event; + readonly MediaStreamEvent: typeof Event; + readonly MutationEvent: typeof Event; + readonly OfflineAudioCompletionEvent: typeof Event; + readonly OverconstrainedError: typeof Event; + readonly PageTransitionEvent: typeof Event; + readonly PaymentRequestUpdateEvent: typeof Event; + readonly PopStateEvent: typeof Event; + readonly RelatedEvent: typeof Event; + readonly RTCDataChannelEvent: typeof Event; + readonly RTCIdentityErrorEvent: typeof Event; + readonly RTCIdentityEvent: typeof Event; + readonly RTCPeerConnectionIceEvent: typeof Event; + readonly SensorEvent: typeof Event; + readonly SVGEvent: typeof Event; + readonly SVGZoomEvent: typeof Event; + readonly TimeEvent: typeof Event; + readonly TouchEvent: typeof Event; + readonly TrackEvent: typeof Event; + readonly TransitionEvent: typeof Event; + readonly UserProximityEvent: typeof Event; + readonly WebGLContextEvent: typeof Event; + readonly TextEvent: typeof Event; + + // Other classes + readonly Image: typeof Image; + readonly Audio: typeof Audio; + readonly NamedNodeMap: typeof NamedNodeMap; + readonly EventTarget: typeof EventTarget; + readonly DataTransfer: typeof DataTransfer; + readonly DataTransferItem: typeof DataTransferItem; + readonly DataTransferItemList: typeof DataTransferItemList; + readonly URL: typeof URL; + readonly URLSearchParams: typeof URLSearchParams; + readonly Location: typeof Location; + readonly CustomElementRegistry: typeof CustomElementRegistry; + readonly Window: typeof BrowserWindow; + readonly XMLSerializer: typeof XMLSerializer; + readonly ResizeObserver: typeof ResizeObserver; + readonly CSSStyleSheet: typeof CSSStyleSheet; + readonly Blob: typeof Blob; + readonly File: typeof File; + readonly FileReader: new () => FileReader; + readonly DOMException: typeof DOMException; + readonly History: typeof History; + readonly Screen: typeof Screen; + readonly Storage: typeof Storage; + readonly HTMLCollection: typeof HTMLCollection; + readonly HTMLFormControlsCollection: typeof HTMLFormControlsCollection; + readonly NodeList: typeof NodeList; + readonly CSSUnitValue: typeof CSSUnitValue; + readonly CSS: CSS; + readonly CSSRule: typeof CSSRule; + readonly CSSContainerRule: typeof CSSContainerRule; + readonly CSSFontFaceRule: typeof CSSFontFaceRule; + readonly CSSKeyframeRule: typeof CSSKeyframeRule; + readonly CSSKeyframesRule: typeof CSSKeyframesRule; + readonly CSSMediaRule: typeof CSSMediaRule; + readonly CSSStyleRule: typeof CSSStyleRule; + readonly CSSSupportsRule: typeof CSSSupportsRule; + readonly Selection: typeof Selection; + readonly Navigator: typeof Navigator; + readonly MimeType: typeof MimeType; + readonly MimeTypeArray: typeof MimeTypeArray; + readonly Plugin: typeof Plugin; + readonly PluginArray: typeof PluginArray; + readonly Headers: typeof Headers; + readonly Request: new (input: RequestInfo, init?: IRequestInit) => Request; + readonly Response: new (body?: IResponseBody, init?: IResponseInit) => Response; + readonly Range: new () => Range; + readonly DOMRect: typeof DOMRect; + readonly XMLHttpRequest: new () => XMLHttpRequest; + readonly XMLHttpRequestUpload: typeof XMLHttpRequestUpload; + readonly XMLHttpRequestEventTarget: typeof XMLHttpRequestEventTarget; + readonly FileList: typeof FileList; + readonly ReadableStream: typeof Stream.Readable; + readonly WritableStream: typeof Stream.Writable; + readonly TransformStream: typeof Stream.Transform; + readonly FormData: typeof FormData; + readonly AbortController: typeof AbortController; + readonly AbortSignal: typeof AbortSignal; + readonly RadioNodeList: typeof RadioNodeList; + readonly ValidityState: typeof ValidityState; + readonly Permissions: typeof Permissions; + readonly PermissionStatus: typeof PermissionStatus; + readonly Clipboard: typeof Clipboard; + readonly ClipboardItem: typeof ClipboardItem; + + readonly NodeFilter: typeof NodeFilter; + readonly NodeIterator: typeof NodeIterator; + readonly TreeWalker: typeof TreeWalker; + readonly DOMParser: new () => DOMParser; + readonly MutationObserver: typeof MutationObserver; + readonly MutationRecord: typeof MutationRecord; + + // Events + onload: ((event: Event) => void) | null; + onerror: ((event: ErrorEvent) => void) | null; + + // Public Properties + readonly document: IDocument; + readonly customElements: CustomElementRegistry; + readonly location: Location; + readonly history: History; + readonly navigator: Navigator; + readonly console: Console; + readonly self: IBrowserWindow; + readonly top: IBrowserWindow | ICrossOriginBrowserWindow; + readonly opener: IBrowserWindow | ICrossOriginBrowserWindow | null; + readonly parent: IBrowserWindow | ICrossOriginBrowserWindow; + readonly window: IBrowserWindow; + readonly globalThis: IBrowserWindow; + readonly screen: Screen; + readonly devicePixelRatio: number; + readonly innerWidth: number; + readonly innerHeight: number; + readonly outerWidth: number; + readonly outerHeight: number; + readonly screenLeft: number; + readonly screenTop: number; + readonly screenX: number; + readonly screenY: number; + readonly sessionStorage: Storage; + readonly localStorage: Storage; + readonly performance: Performance; + readonly pageXOffset: number; + readonly pageYOffset: number; + readonly scrollX: number; + readonly scrollY: number; + readonly crypto: typeof webcrypto; + readonly closed: boolean; + name: string; + + /** + * Returns an object containing the values of all CSS properties of an element. + * + * @param element Element. + * @returns CSS style declaration. + */ + getComputedStyle(element: IElement): CSSStyleDeclaration; + + /** + * Returns selection. + * + * @returns Selection. + */ + getSelection(): Selection; + + /** + * Scrolls to a particular set of coordinates. + * + * @param x X position or options object. + * @param y Y position. + */ + scroll(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void; + + /** + * Scrolls to a particular set of coordinates. + * + * @param x X position or options object. + * @param y Y position. + */ + scrollTo(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void; + + /** + * Shifts focus away from the window. + */ + blur(): void; + + /** + * Gives focus to the window. + */ + focus(): void; + + /** + * Loads a specified resource into a new or existing browsing context (that is, a tab, a window, or an iframe) under a specified name. + * + * @param [url] URL. + * @param [target] Target. + * @param [windowFeatures] Window features. + */ + open( + url?: string, + target?: string, + windowFeatures?: string + ): IBrowserWindow | ICrossOriginBrowserWindow | null; + + /** + * Closes the window. + */ + close(): void; + + /** + * Returns a new MediaQueryList object that can then be used to determine if the document matches the media query string. + * + * @param mediaQueryString A string specifying the media query to parse into a MediaQueryList. + * @returns A new MediaQueryList. + */ + matchMedia(mediaQueryString: string): MediaQueryList; + + /** + * Sets a timer which executes a function once the timer expires. + * + * @param callback Function to be executed. + * @param [delay=0] Delay in ms. + * @param args Arguments passed to the callback function. + * @returns Timeout ID. + */ + setTimeout(callback: Function, delay?: number, ...args: unknown[]): NodeJS.Timeout; + + /** + * Cancels a timeout previously established by calling setTimeout(). + * + * @param id ID of the timeout. + */ + clearTimeout(id: NodeJS.Timeout): void; + + /** + * Calls a function with a fixed time delay between each call. + * + * @param callback Function to be executed. + * @param [delay=0] Delay in ms. + * @param args Arguments passed to the callback function. + * @returns Interval ID. + */ + setInterval(callback: Function, delay?: number, ...args: unknown[]): NodeJS.Timeout; + + /** + * Cancels a timed repeating action which was previously established by a call to setInterval(). + * + * @param id ID of the interval. + */ + clearInterval(id: NodeJS.Timeout): void; + + /** + * Mock animation frames with timeouts. + * + * @param {Function} callback Callback. + * @returns {NodeJS.Timeout} ID. + */ + requestAnimationFrame(callback: (timestamp: number) => void): NodeJS.Immediate; + + /** + * Mock animation frames with timeouts. + * + * @param {NodeJS.Timeout} id ID. + */ + cancelAnimationFrame(id: NodeJS.Immediate): void; + + /** + * This method provides an easy, logical way to fetch resources asynchronously across the network. + * + * @param url URL. + * @param [init] Init. + * @returns Promise. + */ + fetch(url: RequestInfo, init?: IRequestInit): Promise; + + /** + * Creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data). + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/btoa + * @param data Binary data. + * @returns Base64-encoded string. + */ + btoa(data: unknown): string; + + /** + * Decodes a string of data which has been encoded using Base64 encoding. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/atob + * @see https://infra.spec.whatwg.org/#forgiving-base64-encode. + * @see Https://html.spec.whatwg.org/multipage/webappapis.html#btoa. + * @param data Binary string. + * @returns An ASCII string containing decoded data from encodedData. + */ + atob(data: unknown): string; + + /** + * Safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it. + * + * @param message Message. + * @param listener Listener. + */ + postMessage(message: unknown, targetOrigin?: string, transfer?: unknown[]): void; +} diff --git a/packages/happy-dom/src/window/ICrossOriginWindow.ts b/packages/happy-dom/src/window/ICrossOriginBrowserWindow.ts similarity index 66% rename from packages/happy-dom/src/window/ICrossOriginWindow.ts rename to packages/happy-dom/src/window/ICrossOriginBrowserWindow.ts index 543677222..7c98770b4 100644 --- a/packages/happy-dom/src/window/ICrossOriginWindow.ts +++ b/packages/happy-dom/src/window/ICrossOriginBrowserWindow.ts @@ -1,17 +1,17 @@ -import IWindow from './IWindow.js'; +import IBrowserWindow from './IBrowserWindow.js'; import Location from '../location/Location.js'; import IEventTarget from '../event/IEventTarget.js'; /** * Browser window with limited access due to CORS restrictions in iframes. */ -export default interface ICrossOriginWindow extends IEventTarget { - readonly self: ICrossOriginWindow; - readonly window: ICrossOriginWindow; - readonly parent: IWindow | ICrossOriginWindow; - readonly top: IWindow | ICrossOriginWindow; +export default interface ICrossOriginBrowserWindow extends IEventTarget { + readonly self: ICrossOriginBrowserWindow; + readonly window: ICrossOriginBrowserWindow; + readonly parent: IBrowserWindow | ICrossOriginBrowserWindow; + readonly top: IBrowserWindow | ICrossOriginBrowserWindow; readonly location: Location; - readonly opener: IWindow | ICrossOriginWindow | null; + readonly opener: IBrowserWindow | ICrossOriginBrowserWindow | null; readonly closed: boolean; /** diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index 353fcc03b..dacd12a90 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -1,564 +1,10 @@ -import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; -import Document from '../nodes/document/Document.js'; -import IDocument from '../nodes/document/IDocument.js'; -import HTMLDocument from '../nodes/html-document/HTMLDocument.js'; -import XMLDocument from '../nodes/xml-document/XMLDocument.js'; -import SVGDocument from '../nodes/svg-document/SVGDocument.js'; -import DocumentType from '../nodes/document-type/DocumentType.js'; -import Node from '../nodes/node/Node.js'; -import Text from '../nodes/text/Text.js'; -import Comment from '../nodes/comment/Comment.js'; -import ShadowRoot from '../nodes/shadow-root/ShadowRoot.js'; -import Element from '../nodes/element/Element.js'; -import HTMLTemplateElement from '../nodes/html-template-element/HTMLTemplateElement.js'; -import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js'; -import HTMLElement from '../nodes/html-element/HTMLElement.js'; -import HTMLUnknownElement from '../nodes/html-unknown-element/HTMLUnknownElement.js'; -import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; -import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement.js'; -import HTMLTextAreaElement from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; -import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement.js'; -import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement.js'; -import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js'; -import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js'; -import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement.js'; -import HTMLMediaElement from '../nodes/html-media-element/HTMLMediaElement.js'; -import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; -import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement.js'; -import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js'; -import HTMLIFrameElement from '../nodes/html-iframe-element/HTMLIFrameElement.js'; -import SVGSVGElement from '../nodes/svg-element/SVGSVGElement.js'; -import SVGElement from '../nodes/svg-element/SVGElement.js'; -import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js'; -import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; -import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; -import Image from '../nodes/html-image-element/Image.js'; -import Audio from '../nodes/html-audio-element/Audio.js'; -import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; -import CharacterData from '../nodes/character-data/CharacterData.js'; -import NodeIterator from '../tree-walker/NodeIterator.js'; -import TreeWalker from '../tree-walker/TreeWalker.js'; -import Event from '../event/Event.js'; -import CustomEvent from '../event/events/CustomEvent.js'; -import AnimationEvent from '../event/events/AnimationEvent.js'; -import KeyboardEvent from '../event/events/KeyboardEvent.js'; -import ProgressEvent from '../event/events/ProgressEvent.js'; -import MediaQueryListEvent from '../event/events/MediaQueryListEvent.js'; -import EventTarget from '../event/EventTarget.js'; -import { URLSearchParams } from 'url'; -import URL from '../url/URL.js'; -import Location from '../location/Location.js'; -import MutationObserver from '../mutation-observer/MutationObserver.js'; -import MutationRecord from '../mutation-observer/MutationRecord.js'; -import DOMParser from '../dom-parser/DOMParser.js'; -import XMLSerializer from '../xml-serializer/XMLSerializer.js'; -import ResizeObserver from '../resize-observer/ResizeObserver.js'; -import Blob from '../file/Blob.js'; -import File from '../file/File.js'; -import DOMException from '../exception/DOMException.js'; -import FileReader from '../file/FileReader.js'; -import History from '../history/History.js'; -import CSSStyleSheet from '../css/CSSStyleSheet.js'; -import CSSStyleDeclaration from '../css/declaration/CSSStyleDeclaration.js'; -import CSS from '../css/CSS.js'; -import CSSUnitValue from '../css/CSSUnitValue.js'; -import CSSRule from '../css/CSSRule.js'; -import CSSContainerRule from '../css/rules/CSSContainerRule.js'; -import CSSFontFaceRule from '../css/rules/CSSFontFaceRule.js'; -import CSSKeyframeRule from '../css/rules/CSSKeyframeRule.js'; -import CSSKeyframesRule from '../css/rules/CSSKeyframesRule.js'; -import CSSMediaRule from '../css/rules/CSSMediaRule.js'; -import CSSStyleRule from '../css/rules/CSSStyleRule.js'; -import CSSSupportsRule from '../css/rules/CSSSupportsRule.js'; -import PointerEvent from '../event/events/PointerEvent.js'; -import MouseEvent from '../event/events/MouseEvent.js'; -import FocusEvent from '../event/events/FocusEvent.js'; -import WheelEvent from '../event/events/WheelEvent.js'; -import DataTransfer from '../event/DataTransfer.js'; -import DataTransferItem from '../event/DataTransferItem.js'; -import DataTransferItemList from '../event/DataTransferItemList.js'; -import InputEvent from '../event/events/InputEvent.js'; -import UIEvent from '../event/UIEvent.js'; -import ErrorEvent from '../event/events/ErrorEvent.js'; -import StorageEvent from '../event/events/StorageEvent.js'; -import SubmitEvent from '../event/events/SubmitEvent.js'; -import MessageEvent from '../event/events/MessageEvent.js'; -import MessagePort from '../event/MessagePort.js'; -import Screen from '../screen/Screen.js'; -import Storage from '../storage/Storage.js'; -import NodeFilter from '../tree-walker/NodeFilter.js'; -import HTMLCollection from '../nodes/element/HTMLCollection.js'; -import HTMLFormControlsCollection from '../nodes/html-form-element/HTMLFormControlsCollection.js'; -import NodeList from '../nodes/node/NodeList.js'; -import Selection from '../selection/Selection.js'; -import IEventTarget from '../event/IEventTarget.js'; -import Navigator from '../navigator/Navigator.js'; -import MimeType from '../navigator/MimeType.js'; -import MimeTypeArray from '../navigator/MimeTypeArray.js'; -import Plugin from '../navigator/Plugin.js'; -import PluginArray from '../navigator/PluginArray.js'; -import IRequestInit from '../fetch/types/IRequestInit.js'; -import IResponse from '../fetch/types/IResponse.js'; -import Range from '../range/Range.js'; -import MediaQueryList from '../match-media/MediaQueryList.js'; -import XMLHttpRequest from '../xml-http-request/XMLHttpRequest.js'; -import XMLHttpRequestUpload from '../xml-http-request/XMLHttpRequestUpload.js'; -import XMLHttpRequestEventTarget from '../xml-http-request/XMLHttpRequestEventTarget.js'; -import DOMRect from '../nodes/element/DOMRect.js'; -import Window from './Window.js'; -import Attr from '../nodes/attr/Attr.js'; -import NamedNodeMap from '../named-node-map/NamedNodeMap.js'; -import { Performance } from 'perf_hooks'; -import IElement from '../nodes/element/IElement.js'; -import SVGGraphicsElement from '../nodes/svg-element/SVGGraphicsElement.js'; -import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction.js'; -import RequestInfo from '../fetch/types/IRequestInfo.js'; -import FileList from '../nodes/html-input-element/FileList.js'; -import Stream from 'stream'; -import { webcrypto } from 'crypto'; -import FormData from '../form-data/FormData.js'; -import AbortController from '../fetch/AbortController.js'; -import AbortSignal from '../fetch/AbortSignal.js'; -import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; -import ValidityState from '../validity-state/ValidityState.js'; -import INodeJSGlobal from './INodeJSGlobal.js'; -import ICrossOriginWindow from './ICrossOriginWindow.js'; -import Permissions from '../permissions/Permissions.js'; -import PermissionStatus from '../permissions/PermissionStatus.js'; -import Clipboard from '../clipboard/Clipboard.js'; -import ClipboardItem from '../clipboard/ClipboardItem.js'; -import ClipboardEvent from '../event/events/ClipboardEvent.js'; import DetachedWindowAPI from './DetachedWindowAPI.js'; -import Headers from '../fetch/Headers.js'; -import Request from '../fetch/Request.js'; -import Response from '../fetch/Response.js'; -import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; -import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; -import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; -import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; +import IBrowserWindow from './IBrowserWindow.js'; /** - * Window without dependencies to server side specific packages. + * Window. */ -export default interface IWindow extends IEventTarget, INodeJSGlobal { +export default interface IWindow extends IBrowserWindow { // Detached Window API. - readonly happyDOM?: DetachedWindowAPI; - - // Nodes - readonly Node: typeof Node; - readonly Attr: typeof Attr; - readonly SVGSVGElement: typeof SVGSVGElement; - readonly SVGElement: typeof SVGElement; - readonly SVGGraphicsElement: typeof SVGGraphicsElement; - readonly Text: typeof Text; - readonly Comment: typeof Comment; - readonly ShadowRoot: typeof ShadowRoot; - readonly Element: typeof Element; - readonly DocumentFragment: typeof DocumentFragment; - readonly CharacterData: typeof CharacterData; - readonly ProcessingInstruction: typeof ProcessingInstruction; - readonly Document: typeof Document; - readonly HTMLDocument: typeof HTMLDocument; - readonly XMLDocument: typeof XMLDocument; - readonly SVGDocument: typeof SVGDocument; - readonly DocumentType: typeof DocumentType; - - // Element classes - readonly HTMLAnchorElement: typeof HTMLAnchorElement; - readonly HTMLButtonElement: typeof HTMLButtonElement; - readonly HTMLOptGroupElement: typeof HTMLOptGroupElement; - readonly HTMLOptionElement: typeof HTMLOptionElement; - readonly HTMLElement: typeof HTMLElement; - readonly HTMLUnknownElement: typeof HTMLUnknownElement; - readonly HTMLTemplateElement: typeof HTMLTemplateElement; - readonly HTMLFormElement: typeof HTMLFormElement; - readonly HTMLInputElement: typeof HTMLInputElement; - readonly HTMLSelectElement: typeof HTMLSelectElement; - readonly HTMLTextAreaElement: typeof HTMLTextAreaElement; - readonly HTMLImageElement: typeof HTMLImageElement; - readonly HTMLScriptElement: typeof HTMLScriptElement; - readonly HTMLLinkElement: typeof HTMLLinkElement; - readonly HTMLStyleElement: typeof HTMLStyleElement; - readonly HTMLSlotElement: typeof HTMLSlotElement; - readonly HTMLLabelElement: typeof HTMLLabelElement; - readonly HTMLMetaElement: typeof HTMLMetaElement; - readonly HTMLMediaElement: typeof HTMLMediaElement; - readonly HTMLAudioElement: typeof HTMLAudioElement; - readonly HTMLVideoElement: typeof HTMLVideoElement; - readonly HTMLBaseElement: typeof HTMLBaseElement; - readonly HTMLIFrameElement: typeof HTMLIFrameElement; - readonly HTMLDialogElement: typeof HTMLDialogElement; - - /** - * Non-implemented element classes - */ - readonly HTMLHeadElement: typeof HTMLElement; - readonly HTMLTitleElement: typeof HTMLElement; - readonly HTMLBodyElement: typeof HTMLElement; - readonly HTMLHeadingElement: typeof HTMLElement; - readonly HTMLParagraphElement: typeof HTMLElement; - readonly HTMLHRElement: typeof HTMLElement; - readonly HTMLPreElement: typeof HTMLElement; - readonly HTMLUListElement: typeof HTMLElement; - readonly HTMLOListElement: typeof HTMLElement; - readonly HTMLLIElement: typeof HTMLElement; - readonly HTMLMenuElement: typeof HTMLElement; - readonly HTMLDListElement: typeof HTMLElement; - readonly HTMLDivElement: typeof HTMLElement; - readonly HTMLAreaElement: typeof HTMLElement; - readonly HTMLBRElement: typeof HTMLElement; - readonly HTMLCanvasElement: typeof HTMLElement; - readonly HTMLDataElement: typeof HTMLElement; - readonly HTMLDataListElement: typeof HTMLElement; - readonly HTMLDetailsElement: typeof HTMLElement; - readonly HTMLDirectoryElement: typeof HTMLElement; - readonly HTMLFieldSetElement: typeof HTMLElement; - readonly HTMLFontElement: typeof HTMLElement; - readonly HTMLHtmlElement: typeof HTMLElement; - readonly HTMLLegendElement: typeof HTMLElement; - readonly HTMLMapElement: typeof HTMLElement; - readonly HTMLMarqueeElement: typeof HTMLElement; - readonly HTMLMeterElement: typeof HTMLElement; - readonly HTMLModElement: typeof HTMLElement; - readonly HTMLOutputElement: typeof HTMLElement; - readonly HTMLPictureElement: typeof HTMLElement; - readonly HTMLProgressElement: typeof HTMLElement; - readonly HTMLQuoteElement: typeof HTMLElement; - readonly HTMLSourceElement: typeof HTMLElement; - readonly HTMLSpanElement: typeof HTMLElement; - readonly HTMLTableCaptionElement: typeof HTMLElement; - readonly HTMLTableCellElement: typeof HTMLElement; - readonly HTMLTableColElement: typeof HTMLElement; - readonly HTMLTableElement: typeof HTMLElement; - readonly HTMLTimeElement: typeof HTMLElement; - readonly HTMLTableRowElement: typeof HTMLElement; - readonly HTMLTableSectionElement: typeof HTMLElement; - readonly HTMLFrameElement: typeof HTMLElement; - readonly HTMLFrameSetElement: typeof HTMLElement; - readonly HTMLEmbedElement: typeof HTMLElement; - readonly HTMLObjectElement: typeof HTMLElement; - readonly HTMLParamElement: typeof HTMLElement; - readonly HTMLTrackElement: typeof HTMLElement; - - // Event classes - readonly Event: typeof Event; - readonly UIEvent: typeof UIEvent; - readonly CustomEvent: typeof CustomEvent; - readonly AnimationEvent: typeof AnimationEvent; - readonly KeyboardEvent: typeof KeyboardEvent; - readonly PointerEvent: typeof PointerEvent; - readonly MouseEvent: typeof MouseEvent; - readonly FocusEvent: typeof FocusEvent; - readonly WheelEvent: typeof WheelEvent; - readonly InputEvent: typeof InputEvent; - readonly ErrorEvent: typeof ErrorEvent; - readonly StorageEvent: typeof StorageEvent; - readonly SubmitEvent: typeof SubmitEvent; - readonly MessageEvent: typeof MessageEvent; - readonly MessagePort: typeof MessagePort; - readonly ProgressEvent: typeof ProgressEvent; - readonly MediaQueryListEvent: typeof MediaQueryListEvent; - readonly ClipboardEvent: typeof ClipboardEvent; - - /** - * Non-implemented event classes - */ - readonly AudioProcessingEvent: typeof Event; - readonly BeforeInputEvent: typeof Event; - readonly BeforeUnloadEvent: typeof Event; - readonly BlobEvent: typeof Event; - readonly CloseEvent: typeof Event; - readonly CompositionEvent: typeof Event; - readonly CSSFontFaceLoadEvent: typeof Event; - readonly DeviceLightEvent: typeof Event; - readonly DeviceMotionEvent: typeof Event; - readonly DeviceOrientationEvent: typeof Event; - readonly DeviceProximityEvent: typeof Event; - readonly DOMTransactionEvent: typeof Event; - readonly DragEvent: typeof Event; - readonly EditingBeforeInputEvent: typeof Event; - readonly FetchEvent: typeof Event; - readonly GamepadEvent: typeof Event; - readonly HashChangeEvent: typeof Event; - readonly IDBVersionChangeEvent: typeof Event; - readonly MediaStreamEvent: typeof Event; - readonly MutationEvent: typeof Event; - readonly OfflineAudioCompletionEvent: typeof Event; - readonly OverconstrainedError: typeof Event; - readonly PageTransitionEvent: typeof Event; - readonly PaymentRequestUpdateEvent: typeof Event; - readonly PopStateEvent: typeof Event; - readonly RelatedEvent: typeof Event; - readonly RTCDataChannelEvent: typeof Event; - readonly RTCIdentityErrorEvent: typeof Event; - readonly RTCIdentityEvent: typeof Event; - readonly RTCPeerConnectionIceEvent: typeof Event; - readonly SensorEvent: typeof Event; - readonly SVGEvent: typeof Event; - readonly SVGZoomEvent: typeof Event; - readonly TimeEvent: typeof Event; - readonly TouchEvent: typeof Event; - readonly TrackEvent: typeof Event; - readonly TransitionEvent: typeof Event; - readonly UserProximityEvent: typeof Event; - readonly WebGLContextEvent: typeof Event; - readonly TextEvent: typeof Event; - - // Other classes - readonly Image: typeof Image; - readonly Audio: typeof Audio; - readonly NamedNodeMap: typeof NamedNodeMap; - readonly EventTarget: typeof EventTarget; - readonly DataTransfer: typeof DataTransfer; - readonly DataTransferItem: typeof DataTransferItem; - readonly DataTransferItemList: typeof DataTransferItemList; - readonly URL: typeof URL; - readonly URLSearchParams: typeof URLSearchParams; - readonly Location: typeof Location; - readonly CustomElementRegistry: typeof CustomElementRegistry; - readonly Window: typeof Window; - readonly XMLSerializer: typeof XMLSerializer; - readonly ResizeObserver: typeof ResizeObserver; - readonly CSSStyleSheet: typeof CSSStyleSheet; - readonly Blob: typeof Blob; - readonly File: typeof File; - readonly FileReader: typeof FileReader; - readonly DOMException: typeof DOMException; - readonly History: typeof History; - readonly Screen: typeof Screen; - readonly Storage: typeof Storage; - readonly HTMLCollection: typeof HTMLCollection; - readonly HTMLFormControlsCollection: typeof HTMLFormControlsCollection; - readonly NodeList: typeof NodeList; - readonly CSSUnitValue: typeof CSSUnitValue; - readonly CSS: CSS; - readonly CSSRule: typeof CSSRule; - readonly CSSContainerRule: typeof CSSContainerRule; - readonly CSSFontFaceRule: typeof CSSFontFaceRule; - readonly CSSKeyframeRule: typeof CSSKeyframeRule; - readonly CSSKeyframesRule: typeof CSSKeyframesRule; - readonly CSSMediaRule: typeof CSSMediaRule; - readonly CSSStyleRule: typeof CSSStyleRule; - readonly CSSSupportsRule: typeof CSSSupportsRule; - readonly Selection: typeof Selection; - readonly Navigator: typeof Navigator; - readonly MimeType: typeof MimeType; - readonly MimeTypeArray: typeof MimeTypeArray; - readonly Plugin: typeof Plugin; - readonly PluginArray: typeof PluginArray; - readonly Headers: typeof Headers; - readonly Request: typeof Request; - readonly Response: typeof Response; - readonly Range: typeof Range; - readonly DOMRect: typeof DOMRect; - readonly XMLHttpRequest: typeof XMLHttpRequest; - readonly XMLHttpRequestUpload: typeof XMLHttpRequestUpload; - readonly XMLHttpRequestEventTarget: typeof XMLHttpRequestEventTarget; - readonly FileList: typeof FileList; - readonly ReadableStream: typeof Stream.Readable; - readonly WritableStream: typeof Stream.Writable; - readonly TransformStream: typeof Stream.Transform; - readonly FormData: typeof FormData; - readonly AbortController: typeof AbortController; - readonly AbortSignal: typeof AbortSignal; - readonly RadioNodeList: typeof RadioNodeList; - readonly ValidityState: typeof ValidityState; - readonly Permissions: typeof Permissions; - readonly PermissionStatus: typeof PermissionStatus; - readonly Clipboard: typeof Clipboard; - readonly ClipboardItem: typeof ClipboardItem; - - readonly NodeFilter: typeof NodeFilter; - readonly NodeIterator: typeof NodeIterator; - readonly TreeWalker: typeof TreeWalker; - readonly DOMParser: typeof DOMParser; - readonly MutationObserver: typeof MutationObserver; - readonly MutationRecord: typeof MutationRecord; - - // Events - onload: ((event: Event) => void) | null; - onerror: ((event: ErrorEvent) => void) | null; - - // Public Properties - readonly document: IDocument; - readonly customElements: CustomElementRegistry; - readonly location: Location; - readonly history: History; - readonly navigator: Navigator; - readonly console: Console; - readonly self: IWindow; - readonly top: IWindow | ICrossOriginWindow; - readonly opener: IWindow | ICrossOriginWindow | null; - readonly parent: IWindow | ICrossOriginWindow; - readonly window: IWindow; - readonly globalThis: IWindow; - readonly screen: Screen; - readonly devicePixelRatio: number; - readonly innerWidth: number; - readonly innerHeight: number; - readonly outerWidth: number; - readonly outerHeight: number; - readonly screenLeft: number; - readonly screenTop: number; - readonly screenX: number; - readonly screenY: number; - readonly sessionStorage: Storage; - readonly localStorage: Storage; - readonly performance: Performance; - readonly pageXOffset: number; - readonly pageYOffset: number; - readonly scrollX: number; - readonly scrollY: number; - readonly crypto: typeof webcrypto; - readonly closed: boolean; - name: string; - - /** - * Returns an object containing the values of all CSS properties of an element. - * - * @param element Element. - * @returns CSS style declaration. - */ - getComputedStyle(element: IElement): CSSStyleDeclaration; - - /** - * Returns selection. - * - * @returns Selection. - */ - getSelection(): Selection; - - /** - * Scrolls to a particular set of coordinates. - * - * @param x X position or options object. - * @param y Y position. - */ - scroll(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void; - - /** - * Scrolls to a particular set of coordinates. - * - * @param x X position or options object. - * @param y Y position. - */ - scrollTo(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void; - - /** - * Shifts focus away from the window. - */ - blur(): void; - - /** - * Gives focus to the window. - */ - focus(): void; - - /** - * Loads a specified resource into a new or existing browsing context (that is, a tab, a window, or an iframe) under a specified name. - * - * @param [url] URL. - * @param [target] Target. - * @param [windowFeatures] Window features. - */ - open(url?: string, target?: string, windowFeatures?: string): IWindow | ICrossOriginWindow | null; - - /** - * Closes the window. - */ - close(): void; - - /** - * Returns a new MediaQueryList object that can then be used to determine if the document matches the media query string. - * - * @param mediaQueryString A string specifying the media query to parse into a MediaQueryList. - * @returns A new MediaQueryList. - */ - matchMedia(mediaQueryString: string): MediaQueryList; - - /** - * Sets a timer which executes a function once the timer expires. - * - * @param callback Function to be executed. - * @param [delay=0] Delay in ms. - * @param args Arguments passed to the callback function. - * @returns Timeout ID. - */ - setTimeout(callback: Function, delay?: number, ...args: unknown[]): NodeJS.Timeout; - - /** - * Cancels a timeout previously established by calling setTimeout(). - * - * @param id ID of the timeout. - */ - clearTimeout(id: NodeJS.Timeout): void; - - /** - * Calls a function with a fixed time delay between each call. - * - * @param callback Function to be executed. - * @param [delay=0] Delay in ms. - * @param args Arguments passed to the callback function. - * @returns Interval ID. - */ - setInterval(callback: Function, delay?: number, ...args: unknown[]): NodeJS.Timeout; - - /** - * Cancels a timed repeating action which was previously established by a call to setInterval(). - * - * @param id ID of the interval. - */ - clearInterval(id: NodeJS.Timeout): void; - - /** - * Mock animation frames with timeouts. - * - * @param {Function} callback Callback. - * @returns {NodeJS.Timeout} ID. - */ - requestAnimationFrame(callback: (timestamp: number) => void): NodeJS.Immediate; - - /** - * Mock animation frames with timeouts. - * - * @param {NodeJS.Timeout} id ID. - */ - cancelAnimationFrame(id: NodeJS.Immediate): void; - - /** - * This method provides an easy, logical way to fetch resources asynchronously across the network. - * - * @param url URL. - * @param [init] Init. - * @returns Promise. - */ - fetch(url: RequestInfo, init?: IRequestInit): Promise; - - /** - * Creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data). - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/btoa - * @param data Binary data. - * @returns Base64-encoded string. - */ - btoa(data: unknown): string; - - /** - * Decodes a string of data which has been encoded using Base64 encoding. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/atob - * @see https://infra.spec.whatwg.org/#forgiving-base64-encode. - * @see Https://html.spec.whatwg.org/multipage/webappapis.html#btoa. - * @param data Binary string. - * @returns An ASCII string containing decoded data from encodedData. - */ - atob(data: unknown): string; - - /** - * Safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it. - * - * @param message Message. - * @param listener Listener. - */ - postMessage(message: unknown, targetOrigin?: string, transfer?: unknown[]): void; + readonly happyDOM: DetachedWindowAPI; } diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index dab74adfa..f9f97e9a2 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -1,503 +1,19 @@ -import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; -import Document from '../nodes/document/Document.js'; -import HTMLDocument from '../nodes/html-document/HTMLDocument.js'; -import XMLDocument from '../nodes/xml-document/XMLDocument.js'; -import SVGDocument from '../nodes/svg-document/SVGDocument.js'; -import Node from '../nodes/node/Node.js'; -import NodeFilter from '../tree-walker/NodeFilter.js'; -import Text from '../nodes/text/Text.js'; -import Comment from '../nodes/comment/Comment.js'; -import ShadowRoot from '../nodes/shadow-root/ShadowRoot.js'; -import Element from '../nodes/element/Element.js'; -import HTMLTemplateElement from '../nodes/html-template-element/HTMLTemplateElement.js'; -import HTMLFormElement from '../nodes/html-form-element/HTMLFormElement.js'; -import HTMLElement from '../nodes/html-element/HTMLElement.js'; -import HTMLUnknownElement from '../nodes/html-unknown-element/HTMLUnknownElement.js'; -import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; -import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement.js'; -import HTMLTextAreaElement from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; -import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement.js'; -import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement.js'; -import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js'; -import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js'; -import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement.js'; -import HTMLMediaElement from '../nodes/html-media-element/HTMLMediaElement.js'; -import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; -import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement.js'; -import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js'; -import HTMLIFrameElement from '../nodes/html-iframe-element/HTMLIFrameElement.js'; -import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; -import SVGSVGElement from '../nodes/svg-element/SVGSVGElement.js'; -import SVGElement from '../nodes/svg-element/SVGElement.js'; -import SVGGraphicsElement from '../nodes/svg-element/SVGGraphicsElement.js'; -import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js'; -import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; -import CharacterData from '../nodes/character-data/CharacterData.js'; -import DocumentType from '../nodes/document-type/DocumentType.js'; -import NodeIterator from '../tree-walker/NodeIterator.js'; -import TreeWalker from '../tree-walker/TreeWalker.js'; -import Event from '../event/Event.js'; -import CustomEvent from '../event/events/CustomEvent.js'; -import AnimationEvent from '../event/events/AnimationEvent.js'; -import KeyboardEvent from '../event/events/KeyboardEvent.js'; -import MessageEvent from '../event/events/MessageEvent.js'; -import ProgressEvent from '../event/events/ProgressEvent.js'; -import MediaQueryListEvent from '../event/events/MediaQueryListEvent.js'; -import EventTarget from '../event/EventTarget.js'; -import MessagePort from '../event/MessagePort.js'; -import { URLSearchParams } from 'url'; -import URL from '../url/URL.js'; -import Location from '../location/Location.js'; -import MutationObserver from '../mutation-observer/MutationObserver.js'; -import MutationRecord from '../mutation-observer/MutationRecord.js'; -import XMLSerializer from '../xml-serializer/XMLSerializer.js'; -import ResizeObserver from '../resize-observer/ResizeObserver.js'; -import Blob from '../file/Blob.js'; -import File from '../file/File.js'; -import DOMException from '../exception/DOMException.js'; -import History from '../history/History.js'; -import CSSStyleSheet from '../css/CSSStyleSheet.js'; -import CSSStyleDeclaration from '../css/declaration/CSSStyleDeclaration.js'; -import CSS from '../css/CSS.js'; -import CSSUnitValue from '../css/CSSUnitValue.js'; -import CSSRule from '../css/CSSRule.js'; -import CSSContainerRule from '../css/rules/CSSContainerRule.js'; -import CSSFontFaceRule from '../css/rules/CSSFontFaceRule.js'; -import CSSKeyframeRule from '../css/rules/CSSKeyframeRule.js'; -import CSSKeyframesRule from '../css/rules/CSSKeyframesRule.js'; -import CSSMediaRule from '../css/rules/CSSMediaRule.js'; -import CSSStyleRule from '../css/rules/CSSStyleRule.js'; -import CSSSupportsRule from '../css/rules/CSSSupportsRule.js'; -import MouseEvent from '../event/events/MouseEvent.js'; -import PointerEvent from '../event/events/PointerEvent.js'; -import FocusEvent from '../event/events/FocusEvent.js'; -import WheelEvent from '../event/events/WheelEvent.js'; -import DataTransfer from '../event/DataTransfer.js'; -import DataTransferItem from '../event/DataTransferItem.js'; -import DataTransferItemList from '../event/DataTransferItemList.js'; -import InputEvent from '../event/events/InputEvent.js'; -import UIEvent from '../event/UIEvent.js'; -import ErrorEvent from '../event/events/ErrorEvent.js'; -import StorageEvent from '../event/events/StorageEvent.js'; -import SubmitEvent from '../event/events/SubmitEvent.js'; -import Screen from '../screen/Screen.js'; -import Response from '../fetch/Response.js'; -import IResponse from '../fetch/types/IResponse.js'; -import IRequestInit from '../fetch/types/IRequestInit.js'; -import Storage from '../storage/Storage.js'; import IWindow from './IWindow.js'; -import HTMLCollection from '../nodes/element/HTMLCollection.js'; -import HTMLFormControlsCollection from '../nodes/html-form-element/HTMLFormControlsCollection.js'; -import NodeList from '../nodes/node/NodeList.js'; -import MediaQueryList from '../match-media/MediaQueryList.js'; -import Selection from '../selection/Selection.js'; -import Navigator from '../navigator/Navigator.js'; -import MimeType from '../navigator/MimeType.js'; -import MimeTypeArray from '../navigator/MimeTypeArray.js'; -import Plugin from '../navigator/Plugin.js'; -import PluginArray from '../navigator/PluginArray.js'; -import Fetch from '../fetch/Fetch.js'; -import DOMRect from '../nodes/element/DOMRect.js'; -import VMGlobalPropertyScript from './VMGlobalPropertyScript.js'; -import * as PerfHooks from 'perf_hooks'; -import VM from 'vm'; -import { Buffer } from 'buffer'; -import { webcrypto } from 'crypto'; -import XMLHttpRequestUpload from '../xml-http-request/XMLHttpRequestUpload.js'; -import XMLHttpRequestEventTarget from '../xml-http-request/XMLHttpRequestEventTarget.js'; -import Base64 from '../base64/Base64.js'; -import Attr from '../nodes/attr/Attr.js'; -import NamedNodeMap from '../named-node-map/NamedNodeMap.js'; -import IElement from '../nodes/element/IElement.js'; -import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction.js'; -import RequestInfo from '../fetch/types/IRequestInfo.js'; -import FileList from '../nodes/html-input-element/FileList.js'; -import Stream from 'stream'; -import FormData from '../form-data/FormData.js'; -import AbortController from '../fetch/AbortController.js'; -import AbortSignal from '../fetch/AbortSignal.js'; -import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; -import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; -import ValidityState from '../validity-state/ValidityState.js'; -import WindowErrorUtility from './WindowErrorUtility.js'; -import ICrossOriginWindow from './ICrossOriginWindow.js'; -import Permissions from '../permissions/Permissions.js'; -import PermissionStatus from '../permissions/PermissionStatus.js'; -import Clipboard from '../clipboard/Clipboard.js'; -import ClipboardItem from '../clipboard/ClipboardItem.js'; -import ClipboardEvent from '../event/events/ClipboardEvent.js'; import DetachedWindowAPI from './DetachedWindowAPI.js'; -import Headers from '../fetch/Headers.js'; -import WindowClassFactory from './WindowClassFactory.js'; -import Audio from '../nodes/html-audio-element/Audio.js'; -import Image from '../nodes/html-image-element/Image.js'; -import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; -import DOMParser from '../dom-parser/DOMParser.js'; -import FileReader from '../file/FileReader.js'; -import Request from '../fetch/Request.js'; -import Range from '../range/Range.js'; -import XMLHttpRequest from '../xml-http-request/XMLHttpRequest.js'; import IOptionalBrowserSettings from '../browser/types/IOptionalBrowserSettings.js'; -import WindowBrowserSettingsReader from './WindowBrowserSettingsReader.js'; -import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; -import DocumentReadyStateEnum from '../nodes/document/DocumentReadyStateEnum.js'; -import IBrowserFrame from '../browser/types/IBrowserFrame.js'; -import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement.js'; -import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement.js'; -import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement.js'; -import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; -import DetachedBrowser from '../browser/detached-browser/DetachedBrowser.js'; -import WindowPageOpenUtility from './WindowPageOpenUtility.js'; - -const ORIGINAL_SET_TIMEOUT = setTimeout; -const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; -const ORIGINAL_SET_INTERVAL = setInterval; -const ORIGINAL_CLEAR_INTERVAL = clearInterval; -const ORIGINAL_QUEUE_MICROTASK = queueMicrotask; +import BrowserWindow from './BrowserWindow.js'; +import DetachedBrowserPage from '../browser/DetachedBrowserPage.js'; +import Browser from '../browser/Browser.js'; /** - * Browser window. + * Window. * * Reference: * https://developer.mozilla.org/en-US/docs/Web/API/Window. */ -export default class Window extends EventTarget implements IWindow { +export default class Window extends BrowserWindow implements IWindow { // Detached Window API. - public readonly happyDOM?: DetachedWindowAPI; - - // Nodes - public readonly Node: typeof Node; - public readonly Attr: typeof Attr; - public readonly SVGSVGElement: typeof SVGSVGElement; - public readonly SVGElement: typeof SVGElement; - public readonly SVGGraphicsElement: typeof SVGGraphicsElement; - public readonly Text: typeof Text; - public readonly Comment: typeof Comment; - public readonly ShadowRoot: typeof ShadowRoot; - public readonly ProcessingInstruction: typeof ProcessingInstruction; - public readonly Element: typeof Element; - public readonly CharacterData: typeof CharacterData; - public readonly Document: typeof Document; - public readonly HTMLDocument: typeof HTMLDocument; - public readonly XMLDocument: typeof XMLDocument; - public readonly SVGDocument: typeof SVGDocument; - public readonly DocumentType: typeof DocumentType; - - // Element classes - public readonly HTMLAnchorElement: typeof HTMLAnchorElement; - public readonly HTMLButtonElement: typeof HTMLButtonElement; - public readonly HTMLOptGroupElement: typeof HTMLOptGroupElement; - public readonly HTMLOptionElement: typeof HTMLOptionElement; - public readonly HTMLElement: typeof HTMLElement; - public readonly HTMLUnknownElement: typeof HTMLUnknownElement; - public readonly HTMLTemplateElement: typeof HTMLTemplateElement; - public readonly HTMLFormElement: typeof HTMLFormElement; - public readonly HTMLInputElement: typeof HTMLInputElement; - public readonly HTMLSelectElement: typeof HTMLSelectElement; - public readonly HTMLTextAreaElement: typeof HTMLTextAreaElement; - public readonly HTMLImageElement: typeof HTMLImageElement; - public readonly HTMLScriptElement: typeof HTMLScriptElement; - public readonly HTMLLinkElement: typeof HTMLLinkElement; - public readonly HTMLStyleElement: typeof HTMLStyleElement; - public readonly HTMLLabelElement: typeof HTMLLabelElement; - public readonly HTMLSlotElement: typeof HTMLSlotElement; - public readonly HTMLMetaElement: typeof HTMLMetaElement; - public readonly HTMLMediaElement: typeof HTMLMediaElement; - public readonly HTMLAudioElement: typeof HTMLAudioElement; - public readonly HTMLVideoElement: typeof HTMLVideoElement; - public readonly HTMLBaseElement: typeof HTMLBaseElement; - public readonly HTMLIFrameElement: typeof HTMLIFrameElement; - public readonly HTMLDialogElement: typeof HTMLDialogElement; - - // Non-implemented element classes - public readonly HTMLHeadElement: typeof HTMLElement; - public readonly HTMLTitleElement: typeof HTMLElement; - public readonly HTMLBodyElement: typeof HTMLElement; - public readonly HTMLHeadingElement: typeof HTMLElement; - public readonly HTMLParagraphElement: typeof HTMLElement; - public readonly HTMLHRElement: typeof HTMLElement; - public readonly HTMLPreElement: typeof HTMLElement; - public readonly HTMLUListElement: typeof HTMLElement; - public readonly HTMLOListElement: typeof HTMLElement; - public readonly HTMLLIElement: typeof HTMLElement; - public readonly HTMLMenuElement: typeof HTMLElement; - public readonly HTMLDListElement: typeof HTMLElement; - public readonly HTMLDivElement: typeof HTMLElement; - public readonly HTMLAreaElement: typeof HTMLElement; - public readonly HTMLBRElement: typeof HTMLElement; - public readonly HTMLCanvasElement: typeof HTMLElement; - public readonly HTMLDataElement: typeof HTMLElement; - public readonly HTMLDataListElement: typeof HTMLElement; - public readonly HTMLDetailsElement: typeof HTMLElement; - public readonly HTMLDirectoryElement: typeof HTMLElement; - public readonly HTMLFieldSetElement: typeof HTMLElement; - public readonly HTMLFontElement: typeof HTMLElement; - public readonly HTMLHtmlElement: typeof HTMLElement; - public readonly HTMLLegendElement: typeof HTMLElement; - public readonly HTMLMapElement: typeof HTMLElement; - public readonly HTMLMarqueeElement: typeof HTMLElement; - public readonly HTMLMeterElement: typeof HTMLElement; - public readonly HTMLModElement: typeof HTMLElement; - public readonly HTMLOutputElement: typeof HTMLElement; - public readonly HTMLPictureElement: typeof HTMLElement; - public readonly HTMLProgressElement: typeof HTMLElement; - public readonly HTMLQuoteElement: typeof HTMLElement; - public readonly HTMLSourceElement: typeof HTMLElement; - public readonly HTMLSpanElement: typeof HTMLElement; - public readonly HTMLTableCaptionElement: typeof HTMLElement; - public readonly HTMLTableCellElement: typeof HTMLElement; - public readonly HTMLTableColElement: typeof HTMLElement; - public readonly HTMLTableElement: typeof HTMLElement; - public readonly HTMLTimeElement: typeof HTMLElement; - public readonly HTMLTableRowElement: typeof HTMLElement; - public readonly HTMLTableSectionElement: typeof HTMLElement; - public readonly HTMLFrameElement: typeof HTMLElement; - public readonly HTMLFrameSetElement: typeof HTMLElement; - public readonly HTMLEmbedElement: typeof HTMLElement; - public readonly HTMLObjectElement: typeof HTMLElement; - public readonly HTMLParamElement: typeof HTMLElement; - public readonly HTMLTrackElement: typeof HTMLElement; - - // Events classes - public readonly Event = Event; - public readonly UIEvent = UIEvent; - public readonly CustomEvent = CustomEvent; - public readonly AnimationEvent = AnimationEvent; - public readonly KeyboardEvent = KeyboardEvent; - public readonly MessageEvent = MessageEvent; - public readonly MouseEvent = MouseEvent; - public readonly PointerEvent = PointerEvent; - public readonly FocusEvent = FocusEvent; - public readonly WheelEvent = WheelEvent; - public readonly InputEvent = InputEvent; - public readonly ErrorEvent = ErrorEvent; - public readonly StorageEvent = StorageEvent; - public readonly SubmitEvent = SubmitEvent; - public readonly ProgressEvent = ProgressEvent; - public readonly MediaQueryListEvent = MediaQueryListEvent; - public readonly ClipboardEvent = ClipboardEvent; - - // Non-implemented event classes - public readonly AudioProcessingEvent = Event; - public readonly BeforeInputEvent = Event; - public readonly BeforeUnloadEvent = Event; - public readonly BlobEvent = Event; - public readonly CloseEvent = Event; - public readonly CompositionEvent = Event; - public readonly CSSFontFaceLoadEvent = Event; - public readonly DeviceLightEvent = Event; - public readonly DeviceMotionEvent = Event; - public readonly DeviceOrientationEvent = Event; - public readonly DeviceProximityEvent = Event; - public readonly DOMTransactionEvent = Event; - public readonly DragEvent = Event; - public readonly EditingBeforeInputEvent = Event; - public readonly FetchEvent = Event; - public readonly GamepadEvent = Event; - public readonly HashChangeEvent = Event; - public readonly IDBVersionChangeEvent = Event; - public readonly MediaStreamEvent = Event; - public readonly MutationEvent = Event; - public readonly OfflineAudioCompletionEvent = Event; - public readonly OverconstrainedError = Event; - public readonly PageTransitionEvent = Event; - public readonly PaymentRequestUpdateEvent = Event; - public readonly PopStateEvent = Event; - public readonly RelatedEvent = Event; - public readonly RTCDataChannelEvent = Event; - public readonly RTCIdentityErrorEvent = Event; - public readonly RTCIdentityEvent = Event; - public readonly RTCPeerConnectionIceEvent = Event; - public readonly SensorEvent = Event; - public readonly SVGEvent = Event; - public readonly SVGZoomEvent = Event; - public readonly TimeEvent = Event; - public readonly TouchEvent = Event; - public readonly TrackEvent = Event; - public readonly TransitionEvent = Event; - public readonly UserProximityEvent = Event; - public readonly WebGLContextEvent = Event; - public readonly TextEvent = Event; - - // Other classes - public readonly NamedNodeMap = NamedNodeMap; - public readonly NodeFilter = NodeFilter; - public readonly NodeIterator = NodeIterator; - public readonly TreeWalker = TreeWalker; - public readonly MutationObserver = MutationObserver; - public readonly MutationRecord = MutationRecord; - public readonly EventTarget = EventTarget; - public readonly MessagePort = MessagePort; - public readonly DataTransfer = DataTransfer; - public readonly DataTransferItem = DataTransferItem; - public readonly DataTransferItemList = DataTransferItemList; - public readonly URL = URL; - public readonly Location = Location; - public readonly CustomElementRegistry = CustomElementRegistry; - public readonly Window = this.constructor; - public readonly XMLSerializer = XMLSerializer; - public readonly ResizeObserver = ResizeObserver; - public readonly CSSStyleSheet = CSSStyleSheet; - public readonly Blob = Blob; - public readonly File = File; - public readonly DOMException = DOMException; - public readonly History = History; - public readonly Screen = Screen; - public readonly Storage = Storage; - public readonly URLSearchParams = URLSearchParams; - public readonly HTMLCollection = HTMLCollection; - public readonly HTMLFormControlsCollection = HTMLFormControlsCollection; - public readonly NodeList = NodeList; - public readonly CSSUnitValue = CSSUnitValue; - public readonly CSSRule = CSSRule; - public readonly CSSContainerRule = CSSContainerRule; - public readonly CSSFontFaceRule = CSSFontFaceRule; - public readonly CSSKeyframeRule = CSSKeyframeRule; - public readonly CSSKeyframesRule = CSSKeyframesRule; - public readonly CSSMediaRule = CSSMediaRule; - public readonly CSSStyleRule = CSSStyleRule; - public readonly CSSSupportsRule = CSSSupportsRule; - public readonly Selection = Selection; - public readonly Navigator = Navigator; - public readonly MimeType = MimeType; - public readonly MimeTypeArray = MimeTypeArray; - public readonly Plugin = Plugin; - public readonly PluginArray = PluginArray; - public readonly FileList = FileList; - public readonly DOMRect = DOMRect; - public readonly RadioNodeList = RadioNodeList; - public readonly ValidityState = ValidityState; - public readonly Headers = Headers; - public readonly Request: typeof Request; - public readonly Response: typeof Response; - public readonly XMLHttpRequestUpload = XMLHttpRequestUpload; - public readonly XMLHttpRequestEventTarget = XMLHttpRequestEventTarget; - public readonly ReadableStream = Stream.Readable; - public readonly WritableStream = Stream.Writable; - public readonly TransformStream = Stream.Transform; - public readonly AbortController = AbortController; - public readonly AbortSignal = AbortSignal; - public readonly FormData = FormData; - public readonly Permissions = Permissions; - public readonly PermissionStatus = PermissionStatus; - public readonly Clipboard = Clipboard; - public readonly ClipboardItem = ClipboardItem; - public readonly XMLHttpRequest: typeof XMLHttpRequest; - public readonly DOMParser: typeof DOMParser; - public readonly Range: typeof Range; - public readonly FileReader: typeof FileReader; - public readonly Image: typeof Image; - public readonly DocumentFragment: typeof DocumentFragment; - public readonly Audio: typeof Audio; - - // Events - public onload: ((event: Event) => void) | null = null; - public onerror: ((event: ErrorEvent) => void) | null = null; - - // Public properties. - public readonly document: Document; - public readonly customElements: CustomElementRegistry; - public readonly location: Location; - public readonly history: History; - public readonly navigator: Navigator; - public readonly opener: IWindow | null = null; - public readonly self: IWindow = this; - public readonly top: IWindow = this; - public readonly parent: IWindow = this; - public readonly window: IWindow = this; - public readonly globalThis: IWindow = this; - public readonly screen: Screen; - public readonly devicePixelRatio = 1; - public readonly sessionStorage: Storage; - public readonly localStorage: Storage; - public readonly performance = PerfHooks.performance; - public readonly innerWidth: number = 1024; - public readonly innerHeight: number = 768; - public readonly outerWidth: number = 1024; - public readonly outerHeight: number = 768; - public readonly screenLeft: number = 0; - public readonly screenTop: number = 0; - public readonly screenX: number = 0; - public readonly screenY: number = 0; - public readonly crypto = webcrypto; - public readonly closed = false; - public name: string = ''; - - // Node.js Globals - public Array: typeof Array; - public ArrayBuffer: typeof ArrayBuffer; - public Boolean: typeof Boolean; - public Buffer = Buffer; - public DataView: typeof DataView; - public Date: typeof Date; - public Error: typeof Error; - public EvalError: typeof EvalError; - public Float32Array: typeof Float32Array; - public Float64Array: typeof Float64Array; - public Function: typeof Function; - public Infinity: typeof Infinity; - public Int16Array: typeof Int16Array; - public Int32Array: typeof Int32Array; - public Int8Array: typeof Int8Array; - public Intl: typeof Intl; - public JSON: typeof JSON; - public Map: MapConstructor; - public Math: typeof Math; - public NaN: typeof NaN; - public Number: typeof Number; - public Object: typeof Object; - public Promise: typeof Promise; - public RangeError: typeof RangeError; - public ReferenceError: typeof ReferenceError; - public RegExp: typeof RegExp; - public Set: SetConstructor; - public String: typeof String; - public Symbol: Function; - public SyntaxError: typeof SyntaxError; - public TypeError: typeof TypeError; - public URIError: typeof URIError; - public Uint16Array: typeof Uint16Array; - public Uint32Array: typeof Uint32Array; - public Uint8Array: typeof Uint8Array; - public Uint8ClampedArray: typeof Uint8ClampedArray; - public WeakMap: WeakMapConstructor; - public WeakSet: WeakSetConstructor; - public decodeURI: typeof decodeURI; - public decodeURIComponent: typeof decodeURIComponent; - public encodeURI: typeof encodeURI; - public encodeURIComponent: typeof encodeURIComponent; - public eval: typeof eval; - /** - * @deprecated - */ - public escape: (str: string) => string; - public global: typeof globalThis; - public isFinite: typeof isFinite; - public isNaN: typeof isNaN; - public parseFloat: typeof parseFloat; - public parseInt: typeof parseInt; - public undefined: typeof undefined; - /** - * @deprecated - */ - public unescape: (str: string) => string; - public gc: () => void; - public v8debug?: unknown; - - // Public internal properties - - // Used for tracking capture event listeners to improve performance when they are not used. - // See EventTarget class. - public _captureEventListenerCount: { [eventType: string]: number } = {}; - public readonly _readyStateManager = new DocumentReadyStateManager(this); - - // Private properties - #setTimeout: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; - #clearTimeout: (id: NodeJS.Timeout) => void; - #setInterval: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; - #clearInterval: (id: NodeJS.Timeout) => void; - #queueMicrotask: (callback: Function) => void; - #browserFrame: IBrowserFrame; + public readonly happyDOM: DetachedWindowAPI; /** * Constructor. @@ -510,7 +26,6 @@ export default class Window extends EventTarget implements IWindow { * @param [options.url] URL. * @param [options.console] Console. * @param [options.settings] Settings. - * @param [options.browserFrame] Browser frame. */ constructor(options?: { width?: number; @@ -520,582 +35,33 @@ export default class Window extends EventTarget implements IWindow { url?: string; console?: Console; settings?: IOptionalBrowserSettings; - browserFrame?: IBrowserFrame; }) { - super(); + const browser = new Browser(); + const browserPage = new DetachedBrowserPage(browser.defaultContext, BrowserWindow); + const browserFrame = browserPage.mainFrame; - this.customElements = new CustomElementRegistry(); - this.navigator = new Navigator(this); - this.history = new History(); - this.screen = new Screen(); - this.sessionStorage = new Storage(); - this.localStorage = new Storage(); + super(browserFrame); - if (options?.browserFrame) { - this.#browserFrame = options.browserFrame; - } else { - this.#browserFrame = new DetachedBrowser(Window, this, { - console: options?.console, - settings: options?.settings - }).defaultContext.pages[0].mainFrame; - this.happyDOM = new DetachedWindowAPI(this.#browserFrame); - } + browserFrame.window = this; - WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); - - this.location = new Location(this.#browserFrame, options?.url ?? 'about:blank'); + this.happyDOM = new DetachedWindowAPI(browserFrame); if (options) { - if (options.width !== undefined) { - this.innerWidth = options.width; - this.outerWidth = options.width; - } else if (options.innerWidth !== undefined) { - this.innerWidth = options.innerWidth; - this.outerWidth = options.innerWidth; - } - - if (options.height !== undefined) { - this.innerHeight = options.height; - this.outerHeight = options.height; - } else if (options.innerHeight !== undefined) { - this.innerHeight = options.innerHeight; - this.outerHeight = options.innerHeight; - } - } - - this.#setTimeout = ORIGINAL_SET_TIMEOUT; - this.#clearTimeout = ORIGINAL_CLEAR_TIMEOUT; - this.#setInterval = ORIGINAL_SET_INTERVAL; - this.#clearInterval = ORIGINAL_CLEAR_INTERVAL; - this.#queueMicrotask = ORIGINAL_QUEUE_MICROTASK; - - // Binds all methods to "this", so that it will use the correct context when called globally. - for (const key of Object.getOwnPropertyNames(Window.prototype).concat( - Object.getOwnPropertyNames(EventTarget.prototype) - )) { if ( - key !== 'constructor' && - key[0] !== '_' && - key[0] === key[0].toLowerCase() && - typeof this[key] === 'function' + options.width !== undefined || + options.innerWidth !== undefined || + options.height !== undefined || + options.innerHeight !== undefined ) { - this[key] = this[key].bind(this); - } - } - - this._setupVMContext(); - - const classes = WindowClassFactory.getClasses({ - window: this, - browserFrame: this.#browserFrame - }); - - // Classes that require the window to be injected - this.Response = classes.Response; - this.Request = classes.Request; - this.Image = classes.Image; - this.DocumentFragment = classes.DocumentFragment; - this.FileReader = classes.FileReader; - this.DOMParser = classes.DOMParser; - this.XMLHttpRequest = classes.XMLHttpRequest; - this.Range = classes.Range; - this.Audio = classes.Audio; - - // Nodes - this.Node = classes.Node; - this.Attr = classes.Attr; - this.SVGSVGElement = classes.SVGSVGElement; - this.SVGElement = classes.SVGElement; - this.SVGGraphicsElement = classes.SVGGraphicsElement; - this.Text = classes.Text; - this.Comment = classes.Comment; - this.ShadowRoot = classes.ShadowRoot; - this.ProcessingInstruction = classes.ProcessingInstruction; - this.Element = classes.Element; - this.CharacterData = classes.CharacterData; - this.Document = classes.Document; - this.HTMLDocument = classes.HTMLDocument; - this.XMLDocument = classes.XMLDocument; - this.SVGDocument = classes.SVGDocument; - this.DocumentType = classes.DocumentType; - - // HTML Element classes - this.HTMLAnchorElement = classes.HTMLAnchorElement; - this.HTMLButtonElement = classes.HTMLButtonElement; - this.HTMLOptGroupElement = classes.HTMLOptGroupElement; - this.HTMLOptionElement = classes.HTMLOptionElement; - this.HTMLElement = classes.HTMLElement; - this.HTMLUnknownElement = classes.HTMLUnknownElement; - this.HTMLTemplateElement = classes.HTMLTemplateElement; - this.HTMLFormElement = classes.HTMLFormElement; - this.HTMLInputElement = classes.HTMLInputElement; - this.HTMLSelectElement = classes.HTMLSelectElement; - this.HTMLTextAreaElement = classes.HTMLTextAreaElement; - this.HTMLImageElement = classes.HTMLImageElement; - this.HTMLScriptElement = classes.HTMLScriptElement; - this.HTMLLinkElement = classes.HTMLLinkElement; - this.HTMLStyleElement = classes.HTMLStyleElement; - this.HTMLLabelElement = classes.HTMLLabelElement; - this.HTMLSlotElement = classes.HTMLSlotElement; - this.HTMLMetaElement = classes.HTMLMetaElement; - this.HTMLMediaElement = classes.HTMLMediaElement; - this.HTMLAudioElement = classes.HTMLAudioElement; - this.HTMLVideoElement = classes.HTMLVideoElement; - this.HTMLBaseElement = classes.HTMLBaseElement; - this.HTMLIFrameElement = classes.HTMLIFrameElement; - this.HTMLDialogElement = classes.HTMLDialogElement; - - // Non-implemented HTML element classes - this.HTMLHeadElement = classes.HTMLElement; - this.HTMLTitleElement = classes.HTMLElement; - this.HTMLBodyElement = classes.HTMLElement; - this.HTMLHeadingElement = classes.HTMLElement; - this.HTMLParagraphElement = classes.HTMLElement; - this.HTMLHRElement = classes.HTMLElement; - this.HTMLPreElement = classes.HTMLElement; - this.HTMLUListElement = classes.HTMLElement; - this.HTMLOListElement = classes.HTMLElement; - this.HTMLLIElement = classes.HTMLElement; - this.HTMLMenuElement = classes.HTMLElement; - this.HTMLDListElement = classes.HTMLElement; - this.HTMLDivElement = classes.HTMLElement; - this.HTMLAreaElement = classes.HTMLElement; - this.HTMLBRElement = classes.HTMLElement; - this.HTMLCanvasElement = classes.HTMLElement; - this.HTMLDataElement = classes.HTMLElement; - this.HTMLDataListElement = classes.HTMLElement; - this.HTMLDetailsElement = classes.HTMLElement; - this.HTMLDirectoryElement = classes.HTMLElement; - this.HTMLFieldSetElement = classes.HTMLElement; - this.HTMLFontElement = classes.HTMLElement; - this.HTMLHtmlElement = classes.HTMLElement; - this.HTMLLegendElement = classes.HTMLElement; - this.HTMLMapElement = classes.HTMLElement; - this.HTMLMarqueeElement = classes.HTMLElement; - this.HTMLMeterElement = classes.HTMLElement; - this.HTMLModElement = classes.HTMLElement; - this.HTMLOutputElement = classes.HTMLElement; - this.HTMLPictureElement = classes.HTMLElement; - this.HTMLProgressElement = classes.HTMLElement; - this.HTMLQuoteElement = classes.HTMLElement; - this.HTMLSourceElement = classes.HTMLElement; - this.HTMLSpanElement = classes.HTMLElement; - this.HTMLTableCaptionElement = classes.HTMLElement; - this.HTMLTableCellElement = classes.HTMLElement; - this.HTMLTableColElement = classes.HTMLElement; - this.HTMLTableElement = classes.HTMLElement; - this.HTMLTimeElement = classes.HTMLElement; - this.HTMLTableRowElement = classes.HTMLElement; - this.HTMLTableSectionElement = classes.HTMLElement; - this.HTMLFrameElement = classes.HTMLElement; - this.HTMLFrameSetElement = classes.HTMLElement; - this.HTMLEmbedElement = classes.HTMLElement; - this.HTMLObjectElement = classes.HTMLElement; - this.HTMLParamElement = classes.HTMLElement; - this.HTMLTrackElement = classes.HTMLElement; - - // Document - this.document = new this.HTMLDocument(); - (this.document.defaultView) = this; - - // Default document elements - const doctype = this.document.implementation.createDocumentType('html', '', ''); - const documentElement = this.document.createElement('html'); - const bodyElement = this.document.createElement('body'); - const headElement = this.document.createElement('head'); - - this.document.appendChild(doctype); - this.document.appendChild(documentElement); - - documentElement.appendChild(headElement); - documentElement.appendChild(bodyElement); - - // Ready state manager - this._readyStateManager.whenComplete().then(() => { - (this.document.readyState) = DocumentReadyStateEnum.complete; - this.document.dispatchEvent(new Event('readystatechange')); - this.document.dispatchEvent(new Event('load', { bubbles: true })); - }); - } - - /** - * Returns the console. - * - * @returns Console. - */ - public get console(): Console { - return this.#browserFrame.page.console; - } - - /** - * The number of pixels that the document is currently scrolled horizontally. - * - * @returns Scroll X. - */ - public get scrollX(): number { - return this.document?.documentElement?.scrollLeft ?? 0; - } - - /** - * The read-only Window property pageXOffset is an alias for scrollX. - * - * @returns Scroll X. - */ - public get pageXOffset(): number { - return this.scrollX; - } - - /** - * The number of pixels that the document is currently scrolled vertically. - * - * @returns Scroll Y. - */ - public get scrollY(): number { - return this.document?.documentElement?.scrollTop ?? 0; - } - - /** - * The read-only Window property pageYOffset is an alias for scrollY. - * - * @returns Scroll Y. - */ - public get pageYOffset(): number { - return this.scrollY; - } - - /** - * The CSS interface holds useful CSS-related methods. - * - * @returns CSS interface. - */ - public get CSS(): CSS { - return new CSS(); - } - - /** - * Returns an object containing the values of all CSS properties of an element. - * - * @param element Element. - * @returns CSS style declaration. - */ - public getComputedStyle(element: IElement): CSSStyleDeclaration { - element['_computedStyle'] = element['_computedStyle'] || new CSSStyleDeclaration(element, true); - return element['_computedStyle']; - } - - /** - * Returns selection. - * - * @returns Selection. - */ - public getSelection(): Selection { - return this.document.getSelection(); - } - - /** - * Scrolls to a particular set of coordinates. - * - * @param x X position or options object. - * @param y Y position. - */ - public scroll(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void { - if (typeof x === 'object') { - if (x.behavior === 'smooth') { - this.setTimeout(() => { - if (x.top !== undefined) { - (this.document.documentElement.scrollTop) = x.top; - } - if (x.left !== undefined) { - (this.document.documentElement.scrollLeft) = x.left; - } + browserFrame.page.setViewport({ + width: options.width ?? options.innerWidth ?? 1024, + height: options.height ?? options.innerHeight ?? 768 }); - } else { - if (x.top !== undefined) { - (this.document.documentElement.scrollTop) = x.top; - } - if (x.left !== undefined) { - (this.document.documentElement.scrollLeft) = x.left; - } } - } else if (x !== undefined && y !== undefined) { - (this.document.documentElement.scrollLeft) = x; - (this.document.documentElement.scrollTop) = y; - } - } - - /** - * Scrolls to a particular set of coordinates. - * - * @param x X position or options object. - * @param y Y position. - */ - public scrollTo( - x: { top?: number; left?: number; behavior?: string } | number, - y?: number - ): void { - this.scroll(x, y); - } - /** - * Shifts focus away from the window. - */ - public blur(): void { - // TODO: Implement. - } - - /** - * Gives focus to the window. - */ - public focus(): void { - // TODO: Implement. - } - - /** - * Loads a specified resource into a new or existing browsing context (that is, a tab, a window, or an iframe) under a specified name. - * - * @param [url] URL. - * @param [target] Target. - * @param [features] Window features. - * @returns Window. - */ - public open( - url?: string, - target?: string, - features?: string - ): IWindow | ICrossOriginWindow | null { - return WindowPageOpenUtility.openPage(this.#browserFrame, { - url, - target, - features - }); - } - - /** - * Closes the window. - */ - public close(): void { - if (this.#browserFrame.page.mainFrame === this.#browserFrame) { - this.#browserFrame.page.close(); - } - } - - /** - * Returns a new MediaQueryList object that can then be used to determine if the document matches the media query string. - * - * @param mediaQueryString A string specifying the media query to parse into a MediaQueryList. - * @returns A new MediaQueryList. - */ - public matchMedia(mediaQueryString: string): MediaQueryList { - return new MediaQueryList({ ownerWindow: this, media: mediaQueryString }); - } - - /** - * Sets a timer which executes a function once the timer expires. - * - * @param callback Function to be executed. - * @param [delay=0] Delay in ms. - * @param args Arguments passed to the callback function. - * @returns Timeout ID. - */ - public setTimeout(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { - const id = this.#setTimeout(() => { - if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { - callback(...args); - } else { - WindowErrorUtility.captureError(this, () => callback(...args)); - } - this.#browserFrame._asyncTaskManager.endTimer(id); - }, delay); - this.#browserFrame._asyncTaskManager.startTimer(id); - return id; - } - - /** - * Cancels a timeout previously established by calling setTimeout(). - * - * @param id ID of the timeout. - */ - public clearTimeout(id: NodeJS.Timeout): void { - this.#clearTimeout(id); - this.#browserFrame._asyncTaskManager.endTimer(id); - } - - /** - * Calls a function with a fixed time delay between each call. - * - * @param callback Function to be executed. - * @param [delay=0] Delay in ms. - * @param args Arguments passed to the callback function. - * @returns Interval ID. - */ - public setInterval(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { - const id = this.#setInterval(() => { - if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { - callback(...args); - } else { - WindowErrorUtility.captureError( - this, - () => callback(...args), - () => this.clearInterval(id) - ); + if (options.url !== undefined) { + browserFrame.url = options.url; } - }, delay); - this.#browserFrame._asyncTaskManager.startTimer(id); - return id; - } - - /** - * Cancels a timed repeating action which was previously established by a call to setInterval(). - * - * @param id ID of the interval. - */ - public clearInterval(id: NodeJS.Timeout): void { - this.#clearInterval(id); - this.#browserFrame._asyncTaskManager.endTimer(id); - } - - /** - * Mock animation frames with timeouts. - * - * @param callback Callback. - * @returns ID. - */ - public requestAnimationFrame(callback: (timestamp: number) => void): NodeJS.Immediate { - const id = global.setImmediate(() => { - if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { - callback(this.performance.now()); - } else { - WindowErrorUtility.captureError(this, () => callback(this.performance.now())); - } - this.#browserFrame._asyncTaskManager.endImmediate(id); - }); - this.#browserFrame._asyncTaskManager.startImmediate(id); - return id; - } - - /** - * Mock animation frames with timeouts. - * - * @param id ID. - */ - public cancelAnimationFrame(id: NodeJS.Immediate): void { - global.clearImmediate(id); - this.#browserFrame._asyncTaskManager.endImmediate(id); - } - - /** - * Queues a microtask to be executed at a safe time prior to control returning to the browser's event loop. - * - * @param callback Function to be executed. - */ - public queueMicrotask(callback: Function): void { - let isAborted = false; - const taskId = this.#browserFrame._asyncTaskManager.startTask(() => (isAborted = true)); - this.#queueMicrotask(() => { - if (!isAborted) { - if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { - callback(); - } else { - WindowErrorUtility.captureError(this, <() => unknown>callback); - } - this.#browserFrame._asyncTaskManager.endTask(taskId); - } - }); - } - - /** - * This method provides an easy, logical way to fetch resources asynchronously across the network. - * - * @param url URL. - * @param [init] Init. - * @returns Promise. - */ - public async fetch(url: RequestInfo, init?: IRequestInit): Promise { - return await new Fetch({ - browserFrame: this.#browserFrame, - ownerDocument: this.document, - asyncTaskManager: this.#browserFrame._asyncTaskManager, - url, - init - }).send(); - } - - /** - * Creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data). - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/btoa - * @param data Binay data. - * @returns Base64-encoded string. - */ - public btoa(data: unknown): string { - return Base64.btoa(data); - } - - /** - * Decodes a string of data which has been encoded using Base64 encoding. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/atob - * @see https://infra.spec.whatwg.org/#forgiving-base64-encode. - * @see Https://html.spec.whatwg.org/multipage/webappapis.html#btoa. - * @param data Binay string. - * @returns An ASCII string containing decoded data from encodedData. - */ - public atob(data: unknown): string { - return Base64.atob(data); - } - - /** - * Safely enables cross-origin communication between Window objects; e.g., between a page and a pop-up that it spawned, or between a page and an iframe embedded within it. - * - * @param message Message. - * @param [targetOrigin=*] Target origin. - * @param _transfer Transfer. Not implemented. - */ - public postMessage(message: unknown, targetOrigin = '*', _transfer?: unknown[]): void { - // TODO: Implement transfer. - - if (targetOrigin && targetOrigin !== '*' && this.location.origin !== targetOrigin) { - throw new DOMException( - `Failed to execute 'postMessage' on 'Window': The target origin provided ('${targetOrigin}') does not match the recipient window\'s origin ('${this.location.origin}').`, - DOMExceptionNameEnum.securityError - ); - } - - try { - JSON.stringify(message); - } catch (error) { - throw new DOMException( - `Failed to execute 'postMessage' on 'Window': The provided message cannot be serialized.`, - DOMExceptionNameEnum.invalidStateError - ); - } - - this.setTimeout(() => - this.dispatchEvent( - new MessageEvent('message', { - data: message, - origin: this.#browserFrame.parentFrame - ? this.#browserFrame.parentFrame.window.location.origin - : this.#browserFrame.window.location.origin, - source: this.#browserFrame.parentFrame - ? this.#browserFrame.parentFrame.window - : this.#browserFrame.window, - lastEventId: '' - }) - ) - ); - } - - /** - * Setup of VM context. - */ - protected _setupVMContext(): void { - if (!VM.isContext(this)) { - VM.createContext(this); - - // Sets global properties from the VM to the Window object. - // Otherwise "this.Array" will be undefined for example. - VMGlobalPropertyScript.runInContext(this); } } } diff --git a/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts b/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts index 2a65d0c6b..cd406985f 100644 --- a/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts +++ b/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts @@ -1,5 +1,5 @@ import IBrowserSettings from '../browser/types/IBrowserSettings.js'; -import IWindow from './IWindow.js'; +import IBrowserWindow from './IBrowserWindow.js'; /** * Browser settings reader that will allow to read settings more securely as it is not possible to override a settings object to make DOM functionality act on it. @@ -13,7 +13,7 @@ export default class WindowBrowserSettingsReader { * @param window Window. * @returns Settings. */ - public static getSettings(window: IWindow): IBrowserSettings | null { + public static getSettings(window: IBrowserWindow): IBrowserSettings | null { const id = window['__happyDOMSettingsID__']; if (id === undefined || !this.#settings[id]) { @@ -29,7 +29,7 @@ export default class WindowBrowserSettingsReader { * @param window Window. * @param settings Settings. */ - public static setSettings(window: IWindow, settings: IBrowserSettings): void { + public static setSettings(window: IBrowserWindow, settings: IBrowserSettings): void { if (window['__happyDOMSettingsID__'] !== undefined) { return; } @@ -42,7 +42,7 @@ export default class WindowBrowserSettingsReader { * * @param window Window. */ - public static removeSettings(window: IWindow): void { + public static removeSettings(window: IBrowserWindow): void { const id = window['__happyDOMSettingsID__']; if (id !== undefined && this.#settings[id]) { diff --git a/packages/happy-dom/src/window/WindowClassFactory.ts b/packages/happy-dom/src/window/WindowClassFactory.ts index 290e6d839..b8f894a6e 100644 --- a/packages/happy-dom/src/window/WindowClassFactory.ts +++ b/packages/happy-dom/src/window/WindowClassFactory.ts @@ -7,9 +7,8 @@ import RequestImplementation from '../fetch/Request.js'; import ResponseImplementation from '../fetch/Response.js'; import RangeImplementation from '../range/Range.js'; import XMLHttpRequestImplementation from '../xml-http-request/XMLHttpRequest.js'; -import IWindow from './IWindow.js'; +import IBrowserWindow from './IBrowserWindow.js'; import IDocument from '../nodes/document/IDocument.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import HTMLElementImplementation from '../nodes/html-element/HTMLElement.js'; import HTMLUnknownElementImplementation from '../nodes/html-unknown-element/HTMLUnknownElement.js'; import HTMLTemplateElementImplementation from '../nodes/html-template-element/HTMLTemplateElement.js'; @@ -51,7 +50,10 @@ import HTMLButtonElementImplementation from '../nodes/html-button-element/HTMLBu import HTMLOptGroupElementImplementation from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; import HTMLOptionElementImplementation from '../nodes/html-option-element/HTMLOptionElement.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; -import CookieStringUtility from '../cookie/urilities/CookieStringUtility.js'; +import IRequestInfo from '../fetch/types/IRequestInfo.js'; +import IRequestInit from '../fetch/types/IRequestInit.js'; +import IResponseInit from '../fetch/types/IResponseInit.js'; +import IResponseBody from '../fetch/types/IResponseBody.js'; /** * Some classes need to get access to the window object without having a reference to the window in the constructor. @@ -66,7 +68,7 @@ export default class WindowClassFactory { * @param properties.browserFrame Browser frame. * @returns Classes. */ - public static getClasses(properties: { window: IWindow; browserFrame: IBrowserFrame }): { + public static getClasses(properties: { window: IBrowserWindow; browserFrame: IBrowserFrame }): { // Nodes Node: typeof NodeImplementation; Attr: typeof AttrImplementation; @@ -79,10 +81,10 @@ export default class WindowClassFactory { ProcessingInstruction: typeof ProcessingInstructionImplementation; Element: typeof ElementImplementation; CharacterData: typeof CharacterDataImplementation; - Document: typeof DocumentImplementation; - HTMLDocument: typeof HTMLDocumentImplementation; - XMLDocument: typeof XMLDocumentImplementation; - SVGDocument: typeof SVGDocumentImplementation; + Document: new () => DocumentImplementation; + HTMLDocument: new () => HTMLDocumentImplementation; + XMLDocument: new () => XMLDocumentImplementation; + SVGDocument: new () => SVGDocumentImplementation; DocumentType: typeof DocumentTypeImplementation; // HTML Elements @@ -112,14 +114,14 @@ export default class WindowClassFactory { HTMLDialogElement: typeof HTMLDialogElementImplementation; // Other Classes - Response: typeof ResponseImplementation; - Request: typeof RequestImplementation; - XMLHttpRequest: typeof XMLHttpRequestImplementation; + Request: new (input: IRequestInfo, init?: IRequestInit) => RequestImplementation; + Response: new (body?: IResponseBody, init?: IResponseInit) => ResponseImplementation; + XMLHttpRequest: new () => XMLHttpRequestImplementation; Image: typeof ImageImplementation; DocumentFragment: typeof DocumentFragmentImplementation; - FileReader: typeof FileReaderImplementation; - DOMParser: typeof DOMParserImplementation; - Range: typeof RangeImplementation; + FileReader: new () => FileReaderImplementation; + DOMParser: new () => DOMParserImplementation; + Range: new () => RangeImplementation; Audio: typeof AudioImplementation; } { const window = properties.window; @@ -184,76 +186,24 @@ export default class WindowClassFactory { } } class Document extends DocumentImplementation { - public readonly _defaultView: IWindow = window; - - public get cookie(): string { - return CookieStringUtility.cookiesToString( - properties.browserFrame.page.context.cookieContainer.getCookies( - this._defaultView.location, - true - ) - ); - } - - public set cookie(cookie: string) { - properties.browserFrame.page.context.cookieContainer.addCookies([ - CookieStringUtility.stringToCookie(this._defaultView.location, cookie) - ]); + constructor() { + super(properties); } } class HTMLDocument extends HTMLDocumentImplementation { - public readonly _defaultView: IWindow = window; - - public get cookie(): string { - return CookieStringUtility.cookiesToString( - properties.browserFrame.page.context.cookieContainer.getCookies( - this._defaultView.location, - true - ) - ); - } - - public set cookie(cookie: string) { - properties.browserFrame.page.context.cookieContainer.addCookies([ - CookieStringUtility.stringToCookie(this._defaultView.location, cookie) - ]); + constructor() { + super(properties); } } class XMLDocument extends XMLDocumentImplementation { - public readonly _defaultView: IWindow = window; - - public get cookie(): string { - return CookieStringUtility.cookiesToString( - properties.browserFrame.page.context.cookieContainer.getCookies( - this._defaultView.location, - true - ) - ); - } - - public set cookie(cookie: string) { - properties.browserFrame.page.context.cookieContainer.addCookies([ - CookieStringUtility.stringToCookie(this._defaultView.location, cookie) - ]); + constructor() { + super(properties); } } class SVGDocument extends SVGDocumentImplementation { - public readonly _defaultView: IWindow = window; - - public get cookie(): string { - return CookieStringUtility.cookiesToString( - properties.browserFrame.page.context.cookieContainer.getCookies( - this._defaultView.location, - true - ) - ); - } - - public set cookie(cookie: string) { - properties.browserFrame.page.context.cookieContainer.addCookies([ - CookieStringUtility.stringToCookie(this._defaultView.location, cookie) - ]); + constructor() { + super(properties); } } class DocumentType extends DocumentTypeImplementation { @@ -404,27 +354,34 @@ export default class WindowClassFactory { // Other Classes class Request extends RequestImplementation { - protected readonly _asyncTaskManager: AsyncTaskManager = asyncTaskManager; - protected get _ownerDocument(): IDocument { - return window.document; + constructor(input: IRequestInfo, init?: IRequestInit) { + super({ window, asyncTaskManager }, input, init); } } class Response extends ResponseImplementation { - protected readonly _asyncTaskManager: AsyncTaskManager = asyncTaskManager; + protected static _window = window; + constructor(body?: IResponseBody, init?: IResponseInit) { + super({ window, asyncTaskManager }, body, init); + } } class XMLHttpRequest extends XMLHttpRequestImplementation { - protected readonly _asyncTaskManager: AsyncTaskManager = asyncTaskManager; - protected readonly _ownerDocument: IDocument = window.document; + constructor() { + super(properties); + } } class FileReader extends FileReaderImplementation { - public readonly _ownerDocument: IDocument = window.document; + constructor() { + super(properties.window); + } } class DOMParser extends DOMParserImplementation { - public readonly _ownerDocument: IDocument = window.document; + constructor() { + super(properties.window); + } } class Range extends RangeImplementation { - public get _ownerDocument(): IDocument { - return window.document; + constructor() { + super(properties.window); } } diff --git a/packages/happy-dom/src/window/WindowErrorUtility.ts b/packages/happy-dom/src/window/WindowErrorUtility.ts index 27c363760..54bbf6a61 100644 --- a/packages/happy-dom/src/window/WindowErrorUtility.ts +++ b/packages/happy-dom/src/window/WindowErrorUtility.ts @@ -1,4 +1,4 @@ -import IWindow from './IWindow.js'; +import IBrowserWindow from './IBrowserWindow.js'; import ErrorEvent from '../event/events/ErrorEvent.js'; import IElement from '../nodes/element/IElement.js'; @@ -18,7 +18,7 @@ export default class WindowErrorUtility { * @returns Result. */ public static captureError( - elementOrWindow: IWindow | IElement, + elementOrWindow: IBrowserWindow | IElement, callback: () => T, cleanup?: () => void ): T | null { @@ -51,9 +51,9 @@ export default class WindowErrorUtility { * @param elementOrWindow Element or Window. * @param error Error. */ - public static dispatchError(elementOrWindow: IWindow | IElement, error: Error): void { - if ((elementOrWindow).console) { - (elementOrWindow).console.error(error); + public static dispatchError(elementOrWindow: IBrowserWindow | IElement, error: Error): void { + if ((elementOrWindow).console) { + (elementOrWindow).console.error(error); elementOrWindow.dispatchEvent(new ErrorEvent('error', { message: error.message, error })); } else { (elementOrWindow).ownerDocument._defaultView.console.error(error); diff --git a/packages/happy-dom/src/window/WindowPageOpenUtility.ts b/packages/happy-dom/src/window/WindowPageOpenUtility.ts index 5ca080b1f..7967f8725 100644 --- a/packages/happy-dom/src/window/WindowPageOpenUtility.ts +++ b/packages/happy-dom/src/window/WindowPageOpenUtility.ts @@ -1,8 +1,8 @@ -import IWindow from './IWindow.js'; -import CrossOriginWindow from './CrossOriginWindow.js'; +import IBrowserWindow from './IBrowserWindow.js'; +import CrossOriginBrowserWindow from './CrossOriginBrowserWindow.js'; import IBrowserFrame from '../browser/types/IBrowserFrame.js'; import FetchCORSUtility from '../fetch/utilities/FetchCORSUtility.js'; -import ICrossOriginWindow from './ICrossOriginWindow.js'; +import ICrossOriginBrowserWindow from './ICrossOriginBrowserWindow.js'; import BrowserFrameURL from '../browser/utilities/BrowserFrameURL.js'; /** @@ -25,7 +25,7 @@ export default class WindowPageOpenUtility { target?: string; features?: string; } - ): IWindow | ICrossOriginWindow | null { + ): IBrowserWindow | ICrossOriginBrowserWindow | null { const features = this.getWindowFeatures(options?.features || ''); const target = options?.target !== undefined ? String(options.target) : null; const originURL = browserFrame.window.location; @@ -102,8 +102,8 @@ export default class WindowPageOpenUtility { browserFrame.window && targetFrame.window !== browserFrame.window ) { - (targetFrame.window.opener) = isCORS - ? new CrossOriginWindow(browserFrame.window) + (targetFrame.window.opener) = isCORS + ? new CrossOriginBrowserWindow(browserFrame.window) : browserFrame.window; } @@ -112,7 +112,7 @@ export default class WindowPageOpenUtility { } if (isCORS) { - return new CrossOriginWindow(targetFrame.window, browserFrame.window); + return new CrossOriginBrowserWindow(targetFrame.window, browserFrame.window); } return targetFrame.window; diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index d8d266953..b5f8c63cc 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -19,9 +19,9 @@ import XMLHttpRequestCertificate from './XMLHttpRequestCertificate.js'; import XMLHttpRequestSyncRequestScriptBuilder from './utilities/XMLHttpRequestSyncRequestScriptBuilder.js'; import IconvLite from 'iconv-lite'; import ErrorEvent from '../event/events/ErrorEvent.js'; -import Document from '../nodes/document/Document.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; -import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; +import IBrowserFrame from '../browser/types/IBrowserFrame.js'; +import CookieStringUtility from '../cookie/urilities/CookieStringUtility.js'; +import IBrowserWindow from '../window/IBrowserWindow.js'; // These headers are not user setable. // The following are allowed but banned in the spec: @@ -71,10 +71,6 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Public properties public upload: XMLHttpRequestUpload = new XMLHttpRequestUpload(); - // Will be injected by a sub-class in Window. - protected readonly _asyncTaskManager: AsyncTaskManager; - protected readonly _ownerDocument: IDocument; - // Private properties readonly #internal: { state: { @@ -129,6 +125,21 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { password: null } }; + #browserFrame: IBrowserFrame; + #window: IBrowserWindow; + + /** + * Constructor. + * + * @param injected Injected properties. + * @param injected.browserFrame Browser frame. + * @param injected.window Window. + */ + constructor(injected: { browserFrame: IBrowserFrame; window: IBrowserWindow }) { + super(); + this.#browserFrame = injected.browserFrame; + this.#window = injected.window; + } /** * Returns the status. @@ -389,7 +400,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ); } - const { location } = this._ownerDocument._defaultView; + const { location } = this.#window; const url = new URL(this.#internal.settings.url, location); @@ -403,10 +414,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Load files off the local filesystem (file://) if (XMLHttpRequestURLUtility.isLocal(url)) { - if ( - !WindowBrowserSettingsReader.getSettings(this._ownerDocument._defaultView) - .enableFileSystemHttpRequests - ) { + if (!this.#browserFrame.page.context.browser.settings.enableFileSystemHttpRequests) { throw new DOMException( 'File system is disabled by default for security reasons. To enable it, set the Happy DOM setting "enableFileSystemHttpRequests" option to true.', DOMExceptionNameEnum.securityError @@ -529,7 +537,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.readyState = XMLHttpRequestReadyStateEnum.unsent; if (this.#internal.state.asyncTaskID !== null) { - this._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#browserFrame._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } } @@ -578,14 +586,20 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Default request headers. */ private _getDefaultRequestHeaders(): { [key: string]: string } { - const { location, navigator, document } = this._ownerDocument._defaultView; - - return { + const headers: { [k: string]: string } = { accept: '*/*', - referer: location.href, - 'user-agent': navigator.userAgent, - cookie: (document)._cookie.getCookieString(location, false) + referer: this.#window.location.href, + 'user-agent': this.#window.navigator.userAgent }; + const cookie = CookieStringUtility.cookiesToString( + this.#browserFrame.page.context.cookieContainer.getCookies(this.#window.location, false) + ); + + if (cookie) { + headers.cookie = cookie; + } + + return headers; } /** @@ -634,7 +648,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseXML = null; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this._ownerDocument._defaultView.location + this.#window.location ).href; // Set Cookies. this._setCookies(this.#internal.state.incommingMessage.headers); @@ -647,7 +661,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ) { const redirectUrl = new URL( this.#internal.state.incommingMessage.headers['location'], - this._ownerDocument._defaultView.location + this.#window.location ); ssl = redirectUrl.protocol === 'https:'; this.#internal.settings.url = redirectUrl.href; @@ -689,7 +703,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ): Promise { return new Promise((resolve) => { // Starts async task in Happy DOM - this.#internal.state.asyncTaskID = this._asyncTaskManager.startTask(this.abort.bind(this)); + this.#internal.state.asyncTaskID = this.#browserFrame._asyncTaskManager.startTask( + this.abort.bind(this) + ); // Use the proper protocol const sendRequest = ssl ? HTTPS.request : HTTP.request; @@ -709,7 +725,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { resolve(); // Ends async task in Happy DOM - this._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#browserFrame._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } ); this.#internal.state.asyncRequest.on('error', (error: Error) => { @@ -717,7 +733,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { resolve(); // Ends async task in Happy DOM - this._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#browserFrame._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); }); // Node 0.4 and later won't accept empty data. Make sure it's needed. @@ -766,10 +782,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Change URL to the redirect location this.#internal.settings.url = this.#internal.state.incommingMessage.headers.location; // Parse the new URL. - const redirectUrl = new URL( - this.#internal.settings.url, - this._ownerDocument._defaultView.location - ); + const redirectUrl = new URL(this.#internal.settings.url, this.#window.location); this.#internal.settings.url = redirectUrl.href; ssl = redirectUrl.protocol === 'https:'; // Issue the new request @@ -834,7 +847,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseText = responseText; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this._ownerDocument._defaultView.location + this.#window.location ).href; // Discard the 'end' event if the connection has been aborted this._setState(XMLHttpRequestReadyStateEnum.done); @@ -857,7 +870,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @returns Promise. */ private async _sendLocalAsyncRequest(url: UrlObject): Promise { - this.#internal.state.asyncTaskID = this._asyncTaskManager.startTask(this.abort.bind(this)); + this.#internal.state.asyncTaskID = this.#browserFrame._asyncTaskManager.startTask( + this.abort.bind(this) + ); let data: Buffer; @@ -866,7 +881,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } catch (error) { this._onError(error); // Release async task. - this._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#browserFrame._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); return; } @@ -887,7 +902,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } this._setState(XMLHttpRequestReadyStateEnum.done); - this._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#browserFrame._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); } /** @@ -940,7 +955,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.responseText = responseText; this.#internal.state.responseURL = new URL( this.#internal.settings.url, - this._ownerDocument._defaultView.location + this.#window.location ).href; this._setState(XMLHttpRequestReadyStateEnum.done); @@ -971,7 +986,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { case XMLHttpResponseTypeEnum.blob: try { return { - response: new this._ownerDocument._defaultView.Blob([new Uint8Array(data)], { + response: new this.#window.Blob([new Uint8Array(data)], { type: this.getResponseHeader('content-type') || '' }), responseText: null, @@ -981,7 +996,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { return { response: null, responseText: null, responseXML: null }; } case XMLHttpResponseTypeEnum.document: - const window = this._ownerDocument._defaultView; + const window = this.#window; const domParser = new window.DOMParser(); let response: IDocument; @@ -1022,23 +1037,18 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { private _setCookies( headers: { [name: string]: string | string[] } | HTTP.IncomingHttpHeaders ): void { - const originURL = new URL( - this.#internal.settings.url, - this._ownerDocument._defaultView.location - ); + const originURL = new URL(this.#internal.settings.url, this.#window.location); for (const header of ['set-cookie', 'set-cookie2']) { if (Array.isArray(headers[header])) { for (const cookie of headers[header]) { - (this._ownerDocument._defaultView.document)._cookie.addCookieString( - originURL, - cookie - ); + this.#browserFrame.page.context.cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(originURL, cookie) + ]); } } else if (headers[header]) { - (this._ownerDocument._defaultView.document)._cookie.addCookieString( - originURL, - headers[header] - ); + this.#browserFrame.page.context.cookieContainer.addCookies([ + CookieStringUtility.stringToCookie(originURL, headers[header]) + ]); } } } @@ -1061,9 +1071,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { error: errorObject }); - this._ownerDocument._defaultView.console.error(errorObject); + this.#window.console.error(errorObject); this.dispatchEvent(event); - this._ownerDocument._defaultView.dispatchEvent(event); + this.#window.dispatchEvent(event); } /** diff --git a/packages/happy-dom/test/fetch/Fetch.test.ts b/packages/happy-dom/test/fetch/Fetch.test.ts index 22c63f13e..caffd28d7 100644 --- a/packages/happy-dom/test/fetch/Fetch.test.ts +++ b/packages/happy-dom/test/fetch/Fetch.test.ts @@ -302,7 +302,7 @@ describe('Fetch', () => { options: { method: string; headers: { [k: string]: string } }; } | null = null; - window.happyDOM.setURL(baseUrl); + window.happyDOM?.setURL(baseUrl); mockModule('https', { request: (url, options) => { @@ -995,7 +995,7 @@ describe('Fetch', () => { options: { method: string; headers: { [k: string]: string } }; } | null = null; - window.happyDOM.setURL('https://localhost:8080'); + window.happyDOM?.setURL('https://localhost:8080'); window.document.cookie = 'test=cookie'; mockModule('https', { @@ -1052,7 +1052,7 @@ describe('Fetch', () => { options: { method: string; headers: { [k: string]: string } }; } | null = null; - window.happyDOM.setURL(originURL); + window.happyDOM?.setURL(originURL); window.document.cookie = 'test=cookie'; mockModule('https', { @@ -1109,7 +1109,7 @@ describe('Fetch', () => { options: { method: string; headers: { [k: string]: string } }; } | null = null; - window.happyDOM.setURL(originURL); + window.happyDOM?.setURL(originURL); window.document.cookie = 'test=cookie'; mockModule('http', { @@ -1166,7 +1166,7 @@ describe('Fetch', () => { options: { method: string; headers: { [k: string]: string } }; } | null = null; - window.happyDOM.setURL(originURL); + window.happyDOM?.setURL(originURL); for (const cookie of cookies.split(';')) { window.document.cookie = cookie.trim(); @@ -1230,7 +1230,7 @@ describe('Fetch', () => { options: { method: string; headers: { [k: string]: string } }; } | null = null; - window.happyDOM.setURL(originURL); + window.happyDOM?.setURL(originURL); for (const cookie of cookies.split(';')) { window.document.cookie = cookie.trim(); @@ -1286,7 +1286,7 @@ describe('Fetch', () => { }); it('Sets document cookie string if the response contains a "Set-Cookie" header if request cridentials are set to "include".', async () => { - window.happyDOM.setURL('https://localhost:8080'); + window.happyDOM?.setURL('https://localhost:8080'); mockModule('https', { request: () => { @@ -3358,7 +3358,7 @@ describe('Fetch', () => { expect(response.status).toBe(200); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const chunks = ['chunk1', 'chunk2', 'chunk3']; async function* generate(): AsyncGenerator { @@ -3413,7 +3413,7 @@ describe('Fetch', () => { } }); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); window.fetch('https://localhost:8080/test/', { method: 'POST', diff --git a/packages/happy-dom/test/fetch/Request.test.ts b/packages/happy-dom/test/fetch/Request.test.ts index 37c3456da..8d2c22ba2 100644 --- a/packages/happy-dom/test/fetch/Request.test.ts +++ b/packages/happy-dom/test/fetch/Request.test.ts @@ -73,7 +73,7 @@ describe('Request', () => { }); it('Supports relative URL.', () => { - window.happyDOM.setURL('https://example.com/other/path/'); + window.happyDOM?.setURL('https://example.com/other/path/'); const request = new window.Request('/path/'); expect(request.url).toBe('https://example.com/path/'); }); @@ -307,7 +307,7 @@ describe('Request', () => { new window.Request(TEST_URL, { referrer: new URL('https://example.com/path/') }) ); - window.happyDOM.setURL('https://example.com/other/path/'); + window.happyDOM?.setURL('https://example.com/other/path/'); const request7 = new window.Request( new window.Request(TEST_URL, { referrer: 'https://example.com/path/' }) @@ -338,7 +338,7 @@ describe('Request', () => { referrer: new URL('https://example.com/path/') }); - window.happyDOM.setURL('https://example.com/other/path/'); + window.happyDOM?.setURL('https://example.com/other/path/'); const request7 = new window.Request(TEST_URL, { referrer: 'https://example.com/path/' }); const request8 = new window.Request(TEST_URL, { @@ -463,7 +463,7 @@ describe('Request', () => { describe('get referrer()', () => { it('Returns referrer.', () => { - window.happyDOM.setURL('https://example.com/other/path/'); + window.happyDOM?.setURL('https://example.com/other/path/'); const request = new window.Request(TEST_URL, { referrer: 'https://example.com/path/' }); expect(request.referrer).toBe('https://example.com/path/'); }); @@ -491,7 +491,7 @@ describe('Request', () => { expect(Buffer.from(arrayBuffer).toString()).toBe('Hello World'); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; @@ -501,7 +501,7 @@ describe('Request', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); request.arrayBuffer(); setTimeout(() => { @@ -532,7 +532,7 @@ describe('Request', () => { expect(text).toBe('Hello World'); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; @@ -542,7 +542,7 @@ describe('Request', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); request.blob(); setTimeout(() => { @@ -566,7 +566,7 @@ describe('Request', () => { expect(buffer.toString()).toBe('Hello World'); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; @@ -576,7 +576,7 @@ describe('Request', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); request.buffer(); setTimeout(() => { @@ -599,7 +599,7 @@ describe('Request', () => { expect(text).toBe('Hello World'); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const request = new window.Request(TEST_URL, { method: 'POST', body: 'Hello World' }); let isAsyncComplete = false; @@ -609,7 +609,7 @@ describe('Request', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); request.text(); setTimeout(() => { @@ -635,7 +635,7 @@ describe('Request', () => { expect(json).toEqual({ key1: 'value1' }); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const request = new window.Request(TEST_URL, { method: 'POST', @@ -650,7 +650,7 @@ describe('Request', () => { ) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); request.json(); setTimeout(() => { @@ -675,7 +675,7 @@ describe('Request', () => { expect(requestFormData).toEqual(formData); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const formData = new FormData(); formData.append('some', 'test'); @@ -686,7 +686,7 @@ describe('Request', () => { (): Promise => new Promise((resolve) => setTimeout(() => resolve(formData), 10)) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); request.formData(); setTimeout(() => { @@ -703,7 +703,7 @@ describe('Request', () => { describe('clone()', () => { it('Returns a clone.', async () => { - window.happyDOM.setURL('https://example.com/other/path/'); + window.happyDOM?.setURL('https://example.com/other/path/'); const signal = new AbortSignal(); const request = new window.Request(TEST_URL, { diff --git a/packages/happy-dom/test/fetch/Response.test.ts b/packages/happy-dom/test/fetch/Response.test.ts index 5b1bc9d23..44dbe4e8e 100644 --- a/packages/happy-dom/test/fetch/Response.test.ts +++ b/packages/happy-dom/test/fetch/Response.test.ts @@ -114,7 +114,7 @@ describe('Response', () => { expect(Buffer.from(arrayBuffer).toString()).toBe('Hello World'); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const response = new window.Response('Hello World'); let isAsyncComplete = false; @@ -124,7 +124,7 @@ describe('Response', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); response.arrayBuffer(); setTimeout(() => { @@ -153,7 +153,7 @@ describe('Response', () => { expect(text).toBe('Hello World'); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const response = new window.Response('Hello World', { headers: { 'Content-Type': 'text/plain' } @@ -165,7 +165,7 @@ describe('Response', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); response.blob(); setTimeout(() => { @@ -189,7 +189,7 @@ describe('Response', () => { expect(buffer.toString()).toBe('Hello World'); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const response = new window.Response('Hello World'); let isAsyncComplete = false; @@ -199,7 +199,7 @@ describe('Response', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 5)) ); - window.happyDOM.whenAsyncComplete().then(() => { + window.happyDOM?.whenComplete().then(() => { isAsyncComplete = true; }); response.buffer(); @@ -224,7 +224,7 @@ describe('Response', () => { expect(text).toBe('Hello World'); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const response = new window.Response('Hello World'); let isAsyncComplete = false; @@ -234,7 +234,7 @@ describe('Response', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('Hello World')), 10)) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); response.text(); setTimeout(() => { @@ -257,7 +257,7 @@ describe('Response', () => { expect(json).toEqual({ key1: 'value1' }); }); - it('Supports window.happyDOM.whenAsyncComplete().', async () => { + it('Supports window.happyDOM?.whenComplete().', async () => { await new Promise((resolve) => { const response = new window.Response('{ "key1": "value1" }'); let isAsyncComplete = false; @@ -269,7 +269,7 @@ describe('Response', () => { ) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); response.json(); setTimeout(() => { @@ -368,7 +368,7 @@ describe('Response', () => { expect(await file2.arrayBuffer()).toEqual(imageBuffer.buffer); }); - it('Supports window.happyDOM.whenAsyncComplete() for "application/x-www-form-urlencoded" content.', async () => { + it('Supports window.happyDOM?.whenComplete() for "application/x-www-form-urlencoded" content.', async () => { await new Promise((resolve) => { const response = new window.Response(new URLSearchParams()); let isAsyncComplete = false; @@ -378,7 +378,7 @@ describe('Response', () => { new Promise((resolve) => setTimeout(() => resolve(Buffer.from('')), 10)) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); response.formData(); setTimeout(() => { @@ -392,7 +392,7 @@ describe('Response', () => { }); }); - it('Supports window.happyDOM.whenAsyncComplete() for multipart content.', async () => { + it('Supports window.happyDOM?.whenComplete() for multipart content.', async () => { await new Promise((resolve) => { const response = new window.Response(new FormData()); let isAsyncComplete = false; @@ -402,7 +402,7 @@ describe('Response', () => { new Promise((resolve) => setTimeout(() => resolve(new FormData()), 10)) ); - window.happyDOM.whenAsyncComplete().then(() => (isAsyncComplete = true)); + window.happyDOM?.whenComplete().then(() => (isAsyncComplete = true)); response.formData(); setTimeout(() => { diff --git a/packages/happy-dom/test/file/FileReader.test.ts b/packages/happy-dom/test/file/FileReader.test.ts index 0df59c60a..e97bba1c5 100644 --- a/packages/happy-dom/test/file/FileReader.test.ts +++ b/packages/happy-dom/test/file/FileReader.test.ts @@ -22,7 +22,7 @@ describe('FileReader', () => { result = fileReader.result; }); fileReader.readAsDataURL(blob); - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(result).toBe('data:text/plain;charset=utf-8;base64,VEVTVA=='); }); }); diff --git a/packages/happy-dom/test/match-media/MediaQueryList.test.ts b/packages/happy-dom/test/match-media/MediaQueryList.test.ts index c897afec4..c70870ef2 100644 --- a/packages/happy-dom/test/match-media/MediaQueryList.test.ts +++ b/packages/happy-dom/test/match-media/MediaQueryList.test.ts @@ -251,8 +251,8 @@ describe('MediaQueryList', () => { new MediaQueryList({ ownerWindow: window, media: '(orientation: landscape)' }).matches ).toBe(true); - window.happyDOM.setInnerWidth(500); - window.happyDOM.setInnerHeight(1000); + window.happyDOM?.setInnerWidth(500); + window.happyDOM?.setInnerHeight(1000); expect( new MediaQueryList({ ownerWindow: window, media: '(orientation: portrait)' }).matches @@ -532,7 +532,7 @@ describe('MediaQueryList', () => { expect(mediaQueryList.matches).toBe(false); - window.happyDOM.setInnerWidth(1025); + window.happyDOM?.setInnerWidth(1025); expect(((triggeredEvent)).matches).toBe(true); expect(((triggeredEvent)).media).toBe(media); @@ -553,7 +553,7 @@ describe('MediaQueryList', () => { mediaQueryList.addEventListener('change', listener); mediaQueryList.removeEventListener('change', listener); - window.happyDOM.setInnerWidth(1025); + window.happyDOM?.setInnerWidth(1025); expect(triggeredEvent).toBe(null); }); diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts index 8ccedcfb2..d87c51435 100644 --- a/packages/happy-dom/test/nodes/document/Document.test.ts +++ b/packages/happy-dom/test/nodes/document/Document.test.ts @@ -1173,7 +1173,7 @@ describe('Document', () => { let readyChangeEvent: Event | null = null; vi.spyOn(ResourceFetch, 'fetch').mockImplementation( - async (document: IDocument, url: string) => { + async (window: IWindow, url: string) => { if (url.endsWith('.css')) { resourceFetchCSSDocument = document; resourceFetchCSSURL = url; diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts index 4436ddff3..11c961d1e 100644 --- a/packages/happy-dom/test/nodes/element/Element.test.ts +++ b/packages/happy-dom/test/nodes/element/Element.test.ts @@ -1512,7 +1512,7 @@ describe('Element', () => { element[functionName]({ left: 50, top: 60, behavior: 'smooth' }); expect(element.scrollLeft).toBe(0); expect(element.scrollTop).toBe(0); - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(element.scrollLeft).toBe(50); expect(element.scrollTop).toBe(60); }); diff --git a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts index 3d1fe6c20..84dd90684 100644 --- a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts +++ b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts @@ -4,7 +4,7 @@ import IDocument from '../../../src/nodes/document/IDocument.js'; import IHTMLIFrameElement from '../../../src/nodes/html-iframe-element/IHTMLIFrameElement.js'; import IResponse from '../../../src/fetch/types/IResponse.js'; import ErrorEvent from '../../../src/event/events/ErrorEvent.js'; -import CrossOriginWindow from '../../../src/window/CrossOriginWindow.js'; +import CrossOriginBrowserWindow from '../../../src/window/CrossOriginBrowserWindow.js'; import MessageEvent from '../../../src/event/events/MessageEvent.js'; import DOMExceptionNameEnum from '../../../src/exception/DOMExceptionNameEnum.js'; import DOMException from '../../../src/exception/DOMException.js'; @@ -105,7 +105,7 @@ describe('HTMLIFrameElement', () => { }); }); - window.happyDOM.setURL('https://localhost:8080'); + window.happyDOM?.setURL('https://localhost:8080'); element.src = 'https://localhost:8080/iframe.html'; element.addEventListener('error', (event) => { expect((event).message).toBe( @@ -136,7 +136,7 @@ describe('HTMLIFrameElement', () => { }); }); - window.happyDOM.setURL('https://localhost:3000'); + window.happyDOM?.setURL('https://localhost:3000'); element.src = 'https://localhost:8080/iframe.html'; element.addEventListener('error', (event) => { expect((event).message).toBe( @@ -167,7 +167,7 @@ describe('HTMLIFrameElement', () => { }); }); - window.happyDOM.setURL('https://localhost:8080'); + window.happyDOM?.setURL('https://localhost:8080'); element.src = 'https://localhost:8080/iframe.html'; element.addEventListener('load', () => { expect(element.contentDocument?.location.href).toBe('https://localhost:8080/iframe.html'); @@ -197,7 +197,7 @@ describe('HTMLIFrameElement', () => { })); }); - window.happyDOM.setURL('https://localhost:8080'); + window.happyDOM?.setURL('https://localhost:8080'); element.src = 'https://localhost:8080/iframe.html'; element.addEventListener('load', () => { expect(element.contentDocument?.location.href).toBe('https://localhost:8080/iframe.html'); @@ -226,7 +226,7 @@ describe('HTMLIFrameElement', () => { })); }); - window.happyDOM.setURL('https://localhost:8080'); + window.happyDOM?.setURL('https://localhost:8080'); element.src = '/iframe.html'; element.addEventListener('load', () => { expect(element.contentDocument?.location.href).toBe('https://localhost:8080/iframe.html'); @@ -267,7 +267,7 @@ describe('HTMLIFrameElement', () => { }); }); - it('Returns instance of CrossOriginWindow for URL with different origin.', async () => { + it('Returns instance of CrossOriginBrowserWindow for URL with different origin.', async () => { await new Promise((resolve) => { const browser = new Browser(); const page = browser.newPage(); @@ -302,7 +302,7 @@ describe('HTMLIFrameElement', () => { const message = 'test'; let triggeredEvent: MessageEvent | null = null; expect(fetchedURL).toBe(iframeSrc); - expect(element.contentWindow instanceof CrossOriginWindow).toBe(true); + expect(element.contentWindow instanceof CrossOriginBrowserWindow).toBe(true); expect(() => element.contentWindow?.location.href).toThrowError( new DOMException( `Blocked a frame with origin "${documentOrigin}" from accessing a cross-origin frame.`, diff --git a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts index caf5b4f05..e2cf47bd2 100644 --- a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts +++ b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts @@ -101,7 +101,7 @@ describe('HTMLLinkElement', () => { element.rel = 'stylesheet'; element.href = 'test'; - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(loadedDocument).toBe(document); expect(loadedURL).toBe('test'); @@ -128,7 +128,7 @@ describe('HTMLLinkElement', () => { element.rel = 'stylesheet'; element.href = 'test'; - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(((errorEvent)).error).toEqual(thrownError); expect(((errorEvent)).message).toEqual('error'); @@ -180,7 +180,7 @@ describe('HTMLLinkElement', () => { document.body.appendChild(element); - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(loadedDocument).toBe(document); expect(loadedURL).toBe('test'); @@ -206,7 +206,7 @@ describe('HTMLLinkElement', () => { document.body.appendChild(element); - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(((errorEvent)).error).toEqual(thrownError); expect(((errorEvent)).message).toEqual('error'); diff --git a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts index 8867c8348..e4e4259fd 100644 --- a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts +++ b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts @@ -8,6 +8,7 @@ import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; import Event from '../../../src/event/Event.js'; import IRequestInfo from '../../../src/fetch/types/IRequestInfo.js'; import ErrorEvent from '../../../src/event/events/ErrorEvent.js'; +import IWindow from '../../../src/window/IWindow.js'; describe('HTMLScriptElement', () => { let window: Window; @@ -96,7 +97,7 @@ describe('HTMLScriptElement', () => { element.async = true; element.src = 'test'; - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(window['test']).toBe('test'); }); @@ -115,7 +116,7 @@ describe('HTMLScriptElement', () => { element.async = true; element.src = 'test'; - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(window['test']).toBe(undefined); }); @@ -186,7 +187,7 @@ describe('HTMLScriptElement', () => { document.body.appendChild(script); - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(((loadEvent)).target).toBe(script); expect(fetchedURL).toBe('path/to/script/'); @@ -214,7 +215,7 @@ describe('HTMLScriptElement', () => { document.body.appendChild(script); - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(((errorEvent)).message).toBe( 'Failed to perform request to "path/to/script/". Status code: 404' @@ -228,13 +229,11 @@ describe('HTMLScriptElement', () => { window.location.href = 'https://localhost:8080/base/'; - vi.spyOn(ResourceFetch, 'fetchSync').mockImplementation( - (document: IDocument, url: string) => { - fetchedDocument = document; - fetchedURL = url; - return 'globalThis.test = "test";globalThis.currentScript = document.currentScript;'; - } - ); + vi.spyOn(ResourceFetch, 'fetchSync').mockImplementation((window: IWindow, url: string) => { + fetchedDocument = document; + fetchedURL = url; + return 'globalThis.test = "test";globalThis.currentScript = document.currentScript;'; + }); const script = window.document.createElement('script'); script.src = 'path/to/script/'; @@ -449,13 +448,13 @@ describe('HTMLScriptElement', () => { document.body.appendChild(script); - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(((errorEvent)).error?.message).toBe( 'Invalid regular expression: missing /' ); - const consoleOutput = window.happyDOM.virtualConsolePrinter?.readAsString() || ''; + const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || ''; expect(consoleOutput.startsWith('SyntaxError: Invalid regular expression: missing /')).toBe( true ); @@ -477,7 +476,7 @@ describe('HTMLScriptElement', () => { 'Invalid regular expression: missing /' ); - const consoleOutput = window.happyDOM.virtualConsolePrinter?.readAsString() || ''; + const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || ''; expect(consoleOutput.startsWith('SyntaxError: Invalid regular expression: missing /')).toBe( true ); @@ -497,7 +496,7 @@ describe('HTMLScriptElement', () => { 'Invalid regular expression: missing /' ); - const consoleOutput = window.happyDOM.virtualConsolePrinter?.readAsString() || ''; + const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || ''; expect(consoleOutput.startsWith('SyntaxError: Invalid regular expression: missing /')).toBe( true ); diff --git a/packages/happy-dom/test/nodes/node/Node.test.ts b/packages/happy-dom/test/nodes/node/Node.test.ts index fc0cd7318..2340b0add 100644 --- a/packages/happy-dom/test/nodes/node/Node.test.ts +++ b/packages/happy-dom/test/nodes/node/Node.test.ts @@ -867,7 +867,7 @@ describe('Node', () => { node.addEventListener('click', listener); node.dispatchEvent(new Event('click')); expect(((errorEvent)).error?.message).toBe('Test'); - expect(window.happyDOM.virtualConsolePrinter?.readAsString().startsWith('Error: Test')).toBe( + expect(window.happyDOM?.virtualConsolePrinter?.readAsString().startsWith('Error: Test')).toBe( true ); }); @@ -887,7 +887,7 @@ describe('Node', () => { node.dispatchEvent(new Event('click')); await new Promise((resolve) => setTimeout(resolve, 2)); expect(((errorEvent)).error?.message).toBe('Test'); - expect(window.happyDOM.virtualConsolePrinter?.readAsString().startsWith('Error: Test')).toBe( + expect(window.happyDOM?.virtualConsolePrinter?.readAsString().startsWith('Error: Test')).toBe( true ); }); diff --git a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts index 8b420e8e3..7ef16b81e 100644 --- a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts +++ b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts @@ -121,7 +121,7 @@ describe('DetachedWindowAPI', () => { isCalled = true; return Promise.resolve(); }); - await window.happyDOM?.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(isCalled).toBe(true); }); }); @@ -131,7 +131,7 @@ describe('DetachedWindowAPI', () => { await new Promise((resolve) => { window.location.href = 'https://localhost:8080'; let isFirstWhenAsyncCompleteCalled = false; - window.happyDOM?.whenAsyncComplete().then(() => { + window.happyDOM?.whenComplete().then(() => { isFirstWhenAsyncCompleteCalled = true; }); let tasksDone = 0; @@ -200,7 +200,7 @@ describe('DetachedWindowAPI', () => { vi.spyOn(window.happyDOM, 'abort').mockImplementation(() => { isCalled = true; }); - window.happyDOM?.cancelAsync(); + window.happyDOM?.abort(); expect(isCalled).toBe(true); }); }); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index f17f9512e..dfe32fe57 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -29,8 +29,8 @@ import Clipboard from '../../src/clipboard/Clipboard.js'; import PackageVersion from '../../src/version.js'; import IHTMLDialogElement from '../../src/nodes/html-dialog-element/IHTMLDialogElement.js'; import Browser from '../../src/browser/Browser.js'; -import ICrossOriginWindow from '../../src/window/ICrossOriginWindow.js'; -import CrossOriginWindow from '../../src/window/CrossOriginWindow.js'; +import ICrossOriginBrowserWindow from '../../src/window/ICrossOriginBrowserWindow.js'; +import CrossOriginBrowserWindow from '../../src/window/CrossOriginBrowserWindow.js'; import IHTMLIFrameElement from '../../src/nodes/html-iframe-element/IHTMLIFrameElement.js'; import DetachedWindowAPI from '../../src/window/DetachedWindowAPI.js'; import BrowserFrameFactory from '../../src/browser/utilities/BrowserFrameFactory.js'; @@ -1585,11 +1585,11 @@ describe('Window', () => { page.mainFrame.url = 'https://www.github.com/capricorn86/happy-dom/'; - const newWindow = ( + const newWindow = ( page.mainFrame.window.open('https://developer.mozilla.org/en-US/docs/Web/API/Window/open') ); - expect(newWindow instanceof CrossOriginWindow).toBe(true); + expect(newWindow instanceof CrossOriginBrowserWindow).toBe(true); expect(browser.defaultContext.pages.length).toBe(2); expect(browser.defaultContext.pages[0]).toBe(page); expect(browser.defaultContext.pages[1].mainFrame.window === newWindow).toBe(false); diff --git a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts index 16a1f5d58..8c5add8ff 100644 --- a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts +++ b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts @@ -1128,7 +1128,7 @@ describe('XMLHttpRequest', () => { } }); - window.happyDOM.setURL(windowURL); + window.happyDOM?.setURL(windowURL); request.open('GET', REQUEST_URL, false); @@ -1841,7 +1841,7 @@ describe('XMLHttpRequest', () => { request.open('GET', REQUEST_URL, true); request.send(); - await window.happyDOM.whenAsyncComplete(); + await window.happyDOM?.whenComplete(); expect(request.responseText).toBe(responseText); }); @@ -1921,7 +1921,7 @@ describe('XMLHttpRequest', () => { request.send(); request.abort(); - await window.happyDOM.whenComplete(); + await window.happyDOM?.whenComplete(); expect(request.readyState).toBe(XMLHttpRequestReadyStateEnum.unsent); expect(isDestroyed).toBe(true); @@ -1955,7 +1955,7 @@ describe('XMLHttpRequest', () => { request.open('GET', REQUEST_URL, true); request.send(); - window.happyDOM.abort(); + window.happyDOM?.abort(); expect(request.readyState).toBe(XMLHttpRequestReadyStateEnum.unsent); expect(isDestroyed).toBe(true); diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index 4f7b8783a..b24cff803 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -70,9 +70,9 @@ export default class HappyDOMEnvironment implements JestEnvironment { } if (projectConfig.testEnvironmentOptions['url']) { - this.window.happyDOM.setURL(String(projectConfig.testEnvironmentOptions['url'])); + this.window.happyDOM?.setURL(String(projectConfig.testEnvironmentOptions['url'])); } else { - this.window.happyDOM.setURL('http://localhost/'); + this.window.happyDOM?.setURL('http://localhost/'); } this.fakeTimers = new LegacyFakeTimers({ @@ -90,7 +90,7 @@ export default class HappyDOMEnvironment implements JestEnvironment { global: (this.window) }); - // Jest is using the setTimeout function from Happy DOM internally for detecting when a test times out, but this causes Window.happyDOM.whenAsyncComplete() and Window.happyDOM.cancelAsync() to not work as expected. + // Jest is using the setTimeout function from Happy DOM internally for detecting when a test times out, but this causes window.happyDOM?.whenComplete() and window.happyDOM?.abort() to not work as expected. // Hopefully Jest can fix this in the future as this fix is not very pretty. const happyDOMSetTimeout = this.global.setTimeout; (<(...args: unknown[]) => number>this.global.setTimeout) = (...args: unknown[]): number => { diff --git a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts index de07f955d..f68a7bbdb 100644 --- a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts +++ b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts @@ -116,7 +116,7 @@ async function itObservesUncaughtExceptions(): Promise { observer.disconnect(); - const consoleOutput = window.happyDOM.virtualConsolePrinter.readAsString(); + const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString(); if (consoleOutput.startsWith('Error: Test error\nat Timeout.eval')) { throw new Error(`Console output not correct.`); From aec9b0967be3b361021726126d9c6812f91bb586 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 28 Nov 2023 17:31:01 +0100 Subject: [PATCH 31/63] #466@trivial: Continues on implementation. --- .../async-task-manager/AsyncTaskManager.ts | 69 +- packages/happy-dom/src/browser/Browser.ts | 11 +- .../happy-dom/src/browser/BrowserContext.ts | 8 +- .../happy-dom/src/browser/BrowserFrame.ts | 2 +- .../detached-browser/DetachedBrowser.ts | 108 ++ .../DetachedBrowserContext.ts | 75 + .../DetachedBrowserFrame.ts | 50 +- .../DetachedBrowserPage.ts | 27 +- .../src/browser/types/IBrowserFrame.ts | 2 +- .../src/browser/types/IBrowserPageViewport.ts | 2 +- .../utilities/BrowserFrameNavigator.ts | 18 +- .../utilities/BrowserFrameScriptEvaluator.ts | 3 + .../browser/utilities/BrowserPageUtility.ts | 4 +- .../happy-dom/src/cookie/CookieContainer.ts | 18 +- .../cookie/urilities/CookieStringUtility.ts | 2 +- .../HTMLLinkElementUtility.ts | 2 +- .../HTMLScriptElementUtility.ts | 4 +- packages/happy-dom/src/range/Range.ts | 308 ++-- packages/happy-dom/src/selection/Selection.ts | 36 +- .../happy-dom/src/window/BrowserWindow.ts | 24 +- packages/happy-dom/src/window/Window.ts | 38 +- .../test/browser/BrowserFrame.test.ts | 4 +- .../test/browser/BrowserPage.test.ts | 6 +- .../detached-browser/DetachedBrowser.test.ts | 36 +- .../DetachedBrowserContext.test.ts | 19 +- .../DetachedBrowserFrame.test.ts | 113 +- .../DetachedBrowserPage.test.ts | 58 +- .../test/fetch/ResourceFetch.test.ts | 10 +- .../test/nodes/document/Document.test.ts | 15 +- .../HTMLIFrameElement.test.ts | 27 +- .../html-link-element/HTMLLinkElement.test.ts | 33 +- .../HTMLScriptElement.test.ts | 22 +- .../test/window/BrowserWindow.test.ts | 1376 +++++++++++++++ .../test/window/DetachedWindowAPI.test.ts | 2 +- packages/happy-dom/test/window/Window.test.ts | 1488 ++--------------- .../xml-http-request/XMLHttpRequest.test.ts | 20 +- 36 files changed, 2204 insertions(+), 1836 deletions(-) create mode 100644 packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts create mode 100644 packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts rename packages/happy-dom/src/browser/{ => detached-browser}/DetachedBrowserFrame.ts (59%) rename packages/happy-dom/src/browser/{ => detached-browser}/DetachedBrowserPage.ts (75%) create mode 100644 packages/happy-dom/test/window/BrowserWindow.test.ts diff --git a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts index 10b594edc..f70e8a0bd 100644 --- a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts +++ b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts @@ -24,44 +24,16 @@ export default class AsyncTaskManager { /** * Aborts all tasks. - * - * @param destroy Destroy. */ - public abortAll(destroy = false): void { - const runningTimers = this.runningTimers; - const runningImmediates = this.runningImmediates; - const runningTasks = this.runningTasks; - - this.runningTasks = {}; - this.runningTaskCount = 0; - this.runningImmediates = []; - this.runningTimers = []; - - if (this.whenCompleteImmediate) { - global.clearImmediate(this.whenCompleteImmediate); - this.whenCompleteImmediate = null; - } - - for (const immediate of runningImmediates) { - global.clearImmediate(immediate); - } - - for (const timer of runningTimers) { - global.clearTimeout(timer); - } - - for (const key of Object.keys(runningTasks)) { - runningTasks[key](destroy); - } - - this.resolveWhenComplete(); + public abort(): void { + this.abortAll(false); } /** * Destroys the manager. */ public destroy(): void { - this.abortAll(); + this.abortAll(true); } /** @@ -181,4 +153,39 @@ export default class AsyncTaskManager { resolver(); } } + + /** + * Aborts all tasks. + * + * @param destroy Destroy. + */ + private abortAll(destroy: boolean): void { + const runningTimers = this.runningTimers; + const runningImmediates = this.runningImmediates; + const runningTasks = this.runningTasks; + + this.runningTasks = {}; + this.runningTaskCount = 0; + this.runningImmediates = []; + this.runningTimers = []; + + if (this.whenCompleteImmediate) { + global.clearImmediate(this.whenCompleteImmediate); + this.whenCompleteImmediate = null; + } + + for (const immediate of runningImmediates) { + global.clearImmediate(immediate); + } + + for (const timer of runningTimers) { + global.clearTimeout(timer); + } + + for (const key of Object.keys(runningTasks)) { + runningTasks[key](destroy); + } + + this.resolveWhenComplete(); + } } diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index 0f140beec..6b17c337a 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -2,11 +2,10 @@ import IBrowserSettings from './types/IBrowserSettings.js'; import BrowserContext from './BrowserContext.js'; import IOptionalBrowserSettings from './types/IOptionalBrowserSettings.js'; import BrowserSettingsFactory from './BrowserSettingsFactory.js'; -import IBrowserPage from './types/IBrowserPage.js'; +import BrowserPage from './BrowserPage.js'; import IBrowser from './types/IBrowser.js'; -import IBrowserFrame from './types/IBrowserFrame.js'; +import BrowserFrame from './BrowserFrame.js'; import ICookieContainer from '../cookie/types/ICookieContainer.js'; -import IBrowserContext from './types/IBrowserContext.js'; /** * Browser. @@ -14,7 +13,7 @@ import IBrowserContext from './types/IBrowserContext.js'; * Much of the interface for the browser has been taken from Puppeteer and Playwright, so that the API is familiar. */ export default class Browser implements IBrowser { - public readonly contexts: IBrowserContext[]; + public readonly contexts: BrowserContext[]; public readonly settings: IBrowserSettings; public readonly console: Console | null; @@ -40,7 +39,7 @@ export default class Browser implements IBrowser { * * @returns Default context. */ - public get defaultContext(): IBrowserContext { + public get defaultContext(): BrowserContext { if (this.contexts.length === 0) { throw new Error('No default context. The browser has been closed.'); } @@ -98,7 +97,7 @@ export default class Browser implements IBrowser { * @param [opener] Opener. * @returns Page. */ - public newPage(opener?: IBrowserFrame): IBrowserPage { + public newPage(opener?: BrowserFrame): BrowserPage { if (this.contexts.length === 0) { throw new Error('No default context. The browser has been closed.'); } diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index 60f1ebe56..57f4648a6 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -1,15 +1,15 @@ import CookieContainer from '../cookie/CookieContainer.js'; import ICookieContainer from '../cookie/types/ICookieContainer.js'; import Browser from './Browser.js'; -import IBrowserFrame from './types/IBrowserFrame.js'; -import IBrowserPage from './types/IBrowserPage.js'; +import BrowserFrame from './BrowserFrame.js'; +import BrowserPage from './BrowserPage.js'; import IBrowserContext from './types/IBrowserContext.js'; /** * Browser context. */ export default class BrowserContext implements IBrowserContext { - public readonly pages: IBrowserPage[] = []; + public readonly pages: BrowserPage[] = []; public readonly browser: Browser; public readonly cookieContainer: ICookieContainer; @@ -61,7 +61,7 @@ export default class BrowserContext implements IBrowserContext { * @param [opener] Opener. * @returns Page. */ - public newPage(opener?: IBrowserFrame): IBrowserPage { + public newPage(opener?: BrowserFrame): BrowserPage { const page = new BrowserPage(this); ((page.mainFrame.opener)) = opener || null; this.pages.push(page); diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 015f95c6d..ab2727011 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -94,7 +94,7 @@ export default class BrowserFrame implements IBrowserFrame { for (const frame of this.childFrames) { frame.abort(); } - this._asyncTaskManager.abortAll(); + this._asyncTaskManager.abort(); } /** diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts new file mode 100644 index 000000000..c4f453565 --- /dev/null +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts @@ -0,0 +1,108 @@ +import IBrowserSettings from '../types/IBrowserSettings.js'; +import DetachedBrowserContext from './DetachedBrowserContext.js'; +import IOptionalBrowserSettings from '../types/IOptionalBrowserSettings.js'; +import BrowserSettingsFactory from '../BrowserSettingsFactory.js'; +import DetachedBrowserPage from './DetachedBrowserPage.js'; +import IBrowser from '../types/IBrowser.js'; +import IBrowserFrame from '../types/IBrowserFrame.js'; +import DetachedBrowserFrame from './DetachedBrowserFrame.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; + +/** + * Detached browser used when constructing a Window instance without a browser. + * + * Much of the interface for the browser has been taken from Puppeteer and Playwright, so that the API is familiar. + */ +export default class DetachedBrowser implements IBrowser { + public readonly contexts: DetachedBrowserContext[]; + public readonly settings: IBrowserSettings; + public readonly console: Console | null; + public readonly windowClass: new ( + browserFrame: IBrowserFrame, + options?: { url?: string; width?: number; height?: number } + ) => IBrowserWindow | null; + + /** + * Constructor. + * + * @param windowClass Window class. + * @param [options] Options. + * @param [options.settings] Browser settings. + * @param [options.console] Console. + */ + constructor( + windowClass: new ( + browserFrame: IBrowserFrame, + options?: { url?: string; width?: number; height?: number } + ) => IBrowserWindow, + options?: { settings?: IOptionalBrowserSettings; console?: Console } + ) { + this.windowClass = windowClass; + this.console = options?.console || null; + this.settings = BrowserSettingsFactory.getSettings(options?.settings); + this.contexts = []; + this.contexts.push(new DetachedBrowserContext(this)); + } + + /** + * Returns the default context. + * + * @returns Default context. + */ + public get defaultContext(): DetachedBrowserContext { + if (this.contexts.length === 0) { + throw new Error('No default context. The browser has been closed.'); + } + return this.contexts[0]; + } + + /** + * Aborts all ongoing operations and destroys the browser. + */ + public close(): void { + for (const context of this.contexts.slice()) { + context.close(); + } + (this.contexts) = []; + (this.console) = null; + ( IBrowserWindow | null>this.windowClass) = null; + } + + /** + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. + * + * @returns Promise. + */ + public async whenComplete(): Promise { + await Promise.all(this.contexts.map((page) => page.whenComplete())); + } + + /** + * Aborts all ongoing operations. + */ + public abort(): void { + for (const context of this.contexts) { + context.abort(); + } + } + + /** + * Creates a new incognito context. + */ + public newIncognitoContext(): DetachedBrowserContext { + throw new Error('Not possible to create a new context on a detached browser.'); + } + + /** + * Creates a new page. + * + * @param [opener] Opener. + * @returns Page. + */ + public newPage(opener?: DetachedBrowserFrame): DetachedBrowserPage { + if (this.contexts.length === 0) { + throw new Error('No default context. The browser has been closed.'); + } + return this.contexts[0].newPage(opener); + } +} diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts new file mode 100644 index 000000000..cc3bb0eae --- /dev/null +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts @@ -0,0 +1,75 @@ +import DetachedBrowser from './DetachedBrowser.js'; +import DetachedBrowserPage from './DetachedBrowserPage.js'; +import IBrowserContext from '../types/IBrowserContext.js'; +import DetachedBrowserFrame from './DetachedBrowserFrame.js'; +import ICookieContainer from '../../cookie/types/ICookieContainer.js'; +import CookieContainer from '../../cookie/CookieContainer.js'; + +/** + * Detached browser context used when constructing a Window instance without a browser. + */ +export default class DetachedBrowserContext implements IBrowserContext { + public readonly pages: DetachedBrowserPage[]; + public readonly browser: DetachedBrowser; + public readonly cookieContainer: ICookieContainer = new CookieContainer(); + + /** + * Constructor. + * + * @param browser Browser. + */ + constructor(browser: DetachedBrowser) { + this.browser = browser; + this.pages = []; + this.pages.push(new DetachedBrowserPage(this)); + } + + /** + * Aborts all ongoing operations and destroys the context. + */ + public close(): void { + if (!this.browser) { + return; + } + for (const page of this.pages.slice()) { + page.close(); + } + const browser = this.browser; + (this.pages) = []; + (this.browser) = null; + if (browser.defaultContext === this) { + browser.close(); + } + } + + /** + * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete. + * + * @returns Promise. + */ + public async whenComplete(): Promise { + await Promise.all(this.pages.map((page) => page.whenComplete())); + } + + /** + * Aborts all ongoing operations. + */ + public abort(): void { + for (const page of this.pages) { + page.abort(); + } + } + + /** + * Creates a new page. + * + * @param [opener] Opener. + * @returns Page. + */ + public newPage(opener?: DetachedBrowserFrame): DetachedBrowserPage { + const page = new DetachedBrowserPage(this); + ((page.mainFrame.opener)) = opener || null; + this.pages.push(page); + return page; + } +} diff --git a/packages/happy-dom/src/browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts similarity index 59% rename from packages/happy-dom/src/browser/DetachedBrowserFrame.ts rename to packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index aa5dc269e..3c477cf88 100644 --- a/packages/happy-dom/src/browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -1,15 +1,14 @@ -import IBrowserWindow from '../window/IBrowserWindow.js'; import DetachedBrowserPage from './DetachedBrowserPage.js'; -import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; -import IBrowserFrame from './types/IBrowserFrame.js'; -import Location from '../location/Location.js'; -import IResponse from '../fetch/types/IResponse.js'; -import IGoToOptions from './types/IGoToOptions.js'; +import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; +import IBrowserFrame from '../types/IBrowserFrame.js'; +import Location from '../../location/Location.js'; +import IResponse from '../../fetch/types/IResponse.js'; +import IGoToOptions from '../types/IGoToOptions.js'; import { Script } from 'vm'; -import BrowserFrameURL from './utilities/BrowserFrameURL.js'; -import BrowserFrameScriptEvaluator from './utilities/BrowserFrameScriptEvaluator.js'; -import BrowserFrameNavigator from './utilities/BrowserFrameNavigator.js'; -import IWindow from '../window/IWindow.js'; +import BrowserFrameURL from '../utilities/BrowserFrameURL.js'; +import BrowserFrameScriptEvaluator from '../utilities/BrowserFrameScriptEvaluator.js'; +import BrowserFrameNavigator from '../utilities/BrowserFrameNavigator.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; /** * Browser frame used when constructing a Window instance without a browser. @@ -19,22 +18,21 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public readonly parentFrame: DetachedBrowserFrame | null = null; public readonly opener: DetachedBrowserFrame | null = null; public readonly page: DetachedBrowserPage; + // Needs to be injected from the outside when the browser frame is constructed. + public window: IBrowserWindow; public _asyncTaskManager = new AsyncTaskManager(); - // Needs to be injected when constructing the browser frame in Window.ts. - public window: IWindow; - readonly #windowClass: new (browserFrame: IBrowserFrame) => IBrowserWindow; /** * Constructor. * * @param page Page. + * @param [window] Window. */ - constructor( - page: DetachedBrowserPage, - windowClass: new (browserFrame: IBrowserFrame) => IBrowserWindow - ) { + constructor(page: DetachedBrowserPage) { this.page = page; - this.#windowClass = windowClass; + if (page.context.browser.contexts[0]?.pages[0]?.mainFrame) { + this.window = new this.page.context.browser.windowClass(this); + } } /** @@ -43,6 +41,9 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @returns Content. */ public get content(): string { + if (!this.window) { + throw new Error('The frame has been destroyed, the "window" property is not set.'); + } return this.window.document.documentElement.outerHTML; } @@ -52,6 +53,9 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @param content Content. */ public set content(content) { + if (!this.window) { + throw new Error('The frame has been destroyed, the "window" property is not set.'); + } this.window.document['_isFirstWrite'] = true; this.window.document['_isFirstWriteAfterOpen'] = false; this.window.document.open(); @@ -64,6 +68,9 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @returns URL. */ public get url(): string { + if (!this.window) { + throw new Error('The frame has been destroyed, the "window" property is not set.'); + } return this.window.location.href; } @@ -73,6 +80,9 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @param url URL. */ public set url(url) { + if (!this.window) { + throw new Error('The frame has been destroyed, the "window" property is not set.'); + } (this.window.location) = new Location( this, BrowserFrameURL.getRelativeURL(this, url).href @@ -98,7 +108,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { for (const frame of this.childFrames) { frame.abort(); } - this._asyncTaskManager.abortAll(); + this._asyncTaskManager.abort(); } /** @@ -119,6 +129,6 @@ export default class DetachedBrowserFrame implements IBrowserFrame { * @returns Response. */ public goto(url: string, options?: IGoToOptions): Promise { - return BrowserFrameNavigator.goto(this.#windowClass, this, url, options); + return BrowserFrameNavigator.goto(this.page.context.browser.windowClass, this, url, options); } } diff --git a/packages/happy-dom/src/browser/DetachedBrowserPage.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts similarity index 75% rename from packages/happy-dom/src/browser/DetachedBrowserPage.ts rename to packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts index 61a64e7a8..91d50b3d9 100644 --- a/packages/happy-dom/src/browser/DetachedBrowserPage.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts @@ -1,15 +1,13 @@ -import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; -import IBrowserPageViewport from './types/IBrowserPageViewport.js'; +import VirtualConsolePrinter from '../../console/VirtualConsolePrinter.js'; +import IBrowserPageViewport from '../types/IBrowserPageViewport.js'; import DetachedBrowserFrame from './DetachedBrowserFrame.js'; -import IBrowserContext from './types/IBrowserContext.js'; -import VirtualConsole from '../console/VirtualConsole.js'; -import IBrowserPage from './types/IBrowserPage.js'; +import DetachedBrowserContext from './DetachedBrowserContext.js'; +import VirtualConsole from '../../console/VirtualConsole.js'; +import IBrowserPage from '../types/IBrowserPage.js'; import { Script } from 'vm'; -import IGoToOptions from './types/IGoToOptions.js'; -import IResponse from '../fetch/types/IResponse.js'; -import BrowserPageUtility from './utilities/BrowserPageUtility.js'; -import IBrowserFrame from './types/IBrowserFrame.js'; -import IBrowserWindow from '../window/IBrowserWindow.js'; +import IGoToOptions from '../types/IGoToOptions.js'; +import IResponse from '../../fetch/types/IResponse.js'; +import BrowserPageUtility from '../utilities/BrowserPageUtility.js'; /** * Detached browser page used when constructing a Window instance without a browser. @@ -17,7 +15,7 @@ import IBrowserWindow from '../window/IBrowserWindow.js'; export default class DetachedBrowserPage implements IBrowserPage { public readonly virtualConsolePrinter = new VirtualConsolePrinter(); public readonly mainFrame: DetachedBrowserFrame; - public readonly context: IBrowserContext; + public readonly context: DetachedBrowserContext; public readonly console: Console; /** @@ -25,13 +23,10 @@ export default class DetachedBrowserPage implements IBrowserPage { * * @param context Browser context. */ - constructor( - context: IBrowserContext, - windowClass: new (browserFrame: IBrowserFrame) => IBrowserWindow - ) { + constructor(context: DetachedBrowserContext) { this.context = context; this.console = context.browser.console ?? new VirtualConsole(this.virtualConsolePrinter); - this.mainFrame = new DetachedBrowserFrame(this, windowClass); + this.mainFrame = new DetachedBrowserFrame(this); } /** diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index 948dc262b..883007f00 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -15,7 +15,7 @@ export default interface IBrowserFrame { url: string; readonly parentFrame: IBrowserFrame | null; readonly opener: IBrowserFrame | null; - readonly _asyncTaskManager: AsyncTaskManager; + _asyncTaskManager: AsyncTaskManager; readonly page: IBrowserPage; /** diff --git a/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts b/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts index 654493a06..4e5ce1442 100644 --- a/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts +++ b/packages/happy-dom/src/browser/types/IBrowserPageViewport.ts @@ -1,5 +1,5 @@ export default interface IBrowserPageViewport { width?: number; height?: number; - deviceScaleFactor?: number; + devicePixelRatio?: number; } diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts index f6506e046..60788821f 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts @@ -10,6 +10,7 @@ import AbortController from '../../fetch/AbortController.js'; import BrowserFrameFactory from './BrowserFrameFactory.js'; import BrowserFrameURL from './BrowserFrameURL.js'; import BrowserFrameValidator from './BrowserFrameValidator.js'; +import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; /** * Browser frame navigation utility. @@ -26,13 +27,20 @@ export default class BrowserFrameNavigator { * @returns Response. */ public static async goto( - windowClass: new (browserFrame: IBrowserFrame) => IBrowserWindow, + windowClass: new ( + browserFrame: IBrowserFrame, + options?: { url?: string; width?: number; height?: number } + ) => IBrowserWindow, frame: IBrowserFrame, url: string, options?: IGoToOptions ): Promise { const targetURL = BrowserFrameURL.getRelativeURL(frame, url); + if (!frame.window) { + throw new Error('The frame has been destroyed, the "window" property is not set.'); + } + if (targetURL.protocol === 'javascript:') { if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( @@ -71,6 +79,10 @@ export default class BrowserFrameNavigator { return null; } + const width = frame.window.innerWidth; + const height = frame.window.innerHeight; + const devicePixelRatio = frame.window.devicePixelRatio; + for (const childFrame of frame.childFrames) { BrowserFrameFactory.destroyFrame(childFrame); } @@ -78,9 +90,11 @@ export default class BrowserFrameNavigator { (frame.childFrames) = []; (frame.window.closed) = true; frame._asyncTaskManager.destroy(); + frame._asyncTaskManager = new AsyncTaskManager(); WindowBrowserSettingsReader.removeSettings(frame.window); - (frame.window) = new windowClass(frame); + (frame.window) = new windowClass(frame, { url: targetURL.href, width, height }); + (frame.window.devicePixelRatio) = devicePixelRatio; if (options?.referrer) { (frame.window.document.referrer) = options.referrer; diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameScriptEvaluator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameScriptEvaluator.ts index d23d6d739..560da2ce6 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameScriptEvaluator.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameScriptEvaluator.ts @@ -13,6 +13,9 @@ export default class BrowserFrameScriptEvaluator { * @returns Result. */ public static evaluate(frame: IBrowserFrame, script: string | Script): any { + if (!frame.window) { + throw new Error('The frame has been destroyed, the "window" property is not set.'); + } script = typeof script === 'string' ? new Script(script) : script; return script.runInContext(frame.window); } diff --git a/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts b/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts index 0f48ae3bd..595200108 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts @@ -44,8 +44,8 @@ export default class BrowserPageUtility { page.mainFrame.window.dispatchEvent(new Event('resize')); } - if (viewport.deviceScaleFactor !== undefined) { - (page.mainFrame.window.devicePixelRatio) = viewport.deviceScaleFactor; + if (viewport.devicePixelRatio !== undefined) { + (page.mainFrame.window.devicePixelRatio) = viewport.devicePixelRatio; } } diff --git a/packages/happy-dom/src/cookie/CookieContainer.ts b/packages/happy-dom/src/cookie/CookieContainer.ts index 428c48514..5ab06dcec 100644 --- a/packages/happy-dom/src/cookie/CookieContainer.ts +++ b/packages/happy-dom/src/cookie/CookieContainer.ts @@ -31,16 +31,18 @@ export default class CookieContainer implements ICookieContainer { } for (const cookie of cookies) { - // Remove existing cookie with same name, domain and path. - const index = indexMap[getKey(cookie)]; + if (cookie?.key) { + // Remove existing cookie with same name, domain and path. + const index = indexMap[getKey(cookie)]; - if (index !== undefined) { - this.#cookies.splice(index, 1); - } + if (index !== undefined) { + this.#cookies.splice(index, 1); + } - if (!CookieExpireUtility.hasExpired(cookie)) { - indexMap[getKey(cookie)] = this.#cookies.length; - this.#cookies.push(cookie); + if (!CookieExpireUtility.hasExpired(cookie)) { + indexMap[getKey(cookie)] = this.#cookies.length; + this.#cookies.push(cookie); + } } } } diff --git a/packages/happy-dom/src/cookie/urilities/CookieStringUtility.ts b/packages/happy-dom/src/cookie/urilities/CookieStringUtility.ts index 2d6fe0c7e..f3370904f 100644 --- a/packages/happy-dom/src/cookie/urilities/CookieStringUtility.ts +++ b/packages/happy-dom/src/cookie/urilities/CookieStringUtility.ts @@ -20,7 +20,7 @@ export default class CookieStringUtility { const cookie: ICookie = { // Required key: key.trim(), - value: value || null, + value: value ?? null, originURL, // Optional diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts index bd8a87f9e..71e8d67dd 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts @@ -46,7 +46,7 @@ export default class HTMLLinkElementUtility { let error: Error | null = null; try { - code = await ResourceFetch.fetch(element.ownerDocument, href); + code = await ResourceFetch.fetch(element.ownerDocument._defaultView, href); } catch (e) { error = e; } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts index 4233ea2c6..4d648f5e9 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts @@ -48,7 +48,7 @@ export default class HTMLScriptElementUtility { ))._readyStateManager.startTask(); try { - code = await ResourceFetch.fetch(element.ownerDocument, src); + code = await ResourceFetch.fetch(element.ownerDocument._defaultView, src); } catch (e) { error = e; } @@ -58,7 +58,7 @@ export default class HTMLScriptElementUtility { ))._readyStateManager.endTask(); } else { try { - code = ResourceFetch.fetchSync(element.ownerDocument, src); + code = ResourceFetch.fetchSync(element.ownerDocument._defaultView, src); } catch (e) { error = e; } diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts index 457ab2e2d..04e06156d 100644 --- a/packages/happy-dom/src/range/Range.ts +++ b/packages/happy-dom/src/range/Range.ts @@ -35,8 +35,8 @@ export default class Range { public readonly END_TO_START: number = RangeHowEnum.endToStart; public readonly START_TO_END: number = RangeHowEnum.startToEnd; public readonly START_TO_START: number = RangeHowEnum.startToStart; - #start: IRangeBoundaryPoint | null = null; - #end: IRangeBoundaryPoint | null = null; + public _start: IRangeBoundaryPoint | null = null; + public _end: IRangeBoundaryPoint | null = null; #window: IBrowserWindow; public readonly _ownerDocument: IDocument; @@ -48,8 +48,8 @@ export default class Range { constructor(window: IBrowserWindow) { this.#window = window; this._ownerDocument = window.document; - this.#start = { node: window.document, offset: 0 }; - this.#end = { node: window.document, offset: 0 }; + this._start = { node: window.document, offset: 0 }; + this._end = { node: window.document, offset: 0 }; } /** @@ -59,7 +59,7 @@ export default class Range { * @returns Start container. */ public get startContainer(): INode { - return this.#start.node; + return this._start.node; } /** @@ -69,7 +69,7 @@ export default class Range { * @returns End container. */ public get endContainer(): INode { - return this.#end.node; + return this._end.node; } /** @@ -79,14 +79,14 @@ export default class Range { * @returns Start offset. */ public get startOffset(): number { - if (this.#start.offset > 0) { - const length = NodeUtility.getNodeLength(this.#start.node); - if (this.#start.offset > length) { - this.#start.offset = length; + if (this._start.offset > 0) { + const length = NodeUtility.getNodeLength(this._start.node); + if (this._start.offset > length) { + this._start.offset = length; } } - return this.#start.offset; + return this._start.offset; } /** @@ -96,14 +96,14 @@ export default class Range { * @returns End offset. */ public get endOffset(): number { - if (this.#end.offset > 0) { - const length = NodeUtility.getNodeLength(this.#end.node); - if (this.#end.offset > length) { - this.#end.offset = length; + if (this._end.offset > 0) { + const length = NodeUtility.getNodeLength(this._end.node); + if (this._end.offset > length) { + this._end.offset = length; } } - return this.#end.offset; + return this._end.offset; } /** @@ -113,7 +113,7 @@ export default class Range { * @returns Collapsed. */ public get collapsed(): boolean { - return this.#start.node === this.#end.node && this.startOffset === this.endOffset; + return this._start.node === this._end.node && this.startOffset === this.endOffset; } /** @@ -123,10 +123,10 @@ export default class Range { * @returns Node. */ public get commonAncestorContainer(): INode { - let container = this.#start.node; + let container = this._start.node; while (container) { - if (NodeUtility.isInclusiveAncestor(container, this.#end.node)) { + if (NodeUtility.isInclusiveAncestor(container, this._end.node)) { return container; } container = container.parentNode; @@ -143,9 +143,9 @@ export default class Range { */ public collapse(toStart = false): void { if (toStart) { - this.#end = Object.assign({}, this.#start); + this._end = Object.assign({}, this._start); } else { - this.#start = Object.assign({}, this.#end); + this._start = Object.assign({}, this._end); } } @@ -188,27 +188,27 @@ export default class Range { switch (how) { case RangeHowEnum.startToStart: - thisPoint.node = this.#start.node; + thisPoint.node = this._start.node; thisPoint.offset = this.startOffset; - sourcePoint.node = sourceRange.#start.node; + sourcePoint.node = sourceRange._start.node; sourcePoint.offset = sourceRange.startOffset; break; case RangeHowEnum.startToEnd: - thisPoint.node = this.#end.node; + thisPoint.node = this._end.node; thisPoint.offset = this.endOffset; - sourcePoint.node = sourceRange.#start.node; + sourcePoint.node = sourceRange._start.node; sourcePoint.offset = sourceRange.startOffset; break; case RangeHowEnum.endToEnd: - thisPoint.node = this.#end.node; + thisPoint.node = this._end.node; thisPoint.offset = this.endOffset; - sourcePoint.node = sourceRange.#end.node; + sourcePoint.node = sourceRange._end.node; sourcePoint.offset = sourceRange.endOffset; break; case RangeHowEnum.endToStart: - thisPoint.node = this.#start.node; + thisPoint.node = this._start.node; thisPoint.offset = this.startOffset; - sourcePoint.node = sourceRange.#end.node; + sourcePoint.node = sourceRange._end.node; sourcePoint.offset = sourceRange.endOffset; break; } @@ -238,14 +238,14 @@ export default class Range { if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.#start.node, + node: this._start.node, offset: this.startOffset }) === -1 ) { return -1; } else if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.#end.node, + node: this._end.node, offset: this.endOffset }) === 1 ) { @@ -271,24 +271,24 @@ export default class Range { } if ( - this.#start.node === this.#end.node && - (this.#start.node.nodeType === NodeTypeEnum.textNode || - this.#start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.#start.node.nodeType === NodeTypeEnum.commentNode) + this._start.node === this._end.node && + (this._start.node.nodeType === NodeTypeEnum.textNode || + this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || + this._start.node.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this.#start.node).cloneNode(false); + const clone = (this._start.node).cloneNode(false); clone['_data'] = clone.substringData(startOffset, endOffset - startOffset); fragment.appendChild(clone); return fragment; } - let commonAncestor = this.#start.node; - while (!NodeUtility.isInclusiveAncestor(commonAncestor, this.#end.node)) { + let commonAncestor = this._start.node; + while (!NodeUtility.isInclusiveAncestor(commonAncestor, this._end.node)) { commonAncestor = commonAncestor.parentNode; } let firstPartialContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this.#start.node, this.#end.node)) { + if (!NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { let candidate = commonAncestor.firstChild; while (!firstPartialContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -300,7 +300,7 @@ export default class Range { } let lastPartiallyContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this.#end.node, this.#start.node)) { + if (!NodeUtility.isInclusiveAncestor(this._end.node, this._start.node)) { let candidate = commonAncestor.lastChild; while (!lastPartiallyContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -331,10 +331,10 @@ export default class Range { firstPartialContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || firstPartialContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this.#start.node).cloneNode(false); + const clone = (this._start.node).cloneNode(false); clone['_data'] = clone.substringData( startOffset, - NodeUtility.getNodeLength(this.#start.node) - startOffset + NodeUtility.getNodeLength(this._start.node) - startOffset ); fragment.appendChild(clone); @@ -343,10 +343,10 @@ export default class Range { fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange.#start.node = this.#start.node; - subRange.#start.offset = startOffset; - subRange.#end.node = firstPartialContainedChild; - subRange.#end.offset = NodeUtility.getNodeLength(firstPartialContainedChild); + subRange._start.node = this._start.node; + subRange._start.offset = startOffset; + subRange._end.node = firstPartialContainedChild; + subRange._end.offset = NodeUtility.getNodeLength(firstPartialContainedChild); const subDocumentFragment = subRange.cloneContents(); clone.appendChild(subDocumentFragment); @@ -363,7 +363,7 @@ export default class Range { lastPartiallyContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || lastPartiallyContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this.#end.node).cloneNode(false); + const clone = (this._end.node).cloneNode(false); clone['_data'] = clone.substringData(0, endOffset); fragment.appendChild(clone); @@ -372,10 +372,10 @@ export default class Range { fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange.#start.node = lastPartiallyContainedChild; - subRange.#start.offset = 0; - subRange.#end.node = this.#end.node; - subRange.#end.offset = endOffset; + subRange._start.node = lastPartiallyContainedChild; + subRange._start.offset = 0; + subRange._end.node = this._end.node; + subRange._end.offset = endOffset; const subFragment = subRange.cloneContents(); clone.appendChild(subFragment); @@ -393,10 +393,10 @@ export default class Range { public cloneRange(): Range { const clone = new this.#window.Range(); - clone.#start.node = this.#start.node; - clone.#start.offset = this.#start.offset; - clone.#end.node = this.#end.node; - clone.#end.offset = this.#end.offset; + clone._start.node = this._start.node; + clone._start.offset = this._start.offset; + clone._end.node = this._end.node; + clone._end.offset = this._end.offset; return clone; } @@ -427,18 +427,18 @@ export default class Range { } if ( - this.#start.node === this.#end.node && - (this.#start.node.nodeType === NodeTypeEnum.textNode || - this.#start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.#start.node.nodeType === NodeTypeEnum.commentNode) + this._start.node === this._end.node && + (this._start.node.nodeType === NodeTypeEnum.textNode || + this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || + this._start.node.nodeType === NodeTypeEnum.commentNode) ) { - (this.#start.node).replaceData(startOffset, endOffset - startOffset, ''); + (this._start.node).replaceData(startOffset, endOffset - startOffset, ''); return; } const nodesToRemove = []; - let currentNode = this.#start.node; - const endNode = NodeUtility.nextDescendantNode(this.#end.node); + let currentNode = this._start.node; + const endNode = NodeUtility.nextDescendantNode(this._end.node); while (currentNode && currentNode !== endNode) { if ( RangeUtility.isContained(currentNode, this) && @@ -452,15 +452,15 @@ export default class Range { let newNode; let newOffset; - if (NodeUtility.isInclusiveAncestor(this.#start.node, this.#end.node)) { - newNode = this.#start.node; + if (NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { + newNode = this._start.node; newOffset = startOffset; } else { - let referenceNode = this.#start.node; + let referenceNode = this._start.node; while ( referenceNode && - !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this.#end.node) + !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this._end.node) ) { referenceNode = referenceNode.parentNode; } @@ -470,13 +470,13 @@ export default class Range { } if ( - this.#start.node.nodeType === NodeTypeEnum.textNode || - this.#start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.#start.node.nodeType === NodeTypeEnum.commentNode + this._start.node.nodeType === NodeTypeEnum.textNode || + this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || + this._start.node.nodeType === NodeTypeEnum.commentNode ) { - (this.#start.node).replaceData( + (this._start.node).replaceData( this.startOffset, - NodeUtility.getNodeLength(this.#start.node) - this.startOffset, + NodeUtility.getNodeLength(this._start.node) - this.startOffset, '' ); } @@ -487,17 +487,17 @@ export default class Range { } if ( - this.#end.node.nodeType === NodeTypeEnum.textNode || - this.#end.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.#end.node.nodeType === NodeTypeEnum.commentNode + this._end.node.nodeType === NodeTypeEnum.textNode || + this._end.node.nodeType === NodeTypeEnum.processingInstructionNode || + this._end.node.nodeType === NodeTypeEnum.commentNode ) { - (this.#end.node).replaceData(0, endOffset, ''); + (this._end.node).replaceData(0, endOffset, ''); } - this.#start.node = newNode; - this.#start.offset = newOffset; - this.#end.node = newNode; - this.#end.offset = newOffset; + this._start.node = newNode; + this._start.offset = newOffset; + this._end.node = newNode; + this._end.offset = newOffset; } /** @@ -525,28 +525,28 @@ export default class Range { } if ( - this.#start.node === this.#end.node && - (this.#start.node.nodeType === NodeTypeEnum.textNode || - this.#start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.#start.node.nodeType === NodeTypeEnum.commentNode) + this._start.node === this._end.node && + (this._start.node.nodeType === NodeTypeEnum.textNode || + this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || + this._start.node.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this.#start.node.cloneNode(false); + const clone = this._start.node.cloneNode(false); clone['_data'] = clone.substringData(startOffset, endOffset - startOffset); fragment.appendChild(clone); - (this.#start.node).replaceData(startOffset, endOffset - startOffset, ''); + (this._start.node).replaceData(startOffset, endOffset - startOffset, ''); return fragment; } - let commonAncestor = this.#start.node; - while (!NodeUtility.isInclusiveAncestor(commonAncestor, this.#end.node)) { + let commonAncestor = this._start.node; + while (!NodeUtility.isInclusiveAncestor(commonAncestor, this._end.node)) { commonAncestor = commonAncestor.parentNode; } let firstPartialContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this.#start.node, this.#end.node)) { + if (!NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { let candidate = commonAncestor.firstChild; while (!firstPartialContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -558,7 +558,7 @@ export default class Range { } let lastPartiallyContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this.#end.node, this.#start.node)) { + if (!NodeUtility.isInclusiveAncestor(this._end.node, this._start.node)) { let candidate = commonAncestor.lastChild; while (!lastPartiallyContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -585,15 +585,15 @@ export default class Range { let newNode; let newOffset; - if (NodeUtility.isInclusiveAncestor(this.#start.node, this.#end.node)) { - newNode = this.#start.node; + if (NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { + newNode = this._start.node; newOffset = startOffset; } else { - let referenceNode = this.#start.node; + let referenceNode = this._start.node; while ( referenceNode && - !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this.#end.node) + !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this._end.node) ) { referenceNode = referenceNode.parentNode; } @@ -608,17 +608,17 @@ export default class Range { firstPartialContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || firstPartialContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this.#start.node.cloneNode(false); + const clone = this._start.node.cloneNode(false); clone['_data'] = clone.substringData( startOffset, - NodeUtility.getNodeLength(this.#start.node) - startOffset + NodeUtility.getNodeLength(this._start.node) - startOffset ); fragment.appendChild(clone); - (this.#start.node).replaceData( + (this._start.node).replaceData( startOffset, - NodeUtility.getNodeLength(this.#start.node) - startOffset, + NodeUtility.getNodeLength(this._start.node) - startOffset, '' ); } else if (firstPartialContainedChild !== null) { @@ -626,10 +626,10 @@ export default class Range { fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange.#start.node = this.#start.node; - subRange.#start.offset = startOffset; - subRange.#end.node = firstPartialContainedChild; - subRange.#end.offset = NodeUtility.getNodeLength(firstPartialContainedChild); + subRange._start.node = this._start.node; + subRange._start.offset = startOffset; + subRange._end.node = firstPartialContainedChild; + subRange._end.offset = NodeUtility.getNodeLength(firstPartialContainedChild); const subFragment = subRange.extractContents(); clone.appendChild(subFragment); @@ -645,30 +645,30 @@ export default class Range { lastPartiallyContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || lastPartiallyContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this.#end.node.cloneNode(false); + const clone = this._end.node.cloneNode(false); clone['_data'] = clone.substringData(0, endOffset); fragment.appendChild(clone); - (this.#end.node).replaceData(0, endOffset, ''); + (this._end.node).replaceData(0, endOffset, ''); } else if (lastPartiallyContainedChild !== null) { const clone = lastPartiallyContainedChild.cloneNode(false); fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange.#start.node = lastPartiallyContainedChild; - subRange.#start.offset = 0; - subRange.#end.node = this.#end.node; - subRange.#end.offset = endOffset; + subRange._start.node = lastPartiallyContainedChild; + subRange._start.offset = 0; + subRange._end.node = this._end.node; + subRange._end.offset = endOffset; const subFragment = subRange.extractContents(); clone.appendChild(subFragment); } - this.#start.node = newNode; - this.#start.offset = newOffset; - this.#end.node = newNode; - this.#end.offset = newOffset; + this._start.node = newNode; + this._start.offset = newOffset; + this._end.node = newNode; + this._end.offset = newOffset; return fragment; } @@ -712,11 +712,11 @@ export default class Range { if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.#start.node, + node: this._start.node, offset: this.startOffset }) === -1 || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.#end.node, + node: this._end.node, offset: this.endOffset }) === 1 ) { @@ -734,22 +734,22 @@ export default class Range { */ public insertNode(newNode: INode): void { if ( - this.#start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.#start.node.nodeType === NodeTypeEnum.commentNode || - (this.#start.node.nodeType === NodeTypeEnum.textNode && !this.#start.node.parentNode) || - newNode === this.#start.node + this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || + this._start.node.nodeType === NodeTypeEnum.commentNode || + (this._start.node.nodeType === NodeTypeEnum.textNode && !this._start.node.parentNode) || + newNode === this._start.node ) { throw new DOMException('Invalid start node.', DOMExceptionNameEnum.hierarchyRequestError); } let referenceNode = - this.#start.node.nodeType === NodeTypeEnum.textNode - ? this.#start.node - : (this.#start.node)._childNodes[this.startOffset] || null; - const parent = !referenceNode ? this.#start.node : referenceNode.parentNode; + this._start.node.nodeType === NodeTypeEnum.textNode + ? this._start.node + : (this._start.node)._childNodes[this.startOffset] || null; + const parent = !referenceNode ? this._start.node : referenceNode.parentNode; - if (this.#start.node.nodeType === NodeTypeEnum.textNode) { - referenceNode = (this.#start.node).splitText(this.startOffset); + if (this._start.node.nodeType === NodeTypeEnum.textNode) { + referenceNode = (this._start.node).splitText(this.startOffset); } if (newNode === referenceNode) { @@ -772,8 +772,8 @@ export default class Range { parent.insertBefore(newNode, referenceNode); if (this.collapsed) { - this.#end.node = parent; - this.#end.offset = newOffset; + this._end.node = parent; + this._end.offset = newOffset; } } @@ -800,11 +800,11 @@ export default class Range { return ( RangeUtility.compareBoundaryPointsPosition( { node: parent, offset }, - { node: this.#end.node, offset: this.endOffset } + { node: this._end.node, offset: this.endOffset } ) === -1 && RangeUtility.compareBoundaryPointsPosition( { node: parent, offset: offset + 1 }, - { node: this.#start.node, offset: this.startOffset } + { node: this._start.node, offset: this.startOffset } ) === 1 ); } @@ -825,10 +825,10 @@ export default class Range { const index = (node.parentNode)._childNodes.indexOf(node); - this.#start.node = node.parentNode; - this.#start.offset = index; - this.#end.node = node.parentNode; - this.#end.offset = index + 1; + this._start.node = node.parentNode; + this._start.offset = index; + this._end.node = node.parentNode; + this._end.offset = index + 1; } /** @@ -845,10 +845,10 @@ export default class Range { ); } - this.#start.node = node; - this.#start.offset = 0; - this.#end.node = node; - this.#end.offset = NodeUtility.getNodeLength(node); + this._start.node = node; + this._start.offset = 0; + this._end.node = node; + this._end.offset = NodeUtility.getNodeLength(node); } /** @@ -866,16 +866,16 @@ export default class Range { if ( node.ownerDocument !== this._ownerDocument || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.#start.node, + node: this._start.node, offset: this.startOffset }) === -1 ) { - this.#start.node = node; - this.#start.offset = offset; + this._start.node = node; + this._start.offset = offset; } - this.#end.node = node; - this.#end.offset = offset; + this._end.node = node; + this._end.offset = offset; } /** @@ -893,16 +893,16 @@ export default class Range { if ( node.ownerDocument !== this._ownerDocument || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.#end.node, + node: this._end.node, offset: this.endOffset }) === 1 ) { - this.#end.node = node; - this.#end.offset = offset; + this._end.node = node; + this._end.offset = offset; } - this.#start.node = node; - this.#start.offset = offset; + this._start.node = node; + this._start.offset = offset; } /** @@ -1024,18 +1024,18 @@ export default class Range { let string = ''; if ( - this.#start.node === this.#end.node && - this.#start.node.nodeType === NodeTypeEnum.textNode + this._start.node === this._end.node && + this._start.node.nodeType === NodeTypeEnum.textNode ) { - return (this.#start.node).data.slice(startOffset, endOffset); + return (this._start.node).data.slice(startOffset, endOffset); } - if (this.#start.node.nodeType === NodeTypeEnum.textNode) { - string += (this.#start.node).data.slice(startOffset); + if (this._start.node.nodeType === NodeTypeEnum.textNode) { + string += (this._start.node).data.slice(startOffset); } - const endNode = NodeUtility.nextDescendantNode(this.#end.node); - let currentNode = this.#start.node; + const endNode = NodeUtility.nextDescendantNode(this._end.node); + let currentNode = this._start.node; while (currentNode && currentNode !== endNode) { if ( @@ -1048,8 +1048,8 @@ export default class Range { currentNode = NodeUtility.following(currentNode); } - if (this.#end.node.nodeType === NodeTypeEnum.textNode) { - string += (this.#end.node).data.slice(0, endOffset); + if (this._end.node.nodeType === NodeTypeEnum.textNode) { + string += (this._end.node).data.slice(0, endOffset); } return string; diff --git a/packages/happy-dom/src/selection/Selection.ts b/packages/happy-dom/src/selection/Selection.ts index 7ae1974a0..2627cdfad 100644 --- a/packages/happy-dom/src/selection/Selection.ts +++ b/packages/happy-dom/src/selection/Selection.ts @@ -19,9 +19,9 @@ import SelectionDirectionEnum from './SelectionDirectionEnum.js'; * https://developer.mozilla.org/en-US/docs/Web/API/Selection. */ export default class Selection { - private readonly _ownerDocument: IDocument = null; - private _range: Range = null; - private _direction: SelectionDirectionEnum = SelectionDirectionEnum.directionless; + readonly #ownerDocument: IDocument = null; + public _range: Range = null; + public _direction: SelectionDirectionEnum = SelectionDirectionEnum.directionless; /** * Constructor. @@ -29,7 +29,7 @@ export default class Selection { * @param ownerDocument Owner document. */ constructor(ownerDocument: IDocument) { - this._ownerDocument = ownerDocument; + this.#ownerDocument = ownerDocument; } /** @@ -172,7 +172,7 @@ export default class Selection { if (!newRange) { throw new Error('Failed to execute addRange on Selection. Parameter 1 is not of type Range.'); } - if (!this._range && newRange._ownerDocument === this._ownerDocument) { + if (!this._range && newRange._ownerDocument === this.#ownerDocument) { this._associateRange(newRange); } } @@ -245,11 +245,11 @@ export default class Selection { throw new DOMException('Invalid range index.', DOMExceptionNameEnum.indexSizeError); } - if (node.ownerDocument !== this._ownerDocument) { + if (node.ownerDocument !== this.#ownerDocument) { return; } - const newRange = new this._ownerDocument._defaultView.Range(); + const newRange = new this.#ownerDocument._defaultView.Range(); newRange._start.node = node; newRange._start.offset = offset; @@ -285,7 +285,7 @@ export default class Selection { } const { node, offset } = this._range._end; - const newRange = new this._ownerDocument._defaultView.Range(); + const newRange = new this.#ownerDocument._defaultView.Range(); newRange._start.node = node; newRange._start.offset = offset; @@ -309,7 +309,7 @@ export default class Selection { } const { node, offset } = this._range._start; - const newRange = new this._ownerDocument._defaultView.Range(); + const newRange = new this.#ownerDocument._defaultView.Range(); newRange._start.node = node; newRange._start.offset = offset; @@ -328,7 +328,7 @@ export default class Selection { * @returns Always returns "true" for now. */ public containsNode(node: INode, allowPartialContainment = false): boolean { - if (!this._range || node.ownerDocument !== this._ownerDocument) { + if (!this._range || node.ownerDocument !== this.#ownerDocument) { return false; } @@ -366,7 +366,7 @@ export default class Selection { * @param offset Offset. */ public extend(node: INode, offset: number): void { - if (node.ownerDocument !== this._ownerDocument) { + if (node.ownerDocument !== this.#ownerDocument) { return; } @@ -379,7 +379,7 @@ export default class Selection { const anchorNode = this.anchorNode; const anchorOffset = this.anchorOffset; - const newRange = new this._ownerDocument._defaultView.Range(); + const newRange = new this.#ownerDocument._defaultView.Range(); newRange._start.node = node; newRange._start.offset = 0; newRange._end.node = node; @@ -430,12 +430,12 @@ export default class Selection { ); } - if (node.ownerDocument !== this._ownerDocument) { + if (node.ownerDocument !== this.#ownerDocument) { return; } const length = node.childNodes.length; - const newRange = new this._ownerDocument._defaultView.Range(); + const newRange = new this.#ownerDocument._defaultView.Range(); newRange._start.node = node; newRange._start.offset = 0; @@ -471,15 +471,15 @@ export default class Selection { } if ( - anchorNode.ownerDocument !== this._ownerDocument || - focusNode.ownerDocument !== this._ownerDocument + anchorNode.ownerDocument !== this.#ownerDocument || + focusNode.ownerDocument !== this.#ownerDocument ) { return; } const anchor = { node: anchorNode, offset: anchorOffset }; const focus = { node: focusNode, offset: focusOffset }; - const newRange = new this._ownerDocument._defaultView.Range(); + const newRange = new this.#ownerDocument._defaultView.Range(); if (RangeUtility.compareBoundaryPointsPosition(anchor, focus) === -1) { newRange._start = anchor; @@ -518,7 +518,7 @@ export default class Selection { if (oldRange !== this._range) { // https://w3c.github.io/selection-api/#selectionchange-event - this._ownerDocument.dispatchEvent(new Event('selectionchange')); + this.#ownerDocument.dispatchEvent(new Event('selectionchange')); } } } diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index b20f5d212..c1ee65e00 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -500,8 +500,15 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow * Constructor. * * @param browserFrame Browser frame. + * @param [options] Options. + * @param [options.url] URL. + * @param [options.width] Window width. Defaults to "1024". + * @param [options.height] Window height. Defaults to "768". */ - constructor(browserFrame: IBrowserFrame) { + constructor( + browserFrame: IBrowserFrame, + options?: { url?: string; width?: number; height?: number } + ) { super(); this.#browserFrame = browserFrame; @@ -517,10 +524,23 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow this.screen = new Screen(); this.sessionStorage = new Storage(); this.localStorage = new Storage(); + this.location = new Location(this.#browserFrame, options?.url ?? 'about:blank'); WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); - this.location = new Location(this.#browserFrame, 'about:blank'); + if (options) { + if (options.width !== undefined) { + if (options.width !== undefined && this.innerWidth !== options.width) { + (this.innerWidth) = options.width; + (this.outerWidth) = options.width; + } + } + + if (options.height !== undefined && this.innerHeight !== options.height) { + (this.innerHeight) = options.height; + (this.outerHeight) = options.height; + } + } // Binds all methods to "this", so that it will use the correct context when called globally. for (const key of Object.getOwnPropertyNames(BrowserWindow.prototype).concat( diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index f9f97e9a2..deadb04e1 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -2,8 +2,8 @@ import IWindow from './IWindow.js'; import DetachedWindowAPI from './DetachedWindowAPI.js'; import IOptionalBrowserSettings from '../browser/types/IOptionalBrowserSettings.js'; import BrowserWindow from './BrowserWindow.js'; -import DetachedBrowserPage from '../browser/DetachedBrowserPage.js'; -import Browser from '../browser/Browser.js'; +import DetachedBrowser from '../browser/detached-browser/DetachedBrowser.js'; +import Location from '../location/Location.js'; /** * Window. @@ -30,38 +30,28 @@ export default class Window extends BrowserWindow implements IWindow { constructor(options?: { width?: number; height?: number; + /** @deprecated Replaced by the "width" property. */ innerWidth?: number; + /** @deprecated Replaced by the "height" property. */ innerHeight?: number; url?: string; console?: Console; settings?: IOptionalBrowserSettings; }) { - const browser = new Browser(); - const browserPage = new DetachedBrowserPage(browser.defaultContext, BrowserWindow); - const browserFrame = browserPage.mainFrame; + const browser = new DetachedBrowser(BrowserWindow, { + console: options?.console, + settings: options?.settings + }); + const browserFrame = browser.defaultContext.pages[0].mainFrame; - super(browserFrame); + super(browserFrame, { + url: options?.url, + width: options?.width ?? options?.innerWidth, + height: options?.height ?? options?.innerHeight + }); browserFrame.window = this; this.happyDOM = new DetachedWindowAPI(browserFrame); - - if (options) { - if ( - options.width !== undefined || - options.innerWidth !== undefined || - options.height !== undefined || - options.innerHeight !== undefined - ) { - browserFrame.page.setViewport({ - width: options.width ?? options.innerWidth ?? 1024, - height: options.height ?? options.innerHeight ?? 768 - }); - } - - if (options.url !== undefined) { - browserFrame.url = options.url; - } - } } } diff --git a/packages/happy-dom/test/browser/BrowserFrame.test.ts b/packages/happy-dom/test/browser/BrowserFrame.test.ts index eb02a0bd5..692cf431e 100644 --- a/packages/happy-dom/test/browser/BrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/BrowserFrame.test.ts @@ -1,7 +1,7 @@ import { Script } from 'vm'; import Browser from '../../src/browser/Browser'; import Event from '../../src/event/Event'; -import Window from '../../src/window/Window'; +import BrowserWindow from '../../src/window/BrowserWindow'; import IRequest from '../../src/fetch/types/IRequest'; import IResponse from '../../src/fetch/types/IResponse'; import { describe, it, expect, afterEach, vi } from 'vitest'; @@ -52,7 +52,7 @@ describe('BrowserFrame', () => { it('Returns the window.', () => { const browser = new Browser(); const page = browser.defaultContext.newPage(); - expect(page.mainFrame.window).toBeInstanceOf(Window); + expect(page.mainFrame.window).toBeInstanceOf(BrowserWindow); expect(page.mainFrame.window.console).toBe(page.console); }); }); diff --git a/packages/happy-dom/test/browser/BrowserPage.test.ts b/packages/happy-dom/test/browser/BrowserPage.test.ts index b6015f62b..1375bbb47 100644 --- a/packages/happy-dom/test/browser/BrowserPage.test.ts +++ b/packages/happy-dom/test/browser/BrowserPage.test.ts @@ -1,6 +1,6 @@ import Browser from '../../src/browser/Browser'; import BrowserFrame from '../../src/browser/BrowserFrame'; -import Window from '../../src/window/Window'; +import BrowserWindow from '../../src/window/BrowserWindow'; import VirtualConsolePrinter from '../../src/console/VirtualConsolePrinter'; import VirtualConsole from '../../src/console/VirtualConsole'; import IResponse from '../../src/fetch/types/IResponse'; @@ -26,7 +26,7 @@ describe('BrowserPage', () => { const browser = new Browser(); const page = browser.defaultContext.newPage(); expect(page.mainFrame).toBeInstanceOf(BrowserFrame); - expect(page.mainFrame.window).toBeInstanceOf(Window); + expect(page.mainFrame.window).toBeInstanceOf(BrowserWindow); }); }); @@ -196,7 +196,7 @@ describe('BrowserPage', () => { it('Sets the viewport device scale factor.', () => { const browser = new Browser(); const page = browser.newPage(); - page.setViewport({ deviceScaleFactor: 2 }); + page.setViewport({ devicePixelRatio: 2 }); expect(page.mainFrame.window.devicePixelRatio).toBe(2); }); }); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts index c53211341..a016f01f7 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts @@ -2,6 +2,7 @@ import DetachedBrowser from '../../../src/browser/detached-browser/DetachedBrows import DetachedBrowserContext from '../../../src/browser/detached-browser/DetachedBrowserContext'; import DetachedBrowserPage from '../../../src/browser/detached-browser/DetachedBrowserPage'; import DefaultBrowserSettings from '../../../src/browser/DefaultBrowserSettings'; +import BrowserWindow from '../../../src/window/BrowserWindow'; import Window from '../../../src/window/Window'; import { describe, it, expect, afterEach, vi } from 'vitest'; @@ -12,7 +13,8 @@ describe('DetachedBrowser', () => { describe('get contexts()', () => { it('Returns the contexts.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + expect(browser.contexts.length).toBe(1); expect(browser.contexts[0]).toBe(browser.defaultContext); @@ -27,7 +29,7 @@ describe('DetachedBrowser', () => { describe('get settings()', () => { it('Returns the settings.', () => { - expect(new DetachedBrowser(Window, new Window()).settings).toEqual(DefaultBrowserSettings); + expect(new DetachedBrowser(BrowserWindow).settings).toEqual(DefaultBrowserSettings); }); it('Returns the settings with custom settings.', () => { @@ -37,7 +39,7 @@ describe('DetachedBrowser', () => { userAgent: 'test' } }; - expect(new DetachedBrowser(Window, new Window(), { settings }).settings).toEqual({ + expect(new DetachedBrowser(BrowserWindow, { settings }).settings).toEqual({ ...DefaultBrowserSettings, ...settings, navigator: { @@ -50,23 +52,23 @@ describe('DetachedBrowser', () => { describe('get console()', () => { it('Returns "null" if no console is provided.', () => { - expect(new DetachedBrowser(Window, new Window()).console).toBe(null); + expect(new DetachedBrowser(BrowserWindow).console).toBe(null); }); it('Returns console sent into the constructor.', () => { - expect(new DetachedBrowser(Window, new Window(), { console }).console).toBe(console); + expect(new DetachedBrowser(BrowserWindow, { console }).console).toBe(console); }); }); describe('get defaultContext()', () => { it('Returns the default context.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); expect(browser.defaultContext instanceof DetachedBrowserContext).toBe(true); expect(browser.contexts[0]).toBe(browser.defaultContext); }); it('Throws an error if the browser has been closed.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); browser.close(); expect(() => browser.defaultContext).toThrow( 'No default context. The browser has been closed.' @@ -76,10 +78,12 @@ describe('DetachedBrowser', () => { describe('close()', () => { it('Closes the browser.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); const originalClose = browser.defaultContext.close; let isContextClosed = false; + browser.defaultContext.pages[0].mainFrame.window = new Window(); + vi.spyOn(browser.defaultContext, 'close').mockImplementation(() => { isContextClosed = true; originalClose.call(browser.defaultContext); @@ -93,7 +97,7 @@ describe('DetachedBrowser', () => { describe('whenComplete()', () => { it('Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); const page1 = browser.newPage(); const page2 = browser.newPage(); page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); @@ -106,7 +110,7 @@ describe('DetachedBrowser', () => { describe('abort()', () => { it('Aborts all ongoing operations.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); const page1 = browser.newPage(); const page2 = browser.newPage(); page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); @@ -120,7 +124,8 @@ describe('DetachedBrowser', () => { describe('newIncognitoContext()', () => { it('Throws an error as it is not possible to create a new incognito context inside a detached browser.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); browser.close(); expect(() => browser.newIncognitoContext()).toThrow( 'Not possible to create a new context on a detached browser.' @@ -131,7 +136,8 @@ describe('DetachedBrowser', () => { describe('newPage()', () => { it('Creates a new page.', () => { const window = new Window(); - const browser = new DetachedBrowser(Window, window); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = window; const page = browser.newPage(); expect(page instanceof DetachedBrowserPage).toBe(true); expect(browser.contexts.length).toBe(1); @@ -141,14 +147,16 @@ describe('DetachedBrowser', () => { }); it('Throws an error if the browser has been closed.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); browser.close(); expect(() => browser.newPage()).toThrow('No default context. The browser has been closed.'); }); it('Supports opener as parameter.', () => { const window = new Window(); - const browser = new DetachedBrowser(Window, window); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = window; const page1 = browser.newPage(); const page2 = browser.newPage(page1.mainFrame); expect(page2.mainFrame.opener).toBe(page1.mainFrame); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts index 8cfa59031..aacfc2e54 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts @@ -1,6 +1,7 @@ import DetachedBrowser from '../../../src/browser/detached-browser/DetachedBrowser'; import DetachedBrowserPage from '../../../src/browser/detached-browser/DetachedBrowserPage'; import Window from '../../../src/window/Window'; +import BrowserWindow from '../../../src/window/BrowserWindow'; import { describe, it, expect, afterEach, vi } from 'vitest'; describe('DetachedBrowserContext', () => { @@ -11,7 +12,8 @@ describe('DetachedBrowserContext', () => { describe('get pages()', () => { it('Returns the pages.', () => { const window = new Window(); - const browser = new DetachedBrowser(Window, window); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = window; expect(browser.defaultContext.pages.length).toBe(1); expect(browser.defaultContext.pages[0].mainFrame.window).toBe(window); const page = browser.defaultContext.newPage(); @@ -23,14 +25,15 @@ describe('DetachedBrowserContext', () => { describe('get browser()', () => { it('Returns the browser.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); expect(browser.defaultContext.browser).toBe(browser); }); }); describe('close()', () => { it('Closes the context.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const context = browser.defaultContext; const page1 = context.newPage(); const page2 = context.newPage(); @@ -54,7 +57,7 @@ describe('DetachedBrowserContext', () => { describe('whenComplete()', () => { it('Waits for all pages to complete.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); const page1 = browser.newPage(); const page2 = browser.newPage(); page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); @@ -67,7 +70,7 @@ describe('DetachedBrowserContext', () => { describe('abort()', () => { it('Aborts all ongoing operations.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); const page1 = browser.newPage(); const page2 = browser.newPage(); page1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); @@ -82,7 +85,8 @@ describe('DetachedBrowserContext', () => { describe('newPage()', () => { it('Creates a new page.', () => { const window = new Window(); - const browser = new DetachedBrowser(Window, window); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = window; const page = browser.defaultContext.newPage(); expect(page instanceof DetachedBrowserPage).toBe(true); expect(browser.defaultContext.pages.length).toBe(2); @@ -92,7 +96,8 @@ describe('DetachedBrowserContext', () => { it('Supports opener as parameter.', () => { const window = new Window(); - const browser = new DetachedBrowser(Window, window); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = window; const page1 = browser.defaultContext.newPage(); const page2 = browser.defaultContext.newPage(page1.mainFrame); expect(page2.mainFrame.opener).toBe(page1.mainFrame); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts index 158613b1b..2ddf4acf8 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts @@ -2,6 +2,7 @@ import { Script } from 'vm'; import DetachedBrowser from '../../../src/browser/detached-browser/DetachedBrowser'; import Event from '../../../src/event/Event'; import Window from '../../../src/window/Window'; +import BrowserWindow from '../../../src/window/BrowserWindow'; import IRequest from '../../../src/fetch/types/IRequest'; import IResponse from '../../../src/fetch/types/IResponse'; import { describe, it, expect, afterEach, vi } from 'vitest'; @@ -18,7 +19,8 @@ describe('DetachedBrowserFrame', () => { describe('get childFrames()', () => { it('Returns child frames.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; expect(page.mainFrame.childFrames).toEqual([]); const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); @@ -29,7 +31,8 @@ describe('DetachedBrowserFrame', () => { describe('get parentFrame()', () => { it('Returns the parent frame.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; expect(page.mainFrame.parentFrame).toBe(null); const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); @@ -42,7 +45,7 @@ describe('DetachedBrowserFrame', () => { describe('get page()', () => { it('Returns the page.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); const page = browser.defaultContext.pages[0]; expect(page.mainFrame.page).toBe(page); }); @@ -50,16 +53,18 @@ describe('DetachedBrowserFrame', () => { describe('get window()', () => { it('Returns the window.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); - expect(page.mainFrame.window).toBeInstanceOf(Window); + expect(page.mainFrame.window).toBeInstanceOf(BrowserWindow); expect(page.mainFrame.window.console).toBe(page.console); }); }); describe('get content()', () => { it('Returns the document HTML content.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; page.mainFrame.window.document.write('
test
'); expect(page.content).toBe('
test
'); @@ -68,7 +73,8 @@ describe('DetachedBrowserFrame', () => { describe('set content()', () => { it('Sets the document HTML content.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; page.mainFrame.content = '
test
'; expect(page.mainFrame.window.document.documentElement.outerHTML).toBe( @@ -77,9 +83,10 @@ describe('DetachedBrowserFrame', () => { }); it('Removes listeners and child nodes before setting the document HTML content.', () => { - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { disableErrorCapturing: true } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; page.mainFrame.content = '
test
'; page.mainFrame.window.document.addEventListener('load', () => { @@ -99,7 +106,8 @@ describe('DetachedBrowserFrame', () => { describe('get url()', () => { it('Returns the document URL.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; page.mainFrame.url = 'http://localhost:3000'; expect(page.mainFrame.url).toBe('http://localhost:3000/'); @@ -108,7 +116,8 @@ describe('DetachedBrowserFrame', () => { describe('set url()', () => { it('Sets the document URL.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const location = page.mainFrame.window.location; page.mainFrame.url = 'http://localhost:3000'; @@ -119,7 +128,8 @@ describe('DetachedBrowserFrame', () => { describe('whenComplete()', () => { it('Waits for all pages to complete.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); @@ -135,7 +145,8 @@ describe('DetachedBrowserFrame', () => { describe('abort()', () => { it('Aborts all ongoing operations.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.newPage(); const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); @@ -152,14 +163,16 @@ describe('DetachedBrowserFrame', () => { describe('evaluate()', () => { it("Evaluates a code string in the frame's context.", () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; expect(page.mainFrame.evaluate('globalThis.test = 1')).toBe(1); expect(page.mainFrame.window['test']).toBe(1); }); it("Evaluates a VM script in the frame's context.", () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; expect(page.mainFrame.evaluate(new Script('globalThis.test = 1'))).toBe(1); expect(page.mainFrame.window['test']).toBe(1); @@ -178,7 +191,8 @@ describe('DetachedBrowserFrame', () => { }); }); - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.newPage(); const oldWindow = page.mainFrame.window; const response = await page.mainFrame.goto('http://localhost:3000', { @@ -197,7 +211,8 @@ describe('DetachedBrowserFrame', () => { }); it('Navigates to a URL with "javascript:" as protocol.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const oldWindow = page.mainFrame.window; const response = await page.mainFrame.goto('javascript:document.write("test");'); @@ -210,7 +225,8 @@ describe('DetachedBrowserFrame', () => { }); it('Navigates to a URL with "about:" as protocol.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); const oldWindow = page.mainFrame.window; const response = await page.mainFrame.goto('about:blank'); @@ -221,7 +237,8 @@ describe('DetachedBrowserFrame', () => { }); it('Aborts request if it times out.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); const oldWindow = page.mainFrame.window; let error: Error | null = null; @@ -260,7 +277,8 @@ describe('DetachedBrowserFrame', () => { }); }); - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); const oldWindow = page.mainFrame.window; const response = await page.mainFrame.goto('http://localhost:3000'); @@ -281,7 +299,8 @@ describe('DetachedBrowserFrame', () => { return Promise.reject(new Error('Error')); }); - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); const oldWindow = page.mainFrame.window; let error: Error | null = null; @@ -303,13 +322,14 @@ describe('DetachedBrowserFrame', () => { return Promise.reject(new Error('Should not be called.')); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); const oldWindow = childFrame.window; @@ -331,13 +351,14 @@ describe('DetachedBrowserFrame', () => { })); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); const oldWindow = childFrame.window; @@ -355,13 +376,14 @@ describe('DetachedBrowserFrame', () => { return Promise.reject(new Error('Should not be called.')); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childPage = browser.newPage(page.mainFrame); const oldWindow = childPage.mainFrame.window; @@ -383,13 +405,14 @@ describe('DetachedBrowserFrame', () => { })); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childPage = browser.newPage(page.mainFrame); const oldWindow = childPage.mainFrame.window; @@ -407,13 +430,14 @@ describe('DetachedBrowserFrame', () => { return Promise.reject(new Error('Should not be called.')); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.sameOrigin } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); const oldWindow = page.mainFrame.window; @@ -434,13 +458,14 @@ describe('DetachedBrowserFrame', () => { })); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childPage = browser.newPage(page.mainFrame); const oldWindow = childPage.mainFrame.window; @@ -461,13 +486,14 @@ describe('DetachedBrowserFrame', () => { })); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childPage = browser.newPage(page.mainFrame); const oldWindow = childPage.mainFrame.window; @@ -488,13 +514,14 @@ describe('DetachedBrowserFrame', () => { })); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childPage = browser.newPage(page.mainFrame); const oldWindow = childPage.mainFrame.window; @@ -513,13 +540,14 @@ describe('DetachedBrowserFrame', () => { })); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childPage = browser.newPage(page.mainFrame); const oldWindow = childPage.mainFrame.window; @@ -537,13 +565,14 @@ describe('DetachedBrowserFrame', () => { return Promise.reject(new Error('Should not be called.')); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { crossOriginPolicy: BrowserNavigationCrossOriginPolicyEnum.strictOrigin } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); const oldWindow = page.mainFrame.window; @@ -564,7 +593,7 @@ describe('DetachedBrowserFrame', () => { })); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { disableChildFrameNavigation: false, @@ -572,6 +601,7 @@ describe('DetachedBrowserFrame', () => { } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); const oldWindow = childFrame.window; @@ -587,7 +617,7 @@ describe('DetachedBrowserFrame', () => { return Promise.reject(new Error('Should not be called.')); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { disableChildFrameNavigation: true, @@ -595,6 +625,7 @@ describe('DetachedBrowserFrame', () => { } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); const oldWindow = childFrame.window; @@ -614,7 +645,7 @@ describe('DetachedBrowserFrame', () => { })); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { disableChildPageNavigation: false, @@ -622,6 +653,7 @@ describe('DetachedBrowserFrame', () => { } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childPage = browser.newPage(page.mainFrame); const oldWindow = childPage.mainFrame.window; @@ -637,7 +669,7 @@ describe('DetachedBrowserFrame', () => { return Promise.reject(new Error('Should not be called.')); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { disableChildPageNavigation: true, @@ -645,6 +677,7 @@ describe('DetachedBrowserFrame', () => { } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const childPage = browser.newPage(page.mainFrame); const oldWindow = childPage.mainFrame.window; @@ -661,7 +694,7 @@ describe('DetachedBrowserFrame', () => { return Promise.reject(new Error('Should not be called.')); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { disableMainFrameNavigation: true, @@ -669,6 +702,7 @@ describe('DetachedBrowserFrame', () => { } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); const oldWindow = page.mainFrame.window; @@ -685,7 +719,7 @@ describe('DetachedBrowserFrame', () => { }); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { disableMainFrameNavigation: false, @@ -693,6 +727,7 @@ describe('DetachedBrowserFrame', () => { } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); const oldWindow = page.mainFrame.window; @@ -707,13 +742,14 @@ describe('DetachedBrowserFrame', () => { return Promise.reject(new Error('Should not be called.')); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { disableFallbackToSetURL: true } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const oldWindow = page.mainFrame.window; @@ -728,13 +764,14 @@ describe('DetachedBrowserFrame', () => { return Promise.reject(new Error('Should not be called.')); }); - const browser = new DetachedBrowser(Window, new Window(), { + const browser = new DetachedBrowser(BrowserWindow, { settings: { navigation: { disableFallbackToSetURL: false } } }); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.pages[0]; const oldWindow = page.mainFrame.window; diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts index 14399c8ff..b45aeb020 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts @@ -1,6 +1,7 @@ import DetachedBrowser from '../../../src/browser/detached-browser/DetachedBrowser'; import DetachedBrowserFrame from '../../../src/browser/detached-browser/DetachedBrowserFrame'; import Window from '../../../src/window/Window'; +import BrowserWindow from '../../../src/window/BrowserWindow'; import VirtualConsolePrinter from '../../../src/console/VirtualConsolePrinter'; import VirtualConsole from '../../../src/console/VirtualConsole'; import IResponse from '../../../src/fetch/types/IResponse'; @@ -15,7 +16,7 @@ describe('DetachedBrowserPage', () => { describe('get virtualConsolePrinter()', () => { it('Returns the virtual console printer.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); const page = browser.defaultContext.newPage(); expect(page.virtualConsolePrinter).toBeInstanceOf(VirtualConsolePrinter); }); @@ -23,16 +24,17 @@ describe('DetachedBrowserPage', () => { describe('get mainFrame()', () => { it('Returns the mainFrame.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.newPage(); expect(page.mainFrame).toBeInstanceOf(DetachedBrowserFrame); - expect(page.mainFrame.window).toBeInstanceOf(Window); + expect(page.mainFrame.window).toBeInstanceOf(BrowserWindow); }); }); describe('get context()', () => { it('Returns the context.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); const page = browser.defaultContext.newPage(); expect(page.context).toBe(browser.defaultContext); }); @@ -40,7 +42,7 @@ describe('DetachedBrowserPage', () => { describe('get console()', () => { it('Returns a virtual console by default.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); const page = browser.defaultContext.newPage(); expect(page.console).toBeInstanceOf(VirtualConsole); page.console.log('test'); @@ -48,7 +50,7 @@ describe('DetachedBrowserPage', () => { }); it('Returns the browser console if set.', () => { - const browser = new DetachedBrowser(Window, new Window(), { console }); + const browser = new DetachedBrowser(BrowserWindow, { console }); const page = browser.defaultContext.newPage(); expect(page.console).toBe(console); }); @@ -56,7 +58,8 @@ describe('DetachedBrowserPage', () => { describe('get frames()', () => { it('Returns the frames.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.newPage(); const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); @@ -66,7 +69,8 @@ describe('DetachedBrowserPage', () => { describe('get content()', () => { it('Returns the document HTML content.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.newPage(); page.mainFrame.window.document.write('
test
'); expect(page.content).toBe('
test
'); @@ -75,7 +79,8 @@ describe('DetachedBrowserPage', () => { describe('set content()', () => { it('Sets the document HTML content.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.newPage(); page.content = '
test
'; expect(page.mainFrame.window.document.documentElement.outerHTML).toBe( @@ -86,7 +91,8 @@ describe('DetachedBrowserPage', () => { describe('get url()', () => { it('Returns the document URL.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.newPage(); page.mainFrame.url = 'http://localhost:3000'; expect(page.url).toBe('http://localhost:3000/'); @@ -95,7 +101,8 @@ describe('DetachedBrowserPage', () => { describe('set url()', () => { it('Sets the document URL.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.newPage(); page.url = 'http://localhost:3000'; expect(page.mainFrame.window.location.href).toBe('http://localhost:3000/'); @@ -104,7 +111,8 @@ describe('DetachedBrowserPage', () => { describe('close()', () => { it('Closes the page.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.newPage(); const mainFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); @@ -126,7 +134,8 @@ describe('DetachedBrowserPage', () => { describe('whenComplete()', () => { it('Waits for all pages to complete.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); @@ -140,7 +149,8 @@ describe('DetachedBrowserPage', () => { describe('abort()', () => { it('Aborts all ongoing operations.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); @@ -155,7 +165,8 @@ describe('DetachedBrowserPage', () => { describe('evaluate()', () => { it("Evaluates code in the page's context.", () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); let evaluatedCode: string | null = null; vi.spyOn(page.mainFrame, 'evaluate').mockImplementation((code) => { @@ -169,7 +180,8 @@ describe('DetachedBrowserPage', () => { describe('setViewport()', () => { it('Sets the viewport width.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); page.setViewport({ width: 100 }); expect(page.mainFrame.window.innerWidth).toBe(100); @@ -177,7 +189,8 @@ describe('DetachedBrowserPage', () => { }); it('Sets the viewport height.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); page.setViewport({ height: 100 }); expect(page.mainFrame.window.innerHeight).toBe(100); @@ -185,7 +198,8 @@ describe('DetachedBrowserPage', () => { }); it('Sets the viewport width and height.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); page.setViewport({ width: 100, height: 100 }); expect(page.mainFrame.window.innerWidth).toBe(100); @@ -195,16 +209,18 @@ describe('DetachedBrowserPage', () => { }); it('Sets the viewport device scale factor.', () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); - page.setViewport({ deviceScaleFactor: 2 }); + page.setViewport({ devicePixelRatio: 2 }); expect(page.mainFrame.window.devicePixelRatio).toBe(2); }); }); describe('goto()', () => { it('Goes to a page.', async () => { - const browser = new DetachedBrowser(Window, new Window()); + const browser = new DetachedBrowser(BrowserWindow); + browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.newPage(); let usedURL: string | null = null; let usedOptions: IGoToOptions | null = null; diff --git a/packages/happy-dom/test/fetch/ResourceFetch.test.ts b/packages/happy-dom/test/fetch/ResourceFetch.test.ts index aec6cd6a5..1ae4b5907 100644 --- a/packages/happy-dom/test/fetch/ResourceFetch.test.ts +++ b/packages/happy-dom/test/fetch/ResourceFetch.test.ts @@ -1,6 +1,5 @@ import Window from '../../src/window/Window.js'; import IWindow from '../../src/window/IWindow.js'; -import IDocument from '../../src/nodes/document/IDocument.js'; import ResourceFetch from '../../src/fetch/ResourceFetch.js'; import IResponse from '../../src/fetch/types/IResponse.js'; import XMLHttpRequestSyncRequestScriptBuilder from '../../src/xml-http-request/utilities/XMLHttpRequestSyncRequestScriptBuilder.js'; @@ -13,11 +12,9 @@ const URL = 'https://localhost:8080/base/'; describe('ResourceFetch', () => { let window: IWindow; - let document: IDocument; beforeEach(() => { window = new Window({ url: URL }); - document = window.document; }); afterEach(() => { @@ -37,7 +34,7 @@ describe('ResourceFetch', () => { }); }); - const test = await ResourceFetch.fetch(document, 'path/to/script/'); + const test = await ResourceFetch.fetch(window, 'path/to/script/'); expect(fetchedURL).toBe('path/to/script/'); expect(test).toBe('test'); @@ -67,7 +64,6 @@ describe('ResourceFetch', () => { accept: '*/*', referer: 'https://localhost:8080/base/', 'user-agent': window.navigator.userAgent, - cookie: '', host: 'localhost:8080' }, agent: false, @@ -95,7 +91,7 @@ describe('ResourceFetch', () => { } }); - const response = ResourceFetch.fetchSync(document, 'path/to/script/'); + const response = ResourceFetch.fetchSync(window, 'path/to/script/'); expect(response).toBe(expectedResponse); }); @@ -116,7 +112,7 @@ describe('ResourceFetch', () => { } }); expect(() => { - ResourceFetch.fetchSync(document, 'path/to/script/'); + ResourceFetch.fetchSync(window, 'path/to/script/'); }).toThrowError(`Failed to perform request to "${URL}path/to/script/". Status code: 404`); }); }); diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts index d87c51435..7e46c8be3 100644 --- a/packages/happy-dom/test/nodes/document/Document.test.ts +++ b/packages/happy-dom/test/nodes/document/Document.test.ts @@ -37,6 +37,7 @@ import DOMException from '../../../src/exception/DOMException.js'; import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; import IRequestInit from '../../../src/fetch/types/IRequestInit.js'; import IShadowRoot from '../../../src/nodes/shadow-root/IShadowRoot.js'; +import IBrowserWindow from '../../../src/window/IBrowserWindow.js'; /* eslint-disable jsdoc/require-jsdoc */ @@ -1166,21 +1167,21 @@ describe('Document', () => { const jsURL = '/path/to/file.js'; const cssResponse = 'body { background-color: red; }'; const jsResponse = 'globalThis.test = "test";'; - let resourceFetchCSSDocument: IDocument | null = null; + let resourceFetchCSSWindow: IBrowserWindow | null = null; let resourceFetchCSSURL: string | null = null; - let resourceFetchJSDocument: IDocument | null = null; + let resourceFetchJSWindow: IBrowserWindow | null = null; let resourceFetchJSURL: string | null = null; let readyChangeEvent: Event | null = null; vi.spyOn(ResourceFetch, 'fetch').mockImplementation( - async (window: IWindow, url: string) => { + async (window: IBrowserWindow, url: string) => { if (url.endsWith('.css')) { - resourceFetchCSSDocument = document; + resourceFetchCSSWindow = window; resourceFetchCSSURL = url; return cssResponse; } - resourceFetchJSDocument = document; + resourceFetchJSWindow = window; resourceFetchJSURL = url; return jsResponse; } @@ -1204,9 +1205,9 @@ describe('Document', () => { expect(document.readyState).toBe(DocumentReadyStateEnum.interactive); setTimeout(() => { - expect(resourceFetchCSSDocument).toBe(document); + expect(resourceFetchCSSWindow).toBe(window); expect(resourceFetchCSSURL).toBe(cssURL); - expect(resourceFetchJSDocument).toBe(document); + expect(resourceFetchJSWindow).toBe(window); expect(resourceFetchJSURL).toBe(jsURL); expect((readyChangeEvent).target).toBe(document); expect(document.readyState).toBe(DocumentReadyStateEnum.complete); diff --git a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts index 84dd90684..227d930f5 100644 --- a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts +++ b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts @@ -1,4 +1,5 @@ import Window from '../../../src/window/Window.js'; +import BrowserWindow from '../../../src/window/BrowserWindow.js'; import IWindow from '../../../src/window/IWindow.js'; import IDocument from '../../../src/nodes/document/IDocument.js'; import IHTMLIFrameElement from '../../../src/nodes/html-iframe-element/IHTMLIFrameElement.js'; @@ -90,10 +91,8 @@ describe('HTMLIFrameElement', () => { it(`Dispatches an error event if the response of the iframe page has an "x-frame-options" header set to "deny".`, async () => { await new Promise((resolve) => { const responseHTML = 'Test'; - let fetchedURL: string | null = null; - vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { - fetchedURL = url; + vi.spyOn(BrowserWindow.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { return new Promise((resolve) => { setTimeout(() => { resolve(({ @@ -121,10 +120,8 @@ describe('HTMLIFrameElement', () => { it(`Dispatches an error event if the response of the iframe page has an "x-frame-options" header set to "sameorigin" when the origin is different.`, async () => { await new Promise((resolve) => { const responseHTML = 'Test'; - let fetchedURL: string | null = null; - vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { - fetchedURL = url; + vi.spyOn(BrowserWindow.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { return new Promise((resolve) => { setTimeout(() => { resolve(({ @@ -154,7 +151,7 @@ describe('HTMLIFrameElement', () => { const responseHTML = 'Test'; let fetchedURL: string | null = null; - vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { + vi.spyOn(BrowserWindow.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { fetchedURL = url; return new Promise((resolve) => { setTimeout(() => { @@ -188,7 +185,7 @@ describe('HTMLIFrameElement', () => { const responseHTML = 'Test'; let fetchedURL: string | null = null; - vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { + vi.spyOn(BrowserWindow.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { fetchedURL = url; return Promise.resolve(({ text: () => Promise.resolve(responseHTML), @@ -215,10 +212,8 @@ describe('HTMLIFrameElement', () => { it('Returns content window for relative URL.', async () => { await new Promise((resolve) => { const responseHTML = 'Test'; - let fetchedURL: string | null = null; - vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { - fetchedURL = url; + vi.spyOn(BrowserWindow.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { return Promise.resolve(({ text: () => Promise.resolve(responseHTML), ok: true, @@ -236,7 +231,7 @@ describe('HTMLIFrameElement', () => { }); }); - it('Returns content window for without protocol.', async () => { + it('Returns content window for URL without protocol.', async () => { await new Promise((resolve) => { const browser = new Browser(); const page = browser.newPage(); @@ -244,12 +239,10 @@ describe('HTMLIFrameElement', () => { const document = window.document; const element = document.createElement('iframe'); const responseHTML = 'Test'; - let fetchedURL: string | null = null; page.mainFrame.url = 'https://localhost:8080'; - vi.spyOn(Window.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { - fetchedURL = url; + vi.spyOn(BrowserWindow.prototype, 'fetch').mockImplementation((url: IRequestInfo) => { return Promise.resolve(({ text: () => Promise.resolve(responseHTML), ok: true, @@ -281,7 +274,7 @@ describe('HTMLIFrameElement', () => { page.mainFrame.url = documentOrigin; - vi.spyOn(Window.prototype, 'fetch').mockImplementation( + vi.spyOn(BrowserWindow.prototype, 'fetch').mockImplementation( (url: IRequestInfo): Promise => { fetchedURL = url; return new Promise((resolve) => { @@ -336,7 +329,7 @@ describe('HTMLIFrameElement', () => { await new Promise((resolve) => { const error = new Error('Error'); - vi.spyOn(Window.prototype, 'fetch').mockImplementation(() => { + vi.spyOn(BrowserWindow.prototype, 'fetch').mockImplementation(() => { return Promise.resolve(({ text: () => Promise.reject(error), ok: true, diff --git a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts index e2cf47bd2..59247f695 100644 --- a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts +++ b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts @@ -1,5 +1,6 @@ import Window from '../../../src/window/Window.js'; import IWindow from '../../../src/window/IWindow.js'; +import IBrowserWindow from '../../../src/window/IBrowserWindow.js'; import IDocument from '../../../src/nodes/document/IDocument.js'; import IHTMLLinkElement from '../../../src/nodes/html-link-element/IHTMLLinkElement.js'; import ResourceFetch from '../../../src/fetch/ResourceFetch.js'; @@ -80,13 +81,13 @@ describe('HTMLLinkElement', () => { it('Loads and evaluates an external CSS file when the attribute "href" and "rel" is set and the element is connected to DOM.', async () => { const element = document.createElement('link'); const css = 'div { background: red; }'; - let loadedDocument: IDocument | null = null; + let loadedWindow: IBrowserWindow | null = null; let loadedURL: string | null = null; let loadEvent: Event | null = null; vi.spyOn(ResourceFetch, 'fetch').mockImplementation( - async (document: IDocument, url: string) => { - loadedDocument = document; + async (window: IBrowserWindow, url: string) => { + loadedWindow = window; loadedURL = url; return css; } @@ -103,7 +104,7 @@ describe('HTMLLinkElement', () => { await window.happyDOM?.whenComplete(); - expect(loadedDocument).toBe(document); + expect(loadedWindow).toBe(window); expect(loadedURL).toBe('test'); expect(element.sheet.cssRules.length).toBe(1); expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); @@ -137,12 +138,12 @@ describe('HTMLLinkElement', () => { it('Does not load and evaluate external CSS files if the element is not connected to DOM.', () => { const element = document.createElement('link'); const css = 'div { background: red; }'; - let loadedDocument: IDocument | null = null; + let loadedWindow: IBrowserWindow | null = null; let loadedURL: string | null = null; vi.spyOn(ResourceFetch, 'fetch').mockImplementation( - async (document: IDocument, url: string) => { - loadedDocument = document; + async (window: IBrowserWindow, url: string) => { + loadedWindow = window; loadedURL = url; return css; } @@ -151,7 +152,7 @@ describe('HTMLLinkElement', () => { element.rel = 'stylesheet'; element.href = 'test'; - expect(loadedDocument).toBe(null); + expect(loadedWindow).toBe(null); expect(loadedURL).toBe(null); }); }); @@ -161,12 +162,12 @@ describe('HTMLLinkElement', () => { const element = document.createElement('link'); const css = 'div { background: red; }'; let loadEvent: Event | null = null; - let loadedDocument: IDocument | null = null; + let loadedWindow: IBrowserWindow | null = null; let loadedURL: string | null = null; vi.spyOn(ResourceFetch, 'fetch').mockImplementation( - async (document: IDocument, url: string) => { - loadedDocument = document; + async (window: IBrowserWindow, url: string) => { + loadedWindow = window; loadedURL = url; return css; } @@ -182,7 +183,7 @@ describe('HTMLLinkElement', () => { await window.happyDOM?.whenComplete(); - expect(loadedDocument).toBe(document); + expect(loadedWindow).toBe(window); expect(loadedURL).toBe('test'); expect(element.sheet.cssRules.length).toBe(1); expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); @@ -215,12 +216,12 @@ describe('HTMLLinkElement', () => { it('Does not load external CSS file when "href" attribute has been set if the element is not connected to DOM.', () => { const element = document.createElement('link'); const css = 'div { background: red; }'; - let loadedDocument: IDocument | null = null; + let loadedWindow: IBrowserWindow | null = null; let loadedURL: string | null = null; vi.spyOn(ResourceFetch, 'fetch').mockImplementation( - async (document: IDocument, url: string) => { - loadedDocument = document; + async (window: IBrowserWindow, url: string) => { + loadedWindow = window; loadedURL = url; return css; } @@ -229,7 +230,7 @@ describe('HTMLLinkElement', () => { element.rel = 'stylesheet'; element.href = 'test'; - expect(loadedDocument).toBe(null); + expect(loadedWindow).toBe(null); expect(loadedURL).toBe(null); expect(element.sheet).toBe(null); }); diff --git a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts index e4e4259fd..0655ef88f 100644 --- a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts +++ b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts @@ -1,5 +1,4 @@ import Window from '../../../src/window/Window.js'; -import Document from '../../../src/nodes/document/Document.js'; import IHTMLScriptElement from '../../../src/nodes/html-script-element/IHTMLScriptElement.js'; import IDocument from '../../../src/nodes/document/IDocument.js'; import IResponse from '../../../src/fetch/types/IResponse.js'; @@ -9,10 +8,11 @@ import Event from '../../../src/event/Event.js'; import IRequestInfo from '../../../src/fetch/types/IRequestInfo.js'; import ErrorEvent from '../../../src/event/events/ErrorEvent.js'; import IWindow from '../../../src/window/IWindow.js'; +import IBrowserWindow from '../../../src/window/IBrowserWindow.js'; describe('HTMLScriptElement', () => { - let window: Window; - let document: Document; + let window: IWindow; + let document: IDocument; beforeEach(() => { window = new Window(); @@ -223,17 +223,19 @@ describe('HTMLScriptElement', () => { }); it('Loads external script synchronously with relative URL.', async () => { - let fetchedDocument: IDocument | null = null; + let fetchedWindow: IBrowserWindow | null = null; let fetchedURL: string | null = null; let loadEvent: Event | null = null; window.location.href = 'https://localhost:8080/base/'; - vi.spyOn(ResourceFetch, 'fetchSync').mockImplementation((window: IWindow, url: string) => { - fetchedDocument = document; - fetchedURL = url; - return 'globalThis.test = "test";globalThis.currentScript = document.currentScript;'; - }); + vi.spyOn(ResourceFetch, 'fetchSync').mockImplementation( + (window: IBrowserWindow, url: string) => { + fetchedWindow = window; + fetchedURL = url; + return 'globalThis.test = "test";globalThis.currentScript = document.currentScript;'; + } + ); const script = window.document.createElement('script'); script.src = 'path/to/script/'; @@ -244,7 +246,7 @@ describe('HTMLScriptElement', () => { document.body.appendChild(script); expect(((loadEvent)).target).toBe(script); - expect(fetchedDocument).toBe(document); + expect(fetchedWindow).toBe(window); expect(fetchedURL).toBe('path/to/script/'); expect(window['test']).toBe('test'); expect(window['currentScript']).toBe(script); diff --git a/packages/happy-dom/test/window/BrowserWindow.test.ts b/packages/happy-dom/test/window/BrowserWindow.test.ts new file mode 100644 index 000000000..8b1b4d102 --- /dev/null +++ b/packages/happy-dom/test/window/BrowserWindow.test.ts @@ -0,0 +1,1376 @@ +import CSSStyleDeclaration from '../../src/css/declaration/CSSStyleDeclaration.js'; +import IDocument from '../../src/nodes/document/IDocument.js'; +import IHTMLLinkElement from '../../src/nodes/html-link-element/IHTMLLinkElement.js'; +import IHTMLElement from '../../src/nodes/html-element/IHTMLElement.js'; +import ResourceFetch from '../../src/fetch/ResourceFetch.js'; +import IHTMLScriptElement from '../../src/nodes/html-script-element/IHTMLScriptElement.js'; +import IWindow from '../../src/window/IWindow.js'; +import IBrowserWindow from '../../src/window/IBrowserWindow.js'; +import Navigator from '../../src/navigator/Navigator.js'; +import Headers from '../../src/fetch/Headers.js'; +import Selection from '../../src/selection/Selection.js'; +import DOMException from '../../src/exception/DOMException.js'; +import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum.js'; +import CustomElement from '../CustomElement.js'; +import Request from '../../src/fetch/Request.js'; +import Response from '../../src/fetch/Response.js'; +import IRequest from '../../src/fetch/types/IRequest.js'; +import IResponse from '../../src/fetch/types/IResponse.js'; +import Fetch from '../../src/fetch/Fetch.js'; +import MessageEvent from '../../src/event/events/MessageEvent.js'; +import Event from '../../src/event/Event.js'; +import ErrorEvent from '../../src/event/events/ErrorEvent.js'; +import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; +import Permissions from '../../src/permissions/Permissions.js'; +import Clipboard from '../../src/clipboard/Clipboard.js'; +import PackageVersion from '../../src/version.js'; +import IHTMLDialogElement from '../../src/nodes/html-dialog-element/IHTMLDialogElement.js'; +import Browser from '../../src/browser/Browser.js'; +import ICrossOriginBrowserWindow from '../../src/window/ICrossOriginBrowserWindow.js'; +import CrossOriginBrowserWindow from '../../src/window/CrossOriginBrowserWindow.js'; +import BrowserFrameFactory from '../../src/browser/utilities/BrowserFrameFactory.js'; +import IBrowser from '../../src/browser/types/IBrowser.js'; +import IBrowserFrame from '../../src/browser/types/IBrowserFrame.js'; +import BrowserWindow from '../../src/window/BrowserWindow.js'; +import '../types.d.js'; + +const GET_NAVIGATOR_PLATFORM = (): string => { + return ( + 'X11; ' + + process.platform.charAt(0).toUpperCase() + + process.platform.slice(1) + + ' ' + + process.arch + ); +}; + +describe('BrowserWindow', () => { + let browser: IBrowser; + let browserFrame: IBrowserFrame; + let window: IBrowserWindow; + let document: IDocument; + + beforeEach(() => { + browser = new Browser(); + browserFrame = browser.newPage().mainFrame; + window = browserFrame.window; + document = window.document; + window.customElements.define('custom-element', CustomElement); + }); + + afterEach(() => { + resetMockedModules(); + vi.restoreAllMocks(); + }); + describe('get happyDOM()', () => { + it('Returns "undefined" for an attached browser.', () => { + expect(browserFrame.window['happyDOM']).toBeUndefined(); + }); + }); + + describe('get Object()', () => { + it('Is not the same as {}.constructor when inside the VM.', () => { + expect(typeof window.Object).toBe('function'); + expect({}.constructor).not.toBe(window.Object); + }); + + it('Is the same as {}.constructor when using eval().', () => { + expect(window.eval('({}).constructor === window.Object')).toBe(true); + }); + }); + + describe('get Function()', () => { + it('Is not the same as (() => {}).constructorr when inside the VM.', () => { + expect(typeof window.Function).toBe('function'); + expect((() => {}).constructor).not.toBe(window.Function); + }); + + it('Is the same as (() => {}).constructor when using eval().', () => { + expect(window.eval('(() => {}).constructor === window.Function')).toBe(true); + }); + }); + + describe('get Array()', () => { + it('Is not the same as [].constructorr when inside the VM.', () => { + expect(typeof window.Array).toBe('function'); + expect([].constructor).not.toBe(window.Array); + }); + + it('Is the same as [].constructor when using eval().', () => { + expect(window.eval('[].constructor === window.Array')).toBe(true); + }); + }); + + describe('get ArrayBuffer()', () => { + it('Is defined.', () => { + expect(typeof window.ArrayBuffer).toBe('function'); + }); + }); + + describe('get Buffer()', () => { + it('Is defined.', () => { + expect(typeof window.Buffer).toBe('function'); + }); + }); + + describe('get Headers()', () => { + it('Returns Headers class.', () => { + expect(window.Headers).toBe(Headers); + }); + }); + + describe('get Response()', () => { + it('Returns Response class.', () => { + const response = new window.Response(); + expect(response instanceof Response).toBe(true); + }); + }); + + describe('get Request()', () => { + it('Returns Request class.', () => { + const request = new window.Request('https://localhost:8080/test/page/'); + expect(request instanceof Request).toBe(true); + }); + }); + + describe('get performance()', () => { + it('Exposes "performance" from the NodeJS perf_hooks package.', () => { + expect(typeof window.performance.now()).toBe('number'); + }); + }); + + describe('get crypto()', () => { + it('Exposes "crypto" from the NodeJS crypto package.', () => { + const array = new Uint32Array(5); + window.crypto.getRandomValues(array); + expect(array[0]).toBeGreaterThan(0); + expect(array[1]).toBeGreaterThan(0); + expect(array[2]).toBeGreaterThan(0); + expect(array[3]).toBeGreaterThan(0); + expect(array[4]).toBeGreaterThan(0); + }); + }); + + describe('get navigator()', () => { + it('Returns an instance of Navigator with browser data.', () => { + const platform = GET_NAVIGATOR_PLATFORM(); + + expect(window.navigator instanceof Navigator).toBe(true); + + const referenceValues = { + appCodeName: 'Mozilla', + appName: 'Netscape', + appVersion: `5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}`, + cookieEnabled: true, + credentials: null, + doNotTrack: 'unspecified', + geolocation: null, + hardwareConcurrency: 8, + language: 'en-US', + languages: ['en-US', 'en'], + locks: null, + maxTouchPoints: 0, + mimeTypes: { + length: 0 + }, + onLine: true, + permissions: new Permissions(), + clipboard: new Clipboard(window), + platform, + plugins: { + length: 0 + }, + product: 'Gecko', + productSub: '20100101', + userAgent: `Mozilla/5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}`, + vendor: '', + vendorSub: '', + webdriver: true + }; + + for (const propertyKey in referenceValues) { + expect(window.navigator[propertyKey]).toEqual(referenceValues[propertyKey]); + } + }); + }); + + describe('eval()', () => { + it('Respects direct eval.', () => { + const result = window.eval(` + variable = 'globally defined'; + (function () { + var variable = 'locally defined'; + return eval('variable'); + })()`); + expect(result).toBe('locally defined'); + expect(window['variable']).toBe('globally defined'); + }); + + it('Respects indirect eval.', () => { + const result = window.eval(` + variable = 'globally defined'; + (function () { + var variable = 'locally defined'; + return (0,eval)('variable'); + })()`); + expect(result).toBe('globally defined'); + expect(window['variable']).toBe('globally defined'); + }); + + it('Has access to the window and document.', () => { + window.eval(`window.variable = document.characterSet;`); + expect(window['variable']).toBe('UTF-8'); + }); + }); + + describe('getComputedStyle()', () => { + it('Handles default properties "display" and "direction".', () => { + const element = document.createElement('div'); + const computedStyle = window.getComputedStyle(element); + + expect(computedStyle.display).toBe(''); + + window.document.body.appendChild(element); + + expect(computedStyle.direction).toBe('ltr'); + expect(computedStyle.display).toBe('block'); + }); + + it('Handles default properties "display" on a dialog element.', () => { + const element = document.createElement('dialog'); + const computedStyle = window.getComputedStyle(element); + + expect(computedStyle.display).toBe(''); + + window.document.body.appendChild(element); + + expect(computedStyle.display).toBe('none'); + + element.show(); + + expect(computedStyle.display).toBe('block'); + + element.close(); + + expect(computedStyle.display).toBe('none'); + }); + + it('Returns a CSSStyleDeclaration object with computed styles that are live updated whenever the element styles are changed.', () => { + const element = document.createElement('div'); + const computedStyle = window.getComputedStyle(element); + + element.style.color = 'red'; + + expect(computedStyle instanceof CSSStyleDeclaration).toBe(true); + + expect(computedStyle.color).toBe(''); + + window.document.body.appendChild(element); + + expect(computedStyle.color).toBe('red'); + + element.style.color = 'green'; + + expect(computedStyle.color).toBe('green'); + }); + + it('Returns a CSSStyleDeclaration object with computed styles from style sheets.', () => { + 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'); + + browserFrame.page.setViewport({ width: 1024 }); + + parentStyle.innerHTML = ` + div { + font: 12px/1.5 "Helvetica Neue", Helvetica, Arial,sans-serif; + color: red !important; + cursor: pointer; + } + + 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) { + div { + font-size: 14px; + } + } + + @media (max-width: ${768 / 16}rem) { + div { + font-size: 20px; + } + } + `; + + element.className = 'mySpan'; + elementStyle.innerHTML = ` + span { + border: 1px solid #000; + border-radius: 2px; + color: green; + cursor: default; + direction: ltr; + } + `; + + parent.appendChild(elementStyle); + parent.appendChild(element); + + document.body.appendChild(parentStyle); + document.body.appendChild(parent); + + expect(computedStyle.font).toBe('14px / 1.5 "Helvetica Neue", Helvetica, Arial, sans-serif'); + expect(computedStyle.border).toBe('1px solid #000'); + 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.', () => { + const element = document.createElement('span'); + const elementStyle = document.createElement('style'); + const customElement = document.createElement('custom-element'); + const elementComputedStyle = window.getComputedStyle(element); + + elementStyle.innerHTML = ` + span { + color: green; + } + `; + + document.body.appendChild(elementStyle); + document.body.appendChild(element); + document.body.appendChild(customElement); + + const customElementComputedStyle = window.getComputedStyle( + customElement.shadowRoot?.querySelector('span') + ); + + // 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'); + expect(customElementComputedStyle.font).toBe( + '14px "Lucida Grande", Helvetica, Arial, sans-serif' + ); + }); + + it('Returns values defined by a CSS variables.', () => { + 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'); + + browserFrame.page.setViewport({ width: 1024 }); + + parentStyle.innerHTML = ` + html { + font: 14px "Times New Roman"; + } + + div { + --color-variable: #000; + --border-variable: 1px solid var(--color-variable); + --font-variable: 1rem "Tahoma"; + } + `; + + elementStyle.innerHTML = ` + span { + border: var(--border-variable); + font: var(--font-variable); + color: var(--invalid-variable); + } + `; + + parent.appendChild(elementStyle); + parent.appendChild(element); + + document.body.appendChild(parentStyle); + document.body.appendChild(parent); + + expect(computedStyle.border).toBe('1px solid #000'); + 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'); + + browserFrame.page.setViewport({ width: 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 that have not been converted, as it is not supported yet.', () => { + 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'); + + browserFrame.page.setViewport({ width: 1024 }); + + parentStyle.innerHTML = ` + html { + font-size: 62.5%; + } + + div { + font-size: 1.5rem; + } + `; + + elementStyle.innerHTML = ` + span { + width: 80%; + height: 10em; + } + `; + + parent.appendChild(elementStyle); + parent.appendChild(element); + + document.body.appendChild(parentStyle); + document.body.appendChild(parent); + + expect(computedStyle.width).toBe('80%'); + expect(computedStyle.height).toBe('150px'); + }); + + it('Returns a CSSStyleDeclaration object with computed styles containing "rem" and "em" measurement values that has not been converted to pixels if the Happy DOM setting "disableComputedStyleRendering" is set to "true".', () => { + browser.settings.disableComputedStyleRendering = true; + document = window.document; + + 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'); + + 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('10rem'); + expect(computedStyle.height).toBe('10em'); + }); + + 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()', () => { + it('Evaluates code and returns the result.', () => { + const result = <() => number>window.eval('() => 5'); + expect(result()).toBe(5); + }); + }); + + describe('setTimeout()', () => { + it('Sets a timeout.', async () => { + await new Promise((resolve) => { + const timeoutId = window.setTimeout(resolve); + expect(timeoutId.constructor.name).toBe('Timeout'); + }); + }); + + it('Sets a timeout with single argument.', async () => { + await new Promise((resolve) => { + const callbackArgumentOne = 'hello'; + const timeoutId = window.setTimeout( + (message: string) => { + expect(message).toBe(callbackArgumentOne); + resolve(null); + }, + 0, + callbackArgumentOne + ); + expect(timeoutId.constructor.name).toBe('Timeout'); + }); + }); + + it('Sets a timeout with multiple arguments.', async () => { + await new Promise((resolve) => { + const callbackArgumentOne = 'hello'; + const callbackArgumentTwo = 1337; + const timeoutId = window.setTimeout( + (message: string, num: number) => { + expect(message).toBe(callbackArgumentOne); + expect(num).toBe(callbackArgumentTwo); + resolve(null); + }, + 0, + callbackArgumentOne, + callbackArgumentTwo + ); + expect(timeoutId.constructor.name).toBe('Timeout'); + }); + }); + + it('Catches errors thrown in the callback.', async () => { + await new Promise((resolve) => { + let errorEvent: ErrorEvent | null = null; + window.addEventListener('error', (event) => (errorEvent = event)); + window.setTimeout(() => { + throw new window.Error('Test error'); + }); + setTimeout(() => { + expect(((errorEvent)).error).instanceOf(window.Error); + expect(((errorEvent)).error?.message).toBe('Test error'); + expect(((errorEvent)).message).toBe('Test error'); + resolve(null); + }, 2); + }); + }); + + it('Catches async errors thrown in the callback.', async () => { + await new Promise((resolve) => { + let errorEvent: ErrorEvent | null = null; + window.addEventListener('error', (event) => (errorEvent = event)); + window.setTimeout(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + throw new window.Error('Test error'); + }); + setTimeout(() => { + expect(((errorEvent)).error).instanceOf(window.Error); + expect(((errorEvent)).error?.message).toBe('Test error'); + expect(((errorEvent)).message).toBe('Test error'); + resolve(null); + }, 15); + }); + }); + }); + + describe('queueMicrotask()', () => { + it('Queues a microtask.', async () => { + await new Promise((resolve) => { + window.queueMicrotask(() => { + resolve(null); + }); + }); + }); + + it('Makes it possible to cancel an ongoing microtask.', async () => { + await new Promise((resolve) => { + let isCallbackCalled = false; + process.nextTick(() => {}); + window.queueMicrotask(() => { + isCallbackCalled = true; + resolve(null); + }); + browserFrame.abort(); + setTimeout(() => { + expect(isCallbackCalled).toBe(false); + resolve(null); + }); + }); + }); + + it('Catches errors thrown in the callback.', async () => { + await new Promise((resolve) => { + let errorEvent: ErrorEvent | null = null; + window.addEventListener('error', (event) => (errorEvent = event)); + window.queueMicrotask(() => { + throw new window.Error('Test error'); + }); + setTimeout(() => { + expect(((errorEvent)).error).instanceOf(window.Error); + expect(((errorEvent)).error?.message).toBe('Test error'); + expect(((errorEvent)).message).toBe('Test error'); + resolve(null); + }, 2); + }); + }); + + it('Catches async errors thrown in the callback.', async () => { + await new Promise((resolve) => { + let errorEvent: ErrorEvent | null = null; + window.addEventListener('error', (event) => (errorEvent = event)); + window.queueMicrotask(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + throw new window.Error('Test error'); + }); + setTimeout(() => { + expect(((errorEvent)).error).instanceOf(window.Error); + expect(((errorEvent)).error?.message).toBe('Test error'); + expect(((errorEvent)).message).toBe('Test error'); + resolve(null); + }, 10); + }); + }); + }); + + describe('clearTimeout()', () => { + it('Clears a timeout.', () => { + const timeoutId = window.setTimeout(() => { + throw new Error('This timeout should have been canceled.'); + }); + window.clearTimeout(timeoutId); + }); + }); + + describe('setInterval()', () => { + it('Sets an interval.', async () => { + await new Promise((resolve) => { + let count = 0; + const intervalId = window.setInterval(() => { + count++; + if (count > 2) { + clearInterval(intervalId); + resolve(null); + } + }); + }); + }); + + it('Sets an interval with single argument.', async () => { + await new Promise((resolve) => { + const callbackArgumentOne = 'hello'; + let count = 0; + const intervalId = window.setInterval( + (message: string) => { + expect(message).toBe(callbackArgumentOne); + count++; + if (count > 2) { + clearInterval(intervalId); + resolve(null); + } + }, + 0, + callbackArgumentOne + ); + }); + }); + + it('Sets an interval with multiple arguments.', async () => { + await new Promise((resolve) => { + const callbackArgumentOne = 'hello'; + const callbackArgumentTwo = 1337; + let count = 0; + const intervalId = window.setInterval( + (message: string, num: number) => { + expect(message).toBe(callbackArgumentOne); + expect(num).toBe(callbackArgumentTwo); + count++; + if (count > 2) { + clearInterval(intervalId); + resolve(null); + } + }, + 0, + callbackArgumentOne, + callbackArgumentTwo + ); + }); + }); + + it('Catches errors thrown in the callback.', async () => { + await new Promise((resolve) => { + let errorEvent: ErrorEvent | null = null; + window.addEventListener('error', (event) => (errorEvent = event)); + window.setInterval(() => { + throw new window.Error('Test error'); + }); + setTimeout(() => { + expect(((errorEvent)).error).instanceOf(window.Error); + expect(((errorEvent)).error?.message).toBe('Test error'); + expect(((errorEvent)).message).toBe('Test error'); + resolve(null); + }, 2); + }); + }); + + it('Catches async errors thrown in the callback.', async () => { + await new Promise((resolve) => { + let errorEvent: ErrorEvent | null = null; + window.addEventListener('error', (event) => (errorEvent = event)); + window.setInterval(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + throw new window.Error('Test error'); + }); + setTimeout(() => { + expect(((errorEvent)).error).instanceOf(window.Error); + expect(((errorEvent)).error?.message).toBe('Test error'); + expect(((errorEvent)).message).toBe('Test error'); + resolve(null); + }, 10); + }); + }); + }); + + describe('clearInterval()', () => { + it('Clears an interval.', () => { + const intervalId = window.setInterval(() => { + throw new Error('This interval should have been canceled.'); + }); + window.clearInterval(intervalId); + }); + }); + + describe('requestAnimationFrame()', () => { + it('Requests an animation frame.', async () => { + await new Promise((resolve) => { + const timeoutId = window.requestAnimationFrame(resolve); + expect(timeoutId.constructor.name).toBe('Immediate'); + }); + }); + + it('Calls passed callback with current time', async () => { + await new Promise((resolve) => { + window.requestAnimationFrame((now) => { + expect(Math.abs(now - window.performance.now())).toBeLessThan(100); + resolve(null); + }); + }); + }); + + it('Catches errors thrown in the callback.', async () => { + await new Promise((resolve) => { + let errorEvent: ErrorEvent | null = null; + window.addEventListener('error', (event) => (errorEvent = event)); + window.requestAnimationFrame(() => { + throw new window.Error('Test error'); + }); + setTimeout(() => { + expect(((errorEvent)).error).instanceOf(window.Error); + expect(((errorEvent)).error?.message).toBe('Test error'); + expect(((errorEvent)).message).toBe('Test error'); + resolve(null); + }, 2); + }); + }); + + it('Catches async errors thrown in the callback.', async () => { + await new Promise((resolve) => { + let errorEvent: ErrorEvent | null = null; + window.addEventListener('error', (event) => (errorEvent = event)); + window.requestAnimationFrame(() => { + throw new window.Error('Test error'); + }); + setTimeout(() => { + expect(((errorEvent)).error).instanceOf(window.Error); + expect(((errorEvent)).error?.message).toBe('Test error'); + expect(((errorEvent)).message).toBe('Test error'); + resolve(null); + }, 10); + }); + }); + }); + + describe('cancelAnimationFrame()', () => { + it('Cancels an animation frame.', () => { + const timeoutId = window.requestAnimationFrame(() => { + throw new Error('This timeout should have been canceled.'); + }); + window.cancelAnimationFrame(timeoutId); + }); + }); + + describe('matchMedia()', () => { + it('Returns a new MediaQueryList object that can then be used to determine if the document matches the media query string.', () => { + browserFrame.page.setViewport({ width: 1024 }); + + const mediaQueryString = '(max-width: 512px)'; + const mediaQueryList = window.matchMedia(mediaQueryString); + expect(mediaQueryList.matches).toBe(false); + expect(mediaQueryList.media).toBe(mediaQueryString); + expect(mediaQueryList.onchange).toBe(null); + + expect(window.matchMedia('(max-width: 1024px)').matches).toBe(true); + + expect(typeof mediaQueryList.addEventListener).toBe('function'); + expect(typeof mediaQueryList.removeEventListener).toBe('function'); + }); + }); + + describe('fetch()', () => { + it(`Forwards the request to Fetch and calls Fetch.send().`, async () => { + const expectedURL = 'https://localhost:8080/path/'; + const expectedResponse = {}; + const requestInit = { + method: 'PUT', + headers: { + 'test-header': 'test-value' + } + }; + let request: IRequest | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve(expectedResponse); + }); + + const response = await window.fetch(expectedURL, requestInit); + + expect(response).toBe(expectedResponse); + expect(((request)).url).toBe(expectedURL); + expect(((request)).headers.get('test-header')).toBe('test-value'); + }); + }); + + for (const functionName of ['scroll', 'scrollTo']) { + describe(`${functionName}()`, () => { + it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset', () => { + window[functionName](50, 60); + expect(window.document.documentElement.scrollLeft).toBe(50); + expect(window.document.documentElement.scrollTop).toBe(60); + expect(window.pageXOffset).toBe(50); + expect(window.pageYOffset).toBe(60); + expect(window.scrollX).toBe(50); + expect(window.scrollY).toBe(60); + }); + + it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset using object.', () => { + window[functionName]({ left: 50, top: 60 }); + expect(window.document.documentElement.scrollLeft).toBe(50); + expect(window.document.documentElement.scrollTop).toBe(60); + expect(window.pageXOffset).toBe(50); + expect(window.pageYOffset).toBe(60); + expect(window.scrollX).toBe(50); + expect(window.scrollY).toBe(60); + }); + + it('Sets only the property scrollTop, pageYOffset, and scrollY', () => { + window[functionName]({ top: 60 }); + expect(window.document.documentElement.scrollLeft).toBe(0); + expect(window.document.documentElement.scrollTop).toBe(60); + expect(window.pageXOffset).toBe(0); + expect(window.pageYOffset).toBe(60); + expect(window.scrollX).toBe(0); + expect(window.scrollY).toBe(60); + }); + + it('Sets only the property scrollLeft, pageXOffset, and scrollX', () => { + window[functionName]({ left: 60 }); + expect(window.document.documentElement.scrollLeft).toBe(60); + expect(window.document.documentElement.scrollTop).toBe(0); + expect(window.document.documentElement.scrollLeft).toBe(60); + expect(window.document.documentElement.scrollTop).toBe(0); + expect(window.pageXOffset).toBe(60); + expect(window.pageYOffset).toBe(0); + expect(window.scrollX).toBe(60); + expect(window.scrollY).toBe(0); + }); + + it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset with animation.', async () => { + window[functionName]({ left: 50, top: 60, behavior: 'smooth' }); + expect(window.document.documentElement.scrollLeft).toBe(0); + expect(window.document.documentElement.scrollTop).toBe(0); + expect(window.pageXOffset).toBe(0); + expect(window.pageYOffset).toBe(0); + expect(window.scrollX).toBe(0); + expect(window.scrollY).toBe(0); + await browserFrame.whenComplete(); + expect(window.document.documentElement.scrollLeft).toBe(50); + expect(window.document.documentElement.scrollTop).toBe(60); + expect(window.pageXOffset).toBe(50); + expect(window.pageYOffset).toBe(60); + expect(window.scrollX).toBe(50); + expect(window.scrollY).toBe(60); + }); + }); + } + + describe('getSelection()', () => { + it('Returns selection.', () => { + expect(window.getSelection() instanceof Selection).toBe(true); + }); + }); + + describe('addEventListener()', () => { + it('Triggers "load" event if no resources needs to be loaded.', async () => { + await new Promise((resolve) => { + let loadEvent: Event | null = null; + + window.addEventListener('load', (event) => { + loadEvent = event; + }); + + setTimeout(() => { + expect((loadEvent).target).toBe(document); + resolve(null); + }, 10); + }); + }); + + it('Triggers "load" event when all resources have been loaded.', async () => { + await new Promise((resolve) => { + const cssURL = '/path/to/file.css'; + const jsURL = '/path/to/file.js'; + const cssResponse = 'body { background-color: red; }'; + const jsResponse = 'globalThis.test = "test";'; + let resourceFetchCSSWindow: IBrowserWindow | null = null; + let resourceFetchCSSURL: string | null = null; + let resourceFetchJSWindow: IBrowserWindow | null = null; + let resourceFetchJSURL: string | null = null; + let loadEvent: Event | null = null; + + vi.spyOn(ResourceFetch, 'fetch').mockImplementation( + async (window: IBrowserWindow, url: string) => { + if (url.endsWith('.css')) { + resourceFetchCSSWindow = window; + resourceFetchCSSURL = url; + return cssResponse; + } + + resourceFetchJSWindow = window; + resourceFetchJSURL = url; + return jsResponse; + } + ); + + window.addEventListener('load', (event) => { + loadEvent = event; + }); + + const script = document.createElement('script'); + script.async = true; + script.src = jsURL; + + const link = document.createElement('link'); + link.href = cssURL; + link.rel = 'stylesheet'; + + document.body.appendChild(script); + document.body.appendChild(link); + + setTimeout(() => { + expect(resourceFetchCSSWindow === window).toBe(true); + expect(resourceFetchCSSURL).toBe(cssURL); + expect(resourceFetchJSWindow === window).toBe(true); + expect(resourceFetchJSURL).toBe(jsURL); + expect((loadEvent).target).toBe(document); + expect(document.styleSheets.length).toBe(1); + expect(document.styleSheets[0].cssRules[0].cssText).toBe(cssResponse); + + expect(window['test']).toBe('test'); + + resolve(null); + }, 10); + }); + }); + + it('Triggers "error" when an error occurs in the executed code.', async () => { + await new Promise((resolve) => { + const errorEvents: ErrorEvent[] = []; + + window.addEventListener('error', (event) => { + errorEvents.push(event); + }); + + const script = document.createElement('script'); + script.innerText = 'throw new Error("Script error");'; + document.body.appendChild(script); + + window.setTimeout(() => { + throw new Error('Timeout error'); + }); + + setTimeout(() => { + expect(errorEvents.length).toBe(2); + expect(errorEvents[0].target).toBe(window); + expect((errorEvents[0].error).message).toBe('Script error'); + expect(errorEvents[1].target).toBe(window); + expect((errorEvents[1].error).message).toBe('Timeout error'); + + resolve(null); + }, 10); + }); + }); + }); + + describe('atob()', () => { + it('Decode "hello my happy dom!"', () => { + const encoded = 'aGVsbG8gbXkgaGFwcHkgZG9tIQ=='; + const decoded = window.atob(encoded); + expect(decoded).toBe('hello my happy dom!'); + }); + + it('Decode Unicode (throw error)', () => { + expect(() => { + const data = '😄 hello my happy dom! 🐛'; + window.atob(data); + }).toThrowError( + new DOMException( + "Failed to execute 'atob' on 'Window': The string to be decoded contains characters outside of the Latin1 range.", + DOMExceptionNameEnum.invalidCharacterError + ) + ); + }); + + it('Data not in base64list', () => { + expect(() => { + const data = '\x11GVsbG8gbXkgaGFwcHkgZG9tIQ=='; + window.atob(data); + }).toThrowError( + new DOMException( + "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", + DOMExceptionNameEnum.invalidCharacterError + ) + ); + }); + it('Data length not valid', () => { + expect(() => { + const data = 'aGVsbG8gbXkgaGFwcHkgZG9tI'; + window.atob(data); + }).toThrowError( + new DOMException( + "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", + DOMExceptionNameEnum.invalidCharacterError + ) + ); + }); + }); + + describe('btoa()', () => { + it('Encode "hello my happy dom!"', () => { + const data = 'hello my happy dom!'; + const encoded = window.btoa(data); + expect(encoded).toBe('aGVsbG8gbXkgaGFwcHkgZG9tIQ=='); + }); + + it('Encode Unicode (throw error)', () => { + expect(() => { + const data = '😄 hello my happy dom! 🐛'; + window.btoa(data); + }).toThrowError( + new DOMException( + "Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.", + DOMExceptionNameEnum.invalidCharacterError + ) + ); + }); + }); + + describe('postMessage()', () => { + it('Posts a message.', async () => { + await new Promise((resolve) => { + const frame = BrowserFrameFactory.newChildFrame(browserFrame); + + const message = 'test'; + let triggeredEvent: MessageEvent | null = null; + + browserFrame.url = 'https://localhost:8080/test/'; + + frame.url = 'https://localhost:8080/iframe.html'; + frame.window.addEventListener('message', (event) => (triggeredEvent = event)); + frame.window.postMessage(message); + + expect(triggeredEvent).toBe(null); + + setTimeout(() => { + expect((triggeredEvent).data).toBe(message); + expect((triggeredEvent).origin).toBe('https://localhost:8080'); + expect((triggeredEvent).source).toBe(browserFrame.window); + expect((triggeredEvent).lastEventId).toBe(''); + + triggeredEvent = null; + frame.window.postMessage(message, '*'); + expect(triggeredEvent).toBe(null); + + setTimeout(() => { + expect((triggeredEvent).data).toBe(message); + expect((triggeredEvent).origin).toBe('https://localhost:8080'); + expect((triggeredEvent).source).toBe(browserFrame.window); + expect((triggeredEvent).lastEventId).toBe(''); + resolve(null); + }, 10); + }, 10); + }); + }); + + it('Posts a data object as message.', async () => { + await new Promise((resolve) => { + const message = { + test: 'test' + }; + let triggeredEvent: MessageEvent | null = null; + + window.addEventListener('message', (event) => (triggeredEvent = event)); + window.postMessage(message); + + expect(triggeredEvent).toBe(null); + + setTimeout(() => { + expect((triggeredEvent).data).toBe(message); + resolve(null); + }, 10); + }); + }); + + it("Throws an exception if the provided object can't be serialized.", function () { + expect(() => window.postMessage(window)).toThrowError( + new DOMException( + `Failed to execute 'postMessage' on 'Window': The provided message cannot be serialized.`, + DOMExceptionNameEnum.invalidStateError + ) + ); + }); + + it('Throws an exception if the target origin differs from the document origin.', () => { + const message = 'test'; + const targetOrigin = 'https://localhost:8081'; + const documentOrigin = 'https://localhost:8080'; + + browserFrame.url = documentOrigin; + + expect(() => window.postMessage(message, targetOrigin)).toThrowError( + new DOMException( + `Failed to execute 'postMessage' on 'Window': The target origin provided ('${targetOrigin}') does not match the recipient window\'s origin ('${documentOrigin}').`, + DOMExceptionNameEnum.securityError + ) + ); + }); + }); + + describe('open()', () => { + it('Opens a new window without URL.', () => { + const newWindow = window.open(); + expect(newWindow).toBeInstanceOf(BrowserWindow); + expect(newWindow.location.href).toBe('about:blank'); + }); + + it('Opens a URL with Javascript.', async () => { + const newWindow = window.open(`javascript:document.write('Test');`); + expect(newWindow).toBeInstanceOf(BrowserWindow); + expect(newWindow.location.href).toBe('about:blank'); + await new Promise((resolve) => setTimeout(resolve, 1)); + expect(newWindow.document.body.innerHTML).toBe('Test'); + }); + + it('Dispatches error event when the Javascript code is invalid.', async () => { + const newWindow = window.open(`javascript:document.write(test);`); + let errorEvent: ErrorEvent | null = null; + newWindow.addEventListener('error', (event) => (errorEvent = event)); + expect(newWindow).toBeInstanceOf(BrowserWindow); + expect(newWindow.location.href).toBe('about:blank'); + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(String(((errorEvent)).error)).toBe( + 'ReferenceError: test is not defined' + ); + }); + + it('Opens a new window with URL.', async () => { + const html = 'Test'; + let request: IRequest | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + text: () => new Promise((resolve) => setTimeout(() => resolve(html))) + }); + }); + + browserFrame.url = 'https://localhost:8080/test/'; + + const newWindow = window.open('/path/to/file.html'); + expect(newWindow).toBeInstanceOf(BrowserWindow); + expect(newWindow.location.href).toBe('https://localhost:8080/path/to/file.html'); + expect(((request)).url).toBe('https://localhost:8080/path/to/file.html'); + + await new Promise((resolve) => { + newWindow.addEventListener('load', () => { + expect(newWindow.document.body.innerHTML).toBe('Test'); + resolve(null); + }); + }); + }); + + it('Sets width, height, top and left when popup is set as a feature.', () => { + const newWindow = ( + window.open('', '', 'popup=yes,width=100,height=200,top=300,left=400') + ); + expect(newWindow).toBeInstanceOf(BrowserWindow); + expect(newWindow.innerWidth).toBe(100); + expect(newWindow.innerHeight).toBe(200); + expect(newWindow.screenLeft).toBe(400); + expect(newWindow.screenX).toBe(400); + expect(newWindow.screenTop).toBe(300); + expect(newWindow.screenY).toBe(300); + }); + + it(`Doesn't Sets width, height, top and left when popup is set as a feature.`, () => { + const newWindow = window.open('', '', 'width=100,height=200,top=300,left=400'); + expect(newWindow).toBeInstanceOf(BrowserWindow); + expect(newWindow.innerWidth).toBe(1024); + expect(newWindow.innerHeight).toBe(768); + expect(newWindow.screenLeft).toBe(0); + expect(newWindow.screenX).toBe(0); + expect(newWindow.screenTop).toBe(0); + expect(newWindow.screenY).toBe(0); + }); + + it('Sets the target as name on the Window instance.', () => { + const newWindow = window.open('', 'test'); + expect(newWindow).toBeInstanceOf(BrowserWindow); + expect(newWindow.name).toBe('test'); + }); + + it(`Doesn't set opener if "noopener" has been specified as a feature without an URL.`, () => { + const browser = new Browser(); + const page = browser.newPage(); + const newWindow = page.mainFrame.window.open('', '', 'noopener'); + expect(newWindow).toBe(null); + expect(browser.defaultContext.pages[1].mainFrame.window.opener).toBe(null); + }); + + it(`Doesn't set opener if "noopener" has been specified as a feature when opening an URL.`, () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.resolve({ + text: () => Promise.resolve('Test') + }); + }); + + const browser = new Browser(); + const page = browser.newPage(); + page.mainFrame.url = 'https://www.github.com/capricorn86/happy-dom/'; + const newWindow = page.mainFrame.window.open('/test/', '', 'noopener'); + expect(newWindow).toBe(null); + expect(browser.defaultContext.pages[1].mainFrame.window.opener).toBe(null); + }); + + it('Opens a new window with a CORS URL.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + const html = 'Test'; + let request: IRequest | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({ + text: () => new Promise((resolve) => setTimeout(() => resolve(html))) + }); + }); + + page.mainFrame.url = 'https://www.github.com/capricorn86/happy-dom/'; + + const newWindow = ( + page.mainFrame.window.open('https://developer.mozilla.org/en-US/docs/Web/API/Window/open') + ); + + expect(newWindow instanceof CrossOriginBrowserWindow).toBe(true); + expect(browser.defaultContext.pages.length).toBe(2); + expect(browser.defaultContext.pages[0]).toBe(page); + expect(browser.defaultContext.pages[1].mainFrame.window === newWindow).toBe(false); + expect(browser.defaultContext.pages[1].mainFrame.url).toBe( + 'https://developer.mozilla.org/en-US/docs/Web/API/Window/open' + ); + expect(((request)).url).toBe( + 'https://developer.mozilla.org/en-US/docs/Web/API/Window/open' + ); + + await new Promise((resolve) => { + browser.defaultContext.pages[1].mainFrame.window.addEventListener('load', () => { + expect(browser.defaultContext.pages[1].mainFrame.content).toBe( + 'Test' + ); + + newWindow.close(); + + expect(browser.defaultContext.pages.length).toBe(1); + expect(browser.defaultContext.pages[0]).toBe(page); + expect(newWindow.closed).toBe(true); + resolve(null); + }); + }); + }); + + it("Outputs error to the console if the request can't be resolved.", async () => { + const browser = new Browser(); + const page = browser.newPage(); + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + return Promise.reject(new Error('Test error')); + }); + + page.mainFrame.window.open('https://www.github.com/'); + + await new Promise((resolve) => setTimeout(resolve, 1)); + + expect( + browser.defaultContext.pages[1].virtualConsolePrinter + .readAsString() + .startsWith('Error: Test error\n') + ).toBe(true); + }); + }); +}); diff --git a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts index 7ef16b81e..03b37cdb9 100644 --- a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts +++ b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts @@ -234,7 +234,7 @@ describe('DetachedWindowAPI', () => { }); it('Sets the viewport device scale factor.', () => { - window.happyDOM?.setViewport({ deviceScaleFactor: 2 }); + window.happyDOM?.setViewport({ devicePixelRatio: 2 }); expect(window.devicePixelRatio).toBe(2); }); }); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index dfe32fe57..7bd3b8187 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -1,39 +1,17 @@ -import CSSStyleDeclaration from '../../src/css/declaration/CSSStyleDeclaration.js'; import IDocument from '../../src/nodes/document/IDocument.js'; -import IHTMLLinkElement from '../../src/nodes/html-link-element/IHTMLLinkElement.js'; -import IHTMLElement from '../../src/nodes/html-element/IHTMLElement.js'; -import ResourceFetch from '../../src/fetch/ResourceFetch.js'; -import IHTMLScriptElement from '../../src/nodes/html-script-element/IHTMLScriptElement.js'; import Window from '../../src/window/Window.js'; import IWindow from '../../src/window/IWindow.js'; -import Navigator from '../../src/navigator/Navigator.js'; import Headers from '../../src/fetch/Headers.js'; -import Selection from '../../src/selection/Selection.js'; -import DOMException from '../../src/exception/DOMException.js'; -import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum.js'; import CustomElement from '../../test/CustomElement.js'; -import Request from '../../src/fetch/Request.js'; -import Response from '../../src/fetch/Response.js'; -import IRequest from '../../src/fetch/types/IRequest.js'; import IResponse from '../../src/fetch/types/IResponse.js'; import Fetch from '../../src/fetch/Fetch.js'; -import MessageEvent from '../../src/event/events/MessageEvent.js'; -import Event from '../../src/event/Event.js'; -import ErrorEvent from '../../src/event/events/ErrorEvent.js'; -import '../types.d.js'; import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; import VirtualConsole from '../../src/console/VirtualConsole.js'; import VirtualConsolePrinter from '../../src/console/VirtualConsolePrinter.js'; -import Permissions from '../../src/permissions/Permissions.js'; -import Clipboard from '../../src/clipboard/Clipboard.js'; import PackageVersion from '../../src/version.js'; -import IHTMLDialogElement from '../../src/nodes/html-dialog-element/IHTMLDialogElement.js'; -import Browser from '../../src/browser/Browser.js'; -import ICrossOriginBrowserWindow from '../../src/window/ICrossOriginBrowserWindow.js'; -import CrossOriginBrowserWindow from '../../src/window/CrossOriginBrowserWindow.js'; import IHTMLIFrameElement from '../../src/nodes/html-iframe-element/IHTMLIFrameElement.js'; import DetachedWindowAPI from '../../src/window/DetachedWindowAPI.js'; -import BrowserFrameFactory from '../../src/browser/utilities/BrowserFrameFactory.js'; +import '../types.d.js'; const GET_NAVIGATOR_PLATFORM = (): string => { return ( @@ -62,56 +40,112 @@ describe('Window', () => { describe('constructor()', () => { it('Is able to handle multiple instances of Window', () => { - const firstWindow = new Window({ url: 'https://localhost:8080' }); - const secondWindow = new Window({ url: 'https://localhost:8080' }); - const thirdWindow = new Window({ url: 'https://localhost:8080' }); + const window1 = new Window({ url: 'https://localhost:1' }); + const window2 = new Window({ url: 'https://localhost:2' }); + const window3 = new Window({ url: 'https://localhost:3' }); + + // Request + const request1 = new window1.Request('test1'); + const request2 = new window2.Request('test2'); + const request3 = new window3.Request('test3'); + + expect(request1.url).toBe('https://localhost:1/test1'); + expect(request2.url).toBe('https://localhost:2/test2'); + expect(request3.url).toBe('https://localhost:3/test3'); + + // FileReader + let setTimeoutCalls = 0; + window1.setTimeout = () => (setTimeoutCalls++); + window2.setTimeout = () => (setTimeoutCalls++); + window3.setTimeout = () => (setTimeoutCalls++); + + const fileReader1 = new window1.FileReader(); + const fileReader2 = new window2.FileReader(); + const fileReader3 = new window3.FileReader(); + + fileReader1.readAsText(new window1.Blob(['test1'])); + fileReader2.readAsText(new window1.Blob(['test1'])); + fileReader3.readAsText(new window1.Blob(['test1'])); + + expect(setTimeoutCalls).toBe(3); + + // DOMParser + const domParser1 = new window1.DOMParser(); + const domParser2 = new window2.DOMParser(); + const domParser3 = new window3.DOMParser(); + + expect( + domParser1.parseFromString('', 'text/html').childNodes[0].ownerDocument === + window1.document + ).toBe(true); + expect( + domParser2.parseFromString('', 'text/html').childNodes[0].ownerDocument === + window2.document + ).toBe(true); + expect( + domParser3.parseFromString('', 'text/html').childNodes[0].ownerDocument === + window3.document + ).toBe(true); + + // Range + const range1 = window1.document.createRange(); + const range2 = window2.document.createRange(); + const range3 = window3.document.createRange(); + + expect(range1._ownerDocument === window1.document).toBe(true); + expect(range2._ownerDocument === window2.document).toBe(true); + expect(range3._ownerDocument === window3.document).toBe(true); + + // Image + const image1 = new window1.Image(); + const image2 = new window2.Image(); + const image3 = new window3.Image(); - for (const className of [ - 'Request', - 'FileReader', - 'DOMParser', - 'Range', - 'Image', - 'Audio', - 'DocumentFragment' - ]) { - const input = className === 'Request' ? 'test' : undefined; - const thirdInstance = new thirdWindow[className](input); - const firstInstance = new firstWindow[className](input); - const secondInstance = new secondWindow[className](input); - const property = - className === 'Image' || className === 'Audio' || className === 'DocumentFragment' - ? 'ownerDocument' - : '_ownerDocument'; + expect(image1.ownerDocument === window1.document).toBe(true); + expect(image2.ownerDocument === window2.document).toBe(true); + expect(image3.ownerDocument === window3.document).toBe(true); - expect(firstInstance[property] === firstWindow.document).toBe(true); - expect(secondInstance[property] === secondWindow.document).toBe(true); - expect(thirdInstance[property] === thirdWindow.document).toBe(true); - } + // Audio + const audio1 = new window1.Audio(); + const audio2 = new window2.Audio(); + const audio3 = new window3.Audio(); - const thirdElement = thirdWindow.document.createElement('div'); - const firstElement = firstWindow.document.createElement('div'); - const secondElement = secondWindow.document.createElement('div'); + expect(audio1.ownerDocument === window1.document).toBe(true); + expect(audio2.ownerDocument === window2.document).toBe(true); + expect(audio3.ownerDocument === window3.document).toBe(true); - expect(firstElement.ownerDocument === firstWindow.document).toBe(true); - expect(secondElement.ownerDocument === secondWindow.document).toBe(true); - expect(thirdElement.ownerDocument === thirdWindow.document).toBe(true); + // DocumentFragment + const documentFragment1 = new window1.DocumentFragment(); + const documentFragment2 = new window2.DocumentFragment(); + const documentFragment3 = new window3.DocumentFragment(); - const thirdText = thirdWindow.document.createTextNode('Test'); - const firstText = firstWindow.document.createTextNode('Test'); - const secondText = secondWindow.document.createTextNode('Test'); + expect(documentFragment1.ownerDocument === window1.document).toBe(true); + expect(documentFragment2.ownerDocument === window2.document).toBe(true); + expect(documentFragment3.ownerDocument === window3.document).toBe(true); - expect(firstText.ownerDocument === firstWindow.document).toBe(true); - expect(secondText.ownerDocument === secondWindow.document).toBe(true); - expect(thirdText.ownerDocument === thirdWindow.document).toBe(true); + const element1 = window1.document.createElement('div'); + const element2 = window2.document.createElement('div'); + const element3 = window3.document.createElement('div'); - const thirdComment = thirdWindow.document.createComment('Test'); - const firstComment = firstWindow.document.createComment('Test'); - const secondComment = secondWindow.document.createComment('Test'); + expect(element1.ownerDocument === window1.document).toBe(true); + expect(element2.ownerDocument === window2.document).toBe(true); + expect(element3.ownerDocument === window3.document).toBe(true); - expect(firstComment.ownerDocument === firstWindow.document).toBe(true); - expect(secondComment.ownerDocument === secondWindow.document).toBe(true); - expect(thirdComment.ownerDocument === thirdWindow.document).toBe(true); + const text1 = window1.document.createTextNode('Test'); + const text2 = window2.document.createTextNode('Test'); + const text3 = window3.document.createTextNode('Test'); + + expect(text1.ownerDocument === window1.document).toBe(true); + expect(text2.ownerDocument === window2.document).toBe(true); + expect(text3.ownerDocument === window3.document).toBe(true); + + const comment1 = window1.document.createComment('Test'); + const comment2 = window2.document.createComment('Test'); + const comment3 = window3.document.createComment('Test'); + + expect(comment1.ownerDocument === window1.document).toBe(true); + expect(comment2.ownerDocument === window2.document).toBe(true); + expect(comment3.ownerDocument === window3.document).toBe(true); }); it('Initializes by using given options.', () => { @@ -193,12 +227,6 @@ describe('Window', () => { expect(window.happyDOM).toBeInstanceOf(DetachedWindowAPI); }); - it('Returns "undefined" if the Window is not detached.', () => { - const browser = new Browser(); - const page = browser.newPage(); - expect(page.mainFrame.window.happyDOM).toBeUndefined(); - }); - it('Returns "undefined" when navigating an iframe for security reasons. The page loaded can potentially contain malicious code.', async () => { await new Promise((resolve) => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { @@ -243,1260 +271,7 @@ describe('Window', () => { }); }); - describe('get Object()', () => { - it('Is not the same as {}.constructor when inside the VM.', () => { - expect(typeof window.Object).toBe('function'); - expect({}.constructor).not.toBe(window.Object); - }); - - it('Is the same as {}.constructor when using eval().', () => { - expect(window.eval('({}).constructor === window.Object')).toBe(true); - }); - }); - - describe('get Function()', () => { - it('Is not the same as (() => {}).constructorr when inside the VM.', () => { - expect(typeof window.Function).toBe('function'); - expect((() => {}).constructor).not.toBe(window.Function); - }); - - it('Is the same as (() => {}).constructor when using eval().', () => { - expect(window.eval('(() => {}).constructor === window.Function')).toBe(true); - }); - }); - - describe('get Array()', () => { - it('Is not the same as [].constructorr when inside the VM.', () => { - expect(typeof window.Array).toBe('function'); - expect([].constructor).not.toBe(window.Array); - }); - - it('Is the same as [].constructor when using eval().', () => { - expect(window.eval('[].constructor === window.Array')).toBe(true); - }); - }); - - describe('get ArrayBuffer()', () => { - it('Is defined.', () => { - expect(typeof window.ArrayBuffer).toBe('function'); - }); - }); - - describe('get Buffer()', () => { - it('Is defined.', () => { - expect(typeof window.Buffer).toBe('function'); - }); - }); - - describe('get Headers()', () => { - it('Returns Headers class.', () => { - expect(window.Headers).toBe(Headers); - }); - }); - - describe('get Response()', () => { - it('Returns Response class.', () => { - const browser = new Browser(); - const page = browser.newPage(); - const response = new page.mainFrame.window.Response(); - expect(response instanceof Response).toBe(true); - expect(response['_asyncTaskManager']).toBe(page.mainFrame._asyncTaskManager); - }); - }); - - describe('get Request()', () => { - it('Returns Request class.', () => { - const browser = new Browser(); - const page = browser.newPage(); - const request = new page.mainFrame.window.Request('https://localhost:8080/test/page/'); - expect(request instanceof Request).toBe(true); - expect(request['_asyncTaskManager']).toBe(page.mainFrame._asyncTaskManager); - expect(request['_ownerDocument']).toBe(page.mainFrame.window.document); - }); - }); - - describe('get performance()', () => { - it('Exposes "performance" from the NodeJS perf_hooks package.', () => { - expect(typeof window.performance.now()).toBe('number'); - }); - }); - - describe('get crypto()', () => { - it('Exposes "crypto" from the NodeJS crypto package.', () => { - const array = new Uint32Array(5); - window.crypto.getRandomValues(array); - expect(array[0]).toBeGreaterThan(0); - expect(array[1]).toBeGreaterThan(0); - expect(array[2]).toBeGreaterThan(0); - expect(array[3]).toBeGreaterThan(0); - expect(array[4]).toBeGreaterThan(0); - }); - }); - - describe('get navigator()', () => { - it('Returns an instance of Navigator with browser data.', () => { - const platform = GET_NAVIGATOR_PLATFORM(); - - expect(window.navigator instanceof Navigator).toBe(true); - - const referenceValues = { - appCodeName: 'Mozilla', - appName: 'Netscape', - appVersion: `5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}`, - cookieEnabled: true, - credentials: null, - doNotTrack: 'unspecified', - geolocation: null, - hardwareConcurrency: 8, - language: 'en-US', - languages: ['en-US', 'en'], - locks: null, - maxTouchPoints: 0, - mimeTypes: { - length: 0 - }, - onLine: true, - permissions: new Permissions(), - clipboard: new Clipboard(window), - platform, - plugins: { - length: 0 - }, - product: 'Gecko', - productSub: '20100101', - userAgent: `Mozilla/5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}`, - vendor: '', - vendorSub: '', - webdriver: true - }; - - for (const propertyKey in referenceValues) { - expect(window.navigator[propertyKey]).toEqual(referenceValues[propertyKey]); - } - }); - }); - - describe('eval()', () => { - it('Respects direct eval.', () => { - const result = window.eval(` - variable = 'globally defined'; - (function () { - var variable = 'locally defined'; - return eval('variable'); - })()`); - expect(result).toBe('locally defined'); - expect(window['variable']).toBe('globally defined'); - }); - - it('Respects indirect eval.', () => { - const result = window.eval(` - variable = 'globally defined'; - (function () { - var variable = 'locally defined'; - return (0,eval)('variable'); - })()`); - expect(result).toBe('globally defined'); - expect(window['variable']).toBe('globally defined'); - }); - - it('Has access to the window and document.', () => { - window.eval(`window.variable = document.characterSet;`); - expect(window['variable']).toBe('UTF-8'); - }); - }); - - describe('getComputedStyle()', () => { - it('Handles default properties "display" and "direction".', () => { - const element = document.createElement('div'); - const computedStyle = window.getComputedStyle(element); - - expect(computedStyle.display).toBe(''); - - window.document.body.appendChild(element); - - expect(computedStyle.direction).toBe('ltr'); - expect(computedStyle.display).toBe('block'); - }); - - it('Handles default properties "display" on a dialog element.', () => { - const element = document.createElement('dialog'); - const computedStyle = window.getComputedStyle(element); - - expect(computedStyle.display).toBe(''); - - window.document.body.appendChild(element); - - expect(computedStyle.display).toBe('none'); - - element.show(); - - expect(computedStyle.display).toBe('block'); - - element.close(); - - expect(computedStyle.display).toBe('none'); - }); - - it('Returns a CSSStyleDeclaration object with computed styles that are live updated whenever the element styles are changed.', () => { - const element = document.createElement('div'); - const computedStyle = window.getComputedStyle(element); - - element.style.color = 'red'; - - expect(computedStyle instanceof CSSStyleDeclaration).toBe(true); - - expect(computedStyle.color).toBe(''); - - window.document.body.appendChild(element); - - expect(computedStyle.color).toBe('red'); - - element.style.color = 'green'; - - expect(computedStyle.color).toBe('green'); - }); - - it('Returns a CSSStyleDeclaration object with computed styles from style sheets.', () => { - 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?.setViewport({ width: 1024 }); - - parentStyle.innerHTML = ` - div { - font: 12px/1.5 "Helvetica Neue", Helvetica, Arial,sans-serif; - color: red !important; - cursor: pointer; - } - - 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) { - div { - font-size: 14px; - } - } - - @media (max-width: ${768 / 16}rem) { - div { - font-size: 20px; - } - } - `; - - element.className = 'mySpan'; - elementStyle.innerHTML = ` - span { - border: 1px solid #000; - border-radius: 2px; - color: green; - cursor: default; - direction: ltr; - } - `; - - parent.appendChild(elementStyle); - parent.appendChild(element); - - document.body.appendChild(parentStyle); - document.body.appendChild(parent); - - expect(computedStyle.font).toBe('14px / 1.5 "Helvetica Neue", Helvetica, Arial, sans-serif'); - expect(computedStyle.border).toBe('1px solid #000'); - 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.', () => { - const element = document.createElement('span'); - const elementStyle = document.createElement('style'); - const customElement = document.createElement('custom-element'); - const elementComputedStyle = window.getComputedStyle(element); - - elementStyle.innerHTML = ` - span { - color: green; - } - `; - - document.body.appendChild(elementStyle); - document.body.appendChild(element); - document.body.appendChild(customElement); - - const customElementComputedStyle = window.getComputedStyle( - customElement.shadowRoot?.querySelector('span') - ); - - // 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'); - expect(customElementComputedStyle.font).toBe( - '14px "Lucida Grande", Helvetica, Arial, sans-serif' - ); - }); - - it('Returns values defined by a CSS variables.', () => { - 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?.setViewport({ width: 1024 }); - - parentStyle.innerHTML = ` - html { - font: 14px "Times New Roman"; - } - - div { - --color-variable: #000; - --border-variable: 1px solid var(--color-variable); - --font-variable: 1rem "Tahoma"; - } - `; - - elementStyle.innerHTML = ` - span { - border: var(--border-variable); - font: var(--font-variable); - color: var(--invalid-variable); - } - `; - - parent.appendChild(elementStyle); - parent.appendChild(element); - - document.body.appendChild(parentStyle); - document.body.appendChild(parent); - - expect(computedStyle.border).toBe('1px solid #000'); - 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?.setViewport({ width: 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 that have not been converted, as it is not supported yet.', () => { - 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?.setViewport({ width: 1024 }); - - parentStyle.innerHTML = ` - html { - font-size: 62.5%; - } - - div { - font-size: 1.5rem; - } - `; - - elementStyle.innerHTML = ` - span { - width: 80%; - height: 10em; - } - `; - - parent.appendChild(elementStyle); - parent.appendChild(element); - - document.body.appendChild(parentStyle); - document.body.appendChild(parent); - - expect(computedStyle.width).toBe('80%'); - expect(computedStyle.height).toBe('150px'); - }); - - it('Returns a CSSStyleDeclaration object with computed styles containing "rem" and "em" measurement values that has not been converted to pixels if the Happy DOM setting "disableComputedStyleRendering" is set to "true".', () => { - window = new Window({ - width: 1024, - settings: { - disableComputedStyleRendering: true - } - }); - document = window.document; - - 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'); - - 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('10rem'); - expect(computedStyle.height).toBe('10em'); - }); - - 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()', () => { - it('Evaluates code and returns the result.', () => { - const result = <() => number>window.eval('() => 5'); - expect(result()).toBe(5); - }); - }); - - describe('setTimeout()', () => { - it('Sets a timeout.', async () => { - await new Promise((resolve) => { - const timeoutId = window.setTimeout(resolve); - expect(timeoutId.constructor.name).toBe('Timeout'); - }); - }); - - it('Sets a timeout with single argument.', async () => { - await new Promise((resolve) => { - const callbackArgumentOne = 'hello'; - const timeoutId = window.setTimeout( - (message: string) => { - expect(message).toBe(callbackArgumentOne); - resolve(null); - }, - 0, - callbackArgumentOne - ); - expect(timeoutId.constructor.name).toBe('Timeout'); - }); - }); - - it('Sets a timeout with multiple arguments.', async () => { - await new Promise((resolve) => { - const callbackArgumentOne = 'hello'; - const callbackArgumentTwo = 1337; - const timeoutId = window.setTimeout( - (message: string, num: number) => { - expect(message).toBe(callbackArgumentOne); - expect(num).toBe(callbackArgumentTwo); - resolve(null); - }, - 0, - callbackArgumentOne, - callbackArgumentTwo - ); - expect(timeoutId.constructor.name).toBe('Timeout'); - }); - }); - - it('Catches errors thrown in the callback.', async () => { - await new Promise((resolve) => { - let errorEvent: ErrorEvent | null = null; - window.addEventListener('error', (event) => (errorEvent = event)); - window.setTimeout(() => { - throw new window.Error('Test error'); - }); - setTimeout(() => { - expect(((errorEvent)).error).instanceOf(window.Error); - expect(((errorEvent)).error?.message).toBe('Test error'); - expect(((errorEvent)).message).toBe('Test error'); - resolve(null); - }, 2); - }); - }); - - it('Catches async errors thrown in the callback.', async () => { - await new Promise((resolve) => { - let errorEvent: ErrorEvent | null = null; - window.addEventListener('error', (event) => (errorEvent = event)); - window.setTimeout(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - throw new window.Error('Test error'); - }); - setTimeout(() => { - expect(((errorEvent)).error).instanceOf(window.Error); - expect(((errorEvent)).error?.message).toBe('Test error'); - expect(((errorEvent)).message).toBe('Test error'); - resolve(null); - }, 15); - }); - }); - }); - - describe('queueMicrotask()', () => { - it('Queues a microtask.', async () => { - await new Promise((resolve) => { - window.queueMicrotask(() => { - resolve(null); - }); - }); - }); - - it('Makes it possible to cancel an ongoing microtask.', async () => { - await new Promise((resolve) => { - let isCallbackCalled = false; - process.nextTick(() => {}); - window.queueMicrotask(() => { - isCallbackCalled = true; - resolve(null); - }); - window.happyDOM?.abort(); - setTimeout(() => { - expect(isCallbackCalled).toBe(false); - resolve(null); - }); - }); - }); - - it('Catches errors thrown in the callback.', async () => { - await new Promise((resolve) => { - let errorEvent: ErrorEvent | null = null; - window.addEventListener('error', (event) => (errorEvent = event)); - window.queueMicrotask(() => { - throw new window.Error('Test error'); - }); - setTimeout(() => { - expect(((errorEvent)).error).instanceOf(window.Error); - expect(((errorEvent)).error?.message).toBe('Test error'); - expect(((errorEvent)).message).toBe('Test error'); - resolve(null); - }, 2); - }); - }); - - it('Catches async errors thrown in the callback.', async () => { - await new Promise((resolve) => { - let errorEvent: ErrorEvent | null = null; - window.addEventListener('error', (event) => (errorEvent = event)); - window.queueMicrotask(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - throw new window.Error('Test error'); - }); - setTimeout(() => { - expect(((errorEvent)).error).instanceOf(window.Error); - expect(((errorEvent)).error?.message).toBe('Test error'); - expect(((errorEvent)).message).toBe('Test error'); - resolve(null); - }, 10); - }); - }); - }); - - describe('clearTimeout()', () => { - it('Clears a timeout.', () => { - const timeoutId = window.setTimeout(() => { - throw new Error('This timeout should have been canceled.'); - }); - window.clearTimeout(timeoutId); - }); - }); - - describe('setInterval()', () => { - it('Sets an interval.', async () => { - await new Promise((resolve) => { - let count = 0; - const intervalId = window.setInterval(() => { - count++; - if (count > 2) { - clearInterval(intervalId); - resolve(null); - } - }); - }); - }); - - it('Sets an interval with single argument.', async () => { - await new Promise((resolve) => { - const callbackArgumentOne = 'hello'; - let count = 0; - const intervalId = window.setInterval( - (message: string) => { - expect(message).toBe(callbackArgumentOne); - count++; - if (count > 2) { - clearInterval(intervalId); - resolve(null); - } - }, - 0, - callbackArgumentOne - ); - }); - }); - - it('Sets an interval with multiple arguments.', async () => { - await new Promise((resolve) => { - const callbackArgumentOne = 'hello'; - const callbackArgumentTwo = 1337; - let count = 0; - const intervalId = window.setInterval( - (message: string, num: number) => { - expect(message).toBe(callbackArgumentOne); - expect(num).toBe(callbackArgumentTwo); - count++; - if (count > 2) { - clearInterval(intervalId); - resolve(null); - } - }, - 0, - callbackArgumentOne, - callbackArgumentTwo - ); - }); - }); - - it('Catches errors thrown in the callback.', async () => { - await new Promise((resolve) => { - let errorEvent: ErrorEvent | null = null; - window.addEventListener('error', (event) => (errorEvent = event)); - window.setInterval(() => { - throw new window.Error('Test error'); - }); - setTimeout(() => { - expect(((errorEvent)).error).instanceOf(window.Error); - expect(((errorEvent)).error?.message).toBe('Test error'); - expect(((errorEvent)).message).toBe('Test error'); - resolve(null); - }, 2); - }); - }); - - it('Catches async errors thrown in the callback.', async () => { - await new Promise((resolve) => { - let errorEvent: ErrorEvent | null = null; - window.addEventListener('error', (event) => (errorEvent = event)); - window.setInterval(async () => { - await new Promise((resolve) => setTimeout(resolve, 0)); - throw new window.Error('Test error'); - }); - setTimeout(() => { - expect(((errorEvent)).error).instanceOf(window.Error); - expect(((errorEvent)).error?.message).toBe('Test error'); - expect(((errorEvent)).message).toBe('Test error'); - resolve(null); - }, 10); - }); - }); - }); - - describe('clearInterval()', () => { - it('Clears an interval.', () => { - const intervalId = window.setInterval(() => { - throw new Error('This interval should have been canceled.'); - }); - window.clearInterval(intervalId); - }); - }); - - describe('requestAnimationFrame()', () => { - it('Requests an animation frame.', async () => { - await new Promise((resolve) => { - const timeoutId = window.requestAnimationFrame(resolve); - expect(timeoutId.constructor.name).toBe('Immediate'); - }); - }); - - it('Calls passed callback with current time', async () => { - await new Promise((resolve) => { - window.requestAnimationFrame((now) => { - expect(Math.abs(now - window.performance.now())).toBeLessThan(100); - resolve(null); - }); - }); - }); - - it('Catches errors thrown in the callback.', async () => { - await new Promise((resolve) => { - let errorEvent: ErrorEvent | null = null; - window.addEventListener('error', (event) => (errorEvent = event)); - window.requestAnimationFrame(() => { - throw new window.Error('Test error'); - }); - setTimeout(() => { - expect(((errorEvent)).error).instanceOf(window.Error); - expect(((errorEvent)).error?.message).toBe('Test error'); - expect(((errorEvent)).message).toBe('Test error'); - resolve(null); - }, 2); - }); - }); - - it('Catches async errors thrown in the callback.', async () => { - await new Promise((resolve) => { - let errorEvent: ErrorEvent | null = null; - window.addEventListener('error', (event) => (errorEvent = event)); - window.requestAnimationFrame(() => { - throw new window.Error('Test error'); - }); - setTimeout(() => { - expect(((errorEvent)).error).instanceOf(window.Error); - expect(((errorEvent)).error?.message).toBe('Test error'); - expect(((errorEvent)).message).toBe('Test error'); - resolve(null); - }, 10); - }); - }); - }); - - describe('cancelAnimationFrame()', () => { - it('Cancels an animation frame.', () => { - const timeoutId = window.requestAnimationFrame(() => { - throw new Error('This timeout should have been canceled.'); - }); - window.cancelAnimationFrame(timeoutId); - }); - }); - - describe('matchMedia()', () => { - it('Returns a new MediaQueryList object that can then be used to determine if the document matches the media query string.', () => { - window.happyDOM?.setViewport({ width: 1024 }); - - const mediaQueryString = '(max-width: 512px)'; - const mediaQueryList = window.matchMedia(mediaQueryString); - expect(mediaQueryList.matches).toBe(false); - expect(mediaQueryList.media).toBe(mediaQueryString); - expect(mediaQueryList.onchange).toBe(null); - - expect(window.matchMedia('(max-width: 1024px)').matches).toBe(true); - - expect(typeof mediaQueryList.addEventListener).toBe('function'); - expect(typeof mediaQueryList.removeEventListener).toBe('function'); - }); - }); - - describe('fetch()', () => { - it(`Forwards the request to Fetch and calls Fetch.send().`, async () => { - const expectedURL = 'https://localhost:8080/path/'; - const expectedResponse = {}; - const requestInit = { - method: 'PUT', - headers: { - 'test-header': 'test-value' - } - }; - let request: IRequest | null = null; - - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { - request = this.request; - return Promise.resolve(expectedResponse); - }); - - const response = await window.fetch(expectedURL, requestInit); - - expect(response).toBe(expectedResponse); - expect(((request)).url).toBe(expectedURL); - expect(((request)).headers.get('test-header')).toBe('test-value'); - }); - }); - - for (const functionName of ['scroll', 'scrollTo']) { - describe(`${functionName}()`, () => { - it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset', () => { - window[functionName](50, 60); - expect(window.document.documentElement.scrollLeft).toBe(50); - expect(window.document.documentElement.scrollTop).toBe(60); - expect(window.pageXOffset).toBe(50); - expect(window.pageYOffset).toBe(60); - expect(window.scrollX).toBe(50); - expect(window.scrollY).toBe(60); - }); - - it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset using object.', () => { - window[functionName]({ left: 50, top: 60 }); - expect(window.document.documentElement.scrollLeft).toBe(50); - expect(window.document.documentElement.scrollTop).toBe(60); - expect(window.pageXOffset).toBe(50); - expect(window.pageYOffset).toBe(60); - expect(window.scrollX).toBe(50); - expect(window.scrollY).toBe(60); - }); - - it('Sets only the property scrollTop, pageYOffset, and scrollY', () => { - window[functionName]({ top: 60 }); - expect(window.document.documentElement.scrollLeft).toBe(0); - expect(window.document.documentElement.scrollTop).toBe(60); - expect(window.pageXOffset).toBe(0); - expect(window.pageYOffset).toBe(60); - expect(window.scrollX).toBe(0); - expect(window.scrollY).toBe(60); - }); - - it('Sets only the property scrollLeft, pageXOffset, and scrollX', () => { - window[functionName]({ left: 60 }); - expect(window.document.documentElement.scrollLeft).toBe(60); - expect(window.document.documentElement.scrollTop).toBe(0); - expect(window.document.documentElement.scrollLeft).toBe(60); - expect(window.document.documentElement.scrollTop).toBe(0); - expect(window.pageXOffset).toBe(60); - expect(window.pageYOffset).toBe(0); - expect(window.scrollX).toBe(60); - expect(window.scrollY).toBe(0); - }); - - it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset with animation.', async () => { - window[functionName]({ left: 50, top: 60, behavior: 'smooth' }); - expect(window.document.documentElement.scrollLeft).toBe(0); - expect(window.document.documentElement.scrollTop).toBe(0); - expect(window.pageXOffset).toBe(0); - expect(window.pageYOffset).toBe(0); - expect(window.scrollX).toBe(0); - expect(window.scrollY).toBe(0); - await window.happyDOM?.whenComplete(); - expect(window.document.documentElement.scrollLeft).toBe(50); - expect(window.document.documentElement.scrollTop).toBe(60); - expect(window.pageXOffset).toBe(50); - expect(window.pageYOffset).toBe(60); - expect(window.scrollX).toBe(50); - expect(window.scrollY).toBe(60); - }); - }); - } - - describe('getSelection()', () => { - it('Returns selection.', () => { - expect(window.getSelection() instanceof Selection).toBe(true); - }); - }); - - describe('addEventListener()', () => { - it('Triggers "load" event if no resources needs to be loaded.', async () => { - await new Promise((resolve) => { - let loadEvent: Event | null = null; - - window.addEventListener('load', (event) => { - loadEvent = event; - }); - - setTimeout(() => { - expect((loadEvent).target).toBe(document); - resolve(null); - }, 10); - }); - }); - - it('Triggers "load" event when all resources have been loaded.', async () => { - await new Promise((resolve) => { - const cssURL = '/path/to/file.css'; - const jsURL = '/path/to/file.js'; - const cssResponse = 'body { background-color: red; }'; - const jsResponse = 'globalThis.test = "test";'; - let resourceFetchCSSDocument: IDocument | null = null; - let resourceFetchCSSURL: string | null = null; - let resourceFetchJSDocument: IDocument | null = null; - let resourceFetchJSURL: string | null = null; - let loadEvent: Event | null = null; - - vi.spyOn(ResourceFetch, 'fetch').mockImplementation( - async (document: IDocument, url: string) => { - if (url.endsWith('.css')) { - resourceFetchCSSDocument = document; - resourceFetchCSSURL = url; - return cssResponse; - } - - resourceFetchJSDocument = document; - resourceFetchJSURL = url; - return jsResponse; - } - ); - - window.addEventListener('load', (event) => { - loadEvent = event; - }); - - const script = document.createElement('script'); - script.async = true; - script.src = jsURL; - - const link = document.createElement('link'); - link.href = cssURL; - link.rel = 'stylesheet'; - - document.body.appendChild(script); - document.body.appendChild(link); - - setTimeout(() => { - expect(resourceFetchCSSDocument).toBe(document); - expect(resourceFetchCSSURL).toBe(cssURL); - expect(resourceFetchJSDocument).toBe(document); - expect(resourceFetchJSURL).toBe(jsURL); - expect((loadEvent).target).toBe(document); - expect(document.styleSheets.length).toBe(1); - expect(document.styleSheets[0].cssRules[0].cssText).toBe(cssResponse); - - expect(window['test']).toBe('test'); - - resolve(null); - }, 0); - }); - }); - - it('Triggers "error" when an error occurs in the executed code.', async () => { - await new Promise((resolve) => { - const errorEvents: ErrorEvent[] = []; - - window.addEventListener('error', (event) => { - errorEvents.push(event); - }); - - const script = document.createElement('script'); - script.innerText = 'throw new Error("Script error");'; - document.body.appendChild(script); - - window.setTimeout(() => { - throw new Error('Timeout error'); - }); - - setTimeout(() => { - expect(errorEvents.length).toBe(2); - expect(errorEvents[0].target).toBe(window); - expect((errorEvents[0].error).message).toBe('Script error'); - expect(errorEvents[1].target).toBe(window); - expect((errorEvents[1].error).message).toBe('Timeout error'); - - resolve(null); - }, 10); - }); - }); - }); - - describe('atob()', () => { - it('Decode "hello my happy dom!"', () => { - const encoded = 'aGVsbG8gbXkgaGFwcHkgZG9tIQ=='; - const decoded = window.atob(encoded); - expect(decoded).toBe('hello my happy dom!'); - }); - - it('Decode Unicode (throw error)', () => { - expect(() => { - const data = '😄 hello my happy dom! 🐛'; - window.atob(data); - }).toThrowError( - new DOMException( - "Failed to execute 'atob' on 'Window': The string to be decoded contains characters outside of the Latin1 range.", - DOMExceptionNameEnum.invalidCharacterError - ) - ); - }); - - it('Data not in base64list', () => { - expect(() => { - const data = '\x11GVsbG8gbXkgaGFwcHkgZG9tIQ=='; - window.atob(data); - }).toThrowError( - new DOMException( - "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", - DOMExceptionNameEnum.invalidCharacterError - ) - ); - }); - it('Data length not valid', () => { - expect(() => { - const data = 'aGVsbG8gbXkgaGFwcHkgZG9tI'; - window.atob(data); - }).toThrowError( - new DOMException( - "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", - DOMExceptionNameEnum.invalidCharacterError - ) - ); - }); - }); - - describe('btoa()', () => { - it('Encode "hello my happy dom!"', () => { - const data = 'hello my happy dom!'; - const encoded = window.btoa(data); - expect(encoded).toBe('aGVsbG8gbXkgaGFwcHkgZG9tIQ=='); - }); - - it('Encode Unicode (throw error)', () => { - expect(() => { - const data = '😄 hello my happy dom! 🐛'; - window.btoa(data); - }).toThrowError( - new DOMException( - "Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.", - DOMExceptionNameEnum.invalidCharacterError - ) - ); - }); - }); - - describe('postMessage()', () => { - it('Posts a message.', async () => { - await new Promise((resolve) => { - const browser = new Browser(); - const page = browser.newPage(); - const frame = BrowserFrameFactory.newChildFrame(page.mainFrame); - - const message = 'test'; - let triggeredEvent: MessageEvent | null = null; - - page.mainFrame.url = 'https://localhost:8080/test/'; - - frame.url = 'https://localhost:8080/iframe.html'; - frame.window.addEventListener('message', (event) => (triggeredEvent = event)); - frame.window.postMessage(message); - - expect(triggeredEvent).toBe(null); - - setTimeout(() => { - expect((triggeredEvent).data).toBe(message); - expect((triggeredEvent).origin).toBe('https://localhost:8080'); - expect((triggeredEvent).source).toBe(page.mainFrame.window); - expect((triggeredEvent).lastEventId).toBe(''); - - triggeredEvent = null; - frame.window.postMessage(message, '*'); - expect(triggeredEvent).toBe(null); - - setTimeout(() => { - expect((triggeredEvent).data).toBe(message); - expect((triggeredEvent).origin).toBe('https://localhost:8080'); - expect((triggeredEvent).source).toBe(page.mainFrame.window); - expect((triggeredEvent).lastEventId).toBe(''); - resolve(null); - }, 10); - }, 10); - }); - }); - - it('Posts a data object as message.', async () => { - await new Promise((resolve) => { - const message = { - test: 'test' - }; - let triggeredEvent: MessageEvent | null = null; - - window.addEventListener('message', (event) => (triggeredEvent = event)); - window.postMessage(message); - - expect(triggeredEvent).toBe(null); - - setTimeout(() => { - expect((triggeredEvent).data).toBe(message); - resolve(null); - }, 10); - }); - }); - - it("Throws an exception if the provided object can't be serialized.", function () { - expect(() => window.postMessage(window)).toThrowError( - new DOMException( - `Failed to execute 'postMessage' on 'Window': The provided message cannot be serialized.`, - DOMExceptionNameEnum.invalidStateError - ) - ); - }); - - it('Throws an exception if the target origin differs from the document origin.', () => { - const message = 'test'; - const targetOrigin = 'https://localhost:8081'; - const documentOrigin = 'https://localhost:8080'; - - window.happyDOM?.setURL(documentOrigin); - - expect(() => window.postMessage(message, targetOrigin)).toThrowError( - new DOMException( - `Failed to execute 'postMessage' on 'Window': The target origin provided ('${targetOrigin}') does not match the recipient window\'s origin ('${documentOrigin}').`, - DOMExceptionNameEnum.securityError - ) - ); - }); - }); - describe('open()', () => { - it('Opens a new window without URL.', () => { - const newWindow = window.open(); - expect(newWindow).toBeInstanceOf(Window); - expect(newWindow.location.href).toBe('about:blank'); - }); - - it('Opens a URL with Javascript.', async () => { - const newWindow = window.open(`javascript:document.write('Test');`); - expect(newWindow).toBeInstanceOf(Window); - expect(newWindow.location.href).toBe('about:blank'); - await new Promise((resolve) => setTimeout(resolve, 1)); - expect(newWindow.document.body.innerHTML).toBe('Test'); - }); - - it('Dispatches error event when the Javascript code is invalid.', async () => { - const newWindow = window.open(`javascript:document.write(test);`); - let errorEvent: ErrorEvent | null = null; - newWindow.addEventListener('error', (event) => (errorEvent = event)); - expect(newWindow).toBeInstanceOf(Window); - expect(newWindow.location.href).toBe('about:blank'); - await new Promise((resolve) => setTimeout(resolve, 20)); - expect(String(((errorEvent)).error)).toBe( - 'ReferenceError: test is not defined' - ); - }); - - it('Opens a new window with URL.', async () => { - const html = 'Test'; - let request: IRequest | null = null; - - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { - request = this.request; - return Promise.resolve({ - text: () => new Promise((resolve) => setTimeout(() => resolve(html))) - }); - }); - - window.happyDOM?.setURL('https://localhost:8080/test/'); - - const newWindow = window.open('/path/to/file.html'); - expect(newWindow).toBeInstanceOf(Window); - expect(newWindow.location.href).toBe('https://localhost:8080/path/to/file.html'); - expect(((request)).url).toBe('https://localhost:8080/path/to/file.html'); - - await new Promise((resolve) => { - newWindow.addEventListener('load', () => { - expect(newWindow.document.body.innerHTML).toBe('Test'); - resolve(null); - }); - }); - }); - - it('Sets width, height, top and left when popup is set as a feature.', () => { - const newWindow = ( - window.open('', '', 'popup=yes,width=100,height=200,top=300,left=400') - ); - expect(newWindow).toBeInstanceOf(Window); - expect(newWindow.innerWidth).toBe(100); - expect(newWindow.innerHeight).toBe(200); - expect(newWindow.screenLeft).toBe(400); - expect(newWindow.screenX).toBe(400); - expect(newWindow.screenTop).toBe(300); - expect(newWindow.screenY).toBe(300); - }); - - it(`Doesn't Sets width, height, top and left when popup is set as a feature.`, () => { - const newWindow = window.open('', '', 'width=100,height=200,top=300,left=400'); - expect(newWindow).toBeInstanceOf(Window); - expect(newWindow.innerWidth).toBe(1024); - expect(newWindow.innerHeight).toBe(768); - expect(newWindow.screenLeft).toBe(0); - expect(newWindow.screenX).toBe(0); - expect(newWindow.screenTop).toBe(0); - expect(newWindow.screenY).toBe(0); - }); - - it('Sets the target as name on the Window instance.', () => { - const newWindow = window.open('', 'test'); - expect(newWindow).toBeInstanceOf(Window); - expect(newWindow.name).toBe('test'); - }); - - it(`Doesn't set opener if "noopener" has been specified as a feature without an URL.`, () => { - const browser = new Browser(); - const page = browser.newPage(); - const newWindow = page.mainFrame.window.open('', '', 'noopener'); - expect(newWindow).toBe(null); - expect(browser.defaultContext.pages[1].mainFrame.window.opener).toBe(null); - }); - - it(`Doesn't set opener if "noopener" has been specified as a feature when opening an URL.`, () => { - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { - return Promise.resolve({ - text: () => Promise.resolve('Test') - }); - }); - - const browser = new Browser(); - const page = browser.newPage(); - page.mainFrame.url = 'https://www.github.com/capricorn86/happy-dom/'; - const newWindow = page.mainFrame.window.open('/test/', '', 'noopener'); - expect(newWindow).toBe(null); - expect(browser.defaultContext.pages[1].mainFrame.window.opener).toBe(null); - }); - it(`Doesn't navigate the browser if the target is the main frame of a detached browser.`, () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { throw new Error('This should not be called.'); @@ -1569,70 +344,5 @@ describe('Window', () => { expect(newWindow2.document.body.innerHTML).toBe('Test'); }); - - it('Opens a new window with a CORS URL.', async () => { - const browser = new Browser(); - const page = browser.newPage(); - const html = 'Test'; - let request: IRequest | null = null; - - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { - request = this.request; - return Promise.resolve({ - text: () => new Promise((resolve) => setTimeout(() => resolve(html))) - }); - }); - - page.mainFrame.url = 'https://www.github.com/capricorn86/happy-dom/'; - - const newWindow = ( - page.mainFrame.window.open('https://developer.mozilla.org/en-US/docs/Web/API/Window/open') - ); - - expect(newWindow instanceof CrossOriginBrowserWindow).toBe(true); - expect(browser.defaultContext.pages.length).toBe(2); - expect(browser.defaultContext.pages[0]).toBe(page); - expect(browser.defaultContext.pages[1].mainFrame.window === newWindow).toBe(false); - expect(browser.defaultContext.pages[1].mainFrame.url).toBe( - 'https://developer.mozilla.org/en-US/docs/Web/API/Window/open' - ); - expect(((request)).url).toBe( - 'https://developer.mozilla.org/en-US/docs/Web/API/Window/open' - ); - - await new Promise((resolve) => { - browser.defaultContext.pages[1].mainFrame.window.addEventListener('load', () => { - expect(browser.defaultContext.pages[1].mainFrame.content).toBe( - 'Test' - ); - - newWindow.close(); - - expect(browser.defaultContext.pages.length).toBe(1); - expect(browser.defaultContext.pages[0]).toBe(page); - expect(newWindow.closed).toBe(true); - resolve(null); - }); - }); - }); - - it("Outputs error to the console if the request can't be resolved.", async () => { - const browser = new Browser(); - const page = browser.newPage(); - - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { - return Promise.reject(new Error('Test error')); - }); - - page.mainFrame.window.open('https://www.github.com/'); - - await new Promise((resolve) => setTimeout(resolve, 1)); - - expect( - browser.defaultContext.pages[1].virtualConsolePrinter - .readAsString() - .startsWith('Error: Test error\n') - ).toBe(true); - }); }); }); diff --git a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts index 8c5add8ff..f5c18b5e4 100644 --- a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts +++ b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts @@ -666,7 +666,7 @@ describe('XMLHttpRequest', () => { accept: '*/*', referer: WINDOW_URL + '/', 'user-agent': window.navigator.userAgent, - cookie: '', + 'test-header': 'test', host: window.location.host }, @@ -711,7 +711,7 @@ describe('XMLHttpRequest', () => { ) => { expect(options.headers).toEqual({ accept: '*/*', - cookie: '', + host: window.location.host, referer: WINDOW_URL + '/', 'user-agent': window.navigator.userAgent, @@ -1101,7 +1101,7 @@ describe('XMLHttpRequest', () => { accept: '*/*', referer: windowURL + '/', 'user-agent': window.navigator.userAgent, - cookie: '', + host: window.location.host }, agent: false, @@ -1159,7 +1159,7 @@ describe('XMLHttpRequest', () => { accept: '*/*', referer: WINDOW_URL + '/', 'user-agent': window.navigator.userAgent, - cookie: '', + host: window.location.host }, agent: false, @@ -1215,7 +1215,7 @@ describe('XMLHttpRequest', () => { accept: '*/*', referer: windowURL + '/', 'user-agent': window.navigator.userAgent, - cookie: '', + host: window.location.host }, agent: false, @@ -1290,7 +1290,7 @@ describe('XMLHttpRequest', () => { accept: '*/*', referer: WINDOW_URL + '/', 'user-agent': window.navigator.userAgent, - cookie: '', + host: window.location.host }, agent: false, @@ -1408,7 +1408,7 @@ describe('XMLHttpRequest', () => { accept: '*/*', referer: WINDOW_URL + '/', 'user-agent': window.navigator.userAgent, - cookie: '', + host: window.location.host }, agent: false, @@ -1484,7 +1484,7 @@ describe('XMLHttpRequest', () => { accept: '*/*', referer: WINDOW_URL + '/', 'user-agent': window.navigator.userAgent, - cookie: '', + host: window.location.host, authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` }, @@ -1560,7 +1560,7 @@ describe('XMLHttpRequest', () => { accept: '*/*', referer: WINDOW_URL + '/', 'user-agent': window.navigator.userAgent, - cookie: '', + host: window.location.host, authorization: `Basic ${Buffer.from(`${username}:`).toString('base64')}` }, @@ -1627,7 +1627,7 @@ describe('XMLHttpRequest', () => { 'content-type': 'text/plain;charset=UTF-8', referer: WINDOW_URL + '/', 'user-agent': window.navigator.userAgent, - cookie: '', + host: window.location.host }, agent: false, From 46166159a3368ac93522b3cc6f0670e010b2a041 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 28 Nov 2023 20:15:25 +0100 Subject: [PATCH 32/63] #466@trivial: Continues on implementation. --- .../happy-dom/src/browser/BrowserFrame.ts | 10 +- .../detached-browser/DetachedBrowserFrame.ts | 10 +- .../src/browser/types/IBrowserFrame.ts | 2 +- .../browser/utilities/BrowserFrameFactory.ts | 2 +- .../utilities/BrowserFrameNavigator.ts | 12 +- .../happy-dom/src/console/VirtualConsole.ts | 110 +++--- .../src/console/VirtualConsolePrinter.ts | 26 +- packages/happy-dom/src/css/CSS.ts | 4 +- packages/happy-dom/src/css/CSSParser.ts | 2 +- packages/happy-dom/src/css/CSSStyleSheet.ts | 6 +- .../AbstractCSSStyleDeclaration.ts | 108 +++--- .../CSSStyleDeclarationElementStyle.ts | 14 +- .../src/css/rules/CSSFontFaceRule.ts | 14 +- .../src/css/rules/CSSKeyframeRule.ts | 14 +- .../happy-dom/src/css/rules/CSSStyleRule.ts | 14 +- .../custom-element/CustomElementRegistry.ts | 22 +- .../happy-dom/src/dom-parser/DOMParser.ts | 12 +- .../src/dom-token-list/DOMTokenList.ts | 40 +- packages/happy-dom/src/event/Event.ts | 29 +- packages/happy-dom/src/event/EventTarget.ts | 79 ++-- .../happy-dom/src/fetch/AbortController.ts | 2 +- packages/happy-dom/src/fetch/AbortSignal.ts | 2 +- packages/happy-dom/src/fetch/Fetch.ts | 46 +-- packages/happy-dom/src/fetch/Headers.ts | 30 +- packages/happy-dom/src/fetch/Request.ts | 58 +-- packages/happy-dom/src/fetch/Response.ts | 10 +- .../multipart/MultipartFormDataParser.ts | 2 +- .../src/fetch/utilities/FetchBodyUtility.ts | 4 +- .../utilities/FetchRequestHeaderUtility.ts | 4 +- packages/happy-dom/src/file/Blob.ts | 16 +- packages/happy-dom/src/file/FileReader.ts | 12 +- packages/happy-dom/src/form-data/FormData.ts | 40 +- packages/happy-dom/src/history/History.ts | 8 +- .../src/match-media/MediaQueryList.ts | 46 +-- .../src/mutation-observer/MutationObserver.ts | 4 +- .../src/named-node-map/NamedNodeMap.ts | 34 +- .../src/nodes/character-data/CharacterData.ts | 24 +- .../src/nodes/child-node/ChildNodeUtility.ts | 6 +- .../document-fragment/DocumentFragment.ts | 20 +- .../happy-dom/src/nodes/document/Document.ts | 128 +++--- .../happy-dom/src/nodes/document/IDocument.ts | 2 +- .../happy-dom/src/nodes/element/Dataset.ts | 2 +- .../happy-dom/src/nodes/element/Element.ts | 76 ++-- .../src/nodes/element/ElementNamedNodeMap.ts | 86 ++--- .../src/nodes/element/ElementUtility.ts | 28 +- .../src/nodes/element/HTMLCollection.ts | 38 +- .../happy-dom/src/nodes/element/IElement.ts | 4 +- .../html-anchor-element/HTMLAnchorElement.ts | 112 +++--- .../HTMLAnchorElementNamedNodeMap.ts | 20 +- .../html-button-element/HTMLButtonElement.ts | 64 +-- .../HTMLButtonElementNamedNodeMap.ts | 22 +- .../src/nodes/html-element/HTMLElement.ts | 26 +- .../html-element/HTMLElementNamedNodeMap.ts | 14 +- .../nodes/html-element/HTMLElementUtility.ts | 20 +- .../HTMLFormControlsCollection.ts | 46 ++- .../html-form-element/HTMLFormElement.ts | 16 +- .../html-iframe-element/HTMLIFrameElement.ts | 4 +- .../HTMLIFrameElementPageLoader.ts | 2 +- .../html-input-element/HTMLInputElement.ts | 148 +++---- .../HTMLInputElementNamedNodeMap.ts | 22 +- .../html-label-element/HTMLLabelElement.ts | 2 +- .../html-link-element/HTMLLinkElement.ts | 16 +- .../HTMLLinkElementNamedNodeMap.ts | 16 +- .../HTMLLinkElementUtility.ts | 16 +- .../html-option-element/HTMLOptionElement.ts | 36 +- .../HTMLOptionElementNamedNodeMap.ts | 22 +- .../html-script-element/HTMLScriptElement.ts | 22 +- .../HTMLScriptElementNamedNodeMap.ts | 6 +- .../HTMLScriptElementUtility.ts | 28 +- .../HTMLOptionsCollection.ts | 16 +- .../html-select-element/HTMLSelectElement.ts | 52 +-- .../HTMLSelectElementNamedNodeMap.ts | 22 +- .../html-slot-element/HTMLSlotElement.ts | 6 +- .../html-style-element/HTMLStyleElement.ts | 10 +- .../HTMLTemplateElement.ts | 4 +- .../HTMLTextAreaElement.ts | 98 ++--- .../HTMLTextAreaElementNamedNodeMap.ts | 22 +- .../HTMLUnknownElement.ts | 76 ++-- packages/happy-dom/src/nodes/node/Node.ts | 119 +++--- .../happy-dom/src/nodes/node/NodeUtility.ts | 66 ++-- .../nodes/parent-node/ParentNodeUtility.ts | 14 +- .../src/nodes/shadow-root/ShadowRoot.ts | 6 +- .../src/nodes/svg-element/SVGElement.ts | 12 +- .../svg-element/SVGElementNamedNodeMap.ts | 14 +- packages/happy-dom/src/nodes/text/Text.ts | 22 +- .../src/query-selector/QuerySelector.ts | 16 +- .../src/query-selector/SelectorItem.ts | 4 +- packages/happy-dom/src/range/Range.ts | 364 +++++++++--------- packages/happy-dom/src/range/RangeUtility.ts | 2 +- packages/happy-dom/src/selection/Selection.ts | 179 +++++---- packages/happy-dom/src/storage/Storage.ts | 14 +- .../happy-dom/src/tree-walker/NodeIterator.ts | 8 +- .../happy-dom/src/tree-walker/TreeWalker.ts | 8 +- packages/happy-dom/src/url/URL.ts | 2 +- .../src/validity-state/ValidityState.ts | 2 +- .../happy-dom/src/window/BrowserWindow.ts | 35 +- packages/happy-dom/src/window/GlobalWindow.ts | 2 +- .../src/window/WindowClassFactory.ts | 4 +- .../src/window/WindowErrorUtility.ts | 2 +- .../src/xml-http-request/XMLHttpRequest.ts | 118 +++--- .../happy-dom/src/xml-parser/XMLParser.ts | 4 +- .../src/xml-serializer/XMLSerializer.ts | 30 +- .../CustomElementRegistry.test.ts | 6 +- .../happy-dom/test/fetch/AbortSignal.test.ts | 4 +- packages/happy-dom/test/fetch/Fetch.test.ts | 2 +- packages/happy-dom/test/fetch/Request.test.ts | 8 +- packages/happy-dom/test/file/Blob.test.ts | 2 +- .../DocumentFragment.test.ts | 4 +- .../test/nodes/document/Document.test.ts | 2 +- .../test/nodes/element/Element.test.ts | 29 +- .../HTMLTemplateElement.test.ts | 20 +- .../HTMLUnknownElement.test.ts | 28 +- .../test/selection/Selection.test.ts | 4 +- packages/happy-dom/test/window/Window.test.ts | 6 +- .../xml-http-request/XMLHttpRequest.test.ts | 2 +- .../test/xml-serializer/XMLSerializer.test.ts | 2 +- 116 files changed, 1718 insertions(+), 1696 deletions(-) diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index ab2727011..157affc22 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -19,7 +19,7 @@ export default class BrowserFrame implements IBrowserFrame { public readonly opener: BrowserFrame | null = null; public readonly page: BrowserPage; public readonly window: BrowserWindow; - public _asyncTaskManager = new AsyncTaskManager(); + public __asyncTaskManager__ = new AsyncTaskManager(); /** * Constructor. @@ -46,8 +46,8 @@ export default class BrowserFrame implements IBrowserFrame { * @param content Content. */ public set content(content) { - this.window.document['_isFirstWrite'] = true; - this.window.document['_isFirstWriteAfterOpen'] = false; + this.window.document['__isFirstWrite__'] = true; + this.window.document['__isFirstWriteAfterOpen__'] = false; this.window.document.open(); this.window.document.write(content); } @@ -80,7 +80,7 @@ export default class BrowserFrame implements IBrowserFrame { */ public async whenComplete(): Promise { await Promise.all([ - this._asyncTaskManager.whenComplete(), + this.__asyncTaskManager__.whenComplete(), ...this.childFrames.map((frame) => frame.whenComplete()) ]); } @@ -94,7 +94,7 @@ export default class BrowserFrame implements IBrowserFrame { for (const frame of this.childFrames) { frame.abort(); } - this._asyncTaskManager.abort(); + this.__asyncTaskManager__.abort(); } /** diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index 3c477cf88..a8212d911 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -20,7 +20,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public readonly page: DetachedBrowserPage; // Needs to be injected from the outside when the browser frame is constructed. public window: IBrowserWindow; - public _asyncTaskManager = new AsyncTaskManager(); + public __asyncTaskManager__ = new AsyncTaskManager(); /** * Constructor. @@ -56,8 +56,8 @@ export default class DetachedBrowserFrame implements IBrowserFrame { if (!this.window) { throw new Error('The frame has been destroyed, the "window" property is not set.'); } - this.window.document['_isFirstWrite'] = true; - this.window.document['_isFirstWriteAfterOpen'] = false; + this.window.document['__isFirstWrite__'] = true; + this.window.document['__isFirstWriteAfterOpen__'] = false; this.window.document.open(); this.window.document.write(content); } @@ -96,7 +96,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { */ public async whenComplete(): Promise { await Promise.all([ - this._asyncTaskManager.whenComplete(), + this.__asyncTaskManager__.whenComplete(), ...this.childFrames.map((frame) => frame.whenComplete()) ]); } @@ -108,7 +108,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { for (const frame of this.childFrames) { frame.abort(); } - this._asyncTaskManager.abort(); + this.__asyncTaskManager__.abort(); } /** diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index 883007f00..62df6b913 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -15,7 +15,7 @@ export default interface IBrowserFrame { url: string; readonly parentFrame: IBrowserFrame | null; readonly opener: IBrowserFrame | null; - _asyncTaskManager: AsyncTaskManager; + __asyncTaskManager__: AsyncTaskManager; readonly page: IBrowserPage; /** diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts index 677868a3a..42ba7712e 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts @@ -44,7 +44,7 @@ export default class BrowserFrameFactory { } (frame.window.closed) = true; - frame._asyncTaskManager.destroy(); + frame.__asyncTaskManager__.destroy(); WindowBrowserSettingsReader.removeSettings(frame.window); (frame.page) = null; (frame.window) = null; diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts index 60788821f..9044d3c37 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts @@ -43,9 +43,9 @@ export default class BrowserFrameNavigator { if (targetURL.protocol === 'javascript:') { if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { - const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( + const readyStateManager = (<{ __readyStateManager__: DocumentReadyStateManager }>( (frame.window) - ))._readyStateManager; + )).__readyStateManager__; readyStateManager.startTask(); @@ -89,8 +89,8 @@ export default class BrowserFrameNavigator { (frame.childFrames) = []; (frame.window.closed) = true; - frame._asyncTaskManager.destroy(); - frame._asyncTaskManager = new AsyncTaskManager(); + frame.__asyncTaskManager__.destroy(); + frame.__asyncTaskManager__ = new AsyncTaskManager(); WindowBrowserSettingsReader.removeSettings(frame.window); (frame.window) = new windowClass(frame, { url: targetURL.href, width, height }); @@ -104,9 +104,9 @@ export default class BrowserFrameNavigator { return null; } - const readyStateManager = (<{ _readyStateManager: DocumentReadyStateManager }>( + const readyStateManager = (<{ __readyStateManager__: DocumentReadyStateManager }>( (frame.window) - ))._readyStateManager; + )).__readyStateManager__; readyStateManager.startTask(); diff --git a/packages/happy-dom/src/console/VirtualConsole.ts b/packages/happy-dom/src/console/VirtualConsole.ts index 5c23f54eb..56232288b 100644 --- a/packages/happy-dom/src/console/VirtualConsole.ts +++ b/packages/happy-dom/src/console/VirtualConsole.ts @@ -15,11 +15,11 @@ export default class VirtualConsole implements Console { // This is not part of the browser specs. public Console: ConsoleConstructor; - private _printer: IVirtualConsolePrinter; - private _count: { [label: string]: number } = {}; - private _time: { [label: string]: number } = {}; - private _groupID = 0; - private _groups: IVirtualConsoleLogGroup[] = []; + #printer: IVirtualConsolePrinter; + #count: { [label: string]: number } = {}; + #time: { [label: string]: number } = {}; + #groupID = 0; + #groups: IVirtualConsoleLogGroup[] = []; /** * Constructor. @@ -27,7 +27,7 @@ export default class VirtualConsole implements Console { * @param printer Console printer. */ constructor(printer: IVirtualConsolePrinter) { - this._printer = printer; + this.#printer = printer; } /** @@ -38,11 +38,11 @@ export default class VirtualConsole implements Console { */ public assert(assertion: boolean, ...args: Array): void { if (!assertion) { - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.assert, level: VirtualConsoleLogLevelEnum.error, message: ['Assertion failed:', ...args], - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } } @@ -51,7 +51,7 @@ export default class VirtualConsole implements Console { * Clears the console. */ public clear(): void { - this._printer.clear(); + this.#printer.clear(); } /** @@ -60,17 +60,17 @@ export default class VirtualConsole implements Console { * @param [label='default'] Label. */ public count(label = 'default'): void { - if (!this._count[label]) { - this._count[label] = 0; + if (!this.#count[label]) { + this.#count[label] = 0; } - this._count[label]++; + this.#count[label]++; - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.count, level: VirtualConsoleLogLevelEnum.info, - message: [`${label}: ${this._count[label]}`], - group: this._groups[this._groups.length - 1] || null + message: [`${label}: ${this.#count[label]}`], + group: this.#groups[this.#groups.length - 1] || null }); } @@ -80,13 +80,13 @@ export default class VirtualConsole implements Console { * @param [label='default'] Label. */ public countReset(label = 'default'): void { - delete this._count[label]; + delete this.#count[label]; - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.countReset, level: VirtualConsoleLogLevelEnum.warn, message: [`${label}: 0`], - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } @@ -96,11 +96,11 @@ export default class VirtualConsole implements Console { * @param args Arguments. */ public debug(...args: Array): void { - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.debug, level: VirtualConsoleLogLevelEnum.log, message: args, - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } @@ -110,11 +110,11 @@ export default class VirtualConsole implements Console { * @param data Data. */ public dir(data: object): void { - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.dir, level: VirtualConsoleLogLevelEnum.log, message: [data], - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } @@ -124,11 +124,11 @@ export default class VirtualConsole implements Console { * @param data Data. */ public dirxml(data: object): void { - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.dirxml, level: VirtualConsoleLogLevelEnum.log, message: [data], - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } @@ -138,11 +138,11 @@ export default class VirtualConsole implements Console { * @param args Arguments. */ public error(...args: Array): void { - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.error, level: VirtualConsoleLogLevelEnum.error, message: args, - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } @@ -163,15 +163,15 @@ export default class VirtualConsole implements Console { * @param [label] Label. */ public group(label?: string): void { - this._groupID++; + this.#groupID++; const group = { - id: this._groupID, + id: this.#groupID, label: label || 'default', collapsed: false, - parent: this._groups[this._groups.length - 1] || null + parent: this.#groups[this.#groups.length - 1] || null }; - this._groups.push(group); - this._printer.print({ + this.#groups.push(group); + this.#printer.print({ type: VirtualConsoleLogTypeEnum.group, level: VirtualConsoleLogLevelEnum.log, message: [label || 'default'], @@ -185,15 +185,15 @@ export default class VirtualConsole implements Console { * @param [label] Label. */ public groupCollapsed(label?: string): void { - this._groupID++; + this.#groupID++; const group = { - id: this._groupID, + id: this.#groupID, label: label || 'default', collapsed: true, - parent: this._groups[this._groups.length - 1] || null + parent: this.#groups[this.#groups.length - 1] || null }; - this._groups.push(group); - this._printer.print({ + this.#groups.push(group); + this.#printer.print({ type: VirtualConsoleLogTypeEnum.groupCollapsed, level: VirtualConsoleLogLevelEnum.log, message: [label || 'default'], @@ -205,10 +205,10 @@ export default class VirtualConsole implements Console { * Exits the current inline group in the console. */ public groupEnd(): void { - if (this._groups.length === 0) { + if (this.#groups.length === 0) { return; } - this._groups.pop(); + this.#groups.pop(); } /** @@ -216,11 +216,11 @@ export default class VirtualConsole implements Console { * @param args */ public info(...args: Array): void { - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.info, level: VirtualConsoleLogLevelEnum.info, message: args, - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } @@ -230,11 +230,11 @@ export default class VirtualConsole implements Console { * @param args Arguments. */ public log(...args: Array): void { - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.log, level: VirtualConsoleLogLevelEnum.log, message: args, - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } @@ -262,11 +262,11 @@ export default class VirtualConsole implements Console { * @param data Data. */ public table(data: { [key: string]: number | string | boolean } | string[]): void { - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.table, level: VirtualConsoleLogLevelEnum.log, message: [data], - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } @@ -276,7 +276,7 @@ export default class VirtualConsole implements Console { * @param [label=default] Label. */ public time(label = 'default'): void { - this._time[label] = PerfHooks.performance.now(); + this.#time[label] = PerfHooks.performance.now(); } /** @@ -286,14 +286,14 @@ export default class VirtualConsole implements Console { * @param [label=default] Label. */ public timeEnd(label = 'default'): void { - const time = this._time[label]; + const time = this.#time[label]; if (time) { const duration = PerfHooks.performance.now() - time; - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.timeEnd, level: VirtualConsoleLogLevelEnum.info, message: [`${label}: ${duration}ms - timer ended`], - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } } @@ -306,14 +306,14 @@ export default class VirtualConsole implements Console { * @param [args] Arguments. */ public timeLog(label = 'default', ...args: Array): void { - const time = this._time[label]; + const time = this.#time[label]; if (time) { const duration = PerfHooks.performance.now() - time; - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.timeLog, level: VirtualConsoleLogLevelEnum.info, message: [`${label}: ${duration}ms`, ...args], - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } } @@ -333,11 +333,11 @@ export default class VirtualConsole implements Console { * @param args Arguments. */ public trace(...args: Array): void { - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.trace, level: VirtualConsoleLogLevelEnum.log, message: [...args, new Error('stack').stack.replace('Error: stack', '')], - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } @@ -347,11 +347,11 @@ export default class VirtualConsole implements Console { * @param args Arguments. */ public warn(...args: Array): void { - this._printer.print({ + this.#printer.print({ type: VirtualConsoleLogTypeEnum.warn, level: VirtualConsoleLogLevelEnum.warn, message: args, - group: this._groups[this._groups.length - 1] || null + group: this.#groups[this.#groups.length - 1] || null }); } } diff --git a/packages/happy-dom/src/console/VirtualConsolePrinter.ts b/packages/happy-dom/src/console/VirtualConsolePrinter.ts index 934751a31..6b477b806 100644 --- a/packages/happy-dom/src/console/VirtualConsolePrinter.ts +++ b/packages/happy-dom/src/console/VirtualConsolePrinter.ts @@ -8,8 +8,8 @@ import IVirtualConsolePrinter from './types/IVirtualConsolePrinter.js'; * Virtual console printer. */ export default class VirtualConsolePrinter implements IVirtualConsolePrinter { - private _logEntries: IVirtualConsoleLogEntry[] = []; - private _listeners: { + #logEntries: IVirtualConsoleLogEntry[] = []; + #listeners: { print: Array<(event: Event) => void>; clear: Array<(event: Event) => void>; } = { print: [], clear: [] }; @@ -20,7 +20,7 @@ export default class VirtualConsolePrinter implements IVirtualConsolePrinter { * @param logEntry Log entry. */ public print(logEntry: IVirtualConsoleLogEntry): void { - this._logEntries.push(logEntry); + this.#logEntries.push(logEntry); this.dispatchEvent(new Event('print')); } @@ -28,7 +28,7 @@ export default class VirtualConsolePrinter implements IVirtualConsolePrinter { * Clears the output. */ public clear(): void { - this._logEntries = []; + this.#logEntries = []; this.dispatchEvent(new Event('clear')); } @@ -39,10 +39,10 @@ export default class VirtualConsolePrinter implements IVirtualConsolePrinter { * @param listener Listener. */ public addEventListener(eventType: 'print' | 'clear', listener: (event: Event) => void): void { - if (!this._listeners[eventType]) { + if (!this.#listeners[eventType]) { throw new Error(`Event type "${eventType}" is not supported.`); } - this._listeners[eventType].push(listener); + this.#listeners[eventType].push(listener); } /** @@ -52,12 +52,12 @@ export default class VirtualConsolePrinter implements IVirtualConsolePrinter { * @param listener Listener. */ public removeEventListener(eventType: 'print' | 'clear', listener: (event: Event) => void): void { - if (!this._listeners[eventType]) { + if (!this.#listeners[eventType]) { throw new Error(`Event type "${eventType}" is not supported.`); } - const index = this._listeners[eventType].indexOf(listener); + const index = this.#listeners[eventType].indexOf(listener); if (index !== -1) { - this._listeners[eventType].splice(index, 1); + this.#listeners[eventType].splice(index, 1); } } @@ -67,10 +67,10 @@ export default class VirtualConsolePrinter implements IVirtualConsolePrinter { * @param event Event. */ public dispatchEvent(event: Event): void { - if (!this._listeners[event.type]) { + if (!this.#listeners[event.type]) { throw new Error(`Event type "${event.type}" is not supported.`); } - for (const listener of this._listeners[event.type]) { + for (const listener of this.#listeners[event.type]) { listener(event); } } @@ -81,8 +81,8 @@ export default class VirtualConsolePrinter implements IVirtualConsolePrinter { * @returns Console log entries. */ public read(): IVirtualConsoleLogEntry[] { - const logEntries = this._logEntries; - this._logEntries = []; + const logEntries = this.#logEntries; + this.#logEntries = []; return logEntries; } diff --git a/packages/happy-dom/src/css/CSS.ts b/packages/happy-dom/src/css/CSS.ts index 926a45e8e..f91274dfd 100644 --- a/packages/happy-dom/src/css/CSS.ts +++ b/packages/happy-dom/src/css/CSS.ts @@ -24,10 +24,10 @@ export default class CSS { * TODO: Always returns "true" for now, but it should probably be improved in the future. * * @param _condition Property name or condition. - * @param [_value] Value when using property name. + * @param [__value__] Value when using property name. * @returns "true" if supported. */ - public supports(_condition: string, _value?: string): boolean { + public supports(_condition: string, __value__?: string): boolean { return true; } diff --git a/packages/happy-dom/src/css/CSSParser.ts b/packages/happy-dom/src/css/CSSParser.ts index a2b918d77..71bc7b97b 100644 --- a/packages/happy-dom/src/css/CSSParser.ts +++ b/packages/happy-dom/src/css/CSSParser.ts @@ -131,7 +131,7 @@ export default class CSSParser { case CSSRule.FONT_FACE_RULE: case CSSRule.KEYFRAME_RULE: case CSSRule.STYLE_RULE: - (parentRule)._cssText = cssText; + (parentRule).__cssText__ = cssText; break; } } diff --git a/packages/happy-dom/src/css/CSSStyleSheet.ts b/packages/happy-dom/src/css/CSSStyleSheet.ts index ffce83c11..bb5d8b9ab 100644 --- a/packages/happy-dom/src/css/CSSStyleSheet.ts +++ b/packages/happy-dom/src/css/CSSStyleSheet.ts @@ -21,7 +21,7 @@ export default class CSSStyleSheet { public title: string; public alternate: boolean; public disabled: boolean; - private _currentText: string = null; + #currentText: string = null; /** * Constructor. @@ -112,8 +112,8 @@ export default class CSSStyleSheet { * @param text CSS text. */ public replaceSync(text: string): void { - if (this._currentText !== text) { - this._currentText = text; + if (this.#currentText !== text) { + this.#currentText = text; (this.cssRules) = CSSParser.parseFromString(this, text); } } diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index 227b0ac8e..2128cb130 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -12,10 +12,10 @@ import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; */ export default abstract class AbstractCSSStyleDeclaration { public readonly parentRule: CSSRule = null; - protected _style: CSSStyleDeclarationPropertyManager = null; - protected _ownerElement: IElement; - protected _computed: boolean; - protected _elementStyle: CSSStyleDeclarationElementStyle = null; + #style: CSSStyleDeclarationPropertyManager = null; + #ownerElement: IElement; + #computed: boolean; + #elementStyle: CSSStyleDeclarationElementStyle = null; /** * Constructor. @@ -24,11 +24,11 @@ export default abstract class AbstractCSSStyleDeclaration { * @param [computed] Computed. */ constructor(ownerElement: IElement = null, computed = false) { - this._style = !ownerElement ? new CSSStyleDeclarationPropertyManager() : null; - this._ownerElement = ownerElement; - this._computed = ownerElement ? computed : false; - this._elementStyle = ownerElement - ? new CSSStyleDeclarationElementStyle(ownerElement, this._computed) + this.#style = !ownerElement ? new CSSStyleDeclarationPropertyManager() : null; + this.#ownerElement = ownerElement; + this.#computed = ownerElement ? computed : false; + this.#elementStyle = ownerElement + ? new CSSStyleDeclarationElementStyle(ownerElement, this.#computed) : null; } @@ -38,12 +38,12 @@ export default abstract class AbstractCSSStyleDeclaration { * @returns Length. */ public get length(): number { - if (this._ownerElement) { - const style = this._elementStyle.getElementStyle(); + if (this.#ownerElement) { + const style = this.#elementStyle.getElementStyle(); return style.size(); } - return this._style.size(); + return this.#style.size(); } /** @@ -52,15 +52,15 @@ export default abstract class AbstractCSSStyleDeclaration { * @returns CSS text. */ public get cssText(): string { - if (this._ownerElement) { - if (this._computed) { + if (this.#ownerElement) { + if (this.#computed) { return ''; } - return this._elementStyle.getElementStyle().toString(); + return this.#elementStyle.getElementStyle().toString(); } - return this._style.toString(); + return this.#style.toString(); } /** @@ -69,32 +69,32 @@ export default abstract class AbstractCSSStyleDeclaration { * @param cssText CSS text. */ public set cssText(cssText: string) { - if (this._computed) { + if (this.#computed) { throw new DOMException( `Failed to execute 'cssText' on 'CSSStyleDeclaration': These styles are computed, and the properties are therefore read-only.`, DOMExceptionNameEnum.domException ); } - if (this._ownerElement) { + if (this.#ownerElement) { const style = new CSSStyleDeclarationPropertyManager({ cssText }); - let styleAttribute = this._ownerElement.attributes['style']; + let styleAttribute = this.#ownerElement.attributes['style']; if (!styleAttribute) { - styleAttribute = this._ownerElement.ownerDocument.createAttribute('style'); - // We use "_setNamedItemWithoutConsequences" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. - (this._ownerElement.attributes)._setNamedItemWithoutConsequences( + styleAttribute = this.#ownerElement.ownerDocument.createAttribute('style'); + // We use "__setNamedItemWithoutConsequences__" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. + (this.#ownerElement.attributes).__setNamedItemWithoutConsequences__( styleAttribute ); } - if (this._ownerElement.isConnected) { - this._ownerElement.ownerDocument['_cacheID']++; + if (this.#ownerElement.isConnected) { + this.#ownerElement.ownerDocument['__cacheID__']++; } styleAttribute.value = style.toString(); } else { - this._style = new CSSStyleDeclarationPropertyManager({ cssText }); + this.#style = new CSSStyleDeclarationPropertyManager({ cssText }); } } @@ -105,10 +105,10 @@ export default abstract class AbstractCSSStyleDeclaration { * @returns Item. */ public item(index: number): string { - if (this._ownerElement) { - return this._elementStyle.getElementStyle().item(index); + if (this.#ownerElement) { + return this.#elementStyle.getElementStyle().item(index); } - return this._style.item(index); + return this.#style.item(index); } /** @@ -119,7 +119,7 @@ export default abstract class AbstractCSSStyleDeclaration { * @param [priority] Can be "important", or an empty string. */ public setProperty(name: string, value: string, priority?: 'important' | '' | undefined): void { - if (this._computed) { + if (this.#computed) { throw new DOMException( `Failed to execute 'setProperty' on 'CSSStyleDeclaration': These styles are computed, and therefore the '${name}' property is read-only.`, DOMExceptionNameEnum.domException @@ -134,28 +134,28 @@ export default abstract class AbstractCSSStyleDeclaration { if (!stringValue) { this.removeProperty(name); - } else if (this._ownerElement) { - let styleAttribute = this._ownerElement.attributes['style']; + } else if (this.#ownerElement) { + let styleAttribute = this.#ownerElement.attributes['style']; if (!styleAttribute) { - styleAttribute = this._ownerElement.ownerDocument.createAttribute('style'); + styleAttribute = this.#ownerElement.ownerDocument.createAttribute('style'); - // We use "_setNamedItemWithoutConsequences" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. - (this._ownerElement.attributes)._setNamedItemWithoutConsequences( + // We use "__setNamedItemWithoutConsequences__" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. + (this.#ownerElement.attributes).__setNamedItemWithoutConsequences__( styleAttribute ); } - if (this._ownerElement.isConnected) { - this._ownerElement.ownerDocument['_cacheID']++; + if (this.#ownerElement.isConnected) { + this.#ownerElement.ownerDocument['__cacheID__']++; } - const style = this._elementStyle.getElementStyle(); + const style = this.#elementStyle.getElementStyle(); style.set(name, stringValue, !!priority); styleAttribute.value = style.toString(); } else { - this._style.set(name, stringValue, !!priority); + this.#style.set(name, stringValue, !!priority); } } @@ -167,30 +167,32 @@ export default abstract class AbstractCSSStyleDeclaration { * @param [priority] Can be "important", or an empty string. */ public removeProperty(name: string): void { - if (this._computed) { + if (this.#computed) { throw new DOMException( `Failed to execute 'removeProperty' on 'CSSStyleDeclaration': These styles are computed, and therefore the '${name}' property is read-only.`, DOMExceptionNameEnum.domException ); } - if (this._ownerElement) { - const style = this._elementStyle.getElementStyle(); + if (this.#ownerElement) { + const style = this.#elementStyle.getElementStyle(); style.remove(name); const newCSSText = style.toString(); - if (this._ownerElement.isConnected) { - this._ownerElement.ownerDocument['_cacheID']++; + if (this.#ownerElement.isConnected) { + this.#ownerElement.ownerDocument['__cacheID__']++; } if (newCSSText) { - (this._ownerElement.attributes['style']).value = newCSSText; + (this.#ownerElement.attributes['style']).value = newCSSText; } else { - // We use "_removeNamedItemWithoutConsequences" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. - (this._ownerElement.attributes)._removeNamedItemWithoutConsequences('style'); + // We use "__removeNamedItemWithoutConsequences__" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. + (this.#ownerElement.attributes).__removeNamedItemWithoutConsequences__( + 'style' + ); } } else { - this._style.remove(name); + this.#style.remove(name); } } @@ -201,11 +203,11 @@ export default abstract class AbstractCSSStyleDeclaration { * @returns Property value. */ public getPropertyValue(name: string): string { - if (this._ownerElement) { - const style = this._elementStyle.getElementStyle(); + if (this.#ownerElement) { + const style = this.#elementStyle.getElementStyle(); return style.get(name)?.value || ''; } - return this._style.get(name)?.value || ''; + return this.#style.get(name)?.value || ''; } /** @@ -215,10 +217,10 @@ export default abstract class AbstractCSSStyleDeclaration { * @returns "important" if set to be important. */ public getPropertyPriority(name: string): string { - if (this._ownerElement) { - const style = this._elementStyle.getElementStyle(); + if (this.#ownerElement) { + const style = this.#elementStyle.getElementStyle(); return style.get(name)?.important ? 'important' : ''; } - return this._style.get(name)?.important ? 'important' : ''; + return this.#style.get(name)?.important ? 'important' : ''; } } diff --git a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 3d8b62e6a..4ed52936c 100644 --- a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -99,12 +99,12 @@ export default class CSSStyleDeclarationElementStyle { if ( this.cache.propertyManager && - this.cache.documentCacheID === this.element.ownerDocument['_cacheID'] + this.cache.documentCacheID === this.element.ownerDocument['__cacheID__'] ) { return this.cache.propertyManager; } - this.cache.documentCacheID = this.element.ownerDocument['_cacheID']; + 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) { @@ -276,7 +276,7 @@ export default class CSSStyleDeclarationElementStyle { return; } - const ownerWindow = this.element.ownerDocument._defaultView; + const ownerWindow = this.element.ownerDocument.__defaultView__; for (const rule of options.cssRules) { if (rule.type === CSSRuleTypeEnum.styleRule) { @@ -285,7 +285,7 @@ export default class CSSStyleDeclarationElementStyle { if (selectorText.startsWith(':host')) { if (options.hostElement) { options.hostElement.cssTexts.push({ - cssText: (rule)._cssText, + cssText: (rule).__cssText__, priorityWeight: 0 }); } @@ -294,7 +294,7 @@ export default class CSSStyleDeclarationElementStyle { const matchResult = QuerySelector.match(element.element, selectorText); if (matchResult) { element.cssTexts.push({ - cssText: (rule)._cssText, + cssText: (rule).__cssText__, priorityWeight: matchResult.priorityWeight }); } @@ -355,7 +355,7 @@ export default class CSSStyleDeclarationElementStyle { parentSize: string | number | null; }): string { if ( - WindowBrowserSettingsReader.getSettings(this.element.ownerDocument._defaultView) + WindowBrowserSettingsReader.getSettings(this.element.ownerDocument.__defaultView__) .disableComputedStyleRendering ) { return options.value; @@ -368,7 +368,7 @@ export default class CSSStyleDeclarationElementStyle { while ((match = regexp.exec(options.value)) !== null) { if (match[1] !== 'px') { const valueInPixels = CSSMeasurementConverter.toPixels({ - ownerWindow: this.element.ownerDocument._defaultView, + ownerWindow: this.element.ownerDocument.__defaultView__, value: match[0], rootFontSize: options.rootFontSize, parentFontSize: options.parentFontSize, diff --git a/packages/happy-dom/src/css/rules/CSSFontFaceRule.ts b/packages/happy-dom/src/css/rules/CSSFontFaceRule.ts index f9bac5d91..62f5dc2db 100644 --- a/packages/happy-dom/src/css/rules/CSSFontFaceRule.ts +++ b/packages/happy-dom/src/css/rules/CSSFontFaceRule.ts @@ -6,8 +6,8 @@ import CSSStyleDeclaration from '../declaration/CSSStyleDeclaration.js'; */ export default class CSSFontFaceRule extends CSSRule { public readonly type = CSSRule.FONT_FACE_RULE; - public _cssText = ''; - private _style: CSSStyleDeclaration = null; + public __cssText__ = ''; + #style: CSSStyleDeclaration = null; /** * Returns style. @@ -15,11 +15,11 @@ export default class CSSFontFaceRule extends CSSRule { * @returns Style. */ public get style(): CSSStyleDeclaration { - if (!this._style) { - this._style = new CSSStyleDeclaration(); - (this._style.parentRule) = this; - this._style.cssText = this._cssText; + if (!this.#style) { + this.#style = new CSSStyleDeclaration(); + (this.#style.parentRule) = this; + this.#style.cssText = this.__cssText__; } - return this._style; + return this.#style; } } diff --git a/packages/happy-dom/src/css/rules/CSSKeyframeRule.ts b/packages/happy-dom/src/css/rules/CSSKeyframeRule.ts index dcc7dd80b..ab254b20b 100644 --- a/packages/happy-dom/src/css/rules/CSSKeyframeRule.ts +++ b/packages/happy-dom/src/css/rules/CSSKeyframeRule.ts @@ -7,8 +7,8 @@ import CSSStyleDeclaration from '../declaration/CSSStyleDeclaration.js'; export default class CSSKeyframeRule extends CSSRule { public readonly type = CSSRule.KEYFRAME_RULE; public readonly keyText: string; - public _cssText = ''; - private _style: CSSStyleDeclaration = null; + public __cssText__ = ''; + #style: CSSStyleDeclaration = null; /** * Returns style. @@ -16,12 +16,12 @@ export default class CSSKeyframeRule extends CSSRule { * @returns Style. */ public get style(): CSSStyleDeclaration { - if (!this._style) { - this._style = new CSSStyleDeclaration(); - (this._style.parentRule) = this; - this._style.cssText = this._cssText; + if (!this.#style) { + this.#style = new CSSStyleDeclaration(); + (this.#style.parentRule) = this; + this.#style.cssText = this.__cssText__; } - return this._style; + return this.#style; } /** diff --git a/packages/happy-dom/src/css/rules/CSSStyleRule.ts b/packages/happy-dom/src/css/rules/CSSStyleRule.ts index 9a7ef6019..6f69d64fa 100644 --- a/packages/happy-dom/src/css/rules/CSSStyleRule.ts +++ b/packages/happy-dom/src/css/rules/CSSStyleRule.ts @@ -8,8 +8,8 @@ export default class CSSStyleRule extends CSSRule { public readonly type = CSSRule.STYLE_RULE; public readonly selectorText = ''; public readonly styleMap = new Map(); - public _cssText = ''; - private _style: CSSStyleDeclaration = null; + public __cssText__ = ''; + #style: CSSStyleDeclaration = null; /** * Returns style. @@ -17,12 +17,12 @@ export default class CSSStyleRule extends CSSRule { * @returns Style. */ public get style(): CSSStyleDeclaration { - if (!this._style) { - this._style = new CSSStyleDeclaration(); - (this._style.parentRule) = this; - this._style.cssText = this._cssText; + if (!this.#style) { + this.#style = new CSSStyleDeclaration(); + (this.#style.parentRule) = this; + this.#style.cssText = this.__cssText__; } - return this._style; + return this.#style; } /** diff --git a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts index bf1251732..7b35dc7d7 100644 --- a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts +++ b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts @@ -6,8 +6,8 @@ import Node from '../nodes/node/Node.js'; * Custom elements registry. */ export default class CustomElementRegistry { - public _registry: { [k: string]: { elementClass: typeof HTMLElement; extends: string } } = {}; - public _callbacks: { [k: string]: (() => void)[] } = {}; + public __registry__: { [k: string]: { elementClass: typeof HTMLElement; extends: string } } = {}; + public __callbacks__: { [k: string]: (() => void)[] } = {}; /** * Defines a custom element class. @@ -32,19 +32,19 @@ export default class CustomElementRegistry { ); } - this._registry[upperTagName] = { + this.__registry__[upperTagName] = { elementClass, extends: options && options.extends ? options.extends.toLowerCase() : null }; // ObservedAttributes should only be called once by CustomElementRegistry (see #117) if (elementClass.prototype.attributeChangedCallback) { - elementClass._observedAttributes = elementClass.observedAttributes; + elementClass.__observedAttributes__ = elementClass.observedAttributes; } - if (this._callbacks[upperTagName]) { - const callbacks = this._callbacks[upperTagName]; - delete this._callbacks[upperTagName]; + if (this.__callbacks__[upperTagName]) { + const callbacks = this.__callbacks__[upperTagName]; + delete this.__callbacks__[upperTagName]; for (const callback of callbacks) { callback(); } @@ -59,7 +59,9 @@ export default class CustomElementRegistry { */ public get(tagName: string): typeof HTMLElement { const upperTagName = tagName.toUpperCase(); - return this._registry[upperTagName] ? this._registry[upperTagName].elementClass : undefined; + return this.__registry__[upperTagName] + ? this.__registry__[upperTagName].elementClass + : undefined; } /** @@ -85,8 +87,8 @@ export default class CustomElementRegistry { return Promise.resolve(); } return new Promise((resolve) => { - this._callbacks[upperTagName] = this._callbacks[upperTagName] || []; - this._callbacks[upperTagName].push(resolve); + this.__callbacks__[upperTagName] = this.__callbacks__[upperTagName] || []; + this.__callbacks__[upperTagName].push(resolve); }); } } diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index 4cd35d2e9..65d5aeb18 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -35,13 +35,13 @@ export default class DOMParser { throw new DOMException('Second parameter "mimeType" is mandatory.'); } - const newDocument = this._createDocument(mimeType); + const newDocument = this.#createDocument(mimeType); const root = XMLParser.parse(newDocument, string, { evaluateScripts: true }); let documentElement = null; let documentTypeNode = null; - for (const node of root._childNodes) { + for (const node of root.__childNodes__) { if (node['tagName'] === 'HTML') { documentElement = node; } else if (node.nodeType === Node.DOCUMENT_TYPE_NODE) { @@ -60,7 +60,7 @@ export default class DOMParser { newDocument.appendChild(documentElement); const body = newDocument.body; if (body) { - for (const child of root._childNodes.slice()) { + for (const child of root.__childNodes__.slice()) { body.appendChild(child); } } @@ -68,7 +68,7 @@ export default class DOMParser { switch (mimeType) { case 'image/svg+xml': { - for (const node of root._childNodes.slice()) { + for (const node of root.__childNodes__.slice()) { newDocument.appendChild(node); } } @@ -84,7 +84,7 @@ export default class DOMParser { documentElement.appendChild(bodyElement); newDocument.appendChild(documentElement); - for (const node of root._childNodes.slice()) { + for (const node of root.__childNodes__.slice()) { bodyElement.appendChild(node); } } @@ -100,7 +100,7 @@ export default class DOMParser { * @param mimeType Mime type. * @returns IDocument. */ - private _createDocument(mimeType: string): IDocument { + #createDocument(mimeType: string): IDocument { switch (mimeType) { case 'text/html': return new this.#window.HTMLDocument(); diff --git a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts index 52f9cd4ef..e38bf91cd 100644 --- a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts +++ b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts @@ -9,8 +9,8 @@ import IDOMTokenList from './IDOMTokenList.js'; */ export default class DOMTokenList implements IDOMTokenList { public readonly length = 0; - private _ownerElement: Element; - private _attributeName: string; + #ownerElement: Element; + #attributeName: string; /** * Constructor. @@ -19,9 +19,9 @@ export default class DOMTokenList implements IDOMTokenList { * @param attributeName Attribute name. */ constructor(ownerElement: Element, attributeName) { - this._ownerElement = ownerElement; - this._attributeName = attributeName; - this._updateIndices(); + this.#ownerElement = ownerElement; + this.#attributeName = attributeName; + this.__updateIndices__(); } /** @@ -30,14 +30,14 @@ export default class DOMTokenList implements IDOMTokenList { * @param value Value. */ public set value(value: string) { - this._ownerElement.setAttribute(this._attributeName, value); + this.#ownerElement.setAttribute(this.#attributeName, value); } /** * Get value. */ public get value(): string { - return this._ownerElement.getAttribute(this._attributeName); + return this.#ownerElement.getAttribute(this.#attributeName); } /** @@ -57,14 +57,14 @@ export default class DOMTokenList implements IDOMTokenList { * @param newToken NewToken. */ public replace(token: string, newToken: string): boolean { - const attr = this._ownerElement.getAttribute(this._attributeName); + const attr = this.#ownerElement.getAttribute(this.#attributeName); const list = attr ? Array.from(new Set(attr.split(' '))) : []; const index = list.indexOf(token); if (index === -1) { return false; } list[index] = newToken; - this._ownerElement.setAttribute(this._attributeName, list.join(' ')); + this.#ownerElement.setAttribute(this.#attributeName, list.join(' ')); return true; } @@ -81,7 +81,7 @@ export default class DOMTokenList implements IDOMTokenList { * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object. */ public values(): IterableIterator { - const attr = this._ownerElement.getAttribute(this._attributeName); + const attr = this.#ownerElement.getAttribute(this.#attributeName); const list = attr ? Array.from(new Set(attr.split(' '))) : []; return list.values(); } @@ -90,7 +90,7 @@ export default class DOMTokenList implements IDOMTokenList { * Returns an iterator, allowing you to go through all key/value pairs contained in this object. */ public entries(): IterableIterator<[number, string]> { - const attr = this._ownerElement.getAttribute(this._attributeName); + const attr = this.#ownerElement.getAttribute(this.#attributeName); const list = attr ? Array.from(new Set(attr.split(' '))) : []; return list.entries(); } @@ -102,7 +102,7 @@ export default class DOMTokenList implements IDOMTokenList { * @param thisArg */ public forEach(callback: (currentValue, currentIndex, listObj) => void, thisArg?: this): void { - const attr = this._ownerElement.getAttribute(this._attributeName); + const attr = this.#ownerElement.getAttribute(this.#attributeName); const list = attr ? Array.from(new Set(attr.split(' '))) : []; return list.forEach(callback, thisArg); } @@ -112,7 +112,7 @@ export default class DOMTokenList implements IDOMTokenList { * */ public keys(): IterableIterator { - const attr = this._ownerElement.getAttribute(this._attributeName); + const attr = this.#ownerElement.getAttribute(this.#attributeName); const list = attr ? Array.from(new Set(attr.split(' '))) : []; return list.keys(); } @@ -123,7 +123,7 @@ export default class DOMTokenList implements IDOMTokenList { * @param tokens Tokens. */ public add(...tokens: string[]): void { - const attr = this._ownerElement.getAttribute(this._attributeName); + const attr = this.#ownerElement.getAttribute(this.#attributeName); const list = attr ? Array.from(new Set(attr.split(' '))) : []; for (const token of tokens) { @@ -135,7 +135,7 @@ export default class DOMTokenList implements IDOMTokenList { } } - this._ownerElement.setAttribute(this._attributeName, list.join(' ')); + this.#ownerElement.setAttribute(this.#attributeName, list.join(' ')); } /** @@ -144,7 +144,7 @@ export default class DOMTokenList implements IDOMTokenList { * @param tokens Tokens. */ public remove(...tokens: string[]): void { - const attr = this._ownerElement.getAttribute(this._attributeName); + const attr = this.#ownerElement.getAttribute(this.#attributeName); const list = attr ? Array.from(new Set(attr.split(' '))) : []; for (const token of tokens) { @@ -154,7 +154,7 @@ export default class DOMTokenList implements IDOMTokenList { } } - this._ownerElement.setAttribute(this._attributeName, list.join(' ')); + this.#ownerElement.setAttribute(this.#attributeName, list.join(' ')); } /** @@ -164,7 +164,7 @@ export default class DOMTokenList implements IDOMTokenList { * @returns TRUE if it contains. */ public contains(className: string): boolean { - const attr = this._ownerElement.getAttribute(this._attributeName); + const attr = this.#ownerElement.getAttribute(this.#attributeName); return (attr ? attr.split(' ') : []).includes(className); } @@ -197,8 +197,8 @@ export default class DOMTokenList implements IDOMTokenList { /** * Updates indices. */ - public _updateIndices(): void { - const attr = this._ownerElement.getAttribute(this._attributeName); + public __updateIndices__(): void { + const attr = this.#ownerElement.getAttribute(this.#attributeName); const list = attr ? Array.from(new Set(attr.split(' '))) : []; for (let i = list.length - 1, max = this.length; i < max; i++) { diff --git a/packages/happy-dom/src/event/Event.ts b/packages/happy-dom/src/event/Event.ts index 26f6b18d3..db69df9c6 100644 --- a/packages/happy-dom/src/event/Event.ts +++ b/packages/happy-dom/src/event/Event.ts @@ -17,18 +17,19 @@ export default class Event { public cancelable: boolean; public defaultPrevented = false; public eventPhase: EventPhaseEnum = EventPhaseEnum.none; - public _immediatePropagationStopped = false; - public _propagationStopped = false; - public _target: IEventTarget = null; - public _currentTarget: IEventTarget = null; public timeStamp: number = performance.now(); public type: string; - public _isInPassiveEventListener = false; public NONE = EventPhaseEnum.none; public CAPTURING_PHASE = EventPhaseEnum.capturing; public AT_TARGET = EventPhaseEnum.atTarget; public BUBBLING_PHASE = EventPhaseEnum.bubbling; + public __immediatePropagationStopped__ = false; + public __propagationStopped__ = false; + public __target__: IEventTarget = null; + public __currentTarget__: IEventTarget = null; + public __isInPassiveEventListener__ = false; + /** * Constructor. * @@ -49,7 +50,7 @@ export default class Event { * @returns Target. */ public get target(): IEventTarget { - return this._target; + return this.__target__; } /** @@ -58,7 +59,7 @@ export default class Event { * @returns Target. */ public get currentTarget(): IEventTarget { - return this._currentTarget; + return this.__currentTarget__; } /** @@ -67,7 +68,7 @@ export default class Event { * @returns "true" if propagation has been stopped. */ public get cancelBubble(): boolean { - return this._propagationStopped; + return this.__propagationStopped__; } /** @@ -76,13 +77,13 @@ export default class Event { * @returns Composed path. */ public composedPath(): IEventTarget[] { - if (!this._target) { + if (!this.__target__) { return []; } const composedPath = []; let eventTarget: INode | IShadowRoot | IBrowserWindow = ( - (this._target) + (this.__target__) ); while (eventTarget) { @@ -97,7 +98,7 @@ export default class Event { ) { eventTarget = (eventTarget).host; } else if ((eventTarget).nodeType === NodeTypeEnum.documentNode) { - eventTarget = ((eventTarget))._defaultView; + eventTarget = ((eventTarget)).__defaultView__; } else { break; } @@ -124,7 +125,7 @@ export default class Event { * Prevents default. */ public preventDefault(): void { - if (!this._isInPassiveEventListener) { + if (!this.__isInPassiveEventListener__) { this.defaultPrevented = true; } } @@ -133,13 +134,13 @@ export default class Event { * Stops immediate propagation. */ public stopImmediatePropagation(): void { - this._immediatePropagationStopped = true; + this.__immediatePropagationStopped__ = true; } /** * Stops propagation. */ public stopPropagation(): void { - this._propagationStopped = true; + this.__propagationStopped__ = true; } } diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts index 4ecc2ff86..27ebb00e0 100644 --- a/packages/happy-dom/src/event/EventTarget.ts +++ b/packages/happy-dom/src/event/EventTarget.ts @@ -13,10 +13,10 @@ import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.j * Handles events. */ export default abstract class EventTarget implements IEventTarget { - public readonly _listeners: { + public readonly __listeners__: { [k: string]: (((event: Event) => void) | IEventListener)[]; } = {}; - public readonly _listenerOptions: { + public readonly __listenerOptions__: { [k: string]: (IEventListenerOptions | null)[]; } = {}; @@ -42,21 +42,21 @@ export default abstract class EventTarget implements IEventTarget { ): void { const listenerOptions = typeof options === 'boolean' ? { capture: options } : options || null; - this._listeners[type] = this._listeners[type] || []; - this._listenerOptions[type] = this._listenerOptions[type] || []; - if (this._listeners[type].includes(listener)) { + this.__listeners__[type] = this.__listeners__[type] || []; + this.__listenerOptions__[type] = this.__listenerOptions__[type] || []; + if (this.__listeners__[type].includes(listener)) { return; } - this._listeners[type].push(listener); - this._listenerOptions[type].push(listenerOptions); + this.__listeners__[type].push(listener); + this.__listenerOptions__[type].push(listenerOptions); // Tracks the amount of capture event listeners to improve performance when they are not used. if (listenerOptions && listenerOptions.capture) { - const window = this._getWindow(); + const window = this.#getWindow(); if (window) { - window['_captureEventListenerCount'][type] = - window['_captureEventListenerCount'][type] ?? 0; - window['_captureEventListenerCount'][type]++; + window['__captureEventListenerCount__'][type] = + window['__captureEventListenerCount__'][type] ?? 0; + window['__captureEventListenerCount__'][type]++; } } } @@ -71,19 +71,22 @@ export default abstract class EventTarget implements IEventTarget { type: string, listener: ((event: Event) => void) | IEventListener ): void { - if (this._listeners[type]) { - const index = this._listeners[type].indexOf(listener); + if (this.__listeners__[type]) { + const index = this.__listeners__[type].indexOf(listener); if (index !== -1) { // Tracks the amount of capture event listeners to improve performance when they are not used. - if (this._listenerOptions[type][index] && this._listenerOptions[type][index].capture) { - const window = this._getWindow(); - if (window && window['_captureEventListenerCount'][type]) { - window['_captureEventListenerCount'][type]--; + if ( + this.__listenerOptions__[type][index] && + this.__listenerOptions__[type][index].capture + ) { + const window = this.#getWindow(); + if (window && window['__captureEventListenerCount__'][type]) { + window['__captureEventListenerCount__'][type]--; } } - this._listeners[type].splice(index, 1); - this._listenerOptions[type].splice(index, 1); + this.__listeners__[type].splice(index, 1); + this.__listenerOptions__[type].splice(index, 1); } } } @@ -97,22 +100,22 @@ export default abstract class EventTarget implements IEventTarget { * @returns The return value is false if event is cancelable and at least one of the event handlers which handled this event called Event.preventDefault(). */ public dispatchEvent(event: Event): boolean { - const window = this._getWindow(); + const window = this.#getWindow(); if (event.eventPhase === EventPhaseEnum.none) { - event._target = this; + event.__target__ = this; const composedPath = event.composedPath(); // Capturing phase // We only need to iterate over the composed path if there are capture event listeners. - if (window && window['_captureEventListenerCount'][event.type]) { + if (window && window['__captureEventListenerCount__'][event.type]) { event.eventPhase = EventPhaseEnum.capturing; for (let i = composedPath.length - 1; i >= 0; i--) { composedPath[i].dispatchEvent(event); - if (event._propagationStopped || event._immediatePropagationStopped) { + if (event.__propagationStopped__ || event.__immediatePropagationStopped__) { break; } } @@ -124,12 +127,16 @@ export default abstract class EventTarget implements IEventTarget { this.dispatchEvent(event); // Bubbling phase - if (event.bubbles && !event._propagationStopped && !event._immediatePropagationStopped) { + if ( + event.bubbles && + !event.__propagationStopped__ && + !event.__immediatePropagationStopped__ + ) { event.eventPhase = EventPhaseEnum.bubbling; for (let i = 1; i < composedPath.length; i++) { composedPath[i].dispatchEvent(event); - if (event._propagationStopped || event._immediatePropagationStopped) { + if (event.__propagationStopped__ || event.__immediatePropagationStopped__) { break; } } @@ -141,7 +148,7 @@ export default abstract class EventTarget implements IEventTarget { return !(event.cancelable && event.defaultPrevented); } - event._currentTarget = this; + event.__currentTarget__ = this; if (event.eventPhase !== EventPhaseEnum.capturing) { const onEventName = 'on' + event.type.toLowerCase(); @@ -160,10 +167,10 @@ export default abstract class EventTarget implements IEventTarget { } } - if (this._listeners[event.type]) { + if (this.__listeners__[event.type]) { // We need to clone the arrays because the listeners may remove themselves while we are iterating. - const listeners = this._listeners[event.type].slice(); - const listenerOptions = this._listenerOptions[event.type].slice(); + const listeners = this.__listeners__[event.type].slice(); + const listenerOptions = this.__listenerOptions__[event.type].slice(); for (let i = 0, max = listeners.length; i < max; i++) { const listener = listeners[i]; @@ -177,7 +184,7 @@ export default abstract class EventTarget implements IEventTarget { } if (options?.passive) { - event._isInPassiveEventListener = true; + event.__isInPassiveEventListener__ = true; } // We can end up in a never ending loop if the listener for the error event on Window also throws an error. @@ -205,7 +212,7 @@ export default abstract class EventTarget implements IEventTarget { } } - event._isInPassiveEventListener = false; + event.__isInPassiveEventListener__ = false; if (options?.once) { // At this time, listeners and listenersOptions are cloned arrays. When the original value is deleted, @@ -217,7 +224,7 @@ export default abstract class EventTarget implements IEventTarget { max--; } - if (event._immediatePropagationStopped) { + if (event.__immediatePropagationStopped__) { return !(event.cancelable && event.defaultPrevented); } } @@ -259,12 +266,12 @@ export default abstract class EventTarget implements IEventTarget { * * @returns Window. */ - public _getWindow(): IBrowserWindow | null { + #getWindow(): IBrowserWindow | null { if (((this)).ownerDocument) { - return ((this)).ownerDocument._defaultView; + return ((this)).ownerDocument.__defaultView__; } - if (((this))._defaultView) { - return ((this))._defaultView; + if (((this)).__defaultView__) { + return ((this)).__defaultView__; } if (((this)).document) { return (this); diff --git a/packages/happy-dom/src/fetch/AbortController.ts b/packages/happy-dom/src/fetch/AbortController.ts index 8ddf30237..3a73994b7 100644 --- a/packages/happy-dom/src/fetch/AbortController.ts +++ b/packages/happy-dom/src/fetch/AbortController.ts @@ -21,6 +21,6 @@ export default class AbortController { * @param [reason] Reason. */ public abort(reason?: string): void { - this.signal._abort(reason); + this.signal.__abort__(reason); } } diff --git a/packages/happy-dom/src/fetch/AbortSignal.ts b/packages/happy-dom/src/fetch/AbortSignal.ts index 038a999de..269fb23f0 100644 --- a/packages/happy-dom/src/fetch/AbortSignal.ts +++ b/packages/happy-dom/src/fetch/AbortSignal.ts @@ -23,7 +23,7 @@ export default class AbortSignal extends EventTarget { * * @param [reason] Reason. */ - public _abort(reason?: string): void { + public __abort__(reason?: string): void { if (this.aborted) { return; } diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index e87107ce6..7e6401258 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -78,7 +78,7 @@ export default class Fetch { ? new options.browserFrame.window.Request(options.url, options.init) : url; if (options.contentType) { - (this.request._contentType) = options.contentType; + (this.request.__contentType__) = options.contentType; } this.redirectCount = options.redirectCount || 0; } @@ -90,7 +90,7 @@ export default class Fetch { */ public send(): Promise { return new Promise((resolve, reject) => { - const taskID = this.#browserFrame._asyncTaskManager.startTask(() => + const taskID = this.#browserFrame.__asyncTaskManager__.startTask(() => this.onAsyncTaskManagerAbort() ); @@ -99,18 +99,18 @@ export default class Fetch { } this.resolve = (response: IResponse | Promise): void => { - this.#browserFrame._asyncTaskManager.endTask(taskID); + this.#browserFrame.__asyncTaskManager__.endTask(taskID); resolve(response); }; this.reject = (error: Error): void => { - this.#browserFrame._asyncTaskManager.endTask(taskID); + this.#browserFrame.__asyncTaskManager__.endTask(taskID); reject(error); }; this.prepareRequest(); this.validateRequest(); - if (this.request._url.protocol === 'data:') { + if (this.request.__url__.protocol === 'data:') { const result = DataURIParser.parse(this.request.url); this.response = new this.#window.Response(result.buffer, { headers: { 'Content-Type': result.type } @@ -126,9 +126,9 @@ export default class Fetch { this.request.signal.addEventListener('abort', this.listeners.onSignalAbort); - const send = (this.request._url.protocol === 'https:' ? HTTPS : HTTP).request; + const send = (this.request.__url__.protocol === 'https:' ? HTTPS : HTTP).request; - this.nodeRequest = send(this.request._url.href, { + this.nodeRequest = send(this.request.__url__.href, { method: this.request.method, headers: this.getRequestHeaders() }); @@ -429,7 +429,7 @@ export default class Fetch { } const headers = new Headers(this.request.headers); - let body: Stream.Readable | Buffer | null = this.request._bodyBuffer; + let body: Stream.Readable | Buffer | null = this.request.__bodyBuffer__; if (!body && this.request.body) { // Piping a used request body is not possible. @@ -466,7 +466,7 @@ export default class Fetch { headers.delete('cookie2'); } - if (nodeResponse.statusCode !== 303 && this.request.body && !this.request._bodyBuffer) { + if (nodeResponse.statusCode !== 303 && this.request.body && !this.request.__bodyBuffer__) { this.finalizeRequest(); this.reject( new DOMException( @@ -501,7 +501,7 @@ export default class Fetch { url: locationURL, init: requestInit, redirectCount: this.redirectCount + 1, - contentType: !shouldBecomeGetRequest ? this.request._contentType : undefined + contentType: !shouldBecomeGetRequest ? this.request.__contentType__ : undefined }); this.finalizeRequest(); @@ -527,12 +527,12 @@ export default class Fetch { } if (this.request.referrer && this.request.referrer !== 'no-referrer') { - this.request._referrer = FetchRequestReferrerUtility.getSentReferrer( + this.request.__referrer__ = FetchRequestReferrerUtility.getSentReferrer( this.#window, this.request ); } else { - this.request._referrer = 'no-referrer'; + this.request.__referrer__ = 'no-referrer'; } } @@ -542,11 +542,11 @@ export default class Fetch { * @throws {Error} Throws an error if the request is invalid. */ private validateRequest(): void { - if (!SUPPORTED_SCHEMAS.includes(this.request._url.protocol)) { + if (!SUPPORTED_SCHEMAS.includes(this.request.__url__.protocol)) { throw new DOMException( `Failed to fetch from "${ this.request.url - }": URL scheme "${this.request._url.protocol.replace(/:$/, '')}" is not supported.`, + }": URL scheme "${this.request.__url__.protocol.replace(/:$/, '')}" is not supported.`, DOMExceptionNameEnum.notSupportedError ); } @@ -559,7 +559,7 @@ export default class Fetch { */ private getRequestHeaders(): { [key: string]: string } { const headers = new Headers(this.request.headers); - const isCORS = FetchCORSUtility.isCORS(this.#window.location, this.request._url); + const isCORS = FetchCORSUtility.isCORS(this.#window.location, this.request.__url__); // TODO: Maybe we need to add support for OPTIONS request with 'Access-Control-Allow-*' headers? if ( @@ -577,8 +577,8 @@ export default class Fetch { headers.set('User-Agent', this.#window.navigator.userAgent); } - if (this.request._referrer instanceof URL) { - headers.set('Referer', this.request._referrer.href); + if (this.request.__referrer__ instanceof URL) { + headers.set('Referer', this.request.__referrer__.href); } if ( @@ -598,18 +598,18 @@ export default class Fetch { headers.set('Accept', '*/*'); } - if (!headers.has('Content-Length') && this.request._contentLength !== null) { - headers.set('Content-Length', String(this.request._contentLength)); + if (!headers.has('Content-Length') && this.request.__contentLength__ !== null) { + headers.set('Content-Length', String(this.request.__contentLength__)); } - if (!headers.has('Content-Type') && this.request._contentType) { - headers.set('Content-Type', this.request._contentType); + if (!headers.has('Content-Type') && this.request.__contentType__) { + headers.set('Content-Type', this.request.__contentType__); } // We need to convert the headers to Node request headers. const httpRequestHeaders = {}; - for (const header of Object.values(headers._entries)) { + for (const header of Object.values(headers.__entries__)) { httpRequestHeaders[header.name] = header.value; } @@ -646,7 +646,7 @@ export default class Fetch { // "set-cookie" and "set-cookie2" are not allowed in response headers according to spec. if (lowerKey === 'set-cookie' || lowerKey === 'set-cookie2') { this.#browserFrame.page.context.cookieContainer.addCookies([ - CookieStringUtility.stringToCookie(this.request._url, header) + CookieStringUtility.stringToCookie(this.request.__url__, header) ]); } else { headers.append(key, header); diff --git a/packages/happy-dom/src/fetch/Headers.ts b/packages/happy-dom/src/fetch/Headers.ts index 3d549b65f..e5e5b4892 100644 --- a/packages/happy-dom/src/fetch/Headers.ts +++ b/packages/happy-dom/src/fetch/Headers.ts @@ -9,7 +9,7 @@ import IHeadersInit from './types/IHeadersInit.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/Headers */ export default class Headers implements IHeaders { - public _entries: { [k: string]: { name: string; value: string } } = {}; + public __entries__: { [k: string]: { name: string; value: string } } = {}; /** * Constructor. @@ -19,7 +19,7 @@ export default class Headers implements IHeaders { constructor(init?: IHeadersInit) { if (init) { if (init instanceof Headers) { - this._entries = JSON.parse(JSON.stringify(init._entries)); + this.__entries__ = JSON.parse(JSON.stringify(init.__entries__)); } else if (Array.isArray(init)) { for (const entry of init) { if (entry.length !== 2) { @@ -46,10 +46,10 @@ export default class Headers implements IHeaders { */ public append(name: string, value: string): void { const lowerName = name.toLowerCase(); - if (this._entries[lowerName]) { - this._entries[lowerName].value += `, ${value}`; + if (this.__entries__[lowerName]) { + this.__entries__[lowerName].value += `, ${value}`; } else { - this._entries[lowerName] = { + this.__entries__[lowerName] = { name, value }; @@ -62,7 +62,7 @@ export default class Headers implements IHeaders { * @param name Name. */ public delete(name: string): void { - delete this._entries[name.toLowerCase()]; + delete this.__entries__[name.toLowerCase()]; } /** @@ -72,7 +72,7 @@ export default class Headers implements IHeaders { * @returns Value. */ public get(name: string): string | null { - return this._entries[name.toLowerCase()]?.value || null; + return this.__entries__[name.toLowerCase()]?.value || null; } /** @@ -82,7 +82,7 @@ export default class Headers implements IHeaders { * @param value Value. */ public set(name: string, value: string): void { - this._entries[name.toLowerCase()] = { + this.__entries__[name.toLowerCase()] = { name, value }; @@ -95,7 +95,7 @@ export default class Headers implements IHeaders { * @returns "true" if the Headers object contains the key. */ public has(name: string): boolean { - return !!this._entries[name.toLowerCase()]; + return !!this.__entries__[name.toLowerCase()]; } /** @@ -104,8 +104,8 @@ export default class Headers implements IHeaders { * @param callback Callback. */ public forEach(callback: (name: string, value: string, thisArg: IHeaders) => void): void { - for (const key of Object.keys(this._entries)) { - callback(this._entries[key].value, this._entries[key].name, this); + for (const key of Object.keys(this.__entries__)) { + callback(this.__entries__[key].value, this.__entries__[key].name, this); } } @@ -115,7 +115,7 @@ export default class Headers implements IHeaders { * @returns Iterator. */ public *keys(): IterableIterator { - for (const header of Object.values(this._entries)) { + for (const header of Object.values(this.__entries__)) { yield header.name; } } @@ -126,7 +126,7 @@ export default class Headers implements IHeaders { * @returns Iterator. */ public *values(): IterableIterator { - for (const header of Object.values(this._entries)) { + for (const header of Object.values(this.__entries__)) { yield header.value; } } @@ -137,7 +137,7 @@ export default class Headers implements IHeaders { * @returns Iterator. */ public *entries(): IterableIterator<[string, string]> { - for (const header of Object.values(this._entries)) { + for (const header of Object.values(this.__entries__)) { yield [header.name, header.value]; } } @@ -148,7 +148,7 @@ export default class Headers implements IHeaders { * @returns Iterator. */ public *[Symbol.iterator](): IterableIterator<[string, string]> { - for (const header of Object.values(this._entries)) { + for (const header of Object.values(this.__entries__)) { yield [header.name, header.value]; } } diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts index 77d59e2ea..40e8391d1 100644 --- a/packages/happy-dom/src/fetch/Request.ts +++ b/packages/happy-dom/src/fetch/Request.ts @@ -43,11 +43,11 @@ export default class Request implements IRequest { public readonly credentials: IRequestCredentials; // Internal properties - public readonly _contentLength: number | null = null; - public readonly _contentType: string | null = null; - public _referrer: '' | 'no-referrer' | 'client' | URL = 'client'; - public readonly _url: URL; - public readonly _bodyBuffer: Buffer | null; + public readonly __contentLength__: number | null = null; + public readonly __contentType__: string | null = null; + public __referrer__: '' | 'no-referrer' | 'client' | URL = 'client'; + public readonly __url__: URL; + public readonly __bodyBuffer__: Buffer | null; readonly #window: IBrowserWindow; readonly #asyncTaskManager: AsyncTaskManager; @@ -73,12 +73,12 @@ export default class Request implements IRequest { this.method = (init?.method || (input).method || 'GET').toUpperCase(); const { stream, buffer, contentType, contentLength } = FetchBodyUtility.getBodyStream( - input instanceof Request && (input._bodyBuffer || input.body) - ? input._bodyBuffer || FetchBodyUtility.cloneRequestBodyStream(input) + input instanceof Request && (input.__bodyBuffer__ || input.body) + ? input.__bodyBuffer__ || FetchBodyUtility.cloneRequestBodyStream(input) : init?.body ); - this._bodyBuffer = buffer; + this.__bodyBuffer__ = buffer; this.body = stream; this.credentials = init?.credentials || (input).credentials || 'same-origin'; this.headers = new Headers(init?.headers || (input).headers || {}); @@ -86,18 +86,18 @@ export default class Request implements IRequest { FetchRequestHeaderUtility.removeForbiddenHeaders(this.headers); if (contentLength) { - this._contentLength = contentLength; + this.__contentLength__ = contentLength; } else if (!this.body && (this.method === 'POST' || this.method === 'PUT')) { - this._contentLength = 0; + this.__contentLength__ = 0; } if (contentType) { if (!this.headers.has('Content-Type')) { this.headers.set('Content-Type', contentType); } - this._contentType = contentType; - } else if (input instanceof Request && input._contentType) { - this._contentType = input._contentType; + this.__contentType__ = contentType; + } else if (input instanceof Request && input.__contentType__) { + this.__contentType__ = input.__contentType__; } this.redirect = init?.redirect || (input).redirect || 'follow'; @@ -105,7 +105,7 @@ export default class Request implements IRequest { (init?.referrerPolicy || (input).referrerPolicy || '').toLowerCase() ); this.signal = init?.signal || (input).signal || new AbortSignal(); - this._referrer = FetchRequestReferrerUtility.getInitialReferrer( + this.__referrer__ = FetchRequestReferrerUtility.getInitialReferrer( injected.window, init?.referrer !== null && init?.referrer !== undefined ? init?.referrer @@ -113,13 +113,13 @@ export default class Request implements IRequest { ); if (input instanceof URL) { - this._url = input; + this.__url__ = input; } else { try { if (input instanceof Request && input.url) { - this._url = new URL(input.url, injected.window.location); + this.__url__ = new URL(input.url, injected.window.location); } else { - this._url = new URL(input, injected.window.location); + this.__url__ = new URL(input, injected.window.location); } } catch (error) { throw new DOMException( @@ -136,7 +136,7 @@ export default class Request implements IRequest { } FetchRequestValidationUtility.validateBody(this); - FetchRequestValidationUtility.validateURL(this._url); + FetchRequestValidationUtility.validateURL(this.__url__); FetchRequestValidationUtility.validateReferrerPolicy(this.referrerPolicy); FetchRequestValidationUtility.validateRedirect(this.redirect); } @@ -144,8 +144,8 @@ export default class Request implements IRequest { /** * Returns owner document. */ - protected get _ownerDocument(): IDocument { - throw new Error('_ownerDocument needs to be implemented by sub-class.'); + protected get __ownerDocument__(): IDocument { + throw new Error('__ownerDocument__ needs to be implemented by sub-class.'); } /** @@ -154,15 +154,15 @@ export default class Request implements IRequest { * @returns Referrer. */ public get referrer(): string { - if (!this._referrer || this._referrer === 'no-referrer') { + if (!this.__referrer__ || this.__referrer__ === 'no-referrer') { return ''; } - if (this._referrer === 'client') { + if (this.__referrer__ === 'client') { return 'about:client'; } - return this._referrer.toString(); + return this.__referrer__.toString(); } /** @@ -171,7 +171,7 @@ export default class Request implements IRequest { * @returns URL. */ public get url(): string { - return this._url.href; + return this.__url__.href; } /** @@ -198,7 +198,7 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this.#asyncTaskManager.startTask(() => this.signal._abort()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal.__abort__()); let buffer: Buffer; try { @@ -240,7 +240,7 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this.#asyncTaskManager.startTask(() => this.signal._abort()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal.__abort__()); let buffer: Buffer; try { @@ -270,7 +270,7 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this.#asyncTaskManager.startTask(() => this.signal._abort()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal.__abort__()); let buffer: Buffer; try { @@ -310,11 +310,11 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this.#asyncTaskManager.startTask(() => this.signal._abort()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal.__abort__()); let formData: FormData; try { - const type = this._contentType; + const type = this.__contentType__; formData = await MultipartFormDataParser.streamToFormData(this.body, type); } catch (error) { this.#asyncTaskManager.endTask(taskID); diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts index 1037da3e8..bc2f9194b 100644 --- a/packages/happy-dom/src/fetch/Response.ts +++ b/packages/happy-dom/src/fetch/Response.ts @@ -29,7 +29,7 @@ const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; */ export default class Response implements IResponse { // Needs to be injected by sub-class. - protected static _window: IBrowserWindow; + protected static __window__: IBrowserWindow; // Public properties public readonly body: Stream.Readable | null = null; @@ -279,7 +279,7 @@ export default class Response implements IResponse { ); } - return new this._window.Response(null, { + return new this.__window__.Response(null, { headers: { location: new URL(url).toString() }, @@ -295,7 +295,7 @@ export default class Response implements IResponse { * @returns Response. */ public static error(): Response { - const response = new this._window.Response(null, { status: 0, statusText: '' }); + const response = new this.__window__.Response(null, { status: 0, statusText: '' }); (response.type) = 'error'; return response; } @@ -315,13 +315,13 @@ export default class Response implements IResponse { throw new TypeError('data is not JSON serializable'); } - const headers = new this._window.Headers(init && init.headers); + const headers = new this.__window__.Headers(init && init.headers); if (!headers.has('content-type')) { headers.set('content-type', 'application/json'); } - return new this._window.Response(body, { + return new this.__window__.Response(body, { status: 200, ...init, headers diff --git a/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts b/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts index 3062c3cba..d73ac9b8f 100644 --- a/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts +++ b/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts @@ -82,7 +82,7 @@ export default class MultipartFormDataParser { )}"\r\nContent-Type: ${value.type || 'application/octet-stream'}\r\n\r\n` ) ); - chunks.push(value._buffer); + chunks.push(value.__buffer__); chunks.push(Buffer.from('\r\n')); } } diff --git a/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts index 60a8ef127..7148d43fd 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts @@ -39,7 +39,7 @@ export default class FetchBodyUtility { contentLength: buffer.length }; } else if (body instanceof Blob) { - const buffer = (body)._buffer; + const buffer = (body).__buffer__; return { buffer, stream: Stream.Readable.from(buffer), @@ -151,7 +151,7 @@ export default class FetchBodyUtility { if ( (body).readableEnded === false || - (body)['_readableState']?.ended === false + (body)['__readableState__']?.ended === false ) { throw new DOMException( `Premature close of server response.`, diff --git a/packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts index f7bbb3b9f..9a7939799 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts @@ -34,13 +34,13 @@ export default class FetchRequestHeaderUtility { * @param headers Headers. */ public static removeForbiddenHeaders(headers: IHeaders): void { - for (const key of Object.keys((headers)._entries)) { + for (const key of Object.keys((headers).__entries__)) { if ( FORBIDDEN_HEADER_NAMES.includes(key) || key.startsWith('proxy-') || key.startsWith('sec-') ) { - delete (headers)._entries[key]; + delete (headers).__entries__[key]; } } } diff --git a/packages/happy-dom/src/file/Blob.ts b/packages/happy-dom/src/file/Blob.ts index 5812eecfa..f47d04136 100644 --- a/packages/happy-dom/src/file/Blob.ts +++ b/packages/happy-dom/src/file/Blob.ts @@ -8,8 +8,8 @@ import IBlob from './IBlob.js'; * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/file-api/Blob-impl.js (MIT licensed). */ export default class Blob implements IBlob { - public _buffer: Buffer = null; public readonly type: string = ''; + public __buffer__: Buffer = null; /** * Constructor. @@ -31,7 +31,7 @@ export default class Blob implements IBlob { if (bit instanceof ArrayBuffer) { buffer = Buffer.from(new Uint8Array(bit)); } else if (bit instanceof Blob) { - buffer = bit._buffer; + buffer = bit.__buffer__; } else if (bit instanceof Buffer) { buffer = bit; } else if (ArrayBuffer.isView(bit)) { @@ -44,7 +44,7 @@ export default class Blob implements IBlob { } } - this._buffer = Buffer.concat(buffers); + this.__buffer__ = Buffer.concat(buffers); if (options && options.type && options.type.match(/^[\u0020-\u007E]*$/)) { this.type = String(options.type).toLowerCase(); @@ -57,7 +57,7 @@ export default class Blob implements IBlob { * @returns Size. */ public get size(): number { - return this._buffer.length; + return this.__buffer__.length; } /** @@ -100,12 +100,12 @@ export default class Blob implements IBlob { const span = Math.max(relativeEnd - relativeStart, 0); - const buffer = this._buffer; + const buffer = this.__buffer__; const slicedBuffer = buffer.slice(relativeStart, relativeStart + span); const blob = new Blob([], { type: relativeContentType }); - (blob._buffer) = slicedBuffer; + (blob.__buffer__) = slicedBuffer; return blob; } @@ -123,7 +123,7 @@ export default class Blob implements IBlob { * */ public async arrayBuffer(): Promise { - return new Uint8Array(this._buffer).buffer; + return new Uint8Array(this.__buffer__).buffer; } /** @@ -132,7 +132,7 @@ export default class Blob implements IBlob { * @returns Text. */ public async text(): Promise { - return this._buffer.toString(); + return this.__buffer__.toString(); } /** diff --git a/packages/happy-dom/src/file/FileReader.ts b/packages/happy-dom/src/file/FileReader.ts index 9cdeccbef..29ac47e8b 100644 --- a/packages/happy-dom/src/file/FileReader.ts +++ b/packages/happy-dom/src/file/FileReader.ts @@ -48,7 +48,7 @@ export default class FileReader extends EventTarget { * @param blob Blob. */ public readAsArrayBuffer(blob: Blob): void { - this._readFile(blob, FileReaderFormatEnum.buffer); + this.#readFile(blob, FileReaderFormatEnum.buffer); } /** @@ -57,7 +57,7 @@ export default class FileReader extends EventTarget { * @param blob Blob. */ public readAsBinaryString(blob: Blob): void { - this._readFile(blob, FileReaderFormatEnum.binaryString); + this.#readFile(blob, FileReaderFormatEnum.binaryString); } /** @@ -66,7 +66,7 @@ export default class FileReader extends EventTarget { * @param blob Blob. */ public readAsDataURL(blob: Blob): void { - this._readFile(blob, FileReaderFormatEnum.dataURL); + this.#readFile(blob, FileReaderFormatEnum.dataURL); } /** @@ -76,7 +76,7 @@ export default class FileReader extends EventTarget { * @param [encoding] Encoding. */ public readAsText(blob: Blob, encoding: string = null): void { - this._readFile( + this.#readFile( blob, FileReaderFormatEnum.text, WhatwgEncoding.labelToName(encoding) || 'UTF-8' @@ -115,7 +115,7 @@ export default class FileReader extends EventTarget { * @param format Format. * @param [encoding] Encoding. */ - private _readFile(blob: Blob, format: FileReaderFormatEnum, encoding: string = null): void { + #readFile(blob: Blob, format: FileReaderFormatEnum, encoding: string = null): void { if (this.readyState === FileReaderReadyStateEnum.loading) { throw new DOMException( 'The object is in an invalid state.', @@ -133,7 +133,7 @@ export default class FileReader extends EventTarget { this.dispatchEvent(new ProgressEvent(FileReaderEventTypeEnum.loadstart)); - let data = blob._buffer; + let data = blob.__buffer__; if (!data) { data = Buffer.alloc(0); } diff --git a/packages/happy-dom/src/form-data/FormData.ts b/packages/happy-dom/src/form-data/FormData.ts index ba0ae0462..484766299 100644 --- a/packages/happy-dom/src/form-data/FormData.ts +++ b/packages/happy-dom/src/form-data/FormData.ts @@ -18,7 +18,7 @@ const SUBMITTABLE_ELEMENTS = ['BUTTON', 'INPUT', 'OBJECT', 'SELECT', 'TEXTAREA'] * @see https://developer.mozilla.org/en-US/docs/Web/API/FormData */ export default class FormData implements Iterable<[string, string | File]> { - private _entries: FormDataEntry[] = []; + #entries: FormDataEntry[] = []; /** * Constructor. @@ -27,8 +27,8 @@ export default class FormData implements Iterable<[string, string | File]> { */ constructor(form?: IHTMLFormElement) { if (form) { - for (const name of Object.keys((form.elements)._namedItems)) { - let radioNodeList = (form.elements)._namedItems[name]; + for (const name of Object.keys((form.elements).__namedItems__)) { + let radioNodeList = (form.elements).__namedItems__[name]; if ( radioNodeList[0].tagName === 'INPUT' && @@ -69,7 +69,7 @@ export default class FormData implements Iterable<[string, string | File]> { * @param callback Callback. */ public forEach(callback: (key: string, value: string | File, thisArg: FormData) => void): void { - for (const entry of this._entries) { + for (const entry of this.#entries) { callback.call(this, entry.name, entry.value, this); } } @@ -82,9 +82,9 @@ export default class FormData implements Iterable<[string, string | File]> { * @param [filename] Filename. */ public append(name: string, value: string | Blob | File, filename?: string): void { - this._entries.push({ + this.#entries.push({ name, - value: this._parseValue(value, filename) + value: this.#parseValue(value, filename) }); } @@ -95,12 +95,12 @@ export default class FormData implements Iterable<[string, string | File]> { */ public delete(name: string): void { const newEntries: FormDataEntry[] = []; - for (const entry of this._entries) { + for (const entry of this.#entries) { if (entry.name !== name) { newEntries.push(entry); } } - this._entries = newEntries; + this.#entries = newEntries; } /** @@ -110,7 +110,7 @@ export default class FormData implements Iterable<[string, string | File]> { * @returns Value. */ public get(name: string): string | File | null { - for (const entry of this._entries) { + for (const entry of this.#entries) { if (entry.name === name) { return entry.value; } @@ -126,7 +126,7 @@ export default class FormData implements Iterable<[string, string | File]> { */ public getAll(name: string): Array { const values: Array = []; - for (const entry of this._entries) { + for (const entry of this.#entries) { if (entry.name === name) { values.push(entry.value); } @@ -141,7 +141,7 @@ export default class FormData implements Iterable<[string, string | File]> { * @returns "true" if the FormData object contains the key. */ public has(name: string): boolean { - for (const entry of this._entries) { + for (const entry of this.#entries) { if (entry.name === name) { return true; } @@ -157,9 +157,9 @@ export default class FormData implements Iterable<[string, string | File]> { * @param [filename] Filename. */ public set(name: string, value: string | Blob | File, filename?: string): void { - for (const entry of this._entries) { + for (const entry of this.#entries) { if (entry.name === name) { - entry.value = this._parseValue(value, filename); + entry.value = this.#parseValue(value, filename); return; } } @@ -172,7 +172,7 @@ export default class FormData implements Iterable<[string, string | File]> { * @returns Iterator. */ public *keys(): IterableIterator { - for (const entry of this._entries) { + for (const entry of this.#entries) { yield entry.name; } } @@ -183,7 +183,7 @@ export default class FormData implements Iterable<[string, string | File]> { * @returns Iterator. */ public *values(): IterableIterator { - for (const entry of this._entries) { + for (const entry of this.#entries) { yield entry.value; } } @@ -194,7 +194,7 @@ export default class FormData implements Iterable<[string, string | File]> { * @returns Iterator. */ public *entries(): IterableIterator<[string, string | File]> { - for (const entry of this._entries) { + for (const entry of this.#entries) { yield [entry.name, entry.value]; } } @@ -205,7 +205,7 @@ export default class FormData implements Iterable<[string, string | File]> { * @returns Iterator. */ public *[Symbol.iterator](): IterableIterator<[string, string | File]> { - for (const entry of this._entries) { + for (const entry of this.#entries) { yield [entry.name, entry.value]; } } @@ -217,17 +217,17 @@ export default class FormData implements Iterable<[string, string | File]> { * @param [filename] Filename. * @returns Parsed value. */ - private _parseValue(value: string | Blob | File, filename?: string): string | File { + #parseValue(value: string | Blob | File, filename?: string): string | File { if (value instanceof Blob && !(value instanceof File)) { const file = new File([], 'blob', { type: value.type }); - file._buffer = value._buffer; + file.__buffer__ = value.__buffer__; return file; } if (value instanceof File) { if (filename) { const file = new File([], filename, { type: value.type, lastModified: value.lastModified }); - file._buffer = value._buffer; + file.__buffer__ = value.__buffer__; return file; } return value; diff --git a/packages/happy-dom/src/history/History.ts b/packages/happy-dom/src/history/History.ts index 6ad7dd33e..bc2871fd0 100644 --- a/packages/happy-dom/src/history/History.ts +++ b/packages/happy-dom/src/history/History.ts @@ -9,7 +9,7 @@ import HistoryScrollRestorationEnum from './HistoryScrollRestorationEnum.js'; export default class History { public readonly length = 0; public readonly state = null; - private _scrollRestoration = HistoryScrollRestorationEnum.auto; + #scrollRestoration = HistoryScrollRestorationEnum.auto; /** * Returns scroll restoration. @@ -17,7 +17,7 @@ export default class History { * @returns Sroll restoration. */ public get scrollRestoration(): HistoryScrollRestorationEnum { - return this._scrollRestoration; + return this.#scrollRestoration; } /** @@ -26,9 +26,9 @@ export default class History { * @param scrollRestoration Sroll restoration. */ public set scrollRestoration(scrollRestoration: HistoryScrollRestorationEnum) { - this._scrollRestoration = HistoryScrollRestorationEnum[scrollRestoration] + this.#scrollRestoration = HistoryScrollRestorationEnum[scrollRestoration] ? scrollRestoration - : this._scrollRestoration; + : this.#scrollRestoration; } /** diff --git a/packages/happy-dom/src/match-media/MediaQueryList.ts b/packages/happy-dom/src/match-media/MediaQueryList.ts index 4e089350a..43cabc333 100644 --- a/packages/happy-dom/src/match-media/MediaQueryList.ts +++ b/packages/happy-dom/src/match-media/MediaQueryList.ts @@ -14,10 +14,10 @@ import MediaQueryParser from './MediaQueryParser.js'; */ export default class MediaQueryList extends EventTarget { public onchange: (event: Event) => void = null; - private _ownerWindow: IBrowserWindow; - private _items: IMediaQueryItem[] | null = null; - private _media: string; - private _rootFontSize: string | number | null = null; + #ownerWindow: IBrowserWindow; + #items: IMediaQueryItem[] | null = null; + #media: string; + #rootFontSize: string | number | null = null; /** * Constructor. @@ -33,9 +33,9 @@ export default class MediaQueryList extends EventTarget { rootFontSize?: string | number; }) { super(); - this._ownerWindow = options.ownerWindow; - this._media = options.media; - this._rootFontSize = options.rootFontSize || null; + this.#ownerWindow = options.ownerWindow; + this.#media = options.media; + this.#rootFontSize = options.rootFontSize || null; } /** @@ -44,15 +44,15 @@ export default class MediaQueryList extends EventTarget { * @returns Media. */ public get media(): string { - this._items = - this._items || + this.#items = + this.#items || MediaQueryParser.parse({ - ownerWindow: this._ownerWindow, - mediaQuery: this._media, - rootFontSize: this._rootFontSize + ownerWindow: this.#ownerWindow, + mediaQuery: this.#media, + rootFontSize: this.#rootFontSize }); - return this._items.map((item) => item.toString()).join(', '); + return this.#items.map((item) => item.toString()).join(', '); } /** @@ -61,15 +61,15 @@ export default class MediaQueryList extends EventTarget { * @returns Matches. */ public get matches(): boolean { - this._items = - this._items || + this.#items = + this.#items || MediaQueryParser.parse({ - ownerWindow: this._ownerWindow, - mediaQuery: this._media, - rootFontSize: this._rootFontSize + ownerWindow: this.#ownerWindow, + mediaQuery: this.#media, + rootFontSize: this.#rootFontSize }); - for (const item of this._items) { + for (const item of this.#items) { if (!item.matches()) { return false; } @@ -112,8 +112,8 @@ export default class MediaQueryList extends EventTarget { this.dispatchEvent(new MediaQueryListEvent('change', { matches, media: this.media })); } }; - listener['_windowResizeListener'] = resizeListener; - this._ownerWindow.addEventListener('resize', resizeListener); + listener['__windowResizeListener__'] = resizeListener; + this.#ownerWindow.addEventListener('resize', resizeListener); } } @@ -125,8 +125,8 @@ export default class MediaQueryList extends EventTarget { listener: IEventListener | ((event: Event) => void) ): void { super.removeEventListener(type, listener); - if (type === 'change' && listener['_windowResizeListener']) { - this._ownerWindow.removeEventListener('resize', listener['_windowResizeListener']); + if (type === 'change' && listener['__windowResizeListener__']) { + this.#ownerWindow.removeEventListener('resize', listener['__windowResizeListener__']); } } } diff --git a/packages/happy-dom/src/mutation-observer/MutationObserver.ts b/packages/happy-dom/src/mutation-observer/MutationObserver.ts index 414158e82..7894447b4 100644 --- a/packages/happy-dom/src/mutation-observer/MutationObserver.ts +++ b/packages/happy-dom/src/mutation-observer/MutationObserver.ts @@ -49,7 +49,7 @@ export default class MutationObserver { this.listener.callback = this.callback.bind(this); this.listener.observer = this; - (target)._observe(this.listener); + (target).__observe__(this.listener); } /** @@ -57,7 +57,7 @@ export default class MutationObserver { */ public disconnect(): void { if (this.target) { - (this.target)._unobserve(this.listener); + (this.target).__unobserve__(this.listener); this.target = null; } } diff --git a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts index 7dcf80035..4548177cf 100644 --- a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts +++ b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts @@ -11,7 +11,7 @@ import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; export default class NamedNodeMap implements INamedNodeMap { [index: number]: IAttr; public length = 0; - protected _namedItems: { [k: string]: IAttr } = {}; + protected __namedItems__: { [k: string]: IAttr } = {}; /** * Returns string. @@ -49,7 +49,7 @@ export default class NamedNodeMap implements INamedNodeMap { * @returns Itme. */ public getNamedItem(name: string): IAttr | null { - return this._namedItems[name] || null; + return this.__namedItems__[name] || null; } /** @@ -82,7 +82,7 @@ export default class NamedNodeMap implements INamedNodeMap { * @returns Replaced item. */ public setNamedItem(item: IAttr): IAttr | null { - return this._setNamedItemWithoutConsequences(item); + return this.__setNamedItemWithoutConsequences__(item); } /** @@ -104,7 +104,7 @@ export default class NamedNodeMap implements INamedNodeMap { * @returns Removed item. */ public removeNamedItem(name: string): IAttr { - const item = this._removeNamedItem(name); + const item = this.__removeNamedItem__(name); if (!item) { throw new DOMException( `Failed to execute 'removeNamedItem' on 'NamedNodeMap': No item with name '${name}' was found.`, @@ -135,20 +135,20 @@ export default class NamedNodeMap implements INamedNodeMap { * @param item Item. * @returns Replaced item. */ - public _setNamedItemWithoutConsequences(item: IAttr): IAttr | null { + public __setNamedItemWithoutConsequences__(item: IAttr): IAttr | null { if (item.name) { - const replacedItem = this._namedItems[item.name] || null; + const replacedItem = this.__namedItems__[item.name] || null; - this._namedItems[item.name] = item; + this.__namedItems__[item.name] = item; if (replacedItem) { - this._removeNamedItemIndex(replacedItem); + this.__removeNamedItemIndex__(replacedItem); } this[this.length] = item; this.length++; - if (this._isValidPropertyName(item.name)) { + if (this.__isValidPropertyName__(item.name)) { this[item.name] = item; } @@ -163,8 +163,8 @@ export default class NamedNodeMap implements INamedNodeMap { * @param name Name of item. * @returns Removed item, or null if it didn't exist. */ - public _removeNamedItem(name: string): IAttr | null { - return this._removeNamedItemWithoutConsequences(name); + public __removeNamedItem__(name: string): IAttr | null { + return this.__removeNamedItemWithoutConsequences__(name); } /** @@ -173,20 +173,20 @@ export default class NamedNodeMap implements INamedNodeMap { * @param name Name of item. * @returns Removed item, or null if it didn't exist. */ - public _removeNamedItemWithoutConsequences(name: string): IAttr | null { - const removedItem = this._namedItems[name] || null; + public __removeNamedItemWithoutConsequences__(name: string): IAttr | null { + const removedItem = this.__namedItems__[name] || null; if (!removedItem) { return null; } - this._removeNamedItemIndex(removedItem); + this.__removeNamedItemIndex__(removedItem); if (this[name] === removedItem) { delete this[name]; } - delete this._namedItems[name]; + delete this.__namedItems__[name]; return removedItem; } @@ -196,7 +196,7 @@ export default class NamedNodeMap implements INamedNodeMap { * * @param item Item. */ - protected _removeNamedItemIndex(item: IAttr): void { + protected __removeNamedItemIndex__(item: IAttr): void { for (let i = 0; i < this.length; i++) { if (this[i] === item) { for (let b = i; b < this.length; b++) { @@ -218,7 +218,7 @@ export default class NamedNodeMap implements INamedNodeMap { * @param name Name. * @returns True if the property name is valid. */ - protected _isValidPropertyName(name: string): boolean { + protected __isValidPropertyName__(name: string): boolean { return ( !this.constructor.prototype.hasOwnProperty(name) && (isNaN(Number(name)) || name.includes('.')) diff --git a/packages/happy-dom/src/nodes/character-data/CharacterData.ts b/packages/happy-dom/src/nodes/character-data/CharacterData.ts index 1045cdf23..50d6b39c8 100644 --- a/packages/happy-dom/src/nodes/character-data/CharacterData.ts +++ b/packages/happy-dom/src/nodes/character-data/CharacterData.ts @@ -14,7 +14,7 @@ import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; * https://developer.mozilla.org/en-US/docs/Web/API/CharacterData. */ export default abstract class CharacterData extends Node implements ICharacterData { - protected _data = ''; + public __data__ = ''; /** * Constructor. @@ -25,7 +25,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa super(); if (data) { - this._data = data; + this.__data__ = data; } } @@ -35,7 +35,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa * @returns Text content. */ public get length(): number { - return this._data.length; + return this.__data__.length; } /** @@ -44,7 +44,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa * @returns Text content. */ public get data(): string { - return this._data; + return this.__data__; } /** @@ -53,16 +53,16 @@ export default abstract class CharacterData extends Node implements ICharacterDa * @param textContent Text content. */ public set data(data: string) { - const oldValue = this._data; - this._data = String(data); + const oldValue = this.__data__; + this.__data__ = String(data); if (this.isConnected) { - this.ownerDocument['_cacheID']++; + this.ownerDocument['__cacheID__']++; } // MutationObserver - if (this._observers.length > 0) { - for (const observer of this._observers) { + if (this.__observers__.length > 0) { + for (const observer of this.__observers__) { if (observer.options.characterData) { const record = new MutationRecord(); record.target = this; @@ -80,7 +80,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa * @returns Text content. */ public get textContent(): string { - return this._data; + return this.__data__; } /** @@ -98,7 +98,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa * @returns Node value. */ public get nodeValue(): string { - return this._data; + return this.__data__; } /** @@ -221,7 +221,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa */ public cloneNode(deep = false): ICharacterData { const clone = super.cloneNode(deep); - clone._data = this._data; + clone.__data__ = this.__data__; return clone; } } diff --git a/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts b/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts index 96052093f..c706c898c 100644 --- a/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts +++ b/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts @@ -38,7 +38,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode.ownerDocument, node) - ))._childNodes.slice(); + )).__childNodes__.slice(); for (const newChildNode of newChildNodes) { parent.insertBefore(newChildNode, childNode); } @@ -67,7 +67,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode.ownerDocument, node) - ))._childNodes.slice(); + )).__childNodes__.slice(); for (const newChildNode of newChildNodes) { parent.insertBefore(newChildNode, childNode); } @@ -96,7 +96,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode.ownerDocument, node) - ))._childNodes.slice(); + )).__childNodes__.slice(); for (const newChildNode of newChildNodes) { if (!nextSibling) { parent.appendChild(newChildNode); diff --git a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts index 9b918f8da..4182c5616 100644 --- a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts +++ b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts @@ -14,14 +14,14 @@ import INodeList from '../node/INodeList.js'; */ export default class DocumentFragment extends Node implements IDocumentFragment { public nodeType = Node.DOCUMENT_FRAGMENT_NODE; - public readonly _children: IHTMLCollection = new HTMLCollection(); - public _rootNode: INode = this; + public readonly __children__: IHTMLCollection = new HTMLCollection(); + public __rootNode__: INode = this; /** * Returns the document fragment children. */ public get children(): IHTMLCollection { - return this._children; + return this.__children__; } /** @@ -30,7 +30,7 @@ export default class DocumentFragment extends Node implements IDocumentFragment * @returns Element. */ public get childElementCount(): number { - return this._children.length; + return this.__children__.length; } /** @@ -39,7 +39,7 @@ export default class DocumentFragment extends Node implements IDocumentFragment * @returns Element. */ public get firstElementChild(): IElement { - return this._children[0] ?? null; + return this.__children__[0] ?? null; } /** @@ -48,7 +48,7 @@ export default class DocumentFragment extends Node implements IDocumentFragment * @returns Element. */ public get lastElementChild(): IElement { - return this._children[this._children.length - 1] ?? null; + return this.__children__[this.__children__.length - 1] ?? null; } /** @@ -58,7 +58,7 @@ export default class DocumentFragment extends Node implements IDocumentFragment */ public get textContent(): string { let result = ''; - for (const childNode of this._childNodes) { + for (const childNode of this.__childNodes__) { if (childNode.nodeType === Node.ELEMENT_NODE || childNode.nodeType === Node.TEXT_NODE) { result += childNode.textContent; } @@ -72,7 +72,7 @@ export default class DocumentFragment extends Node implements IDocumentFragment * @param textContent Text content. */ public set textContent(textContent: string) { - for (const child of this._childNodes.slice()) { + for (const child of this.__childNodes__.slice()) { this.removeChild(child); } if (textContent) { @@ -148,9 +148,9 @@ export default class DocumentFragment extends Node implements IDocumentFragment const clone = super.cloneNode(deep); if (deep) { - for (const node of clone._childNodes) { + for (const node of clone.__childNodes__) { if (node.nodeType === Node.ELEMENT_NODE) { - clone._children.push(node); + clone.__children__.push(node); } } } diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 2992b652a..180409115 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -57,22 +57,21 @@ export default class Document extends Node implements IDocument { public readonly readyState = DocumentReadyStateEnum.interactive; public readonly isConnected: boolean = true; public readonly defaultView: IBrowserWindow | null = null; - public readonly _defaultView: IBrowserWindow; + public readonly __defaultView__: IBrowserWindow; public readonly referrer = ''; - public readonly _windowClass: {} | null = null; - public readonly _children: IHTMLCollection = new HTMLCollection(); - public _activeElement: IHTMLElement = null; - public _nextActiveElement: IHTMLElement = null; - public _currentScript: IHTMLScriptElement = null; - public _rootNode = this; + public readonly __children__: IHTMLCollection = new HTMLCollection(); + public __activeElement__: IHTMLElement = null; + public __nextActiveElement__: IHTMLElement = null; + public __currentScript__: IHTMLScriptElement = null; + public __rootNode__ = this; // Used as an unique identifier which is updated whenever the DOM gets modified. - public _cacheID = 0; + public __cacheID__ = 0; - protected _isFirstWrite = true; - protected _isFirstWriteAfterOpen = false; + public __isFirstWrite__ = true; + public __isFirstWriteAfterOpen__ = false; - private _selection: Selection = null; + private __selection__: Selection = null; #browserFrame: IBrowserFrame; // Events @@ -197,7 +196,7 @@ export default class Document extends Node implements IDocument { super(); this.#browserFrame = injected.browserFrame; this.implementation = new DOMImplementation(injected.window); - this._defaultView = injected.window; + this.__defaultView__ = injected.window; } /** @@ -213,7 +212,7 @@ export default class Document extends Node implements IDocument { * Returns document children. */ public get children(): IHTMLCollection { - return this._children; + return this.__children__; } /** @@ -277,7 +276,7 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public get childElementCount(): number { - return this._children.length; + return this.__children__.length; } /** @@ -286,7 +285,7 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public get firstElementChild(): IElement { - return this._children[0] ?? null; + return this.__children__[0] ?? null; } /** @@ -295,7 +294,7 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public get lastElementChild(): IElement { - return this._children[this._children.length - 1] ?? null; + return this.__children__[this.__children__.length - 1] ?? null; } /** @@ -305,7 +304,10 @@ export default class Document extends Node implements IDocument { */ public get cookie(): string { return CookieStringUtility.cookiesToString( - this.#browserFrame.page.context.cookieContainer.getCookies(this._defaultView.location, true) + this.#browserFrame.page.context.cookieContainer.getCookies( + this.__defaultView__.location, + true + ) ); } @@ -316,7 +318,7 @@ export default class Document extends Node implements IDocument { */ public set cookie(cookie: string) { this.#browserFrame.page.context.cookieContainer.addCookies([ - CookieStringUtility.stringToCookie(this._defaultView.location, cookie) + CookieStringUtility.stringToCookie(this.__defaultView__.location, cookie) ]); } @@ -344,7 +346,7 @@ export default class Document extends Node implements IDocument { * @returns Document type. */ public get doctype(): IDocumentType { - for (const node of this._childNodes) { + for (const node of this.__childNodes__) { if (node instanceof DocumentType) { return node; } @@ -395,22 +397,22 @@ export default class Document extends Node implements IDocument { * @returns Active element. */ public get activeElement(): IHTMLElement { - if (this._activeElement && !this._activeElement.isConnected) { - this._activeElement = null; + if (this.__activeElement__ && !this.__activeElement__.isConnected) { + this.__activeElement__ = null; } - if (this._activeElement && this._activeElement instanceof Element) { + if (this.__activeElement__ && this.__activeElement__ instanceof Element) { let rootNode: IShadowRoot | IDocument = ( - this._activeElement.getRootNode() + this.__activeElement__.getRootNode() ); - let activeElement: IHTMLElement = this._activeElement; + let activeElement: IHTMLElement = this.__activeElement__; while (rootNode !== this) { activeElement = (rootNode).host; rootNode = activeElement ? activeElement.getRootNode() : this; } return activeElement; } - return this._activeElement || this.body || this.documentElement || null; + return this.__activeElement__ || this.body || this.documentElement || null; } /** @@ -428,7 +430,7 @@ export default class Document extends Node implements IDocument { * @returns Location. */ public get location(): Location { - return this._defaultView.location; + return this.__defaultView__.location; } /** @@ -451,7 +453,7 @@ export default class Document extends Node implements IDocument { if (element) { return element.href; } - return this._defaultView.location.href; + return this.__defaultView__.location.href; } /** @@ -460,7 +462,7 @@ export default class Document extends Node implements IDocument { * @returns the URL of the current document. * */ public get URL(): string { - return this._defaultView.location.href; + return this.__defaultView__.location.href; } /** @@ -504,7 +506,7 @@ export default class Document extends Node implements IDocument { * @returns the currently executing script element. */ public get currentScript(): IHTMLScriptElement { - return this._currentScript; + return this.__currentScript__; } /** @@ -607,7 +609,7 @@ export default class Document extends Node implements IDocument { name: string ): INodeList => { const matches = new NodeList(); - for (const child of (parentNode)._children) { + for (const child of (parentNode).__children__) { if (child.getAttributeNS(null, 'name') === name) { matches.push(child); } @@ -631,9 +633,9 @@ export default class Document extends Node implements IDocument { const clone = super.cloneNode(deep); if (deep) { - for (const node of clone._childNodes) { + for (const node of clone.__childNodes__) { if (node.nodeType === Node.ELEMENT_NODE) { - clone._children.push(node); + clone.__children__.push(node); } } } @@ -679,20 +681,20 @@ export default class Document extends Node implements IDocument { public write(html: string): void { const root = XMLParser.parse(this, html, { evaluateScripts: true }); - if (this._isFirstWrite || this._isFirstWriteAfterOpen) { - if (this._isFirstWrite) { - if (!this._isFirstWriteAfterOpen) { + if (this.__isFirstWrite__ || this.__isFirstWriteAfterOpen__) { + if (this.__isFirstWrite__) { + if (!this.__isFirstWriteAfterOpen__) { this.open(); } - this._isFirstWrite = false; + this.__isFirstWrite__ = false; } - this._isFirstWriteAfterOpen = false; + this.__isFirstWriteAfterOpen__ = false; let documentElement = null; let documentTypeNode = null; - for (const node of root._childNodes) { + for (const node of root.__childNodes__) { if (node['tagName'] === 'HTML') { documentElement = node; } else if (node.nodeType === NodeTypeEnum.documentTypeNode) { @@ -727,7 +729,7 @@ export default class Document extends Node implements IDocument { const rootBody = ParentNodeUtility.getElementByTagName(root, 'body'); const body = ParentNodeUtility.getElementByTagName(this, 'body'); if (rootBody && body) { - for (const child of rootBody._childNodes.slice()) { + for (const child of rootBody.__childNodes__.slice()) { body.appendChild(child); } } @@ -736,7 +738,7 @@ export default class Document extends Node implements IDocument { // Remaining nodes outside the element are added to the element. const body = ParentNodeUtility.getElementByTagName(this, 'body'); if (body) { - for (const child of root._childNodes.slice()) { + for (const child of root.__childNodes__.slice()) { if (child['tagName'] !== 'HTML' && child.nodeType !== NodeTypeEnum.documentTypeNode) { body.appendChild(child); } @@ -747,7 +749,7 @@ export default class Document extends Node implements IDocument { const bodyElement = this.createElement('body'); const headElement = this.createElement('head'); - for (const child of root._childNodes.slice()) { + for (const child of root.__childNodes__.slice()) { bodyElement.appendChild(child); } @@ -759,7 +761,7 @@ export default class Document extends Node implements IDocument { } else { const bodyNode = ParentNodeUtility.getElementByTagName(root, 'body'); const body = ParentNodeUtility.getElementByTagName(this, 'body'); - for (const child of ((bodyNode || root))._childNodes.slice()) { + for (const child of ((bodyNode || root)).__childNodes__.slice()) { body.appendChild(child); } } @@ -771,10 +773,10 @@ export default class Document extends Node implements IDocument { * @returns Document. */ public open(): IDocument { - this._isFirstWriteAfterOpen = true; + this.__isFirstWriteAfterOpen__ = true; - for (const eventType of Object.keys(this._listeners)) { - const listeners = this._listeners[eventType]; + for (const eventType of Object.keys(this.__listeners__)) { + const listeners = this.__listeners__[eventType]; if (listeners) { for (const listener of listeners) { this.removeEventListener(eventType, listener); @@ -782,7 +784,7 @@ export default class Document extends Node implements IDocument { } } - for (const child of this._childNodes.slice()) { + for (const child of this.__childNodes__.slice()) { this.removeChild(child); } @@ -826,22 +828,22 @@ export default class Document extends Node implements IDocument { let customElementClass; if (options && options.is) { - customElementClass = this._defaultView.customElements.get(String(options.is)); + customElementClass = this.__defaultView__.customElements.get(String(options.is)); } else { - customElementClass = this._defaultView.customElements.get(tagName); + customElementClass = this.__defaultView__.customElements.get(tagName); } const elementClass: typeof Element = - customElementClass || this._defaultView[ElementTag[tagName]] || HTMLUnknownElement; + customElementClass || this.__defaultView__[ElementTag[tagName]] || HTMLUnknownElement; - elementClass._ownerDocument = this; + elementClass.__ownerDocument__ = this; const element = new elementClass(); - elementClass._ownerDocument = null; + elementClass.__ownerDocument__ = null; element.tagName = tagName; (element.namespaceURI) = namespaceURI; if (element instanceof Element && options && options.is) { - element._isValue = String(options.is); + element.__isValue__ = String(options.is); } return element; @@ -856,7 +858,7 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createTextNode(data?: string): IText { - return new this._defaultView.Text(data); + return new this.__defaultView__.Text(data); } /** @@ -866,7 +868,7 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createComment(data?: string): IComment { - return new this._defaultView.Comment(data); + return new this.__defaultView__.Comment(data); } /** @@ -875,7 +877,7 @@ export default class Document extends Node implements IDocument { * @returns Document fragment. */ public createDocumentFragment(): IDocumentFragment { - return new this._defaultView.DocumentFragment(); + return new this.__defaultView__.DocumentFragment(); } /** @@ -912,8 +914,8 @@ export default class Document extends Node implements IDocument { * @returns Event. */ public createEvent(type: string): Event { - if (typeof this._defaultView[type] === 'function') { - return new this._defaultView[type]('init'); + if (typeof this.__defaultView__[type] === 'function') { + return new this.__defaultView__[type]('init'); } return new Event('init'); } @@ -936,7 +938,7 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public createAttributeNS(namespaceURI: string, qualifiedName: string): IAttr { - const attribute = new this._defaultView.Attr(); + const attribute = new this.__defaultView__.Attr(); attribute.namespaceURI = namespaceURI; attribute.name = qualifiedName; return attribute; @@ -964,7 +966,7 @@ export default class Document extends Node implements IDocument { * @returns Range. */ public createRange(): Range { - return new this._defaultView.Range(); + return new this.__defaultView__.Range(); } /** @@ -990,10 +992,10 @@ export default class Document extends Node implements IDocument { * @returns Selection. */ public getSelection(): Selection { - if (!this._selection) { - this._selection = new Selection(this); + if (!this.__selection__) { + this.__selection__ = new Selection(this); } - return this._selection; + return this.__selection__; } /** @@ -1023,7 +1025,7 @@ export default class Document extends Node implements IDocument { `Failed to execute 'createProcessingInstruction' on 'Document': The data provided ('?>') contains '?>'` ); } - const processingInstruction = new this._defaultView.ProcessingInstruction(data); + const processingInstruction = new this.__defaultView__.ProcessingInstruction(data); processingInstruction.target = target; return processingInstruction; } diff --git a/packages/happy-dom/src/nodes/document/IDocument.ts b/packages/happy-dom/src/nodes/document/IDocument.ts index 3a4948b3d..7518eeca9 100644 --- a/packages/happy-dom/src/nodes/document/IDocument.ts +++ b/packages/happy-dom/src/nodes/document/IDocument.ts @@ -28,7 +28,7 @@ import VisibilityStateEnum from './VisibilityStateEnum.js'; */ export default interface IDocument extends IParentNode { readonly defaultView: IBrowserWindow | null; - readonly _defaultView: IBrowserWindow; + readonly __defaultView__: IBrowserWindow; readonly implementation: DOMImplementation; readonly documentElement: IHTMLElement; readonly doctype: IDocumentType; diff --git a/packages/happy-dom/src/nodes/element/Dataset.ts b/packages/happy-dom/src/nodes/element/Dataset.ts index 35e59daac..c163e5a33 100644 --- a/packages/happy-dom/src/nodes/element/Dataset.ts +++ b/packages/happy-dom/src/nodes/element/Dataset.ts @@ -47,7 +47,7 @@ export default class Dataset { return true; }, deleteProperty(dataset: DatasetRecord, key: string): boolean { - (element.attributes)._removeNamedItem( + (element.attributes).__removeNamedItem__( 'data-' + Dataset.camelCaseToKebab(key) ); return delete dataset[key]; diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index a33afad5b..741794a9f 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -37,8 +37,8 @@ import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReade */ export default class Element extends Node implements IElement { // ObservedAttributes should only be called once by CustomElementRegistry (see #117) - // CustomElementRegistry will therefore populate _observedAttributes when CustomElementRegistry.define() is called - public static _observedAttributes: string[]; + // CustomElementRegistry will therefore populate "__observedAttributes__" when CustomElementRegistry.define() is called + public static __observedAttributes__: string[]; public static observedAttributes: string[]; public tagName: string = null; public nodeType = Node.ELEMENT_NODE; @@ -87,21 +87,21 @@ export default class Element extends Node implements IElement { public ontouchmove: (event: Event) => void | null = null; public ontouchstart: (event: Event) => void | null = null; - public readonly _children: IHTMLCollection = new HTMLCollection(); + public readonly __children__: IHTMLCollection = new HTMLCollection(); // Used for being able to access closed shadow roots - public _shadowRoot: IShadowRoot = null; public readonly attributes: INamedNodeMap = new ElementNamedNodeMap(this); - public _classList: DOMTokenList = null; - public _isValue?: string | null = null; - public _computedStyle: CSSStyleDeclaration | null = null; + public __shadowRoot__: IShadowRoot = null; + public __classList__: DOMTokenList = null; + public __isValue__: string | null = null; + public __computedStyle__: CSSStyleDeclaration | null = null; /** * Returns element children. */ public get children(): IHTMLCollection { - return this._children; + return this.__children__; } /** @@ -110,10 +110,10 @@ export default class Element extends Node implements IElement { * @returns Class list. */ public get classList(): IDOMTokenList { - if (!this._classList) { - this._classList = new DOMTokenList(this, 'class'); + if (!this.__classList__) { + this.__classList__ = new DOMTokenList(this, 'class'); } - return this._classList; + return this.__classList__; } /** @@ -213,7 +213,7 @@ export default class Element extends Node implements IElement { */ public get textContent(): string { let result = ''; - for (const childNode of this._childNodes) { + for (const childNode of this.__childNodes__) { if (childNode.nodeType === Node.ELEMENT_NODE || childNode.nodeType === Node.TEXT_NODE) { result += childNode.textContent; } @@ -227,7 +227,7 @@ export default class Element extends Node implements IElement { * @param textContent Text content. */ public set textContent(textContent: string) { - for (const child of this._childNodes.slice()) { + for (const child of this.__childNodes__.slice()) { this.removeChild(child); } if (textContent) { @@ -250,7 +250,7 @@ export default class Element extends Node implements IElement { * @param html HTML. */ public set innerHTML(html: string) { - for (const child of this._childNodes.slice()) { + for (const child of this.__childNodes__.slice()) { this.removeChild(child); } @@ -281,7 +281,7 @@ export default class Element extends Node implements IElement { * @returns Element. */ public get firstElementChild(): IElement { - return this._children[0] ?? null; + return this.__children__[0] ?? null; } /** @@ -290,7 +290,7 @@ export default class Element extends Node implements IElement { * @returns Element. */ public get lastElementChild(): IElement { - return this._children[this._children.length - 1] ?? null; + return this.__children__[this.__children__.length - 1] ?? null; } /** @@ -299,7 +299,7 @@ export default class Element extends Node implements IElement { * @returns Element. */ public get childElementCount(): number { - return this._children.length; + return this.__children__.length; } /** @@ -346,7 +346,7 @@ export default class Element extends Node implements IElement { escapeEntities: false }); let xml = ''; - for (const node of this._childNodes) { + for (const node of this.__childNodes__) { xml += xmlSerializer.serializeToString(node); } return xml; @@ -373,9 +373,9 @@ export default class Element extends Node implements IElement { } if (deep) { - for (const node of clone._childNodes) { + for (const node of clone.__childNodes__) { if (node.nodeType === Node.ELEMENT_NODE) { - clone._children.push(node); + clone.__children__.push(node); } } } @@ -519,7 +519,7 @@ export default class Element extends Node implements IElement { public insertAdjacentHTML(position: TInsertAdjacentPositions, text: string): void { for (const node of (( XMLParser.parse(this.ownerDocument, text) - ))._childNodes.slice()) { + )).__childNodes__.slice()) { this.insertAdjacentElement(position, node); } } @@ -688,20 +688,20 @@ export default class Element extends Node implements IElement { * @returns Shadow root. */ public attachShadow(shadowRootInit: { mode: string }): IShadowRoot { - if (this._shadowRoot) { + if (this.__shadowRoot__) { throw new DOMException('Shadow root has already been attached.'); } - (this._shadowRoot) = new this.ownerDocument._defaultView.ShadowRoot(); - (this._shadowRoot.host) = this; - (this._shadowRoot.mode) = shadowRootInit.mode; - (this._shadowRoot)._connectToNode(this); + (this.__shadowRoot__) = new this.ownerDocument.__defaultView__.ShadowRoot(); + (this.__shadowRoot__.host) = this; + (this.__shadowRoot__.mode) = shadowRootInit.mode; + (this.__shadowRoot__).__connectToNode__(this); - if (this._shadowRoot.mode === 'open') { - (this.shadowRoot) = this._shadowRoot; + if (this.__shadowRoot__.mode === 'open') { + (this.shadowRoot) = this.__shadowRoot__; } - return this._shadowRoot; + return this.__shadowRoot__; } /** @@ -885,7 +885,7 @@ export default class Element extends Node implements IElement { public scroll(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void { if (typeof x === 'object') { if (x.behavior === 'smooth') { - this.ownerDocument._defaultView.setTimeout(() => { + this.ownerDocument.__defaultView__.setTimeout(() => { if (x.top !== undefined) { (this.scrollTop) = x.top; } @@ -926,7 +926,7 @@ export default class Element extends Node implements IElement { public override dispatchEvent(event: Event): boolean { const returnValue = super.dispatchEvent(event); const browserSettings = WindowBrowserSettingsReader.getSettings( - this.ownerDocument._defaultView + this.ownerDocument.__defaultView__ ); if ( @@ -934,17 +934,17 @@ export default class Element extends Node implements IElement { !browserSettings.disableJavaScriptEvaluation && (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && - !event._immediatePropagationStopped + !event.__immediatePropagationStopped__ ) { const attribute = this.getAttribute('on' + event.type); - if (attribute && !event._immediatePropagationStopped) { - const code = `//# sourceURL=${this.ownerDocument._defaultView.location.href}\n${attribute}`; + if (attribute && !event.__immediatePropagationStopped__) { + const code = `//# sourceURL=${this.ownerDocument.__defaultView__.location.href}\n${attribute}`; if (browserSettings.disableErrorCapturing) { - this.ownerDocument._defaultView.eval(code); + this.ownerDocument.__defaultView__.eval(code); } else { - WindowErrorUtility.captureError(this.ownerDocument._defaultView, () => - this.ownerDocument._defaultView.eval(code) + WindowErrorUtility.captureError(this.ownerDocument.__defaultView__, () => + this.ownerDocument.__defaultView__.eval(code) ); } } @@ -959,7 +959,7 @@ export default class Element extends Node implements IElement { * @param name Name. * @returns Attribute name based on namespace. */ - protected _getAttributeName(name): string { + protected __getAttributeName__(name): string { if (this.namespaceURI === NamespaceURI.svg) { return name; } diff --git a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts index de36ef702..599171b36 100644 --- a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts @@ -13,7 +13,7 @@ import IElement from './IElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class ElementNamedNodeMap extends NamedNodeMap { - protected _ownerElement: Element; + protected __ownerElement__: Element; /** * Constructor. @@ -22,21 +22,21 @@ export default class ElementNamedNodeMap extends NamedNodeMap { */ constructor(ownerElement: Element) { super(); - this._ownerElement = ownerElement; + this.__ownerElement__ = ownerElement; } /** * @override */ public override getNamedItem(name: string): IAttr | null { - return this._namedItems[this._getAttributeName(name)] || null; + return this.__namedItems__[this.__getAttributeName__(name)] || null; } /** * @override */ public override getNamedItemNS(namespace: string, localName: string): IAttr | null { - return super.getNamedItemNS(namespace, this._getAttributeName(localName)); + return super.getNamedItemNS(namespace, this.__getAttributeName__(localName)); } /** @@ -47,57 +47,57 @@ export default class ElementNamedNodeMap extends NamedNodeMap { return null; } - item.name = this._getAttributeName(item.name); - (item.ownerElement) = this._ownerElement; + item.name = this.__getAttributeName__(item.name); + (item.ownerElement) = this.__ownerElement__; const replacedItem = super.setNamedItem(item); const oldValue = replacedItem ? replacedItem.value : null; - if (this._ownerElement.isConnected) { - this._ownerElement.ownerDocument['_cacheID']++; + if (this.__ownerElement__.isConnected) { + this.__ownerElement__.ownerDocument['__cacheID__']++; } - if (item.name === 'class' && this._ownerElement._classList) { - this._ownerElement._classList._updateIndices(); + if (item.name === 'class' && this.__ownerElement__.__classList__) { + this.__ownerElement__.__classList__.__updateIndices__(); } if (item.name === 'id' || item.name === 'name') { if ( - this._ownerElement.parentNode && - (this._ownerElement.parentNode)._children && + this.__ownerElement__.parentNode && + (this.__ownerElement__.parentNode).__children__ && item.value !== oldValue ) { if (oldValue) { (>( - (this._ownerElement.parentNode)._children - ))._removeNamedItem(this._ownerElement, oldValue); + (this.__ownerElement__.parentNode).__children__ + )).__removeNamedItem__(this.__ownerElement__, oldValue); } if (item.value) { (>( - (this._ownerElement.parentNode)._children - ))._appendNamedItem(this._ownerElement, item.value); + (this.__ownerElement__.parentNode).__children__ + )).__appendNamedItem__(this.__ownerElement__, item.value); } } } if ( - this._ownerElement.attributeChangedCallback && - (this._ownerElement.constructor)._observedAttributes && - (this._ownerElement.constructor)._observedAttributes.includes(item.name) + this.__ownerElement__.attributeChangedCallback && + (this.__ownerElement__.constructor).__observedAttributes__ && + (this.__ownerElement__.constructor).__observedAttributes__.includes(item.name) ) { - this._ownerElement.attributeChangedCallback(item.name, oldValue, item.value); + this.__ownerElement__.attributeChangedCallback(item.name, oldValue, item.value); } // MutationObserver - if (this._ownerElement._observers.length > 0) { - for (const observer of this._ownerElement._observers) { + if (this.__ownerElement__.__observers__.length > 0) { + for (const observer of this.__ownerElement__.__observers__) { if ( observer.options.attributes && (!observer.options.attributeFilter || observer.options.attributeFilter.includes(item.name)) ) { const record = new MutationRecord(); - record.target = this._ownerElement; + record.target = this.__ownerElement__; record.type = MutationTypeEnum.attributes; record.attributeName = item.name; record.oldValue = observer.options.attributeOldValue ? oldValue : null; @@ -112,53 +112,53 @@ export default class ElementNamedNodeMap extends NamedNodeMap { /** * @override */ - public override _removeNamedItem(name: string): IAttr | null { - const removedItem = super._removeNamedItem(this._getAttributeName(name)); + public override __removeNamedItem__(name: string): IAttr | null { + const removedItem = super.__removeNamedItem__(this.__getAttributeName__(name)); if (!removedItem) { return null; } - if (this._ownerElement.isConnected) { - this._ownerElement.ownerDocument['_cacheID']++; + if (this.__ownerElement__.isConnected) { + this.__ownerElement__.ownerDocument['__cacheID__']++; } - if (removedItem.name === 'class' && this._ownerElement._classList) { - this._ownerElement._classList._updateIndices(); + if (removedItem.name === 'class' && this.__ownerElement__.__classList__) { + this.__ownerElement__.__classList__.__updateIndices__(); } if (removedItem.name === 'id' || removedItem.name === 'name') { if ( - this._ownerElement.parentNode && - (this._ownerElement.parentNode)._children && + this.__ownerElement__.parentNode && + (this.__ownerElement__.parentNode).__children__ && removedItem.value ) { (>( - (this._ownerElement.parentNode)._children - ))._removeNamedItem(this._ownerElement, removedItem.value); + (this.__ownerElement__.parentNode).__children__ + )).__removeNamedItem__(this.__ownerElement__, removedItem.value); } } if ( - this._ownerElement.attributeChangedCallback && - (this._ownerElement.constructor)._observedAttributes && - (this._ownerElement.constructor)._observedAttributes.includes( + this.__ownerElement__.attributeChangedCallback && + (this.__ownerElement__.constructor).__observedAttributes__ && + (this.__ownerElement__.constructor).__observedAttributes__.includes( removedItem.name ) ) { - this._ownerElement.attributeChangedCallback(removedItem.name, removedItem.value, null); + this.__ownerElement__.attributeChangedCallback(removedItem.name, removedItem.value, null); } // MutationObserver - if (this._ownerElement._observers.length > 0) { - for (const observer of this._ownerElement._observers) { + if (this.__ownerElement__.__observers__.length > 0) { + for (const observer of this.__ownerElement__.__observers__) { if ( observer.options.attributes && (!observer.options.attributeFilter || observer.options.attributeFilter.includes(removedItem.name)) ) { const record = new MutationRecord(); - record.target = this._ownerElement; + record.target = this.__ownerElement__; record.type = MutationTypeEnum.attributes; record.attributeName = removedItem.name; record.oldValue = observer.options.attributeOldValue ? removedItem.value : null; @@ -174,7 +174,7 @@ export default class ElementNamedNodeMap extends NamedNodeMap { * @override */ public override removeNamedItemNS(namespace: string, localName: string): IAttr | null { - return super.removeNamedItemNS(namespace, this._getAttributeName(localName)); + return super.removeNamedItemNS(namespace, this.__getAttributeName__(localName)); } /** @@ -183,8 +183,8 @@ export default class ElementNamedNodeMap extends NamedNodeMap { * @param name Name. * @returns Attribute name based on namespace. */ - protected _getAttributeName(name): string { - if (this._ownerElement.namespaceURI === NamespaceURI.svg) { + protected __getAttributeName__(name): string { + if (this.__ownerElement__.namespaceURI === NamespaceURI.svg) { return name; } return name.toLowerCase(); diff --git a/packages/happy-dom/src/nodes/element/ElementUtility.ts b/packages/happy-dom/src/nodes/element/ElementUtility.ts index 5968a7e8f..b1d308530 100644 --- a/packages/happy-dom/src/nodes/element/ElementUtility.ts +++ b/packages/happy-dom/src/nodes/element/ElementUtility.ts @@ -42,7 +42,7 @@ export default class ElementUtility { } if (node.parentNode) { const parentNodeChildren = >( - (node.parentNode)._children + (node.parentNode).__children__ ); if (parentNodeChildren) { @@ -51,7 +51,7 @@ export default class ElementUtility { for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const attribute = (node).attributes.getNamedItem(attributeName); if (attribute) { - parentNodeChildren._removeNamedItem(node, attribute.value); + parentNodeChildren.__removeNamedItem__(node, attribute.value); } } @@ -59,12 +59,14 @@ export default class ElementUtility { } } } - const ancestorNodeChildren = >(ancestorNode)._children; + const ancestorNodeChildren = >( + (ancestorNode).__children__ + ); for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const attribute = (node).attributes.getNamedItem(attributeName); if (attribute) { - ancestorNodeChildren._appendNamedItem(node, attribute.value); + ancestorNodeChildren.__appendNamedItem__(node, attribute.value); } } @@ -90,13 +92,15 @@ export default class ElementUtility { node: INode ): INode { if (node.nodeType === NodeTypeEnum.elementNode) { - const ancestorNodeChildren = >(ancestorNode)._children; + const ancestorNodeChildren = >( + (ancestorNode).__children__ + ); const index = ancestorNodeChildren.indexOf(node); if (index !== -1) { for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const attribute = (node).attributes.getNamedItem(attributeName); if (attribute) { - ancestorNodeChildren._removeNamedItem(node, attribute.value); + ancestorNodeChildren.__removeNamedItem__(node, attribute.value); } } ancestorNodeChildren.splice(index, 1); @@ -138,7 +142,7 @@ export default class ElementUtility { } if (newNode.parentNode) { const parentNodeChildren = >( - (newNode.parentNode)._children + (newNode.parentNode).__children__ ); if (parentNodeChildren) { @@ -147,7 +151,7 @@ export default class ElementUtility { for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const attribute = (newNode).attributes.getNamedItem(attributeName); if (attribute) { - parentNodeChildren._removeNamedItem(newNode, attribute.value); + parentNodeChildren.__removeNamedItem__(newNode, attribute.value); } } @@ -156,7 +160,9 @@ export default class ElementUtility { } } - const ancestorNodeChildren = >(ancestorNode)._children; + const ancestorNodeChildren = >( + (ancestorNode).__children__ + ); if (referenceNode.nodeType === NodeTypeEnum.elementNode) { const index = ancestorNodeChildren.indexOf(referenceNode); @@ -166,7 +172,7 @@ export default class ElementUtility { } else { ancestorNodeChildren.length = 0; - for (const node of (ancestorNode)._childNodes) { + for (const node of (ancestorNode).__childNodes__) { if (node === referenceNode) { ancestorNodeChildren.push(newNode); } @@ -179,7 +185,7 @@ export default class ElementUtility { for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const attribute = (newNode).attributes.getNamedItem(attributeName); if (attribute) { - ancestorNodeChildren._appendNamedItem(newNode, attribute.value); + ancestorNodeChildren.__appendNamedItem__(newNode, attribute.value); } } diff --git a/packages/happy-dom/src/nodes/element/HTMLCollection.ts b/packages/happy-dom/src/nodes/element/HTMLCollection.ts index cb008c27b..0937c8b9d 100644 --- a/packages/happy-dom/src/nodes/element/HTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/HTMLCollection.ts @@ -4,7 +4,7 @@ import IHTMLCollection from './IHTMLCollection.js'; * HTML collection. */ export default class HTMLCollection extends Array implements IHTMLCollection { - protected _namedItems: { [k: string]: T[] } = {}; + protected __namedItems__: { [k: string]: T[] } = {}; /** * Returns item by index. @@ -22,8 +22,8 @@ export default class HTMLCollection extends Array implements IHTMLCollection< * @returns Node. */ public namedItem(name: string): T | null { - return this._namedItems[name] && this._namedItems[name].length - ? this._namedItems[name][0] + return this.__namedItems__[name] && this.__namedItems__[name].length + ? this.__namedItems__[name][0] : null; } @@ -33,16 +33,16 @@ export default class HTMLCollection extends Array implements IHTMLCollection< * @param node Node. * @param name Name. */ - public _appendNamedItem(node: T, name: string): void { + public __appendNamedItem__(node: T, name: string): void { if (name) { - this._namedItems[name] = this._namedItems[name] || []; + this.__namedItems__[name] = this.__namedItems__[name] || []; - if (!this._namedItems[name].includes(node)) { - this._namedItems[name].push(node); + if (!this.__namedItems__[name].includes(node)) { + this.__namedItems__[name].push(node); } - if (!this.hasOwnProperty(name) && this._isValidPropertyName(name)) { - this[name] = this._namedItems[name][0]; + if (!this.hasOwnProperty(name) && this.__isValidPropertyName__(name)) { + this[name] = this.__namedItems__[name][0]; } } } @@ -53,20 +53,20 @@ export default class HTMLCollection extends Array implements IHTMLCollection< * @param node Node. * @param name Name. */ - public _removeNamedItem(node: T, name: string): void { - if (name && this._namedItems[name]) { - const index = this._namedItems[name].indexOf(node); + public __removeNamedItem__(node: T, name: string): void { + if (name && this.__namedItems__[name]) { + const index = this.__namedItems__[name].indexOf(node); if (index > -1) { - this._namedItems[name].splice(index, 1); + this.__namedItems__[name].splice(index, 1); - if (this._namedItems[name].length === 0) { - delete this._namedItems[name]; - if (this.hasOwnProperty(name) && this._isValidPropertyName(name)) { + if (this.__namedItems__[name].length === 0) { + delete this.__namedItems__[name]; + if (this.hasOwnProperty(name) && this.__isValidPropertyName__(name)) { delete this[name]; } - } else if (this._isValidPropertyName(name)) { - this[name] = this._namedItems[name][0]; + } else if (this.__isValidPropertyName__(name)) { + this[name] = this.__namedItems__[name][0]; } } } @@ -78,7 +78,7 @@ export default class HTMLCollection extends Array implements IHTMLCollection< * @param name Name. * @returns True if the property name is valid. */ - protected _isValidPropertyName(name: string): boolean { + protected __isValidPropertyName__(name: string): boolean { return ( !this.constructor.prototype.hasOwnProperty(name) && !Array.prototype.hasOwnProperty(name) && diff --git a/packages/happy-dom/src/nodes/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts index be9313f74..e49c7379f 100644 --- a/packages/happy-dom/src/nodes/element/IElement.ts +++ b/packages/happy-dom/src/nodes/element/IElement.ts @@ -182,10 +182,10 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode, /** * Attaches a shadow root. * - * @param _shadowRootInit Shadow root init. + * @param __shadowRoot__Init Shadow root init. * @returns Shadow root. */ - attachShadow(_shadowRootInit: { mode: string }): IShadowRoot; + attachShadow(__shadowRoot__Init: { mode: string }): IShadowRoot; /** * Scrolls to a particular set of coordinates. diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts index 067669c64..e6990bc8a 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -17,8 +17,8 @@ import EventPhaseEnum from '../../event/EventPhaseEnum.js'; */ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAnchorElement { public override readonly attributes: INamedNodeMap = new HTMLAnchorElementNamedNodeMap(this); - public _relList: DOMTokenList = null; - public _url: URL | null = null; + public __relList__: DOMTokenList = null; + public __url__: URL | null = null; /** * Returns download. @@ -44,7 +44,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Hash. */ public get hash(): string { - return this._url?.hash ?? ''; + return this.__url__?.hash ?? ''; } /** @@ -53,9 +53,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param hash Hash. */ public set hash(hash: string) { - if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { - this._url.hash = hash; - this.setAttribute('href', this._url.toString()); + if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { + this.__url__.hash = hash; + this.setAttribute('href', this.__url__.toString()); } } @@ -65,8 +65,8 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Href. */ public get href(): string | null { - if (this._url) { - return this._url.toString(); + if (this.__url__) { + return this.__url__.toString(); } return this.getAttribute('href') || ''; @@ -105,7 +105,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Origin. */ public get origin(): string { - return this._url?.origin ?? ''; + return this.__url__?.origin ?? ''; } /** @@ -132,7 +132,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Protocol. */ public get protocol(): string { - return this._url?.protocol ?? ''; + return this.__url__?.protocol ?? ''; } /** @@ -141,9 +141,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param protocol Protocol. */ public set protocol(protocol: string) { - if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { - this._url.protocol = protocol; - this.setAttribute('href', this._url.toString()); + if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { + this.__url__.protocol = protocol; + this.setAttribute('href', this.__url__.toString()); } } @@ -153,7 +153,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Username. */ public get username(): string { - return this._url?.username ?? ''; + return this.__url__?.username ?? ''; } /** @@ -163,13 +163,13 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho */ public set username(username: string) { if ( - this._url && - !HTMLAnchorElementUtility.isBlobURL(this._url) && - this._url.host && - this._url.protocol != 'file' + this.__url__ && + !HTMLAnchorElementUtility.isBlobURL(this.__url__) && + this.__url__.host && + this.__url__.protocol != 'file' ) { - this._url.username = username; - this.setAttribute('href', this._url.toString()); + this.__url__.username = username; + this.setAttribute('href', this.__url__.toString()); } } @@ -179,7 +179,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Password. */ public get password(): string { - return this._url?.password ?? ''; + return this.__url__?.password ?? ''; } /** @@ -189,13 +189,13 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho */ public set password(password: string) { if ( - this._url && - !HTMLAnchorElementUtility.isBlobURL(this._url) && - this._url.host && - this._url.protocol != 'file' + this.__url__ && + !HTMLAnchorElementUtility.isBlobURL(this.__url__) && + this.__url__.host && + this.__url__.protocol != 'file' ) { - this._url.password = password; - this.setAttribute('href', this._url.toString()); + this.__url__.password = password; + this.setAttribute('href', this.__url__.toString()); } } @@ -205,7 +205,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Pathname. */ public get pathname(): string { - return this._url?.pathname ?? ''; + return this.__url__?.pathname ?? ''; } /** @@ -214,9 +214,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param pathname Pathname. */ public set pathname(pathname: string) { - if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { - this._url.pathname = pathname; - this.setAttribute('href', this._url.toString()); + if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { + this.__url__.pathname = pathname; + this.setAttribute('href', this.__url__.toString()); } } @@ -226,7 +226,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Port. */ public get port(): string { - return this._url?.port ?? ''; + return this.__url__?.port ?? ''; } /** @@ -236,13 +236,13 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho */ public set port(port: string) { if ( - this._url && - !HTMLAnchorElementUtility.isBlobURL(this._url) && - this._url.host && - this._url.protocol != 'file' + this.__url__ && + !HTMLAnchorElementUtility.isBlobURL(this.__url__) && + this.__url__.host && + this.__url__.protocol != 'file' ) { - this._url.port = port; - this.setAttribute('href', this._url.toString()); + this.__url__.port = port; + this.setAttribute('href', this.__url__.toString()); } } @@ -252,7 +252,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Host. */ public get host(): string { - return this._url?.host ?? ''; + return this.__url__?.host ?? ''; } /** @@ -261,9 +261,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param host Host. */ public set host(host: string) { - if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { - this._url.host = host; - this.setAttribute('href', this._url.toString()); + if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { + this.__url__.host = host; + this.setAttribute('href', this.__url__.toString()); } } @@ -273,7 +273,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Hostname. */ public get hostname(): string { - return this._url?.hostname ?? ''; + return this.__url__?.hostname ?? ''; } /** @@ -282,9 +282,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param hostname Hostname. */ public set hostname(hostname: string) { - if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { - this._url.hostname = hostname; - this.setAttribute('href', this._url.toString()); + if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { + this.__url__.hostname = hostname; + this.setAttribute('href', this.__url__.toString()); } } @@ -330,10 +330,10 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Rel list. */ public get relList(): IDOMTokenList { - if (!this._relList) { - this._relList = new DOMTokenList(this, 'rel'); + if (!this.__relList__) { + this.__relList__ = new DOMTokenList(this, 'rel'); } - return this._relList; + return this.__relList__; } /** @@ -342,7 +342,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Search. */ public get search(): string { - return this._url?.search ?? ''; + return this.__url__?.search ?? ''; } /** @@ -351,9 +351,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param search Search. */ public set search(search: string) { - if (this._url && !HTMLAnchorElementUtility.isBlobURL(this._url)) { - this._url.search = search; - this.setAttribute('href', this._url.toString()); + if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { + this.__url__.search = search; + this.setAttribute('href', this.__url__.toString()); } } @@ -429,10 +429,10 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && !event.defaultPrevented && - this._url + this.__url__ ) { - this.ownerDocument._defaultView.open(this._url.toString(), this.target || '_self'); - if (this.ownerDocument._defaultView.closed) { + this.ownerDocument.__defaultView__.open(this.__url__.toString(), this.target || '_self'); + if (this.ownerDocument.__defaultView__.closed) { event.stopImmediatePropagation(); } } diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts index 33843cc0f..0115d2977 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts @@ -9,7 +9,7 @@ import HTMLAnchorElementUtility from './HTMLAnchorElementUtility.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLAnchorElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected _ownerElement: HTMLAnchorElement; + protected __ownerElement__: HTMLAnchorElement; /** * @override @@ -17,11 +17,11 @@ export default class HTMLAnchorElementNamedNodeMap extends HTMLElementNamedNodeM public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if (item.name === 'rel' && this._ownerElement._relList) { - this._ownerElement._relList._updateIndices(); + if (item.name === 'rel' && this.__ownerElement__.__relList__) { + this.__ownerElement__.__relList__.__updateIndices__(); } else if (item.name === 'href') { - this._ownerElement._url = HTMLAnchorElementUtility.getUrl( - this._ownerElement.ownerDocument, + this.__ownerElement__.__url__ = HTMLAnchorElementUtility.getUrl( + this.__ownerElement__.ownerDocument, item.value ); } @@ -32,14 +32,14 @@ export default class HTMLAnchorElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override _removeNamedItem(name: string): IAttr | null { - const removedItem = super._removeNamedItem(name); + public override __removeNamedItem__(name: string): IAttr | null { + const removedItem = super.__removeNamedItem__(name); if (removedItem) { - if (removedItem.name === 'rel' && this._ownerElement._relList) { - this._ownerElement._relList._updateIndices(); + if (removedItem.name === 'rel' && this.__ownerElement__.__relList__) { + this.__ownerElement__.__relList__.__updateIndices__(); } else if (removedItem.name === 'href') { - this._ownerElement._url = null; + this.__ownerElement__.__url__ = null; } } diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts index 65aa54fb7..130938e4f 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts @@ -89,7 +89,7 @@ export default class HTMLButtonElement extends HTMLElement implements IHTMLButto * @returns Type */ public get type(): string { - return this._sanitizeType(this.getAttribute('type')); + return this.#sanitizeType(this.getAttribute('type')); } /** @@ -98,7 +98,7 @@ export default class HTMLButtonElement extends HTMLElement implements IHTMLButto * @param v Type */ public set type(v: string) { - this.setAttribute('type', this._sanitizeType(v)); + this.setAttribute('type', this.#sanitizeType(v)); } /** @@ -129,7 +129,7 @@ export default class HTMLButtonElement extends HTMLElement implements IHTMLButto * @returns Form. */ public get form(): IHTMLFormElement { - return this._formNode; + return this.__formNode__; } /** @@ -173,24 +173,6 @@ export default class HTMLButtonElement extends HTMLElement implements IHTMLButto (this.validationMessage) = String(message); } - /** - * Sanitizes type. - * - * TODO: We can improve performance a bit if we make the types as a constant. - * - * @param type Type. - * @returns Type sanitized. - */ - protected _sanitizeType(type: string): string { - type = (type && type.toLowerCase()) || 'submit'; - - if (!BUTTON_TYPES.includes(type)) { - type = 'submit'; - } - - return type; - } - /** * @override */ @@ -205,10 +187,10 @@ export default class HTMLButtonElement extends HTMLElement implements IHTMLButto event.type === 'click' && (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && - this._formNode && + this.__formNode__ && this.isConnected ) { - const form = this._formNode; + const form = this.__formNode__; switch (this.type) { case 'submit': form.requestSubmit(); @@ -225,20 +207,38 @@ export default class HTMLButtonElement extends HTMLElement implements IHTMLButto /** * @override */ - public override _connectToNode(parentNode: INode = null): void { - const oldFormNode = this._formNode; + public override __connectToNode__(parentNode: INode = null): void { + const oldFormNode = this.__formNode__; - super._connectToNode(parentNode); + super.__connectToNode__(parentNode); - if (oldFormNode !== this._formNode) { + if (oldFormNode !== this.__formNode__) { if (oldFormNode) { - oldFormNode._removeFormControlItem(this, this.name); - oldFormNode._removeFormControlItem(this, this.id); + oldFormNode.__removeFormControlItem__(this, this.name); + oldFormNode.__removeFormControlItem__(this, this.id); } - if (this._formNode) { - (this._formNode)._appendFormControlItem(this, this.name); - (this._formNode)._appendFormControlItem(this, this.id); + if (this.__formNode__) { + (this.__formNode__).__appendFormControlItem__(this, this.name); + (this.__formNode__).__appendFormControlItem__(this, this.id); } } } + + /** + * Sanitizes type. + * + * TODO: We can improve performance a bit if we make the types as a constant. + * + * @param type Type. + * @returns Type sanitized. + */ + #sanitizeType(type: string): string { + type = (type && type.toLowerCase()) || 'submit'; + + if (!BUTTON_TYPES.includes(type)) { + type = 'submit'; + } + + return type; + } } diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts index f145d2017..1ee5e2dbe 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts @@ -9,7 +9,7 @@ import HTMLButtonElement from './HTMLButtonElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLButtonElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected _ownerElement: HTMLButtonElement; + protected __ownerElement__: HTMLButtonElement; /** * @override @@ -17,16 +17,16 @@ export default class HTMLButtonElementNamedNodeMap extends HTMLElementNamedNodeM public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if ((item.name === 'id' || item.name === 'name') && this._ownerElement._formNode) { + if ((item.name === 'id' || item.name === 'name') && this.__ownerElement__.__formNode__) { if (replacedItem?.value) { - (this._ownerElement._formNode)._removeFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__removeFormControlItem__( + this.__ownerElement__, replacedItem.value ); } if (item.value) { - (this._ownerElement._formNode)._appendFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__appendFormControlItem__( + this.__ownerElement__, item.value ); } @@ -38,16 +38,16 @@ export default class HTMLButtonElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override _removeNamedItem(name: string): IAttr | null { - const removedItem = super._removeNamedItem(name); + public override __removeNamedItem__(name: string): IAttr | null { + const removedItem = super.__removeNamedItem__(name); if ( removedItem && (removedItem.name === 'id' || removedItem.name === 'name') && - this._ownerElement._formNode + this.__ownerElement__.__formNode__ ) { - (this._ownerElement._formNode)._removeFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__removeFormControlItem__( + this.__ownerElement__, removedItem.value ); } diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 2d303e13a..9e513c6ca 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -31,8 +31,8 @@ export default class HTMLElement extends Element implements IHTMLElement { public readonly clientLeft = 0; public readonly clientTop = 0; - public _style: CSSStyleDeclaration = null; - private _dataset: Dataset = null; + public __style__: CSSStyleDeclaration = null; + #dataset: Dataset = null; // Events public oncopy: (event: Event) => void | null = null; @@ -97,10 +97,10 @@ export default class HTMLElement extends Element implements IHTMLElement { let result = ''; - for (const childNode of this._childNodes) { + for (const childNode of this.__childNodes__) { if (childNode.nodeType === NodeTypeEnum.elementNode) { const childElement = childNode; - const computedStyle = this.ownerDocument._defaultView.getComputedStyle(childElement); + const computedStyle = this.ownerDocument.__defaultView__.getComputedStyle(childElement); if (childElement.tagName !== 'SCRIPT' && childElement.tagName !== 'STYLE') { const display = computedStyle.display; @@ -143,7 +143,7 @@ export default class HTMLElement extends Element implements IHTMLElement { * @param innerText Inner text. */ public set innerText(text: string) { - for (const child of this._childNodes.slice()) { + for (const child of this.__childNodes__.slice()) { this.removeChild(child); } @@ -198,10 +198,10 @@ export default class HTMLElement extends Element implements IHTMLElement { * @returns Style. */ public get style(): CSSStyleDeclaration { - if (!this._style) { - this._style = new CSSStyleDeclaration(this); + if (!this.__style__) { + this.__style__ = new CSSStyleDeclaration(this); } - return this._style; + return this.__style__; } /** @@ -220,7 +220,7 @@ export default class HTMLElement extends Element implements IHTMLElement { * @returns Data set. */ public get dataset(): { [key: string]: string } { - return (this._dataset ??= new Dataset(this)).proxy; + return (this.#dataset ??= new Dataset(this)).proxy; } /** @@ -307,8 +307,8 @@ export default class HTMLElement extends Element implements IHTMLElement { bubbles: true, composed: true }); - event._target = this; - event._currentTarget = this; + event.__target__ = this; + event.__currentTarget__ = this; this.dispatchEvent(event); } @@ -337,8 +337,8 @@ export default class HTMLElement extends Element implements IHTMLElement { (clone.contentEditable) = this.contentEditable; (clone.isContentEditable) = this.isContentEditable; - if (this._style) { - clone.style.cssText = this._style.cssText; + if (this.__style__) { + clone.style.cssText = this.__style__.cssText; } return clone; diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts index faa2cd511..54252115b 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts @@ -8,7 +8,7 @@ import HTMLElement from './HTMLElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLElementNamedNodeMap extends ElementNamedNodeMap { - protected _ownerElement: HTMLElement; + protected __ownerElement__: HTMLElement; /** * @override @@ -16,8 +16,8 @@ export default class HTMLElementNamedNodeMap extends ElementNamedNodeMap { public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if (item.name === 'style' && this._ownerElement._style) { - this._ownerElement._style.cssText = item.value; + if (item.name === 'style' && this.__ownerElement__.__style__) { + this.__ownerElement__.__style__.cssText = item.value; } return replacedItem || null; @@ -26,11 +26,11 @@ export default class HTMLElementNamedNodeMap extends ElementNamedNodeMap { /** * @override */ - public override _removeNamedItem(name: string): IAttr | null { - const removedItem = super._removeNamedItem(name); + public override __removeNamedItem__(name: string): IAttr | null { + const removedItem = super.__removeNamedItem__(name); - if (removedItem && removedItem.name === 'style' && this._ownerElement._style) { - this._ownerElement._style.cssText = ''; + if (removedItem && removedItem.name === 'style' && this.__ownerElement__.__style__) { + this.__ownerElement__.__style__.cssText = ''; } return removedItem; diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElementUtility.ts b/packages/happy-dom/src/nodes/html-element/HTMLElementUtility.ts index 435f8ebf2..b0e954bf8 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElementUtility.ts @@ -12,13 +12,13 @@ export default class HTMLElementUtility { * @param element Element. */ public static blur(element: IHTMLElement | ISVGElement): void { - if (element.ownerDocument['_activeElement'] !== element || !element.isConnected) { + if (element.ownerDocument['__activeElement__'] !== element || !element.isConnected) { return; } - const relatedTarget = element.ownerDocument['_nextActiveElement'] ?? null; + const relatedTarget = element.ownerDocument['__nextActiveElement__'] ?? null; - element.ownerDocument['_activeElement'] = null; + element.ownerDocument['__activeElement__'] = null; element.dispatchEvent( new FocusEvent('blur', { @@ -42,23 +42,23 @@ export default class HTMLElementUtility { * @param element Element. */ public static focus(element: IHTMLElement | ISVGElement): void { - if (element.ownerDocument['_activeElement'] === element || !element.isConnected) { + if (element.ownerDocument['__activeElement__'] === element || !element.isConnected) { return; } // Set the next active element so `blur` can use it for `relatedTarget`. - element.ownerDocument['_nextActiveElement'] = element; + element.ownerDocument['__nextActiveElement__'] = element; - const relatedTarget = element.ownerDocument['_activeElement']; + const relatedTarget = element.ownerDocument['__activeElement__']; - if (element.ownerDocument['_activeElement'] !== null) { - element.ownerDocument['_activeElement'].blur(); + if (element.ownerDocument['__activeElement__'] !== null) { + element.ownerDocument['__activeElement__'].blur(); } // Clean up after blur, so it does not affect next blur call. - element.ownerDocument['_nextActiveElement'] = null; + element.ownerDocument['__nextActiveElement__'] = null; - element.ownerDocument['_activeElement'] = element; + element.ownerDocument['__activeElement__'] = element; element.dispatchEvent( new FocusEvent('focus', { diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts index e8bada726..a2f5e78a6 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts @@ -14,7 +14,7 @@ export default class HTMLFormControlsCollection extends Array implements IHTMLFormControlsCollection { - public _namedItems: { [k: string]: RadioNodeList } = {}; + public __namedItems__: { [k: string]: RadioNodeList } = {}; /** * Returns item by index. @@ -42,11 +42,11 @@ export default class HTMLFormControlsCollection | IHTMLButtonElement | RadioNodeList | null { - if (this._namedItems[name] && this._namedItems[name].length) { - if (this._namedItems[name].length === 1) { - return this._namedItems[name][0]; + if (this.__namedItems__[name] && this.__namedItems__[name].length) { + if (this.__namedItems__[name].length === 1) { + return this.__namedItems__[name][0]; } - return this._namedItems[name]; + return this.__namedItems__[name]; } return null; } @@ -57,20 +57,22 @@ export default class HTMLFormControlsCollection * @param node Node. * @param name Name. */ - public _appendNamedItem( + public __appendNamedItem__( node: IHTMLInputElement | IHTMLTextAreaElement | IHTMLSelectElement | IHTMLButtonElement, name: string ): void { if (name) { - this._namedItems[name] = this._namedItems[name] || new RadioNodeList(); + this.__namedItems__[name] = this.__namedItems__[name] || new RadioNodeList(); - if (!this._namedItems[name].includes(node)) { - this._namedItems[name].push(node); + if (!this.__namedItems__[name].includes(node)) { + this.__namedItems__[name].push(node); } - if (this._isValidPropertyName(name)) { + if (this.__isValidPropertyName__(name)) { this[name] = - this._namedItems[name].length > 1 ? this._namedItems[name] : this._namedItems[name][0]; + this.__namedItems__[name].length > 1 + ? this.__namedItems__[name] + : this.__namedItems__[name][0]; } } } @@ -81,24 +83,26 @@ export default class HTMLFormControlsCollection * @param node Node. * @param name Name. */ - public _removeNamedItem( + public __removeNamedItem__( node: IHTMLInputElement | IHTMLTextAreaElement | IHTMLSelectElement | IHTMLButtonElement, name: string ): void { - if (name && this._namedItems[name]) { - const index = this._namedItems[name].indexOf(node); + if (name && this.__namedItems__[name]) { + const index = this.__namedItems__[name].indexOf(node); if (index > -1) { - this._namedItems[name].splice(index, 1); + this.__namedItems__[name].splice(index, 1); - if (this._namedItems[name].length === 0) { - delete this._namedItems[name]; - if (this.hasOwnProperty(name) && this._isValidPropertyName(name)) { + if (this.__namedItems__[name].length === 0) { + delete this.__namedItems__[name]; + if (this.hasOwnProperty(name) && this.__isValidPropertyName__(name)) { delete this[name]; } - } else if (this._isValidPropertyName(name)) { + } else if (this.__isValidPropertyName__(name)) { this[name] = - this._namedItems[name].length > 1 ? this._namedItems[name] : this._namedItems[name][0]; + this.__namedItems__[name].length > 1 + ? this.__namedItems__[name] + : this.__namedItems__[name][0]; } } } @@ -110,7 +114,7 @@ export default class HTMLFormControlsCollection * @param name Name. * @returns True if the property name is valid. */ - protected _isValidPropertyName(name: string): boolean { + protected __isValidPropertyName__(name: string): boolean { return ( !this.constructor.prototype.hasOwnProperty(name) && !Array.prototype.hasOwnProperty(name) && diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index 068265503..c2cc17942 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -27,7 +27,7 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle public onsubmit: (event: Event) => void | null = null; // Private properties - public _formNode: INode = this; + public __formNode__: INode = this; /** * Returns name. @@ -222,10 +222,10 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle public reset(): void { for (const element of this.elements) { if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { - element['_value'] = null; - element['_checked'] = null; + element['__value__'] = null; + element['__checked__'] = null; } else if (element.tagName === 'TEXTAREA') { - element['_value'] = null; + element['__value__'] = null; } else if (element.tagName === 'SELECT') { let hasSelectedAttribute = false; for (const option of (element).options) { @@ -295,7 +295,7 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle * @param node Node. * @param name Name */ - public _appendFormControlItem( + public __appendFormControlItem__( node: IHTMLInputElement | IHTMLTextAreaElement | IHTMLSelectElement | IHTMLButtonElement, name: string ): void { @@ -305,7 +305,7 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle (this.length) = this.elements.length; } - (this.elements)._appendNamedItem(node, name); + (this.elements).__appendNamedItem__(node, name); this[name] = this.elements[name]; } @@ -315,7 +315,7 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle * @param node Node. * @param name Name. */ - public _removeFormControlItem( + public __removeFormControlItem__( node: IHTMLInputElement | IHTMLTextAreaElement | IHTMLSelectElement | IHTMLButtonElement, name: string ): void { @@ -330,7 +330,7 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle (this.length)--; } - (this.elements)._removeNamedItem(node, name); + (this.elements).__removeNamedItem__(node, name); if (this.elements[name]) { this[name] = this.elements[name]; diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index 0ce7c18f2..7a98283ef 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -191,11 +191,11 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram /** * @override */ - public override _connectToNode(parentNode: INode = null): void { + public override __connectToNode__(parentNode: INode = null): void { const isConnected = this.isConnected; const isParentConnected = parentNode ? parentNode.isConnected : false; - super._connectToNode(parentNode); + super.__connectToNode__(parentNode); if (isConnected !== isParentConnected) { if (isParentConnected) { diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts index 4cb757234..7192bdbe3 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts @@ -51,7 +51,7 @@ export default class HTMLIFrameElementPageLoader { return; } - const window = this.#element.ownerDocument._defaultView; + const window = this.#element.ownerDocument.__defaultView__; const originURL = this.#browserParentFrame.window.location; const targetURL = BrowserFrameURL.getRelativeURL(this.#browserParentFrame, this.#element.src); diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 75781d48b..1ed499093 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -40,13 +40,13 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE public formMethod = ''; // Any type of input - public _value = null; - public _height = 0; - public _width = 0; + public __value__ = null; + public __height__ = 0; + public __width__ = 0; // Type specific: checkbox/radio public defaultChecked = false; - public _checked: boolean | null = null; + public __checked__: boolean | null = null; // Type specific: file public files: IFileList = new FileList(); @@ -61,9 +61,9 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE public onselectionchange: (event: Event) => void | null = null; // Type specific: text/password/search/tel/url/week/month - private _selectionStart: number = null; - private _selectionEnd: number = null; - private _selectionDirection: HTMLInputElementSelectionDirectionEnum = + #selectionStart: number = null; + #selectionEnd: number = null; + #selectionDirection: HTMLInputElementSelectionDirectionEnum = HTMLInputElementSelectionDirectionEnum.none; /** @@ -72,7 +72,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns Height. */ public get height(): number { - return this._height; + return this.__height__; } /** @@ -81,7 +81,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @param height Height. */ public set height(height: number) { - this._height = height; + this.__height__ = height; this.setAttribute('height', String(height)); } @@ -91,7 +91,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns Width. */ public get width(): number { - return this._width; + return this.__width__; } /** @@ -100,7 +100,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @param width Width. */ public set width(width: number) { - this._width = width; + this.__width__ = width; this.setAttribute('width', String(width)); } @@ -560,8 +560,8 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns Checked. */ public get checked(): boolean { - if (this._checked !== null) { - return this._checked; + if (this.__checked__ !== null) { + return this.__checked__; } return this.getAttribute('checked') !== null; } @@ -572,7 +572,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @param checked Checked. */ public set checked(checked: boolean) { - this._setChecked(checked); + this.#setChecked(checked); } /** @@ -596,11 +596,11 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE return this.files.length > 0 ? '/fake/path/' + this.files[0].name : ''; } - if (this._value === null) { + if (this.__value__ === null) { return this.getAttribute('value') || ''; } - return this._value; + return this.__value__; } /** @@ -630,13 +630,13 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE } break; default: - const oldValue = this._value; - this._value = HTMLInputElementValueSanitizer.sanitize(this, value); + const oldValue = this.__value__; + this.__value__ = HTMLInputElementValueSanitizer.sanitize(this, value); - if (oldValue !== this._value) { - this._selectionStart = this._value.length; - this._selectionEnd = this._value.length; - this._selectionDirection = HTMLInputElementSelectionDirectionEnum.none; + if (oldValue !== this.__value__) { + this.#selectionStart = this.__value__.length; + this.#selectionEnd = this.__value__.length; + this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none; } break; @@ -649,15 +649,15 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns Selection start. */ public get selectionStart(): number { - if (!this._isSelectionSupported()) { + if (!this.#isSelectionSupported()) { return null; } - if (this._selectionStart === null) { + if (this.#selectionStart === null) { return this.value.length; } - return this._selectionStart; + return this.#selectionStart; } /** @@ -666,14 +666,14 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @param start Start. */ public set selectionStart(start: number) { - if (!this._isSelectionSupported()) { + if (!this.#isSelectionSupported()) { throw new DOMException( `The input element's type (${this.type}) does not support selection.`, DOMExceptionNameEnum.invalidStateError ); } - this.setSelectionRange(start, Math.max(start, this.selectionEnd), this._selectionDirection); + this.setSelectionRange(start, Math.max(start, this.selectionEnd), this.#selectionDirection); } /** @@ -682,15 +682,15 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns Selection end. */ public get selectionEnd(): number { - if (!this._isSelectionSupported()) { + if (!this.#isSelectionSupported()) { return null; } - if (this._selectionEnd === null) { + if (this.#selectionEnd === null) { return this.value.length; } - return this._selectionEnd; + return this.#selectionEnd; } /** @@ -699,14 +699,14 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @param end End. */ public set selectionEnd(end: number) { - if (!this._isSelectionSupported()) { + if (!this.#isSelectionSupported()) { throw new DOMException( `The input element's type (${this.type}) does not support selection.`, DOMExceptionNameEnum.invalidStateError ); } - this.setSelectionRange(this.selectionStart, end, this._selectionDirection); + this.setSelectionRange(this.selectionStart, end, this.#selectionDirection); } /** @@ -715,11 +715,11 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns Selection direction. */ public get selectionDirection(): string { - if (!this._isSelectionSupported()) { + if (!this.#isSelectionSupported()) { return null; } - return this._selectionDirection; + return this.#selectionDirection; } /** @@ -728,14 +728,14 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @param direction Direction. */ public set selectionDirection(direction: string) { - if (!this._isSelectionSupported()) { + if (!this.#isSelectionSupported()) { throw new DOMException( `The input element's type (${this.type}) does not support selection.`, DOMExceptionNameEnum.invalidStateError ); } - this.setSelectionRange(this._selectionStart, this._selectionEnd, direction); + this.setSelectionRange(this.#selectionStart, this.#selectionEnd, direction); } /** @@ -766,7 +766,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns Form. */ public get form(): IHTMLFormElement { - return this._formNode; + return this.__formNode__; } /** @@ -976,13 +976,13 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * Selects the text. */ public select(): void { - if (!this._isSelectionSupported()) { + if (!this.#isSelectionSupported()) { return null; } - this._selectionStart = 0; - this._selectionEnd = this.value.length; - this._selectionDirection = HTMLInputElementSelectionDirectionEnum.none; + this.#selectionStart = 0; + this.#selectionEnd = this.value.length; + this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none; this.dispatchEvent(new Event('select', { bubbles: true, cancelable: true })); } @@ -995,16 +995,16 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @param [direction="none"] Direction. */ public setSelectionRange(start: number, end: number, direction = 'none'): void { - if (!this._isSelectionSupported()) { + if (!this.#isSelectionSupported()) { throw new DOMException( `The input element's type (${this.type}) does not support selection.`, DOMExceptionNameEnum.invalidStateError ); } - this._selectionEnd = Math.min(end, this.value.length); - this._selectionStart = Math.min(start, this._selectionEnd); - this._selectionDirection = + this.#selectionEnd = Math.min(end, this.value.length); + this.#selectionStart = Math.min(start, this.#selectionEnd); + this.#selectionDirection = direction === HTMLInputElementSelectionDirectionEnum.forward || direction === HTMLInputElementSelectionDirectionEnum.backward ? direction @@ -1027,7 +1027,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE end: number = null, selectionMode = HTMLInputElementSelectionModeEnum.preserve ): void { - if (!this._isSelectionSupported()) { + if (!this.#isSelectionSupported()) { throw new DOMException( `The input element's type (${this.type}) does not support selection.`, DOMExceptionNameEnum.invalidStateError @@ -1035,10 +1035,10 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE } if (start === null) { - start = this._selectionStart; + start = this.#selectionStart; } if (end === null) { - end = this._selectionEnd; + end = this.#selectionEnd; } if (start > end) { @@ -1052,8 +1052,8 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE end = Math.min(end, this.value.length); const val = this.value; - let selectionStart = this._selectionStart; - let selectionEnd = this._selectionEnd; + let selectionStart = this.#selectionStart; + let selectionEnd = this.#selectionEnd; this.value = val.slice(0, start) + replacement + val.slice(end); @@ -1152,14 +1152,14 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE const clone = super.cloneNode(deep); clone.formAction = this.formAction; clone.formMethod = this.formMethod; - clone._value = this._value; - clone._height = this._height; - clone._width = this._width; + clone.__value__ = this.__value__; + clone.__height__ = this.__height__; + clone.__width__ = this.__width__; clone.defaultChecked = this.defaultChecked; clone.files = this.files.slice(); - clone._selectionStart = this._selectionStart; - clone._selectionEnd = this._selectionEnd; - clone._selectionDirection = this._selectionDirection; + clone.#selectionStart = this.#selectionStart; + clone.#selectionEnd = this.#selectionEnd; + clone.#selectionDirection = this.#selectionDirection; return clone; } @@ -1184,7 +1184,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE const inputType = this.type; if (inputType === 'checkbox' || inputType === 'radio') { previousCheckedValue = this.checked; - this._setChecked(inputType === 'checkbox' ? !previousCheckedValue : true); + this.#setChecked(inputType === 'checkbox' ? !previousCheckedValue : true); } } @@ -1203,12 +1203,12 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE this.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); this.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); } else if (inputType === 'submit') { - const form = this._formNode; + const form = this.__formNode__; if (form) { form.requestSubmit(); } } else if (inputType === 'reset' && this.isConnected) { - const form = this._formNode; + const form = this.__formNode__; if (form) { form.reset(); } @@ -1226,7 +1226,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE ) { const inputType = this.type; if (inputType === 'checkbox' || inputType === 'radio') { - this._setChecked(previousCheckedValue); + this.#setChecked(previousCheckedValue); } } @@ -1236,19 +1236,19 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE /** * @override */ - public override _connectToNode(parentNode: INode = null): void { - const oldFormNode = this._formNode; + public override __connectToNode__(parentNode: INode = null): void { + const oldFormNode = this.__formNode__; - super._connectToNode(parentNode); + super.__connectToNode__(parentNode); - if (oldFormNode !== this._formNode) { + if (oldFormNode !== this.__formNode__) { if (oldFormNode) { - oldFormNode._removeFormControlItem(this, this.name); - oldFormNode._removeFormControlItem(this, this.id); + oldFormNode.__removeFormControlItem__(this, this.name); + oldFormNode.__removeFormControlItem__(this, this.id); } - if (this._formNode) { - (this._formNode)._appendFormControlItem(this, this.name); - (this._formNode)._appendFormControlItem(this, this.id); + if (this.__formNode__) { + (this.__formNode__).__appendFormControlItem__(this, this.name); + (this.__formNode__).__appendFormControlItem__(this, this.id); } } } @@ -1258,7 +1258,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * * @returns "true" if selection is supported. */ - private _isSelectionSupported(): boolean { + #isSelectionSupported(): boolean { const inputType = this.type; return ( inputType === 'text' || @@ -1274,16 +1274,16 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * * @param checked Checked. */ - private _setChecked(checked: boolean): void { - this._checked = checked; + #setChecked(checked: boolean): void { + this.__checked__ = checked; if (checked && this.type === 'radio' && this.name) { - const root = (this._formNode || this.getRootNode()); + const root = (this.__formNode__ || this.getRootNode()); const radioButtons = root.querySelectorAll(`input[type="radio"][name="${this.name}"]`); for (const radioButton of radioButtons) { if (radioButton !== this) { - radioButton['_checked'] = false; + radioButton['__checked__'] = false; } } } diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts index ef72fd0f2..3aa44d717 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts @@ -9,7 +9,7 @@ import HTMLInputElement from './HTMLInputElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLInputElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected _ownerElement: HTMLInputElement; + protected __ownerElement__: HTMLInputElement; /** * @override @@ -17,16 +17,16 @@ export default class HTMLInputElementNamedNodeMap extends HTMLElementNamedNodeMa public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if ((item.name === 'id' || item.name === 'name') && this._ownerElement._formNode) { + if ((item.name === 'id' || item.name === 'name') && this.__ownerElement__.__formNode__) { if (replacedItem && replacedItem.value) { - (this._ownerElement._formNode)._removeFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__removeFormControlItem__( + this.__ownerElement__, replacedItem.value ); } if (item.value) { - (this._ownerElement._formNode)._appendFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__appendFormControlItem__( + this.__ownerElement__, item.value ); } @@ -38,16 +38,16 @@ export default class HTMLInputElementNamedNodeMap extends HTMLElementNamedNodeMa /** * @override */ - public override _removeNamedItem(name: string): IAttr | null { - const removedItem = super._removeNamedItem(name); + public override __removeNamedItem__(name: string): IAttr | null { + const removedItem = super.__removeNamedItem__(name); if ( removedItem && (removedItem.name === 'id' || removedItem.name === 'name') && - this._ownerElement._formNode + this.__ownerElement__.__formNode__ ) { - (this._ownerElement._formNode)._removeFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__removeFormControlItem__( + this.__ownerElement__, removedItem.value ); } diff --git a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts index 7843407c5..0ccf1476a 100644 --- a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts +++ b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts @@ -57,7 +57,7 @@ export default class HTMLLabelElement extends HTMLElement implements IHTMLLabelE * @returns Form. */ public get form(): IHTMLFormElement { - return this._formNode; + return this.__formNode__; } /** diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts index 04d732761..b70023261 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts @@ -21,8 +21,8 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle public onerror: (event: ErrorEvent) => void = null; public onload: (event: Event) => void = null; public readonly sheet: CSSStyleSheet = null; - public _evaluateCSS = true; - public _relList: DOMTokenList = null; + public __evaluateCSS__ = true; + public __relList__: DOMTokenList = null; /** * Returns rel list. @@ -30,10 +30,10 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle * @returns Rel list. */ public get relList(): IDOMTokenList { - if (!this._relList) { - this._relList = new DOMTokenList(this, 'rel'); + if (!this.__relList__) { + this.__relList__ = new DOMTokenList(this, 'rel'); } - return this._relList; + return this.__relList__; } /** @@ -183,13 +183,13 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle /** * @override */ - public override _connectToNode(parentNode: INode = null): void { + public override __connectToNode__(parentNode: INode = null): void { const isConnected = this.isConnected; const isParentConnected = parentNode ? parentNode.isConnected : false; - super._connectToNode(parentNode); + super.__connectToNode__(parentNode); - if (isParentConnected && isConnected !== isParentConnected && this._evaluateCSS) { + if (isParentConnected && isConnected !== isParentConnected && this.__evaluateCSS__) { HTMLLinkElementUtility.loadExternalStylesheet(this); } } diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts index e14435c0b..b438ef747 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts @@ -9,7 +9,7 @@ import HTMLLinkElementUtility from './HTMLLinkElementUtility.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLLinkElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected _ownerElement: HTMLLinkElement; + protected __ownerElement__: HTMLLinkElement; /** * @override @@ -17,12 +17,12 @@ export default class HTMLLinkElementNamedNodeMap extends HTMLElementNamedNodeMap public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if (item.name === 'rel' && this._ownerElement._relList) { - this._ownerElement._relList._updateIndices(); + if (item.name === 'rel' && this.__ownerElement__.__relList__) { + this.__ownerElement__.__relList__.__updateIndices__(); } if (item.name === 'rel' || item.name === 'href') { - HTMLLinkElementUtility.loadExternalStylesheet(this._ownerElement); + HTMLLinkElementUtility.loadExternalStylesheet(this.__ownerElement__); } return replacedItem || null; @@ -31,11 +31,11 @@ export default class HTMLLinkElementNamedNodeMap extends HTMLElementNamedNodeMap /** * @override */ - public override _removeNamedItem(name: string): IAttr | null { - const removedItem = super._removeNamedItem(name); + public override __removeNamedItem__(name: string): IAttr | null { + const removedItem = super.__removeNamedItem__(name); - if (removedItem && removedItem.name === 'rel' && this._ownerElement._relList) { - this._ownerElement._relList._updateIndices(); + if (removedItem && removedItem.name === 'rel' && this.__ownerElement__.__relList__) { + this.__ownerElement__.__relList__.__updateIndices__(); } return removedItem; diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts index 71e8d67dd..a8c375d29 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts @@ -23,7 +23,7 @@ export default class HTMLLinkElementUtility { const href = element.getAttribute('href'); const rel = element.getAttribute('rel'); const browserSettings = WindowBrowserSettingsReader.getSettings( - element.ownerDocument._defaultView + element.ownerDocument.__defaultView__ ); if (href !== null && rel && rel.toLowerCase() === 'stylesheet' && element.isConnected) { @@ -38,22 +38,22 @@ export default class HTMLLinkElementUtility { return; } - (<{ _readyStateManager: DocumentReadyStateManager }>( - (element.ownerDocument._defaultView) - ))._readyStateManager.startTask(); + (<{ __readyStateManager__: DocumentReadyStateManager }>( + (element.ownerDocument.__defaultView__) + )).__readyStateManager__.startTask(); let code: string | null = null; let error: Error | null = null; try { - code = await ResourceFetch.fetch(element.ownerDocument._defaultView, href); + code = await ResourceFetch.fetch(element.ownerDocument.__defaultView__, href); } catch (e) { error = e; } - (<{ _readyStateManager: DocumentReadyStateManager }>( - (element.ownerDocument._defaultView) - ))._readyStateManager.endTask(); + (<{ __readyStateManager__: DocumentReadyStateManager }>( + (element.ownerDocument.__defaultView__) + )).__readyStateManager__.endTask(); if (error) { WindowErrorUtility.dispatchError(element, error); diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts index f676de1aa..37b6fd906 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts @@ -2,6 +2,7 @@ import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import HTMLElement from '../html-element/HTMLElement.js'; import IHTMLFormElement from '../html-form-element/IHTMLFormElement.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; +import IHTMLSelectElement from '../html-select-element/IHTMLSelectElement.js'; import INode from '../node/INode.js'; import HTMLOptionElementNamedNodeMap from './HTMLOptionElementNamedNodeMap.js'; import IHTMLOptionElement from './IHTMLOptionElement.js'; @@ -14,9 +15,8 @@ import IHTMLOptionElement from './IHTMLOptionElement.js'; */ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptionElement { public override readonly attributes: INamedNodeMap = new HTMLOptionElementNamedNodeMap(this); - public _index: number; - public _selectedness = false; - public _dirtyness = false; + public __selectedness__ = false; + public __dirtyness__ = false; /** * Returns inner text, which is the rendered appearance of text. @@ -42,7 +42,9 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @returns Index. */ public get index(): number { - return this._index; + return this.__selectNode__ + ? (this.__selectNode__).options.indexOf(this) + : 0; } /** @@ -51,7 +53,7 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @returns Form. */ public get form(): IHTMLFormElement { - return this._formNode; + return this.__formNode__; } /** @@ -60,7 +62,7 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @returns Selected. */ public get selected(): boolean { - return this._selectedness; + return this.__selectedness__; } /** @@ -69,13 +71,13 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @param selected Selected. */ public set selected(selected: boolean) { - const selectNode = this._selectNode; + const selectNode = this.__selectNode__; - this._dirtyness = true; - this._selectedness = Boolean(selected); + this.__dirtyness__ = true; + this.__selectedness__ = Boolean(selected); if (selectNode) { - selectNode._updateOptionItems(this._selectedness ? this : null); + selectNode.__updateOptionItems__(this.__selectedness__ ? this : null); } } @@ -122,17 +124,17 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio /** * @override */ - public override _connectToNode(parentNode: INode = null): void { - const oldSelectNode = this._selectNode; + public override __connectToNode__(parentNode: INode = null): void { + const oldSelectNode = this.__selectNode__; - super._connectToNode(parentNode); + super.__connectToNode__(parentNode); - if (oldSelectNode !== this._selectNode) { + if (oldSelectNode !== this.__selectNode__) { if (oldSelectNode) { - oldSelectNode._updateOptionItems(); + oldSelectNode.__updateOptionItems__(); } - if (this._selectNode) { - (this._selectNode)._updateOptionItems(); + if (this.__selectNode__) { + (this.__selectNode__).__updateOptionItems__(); } } } diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts index e9cdbc6ba..0ac134765 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts @@ -9,7 +9,7 @@ import HTMLOptionElement from './HTMLOptionElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLOptionElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected _ownerElement: HTMLOptionElement; + protected __ownerElement__: HTMLOptionElement; /** * @override @@ -18,16 +18,16 @@ export default class HTMLOptionElementNamedNodeMap extends HTMLElementNamedNodeM const replacedItem = super.setNamedItem(item); if ( - !this._ownerElement._dirtyness && + !this.__ownerElement__.__dirtyness__ && item.name === 'selected' && replacedItem?.value !== item.value ) { - const selectNode = this._ownerElement._selectNode; + const selectNode = this.__ownerElement__.__selectNode__; - this._ownerElement._selectedness = true; + this.__ownerElement__.__selectedness__ = true; if (selectNode) { - selectNode._updateOptionItems(this._ownerElement); + selectNode.__updateOptionItems__(this.__ownerElement__); } } @@ -37,16 +37,16 @@ export default class HTMLOptionElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override _removeNamedItem(name: string): IAttr | null { - const removedItem = super._removeNamedItem(name); + public override __removeNamedItem__(name: string): IAttr | null { + const removedItem = super.__removeNamedItem__(name); - if (removedItem && !this._ownerElement._dirtyness && removedItem.name === 'selected') { - const selectNode = this._ownerElement._selectNode; + if (removedItem && !this.__ownerElement__.__dirtyness__ && removedItem.name === 'selected') { + const selectNode = this.__ownerElement__.__selectNode__; - this._ownerElement._selectedness = false; + this.__ownerElement__.__selectedness__ = false; if (selectNode) { - selectNode._updateOptionItems(); + selectNode.__updateOptionItems__(); } } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index b14762487..f1e2ea0f8 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -19,7 +19,7 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip public override readonly attributes: INamedNodeMap = new HTMLScriptElementNamedNodeMap(this); public onerror: (event: ErrorEvent) => void = null; public onload: (event: Event) => void = null; - public _evaluateScript = true; + public __evaluateScript__ = true; /** * Returns type. @@ -169,16 +169,16 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip /** * @override */ - public override _connectToNode(parentNode: INode = null): void { + public override __connectToNode__(parentNode: INode = null): void { const isConnected = this.isConnected; const isParentConnected = parentNode ? parentNode.isConnected : false; const browserSettings = WindowBrowserSettingsReader.getSettings( - this.ownerDocument._defaultView + this.ownerDocument.__defaultView__ ); - super._connectToNode(parentNode); + super.__connectToNode__(parentNode); - if (isParentConnected && isConnected !== isParentConnected && this._evaluateScript) { + if (isParentConnected && isConnected !== isParentConnected && this.__evaluateScript__) { const src = this.getAttribute('src'); if (src !== null) { @@ -193,20 +193,20 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip type === 'application/x-javascript' || type.startsWith('text/javascript')) ) { - this.ownerDocument['_currentScript'] = this; + this.ownerDocument['__currentScript__'] = this; const code = - `//# sourceURL=${this.ownerDocument._defaultView.location.href}\n` + textContent; + `//# sourceURL=${this.ownerDocument.__defaultView__.location.href}\n` + textContent; if (browserSettings.disableErrorCapturing) { - this.ownerDocument._defaultView.eval(code); + this.ownerDocument.__defaultView__.eval(code); } else { - WindowErrorUtility.captureError(this.ownerDocument._defaultView, () => - this.ownerDocument._defaultView.eval(code) + WindowErrorUtility.captureError(this.ownerDocument.__defaultView__, () => + this.ownerDocument.__defaultView__.eval(code) ); } - this.ownerDocument['_currentScript'] = null; + this.ownerDocument['__currentScript__'] = null; } } } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts index 69be7f9f7..ed9e2f36b 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts @@ -9,7 +9,7 @@ import HTMLScriptElementUtility from './HTMLScriptElementUtility.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLScriptElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected _ownerElement: HTMLScriptElement; + protected __ownerElement__: HTMLScriptElement; /** * @override @@ -17,8 +17,8 @@ export default class HTMLScriptElementNamedNodeMap extends HTMLElementNamedNodeM public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if (item.name === 'src' && item.value !== null && this._ownerElement.isConnected) { - HTMLScriptElementUtility.loadExternalScript(this._ownerElement); + if (item.name === 'src' && item.value !== null && this.__ownerElement__.isConnected) { + HTMLScriptElementUtility.loadExternalScript(this.__ownerElement__); } return replacedItem || null; diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts index 4d648f5e9..01ca35f17 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts @@ -22,7 +22,7 @@ export default class HTMLScriptElementUtility { const src = element.getAttribute('src'); const async = element.getAttribute('async') !== null; const browserSettings = WindowBrowserSettingsReader.getSettings( - element.ownerDocument._defaultView + element.ownerDocument.__defaultView__ ); if ( @@ -43,22 +43,22 @@ export default class HTMLScriptElementUtility { let error: Error | null = null; if (async) { - (<{ _readyStateManager: DocumentReadyStateManager }>( - (element.ownerDocument._defaultView) - ))._readyStateManager.startTask(); + (<{ __readyStateManager__: DocumentReadyStateManager }>( + (element.ownerDocument.__defaultView__) + )).__readyStateManager__.startTask(); try { - code = await ResourceFetch.fetch(element.ownerDocument._defaultView, src); + code = await ResourceFetch.fetch(element.ownerDocument.__defaultView__, src); } catch (e) { error = e; } - (<{ _readyStateManager: DocumentReadyStateManager }>( - (element.ownerDocument._defaultView) - ))._readyStateManager.endTask(); + (<{ __readyStateManager__: DocumentReadyStateManager }>( + (element.ownerDocument.__defaultView__) + )).__readyStateManager__.endTask(); } else { try { - code = ResourceFetch.fetchSync(element.ownerDocument._defaultView, src); + code = ResourceFetch.fetchSync(element.ownerDocument.__defaultView__, src); } catch (e) { error = e; } @@ -70,16 +70,16 @@ export default class HTMLScriptElementUtility { throw error; } } else { - element.ownerDocument['_currentScript'] = element; + element.ownerDocument['__currentScript__'] = element; code = '//# sourceURL=' + src + '\n' + code; if (browserSettings.disableErrorCapturing) { - element.ownerDocument._defaultView.eval(code); + element.ownerDocument.__defaultView__.eval(code); } else { - WindowErrorUtility.captureError(element.ownerDocument._defaultView, () => - element.ownerDocument._defaultView.eval(code) + WindowErrorUtility.captureError(element.ownerDocument.__defaultView__, () => + element.ownerDocument.__defaultView__.eval(code) ); } - element.ownerDocument['_currentScript'] = null; + element.ownerDocument['__currentScript__'] = null; element.dispatchEvent(new Event('load')); } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts index 79c07daa0..de78bf375 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLOptionsCollection.ts @@ -14,7 +14,7 @@ export default class HTMLOptionsCollection extends HTMLCollection implements IHTMLOptionsCollection { - private _selectElement: IHTMLSelectElement; + #selectElement: IHTMLSelectElement; /** * @@ -23,7 +23,7 @@ export default class HTMLOptionsCollection constructor(selectElement: IHTMLSelectElement) { super(); - this._selectElement = selectElement; + this.#selectElement = selectElement; } /** @@ -32,7 +32,7 @@ export default class HTMLOptionsCollection * @returns SelectedIndex. */ public get selectedIndex(): number { - return this._selectElement.selectedIndex; + return this.#selectElement.selectedIndex; } /** @@ -41,7 +41,7 @@ export default class HTMLOptionsCollection * @param selectedIndex SelectedIndex. */ public set selectedIndex(selectedIndex: number) { - this._selectElement.selectedIndex = selectedIndex; + this.#selectElement.selectedIndex = selectedIndex; } /** @@ -60,7 +60,7 @@ export default class HTMLOptionsCollection */ public add(element: IHTMLOptionElement, before?: number | IHTMLOptionElement): void { if (!before && before !== 0) { - this._selectElement.appendChild(element); + this.#selectElement.appendChild(element); return; } @@ -69,7 +69,7 @@ export default class HTMLOptionsCollection return; } - this._selectElement.insertBefore(element, this[before]); + this.#selectElement.insertBefore(element, this[before]); return; } @@ -81,7 +81,7 @@ export default class HTMLOptionsCollection ); } - this._selectElement.insertBefore(element, this[index]); + this.#selectElement.insertBefore(element, this[index]); } /** @@ -91,7 +91,7 @@ export default class HTMLOptionsCollection */ public remove(index: number): void { if (this[index]) { - this._selectElement.removeChild(this[index]); + this.#selectElement.removeChild(this[index]); } } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index f7edfae77..ab32158d1 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -34,7 +34,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec public readonly validity = new ValidityState(this); // Private properties - public _selectNode: INode = this; + public __selectNode__: INode = this; // Events public onchange: (event: Event) => void | null = null; @@ -163,7 +163,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec public get value(): string { for (let i = 0, max = this.options.length; i < max; i++) { const option = this.options[i]; - if (option._selectedness) { + if (option.__selectedness__) { return option.value; } } @@ -180,10 +180,10 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec for (let i = 0, max = this.options.length; i < max; i++) { const option = this.options[i]; if (option.value === value) { - option._selectedness = true; - option._dirtyness = true; + option.__selectedness__ = true; + option.__dirtyness__ = true; } else { - option._selectedness = false; + option.__selectedness__ = false; } } } @@ -195,7 +195,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec */ public get selectedIndex(): number { for (let i = 0, max = this.options.length; i < max; i++) { - if ((this.options[i])._selectedness) { + if ((this.options[i]).__selectedness__) { return i; } } @@ -210,13 +210,13 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec public set selectedIndex(selectedIndex: number) { if (typeof selectedIndex === 'number' && !isNaN(selectedIndex)) { for (let i = 0, max = this.options.length; i < max; i++) { - (this.options[i])._selectedness = false; + (this.options[i]).__selectedness__ = false; } const selectedOption = this.options[selectedIndex]; if (selectedOption) { - selectedOption._selectedness = true; - selectedOption._dirtyness = true; + selectedOption.__selectedness__ = true; + selectedOption.__dirtyness__ = true; } } } @@ -236,7 +236,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec * @returns Form. */ public get form(): IHTMLFormElement { - return this._formNode; + return this.__formNode__; } /** @@ -326,7 +326,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec * @see https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm * @param [selectedOption] Selected option. */ - public _updateOptionItems(selectedOption?: IHTMLOptionElement): void { + public __updateOptionItems__(selectedOption?: IHTMLOptionElement): void { const optionElements = >this.getElementsByTagName('option'); if (optionElements.length < this.options.length) { @@ -346,11 +346,11 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec if (!isMultiple) { if (selectedOption) { - (optionElements[i])._selectedness = + (optionElements[i]).__selectedness__ = optionElements[i] === selectedOption; } - if ((optionElements[i])._selectedness) { + if ((optionElements[i]).__selectedness__) { selected.push(optionElements[i]); } } @@ -358,7 +358,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec (this.length) = optionElements.length; - const size = this._getDisplaySize(); + const size = this.#getDisplaySize(); if (size === 1 && !selected.length) { for (let i = 0, max = optionElements.length; i < max; i++) { @@ -376,13 +376,13 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec } if (!disabled) { - option._selectedness = true; + option.__selectedness__ = true; break; } } } else if (selected.length >= 2) { for (let i = 0, max = optionElements.length; i < max; i++) { - (optionElements[i])._selectedness = i === selected.length - 1; + (optionElements[i]).__selectedness__ = i === selected.length - 1; } } } @@ -390,19 +390,19 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec /** * @override */ - public override _connectToNode(parentNode: INode = null): void { - const oldFormNode = this._formNode; + public override __connectToNode__(parentNode: INode = null): void { + const oldFormNode = this.__formNode__; - super._connectToNode(parentNode); + super.__connectToNode__(parentNode); - if (oldFormNode !== this._formNode) { + if (oldFormNode !== this.__formNode__) { if (oldFormNode) { - oldFormNode._removeFormControlItem(this, this.name); - oldFormNode._removeFormControlItem(this, this.id); + oldFormNode.__removeFormControlItem__(this, this.name); + oldFormNode.__removeFormControlItem__(this, this.id); } - if (this._formNode) { - (this._formNode)._appendFormControlItem(this, this.name); - (this._formNode)._appendFormControlItem(this, this.id); + if (this.__formNode__) { + (this.__formNode__).__appendFormControlItem__(this, this.name); + (this.__formNode__).__appendFormControlItem__(this, this.id); } } } @@ -412,7 +412,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec * * @returns Display size. */ - protected _getDisplaySize(): number { + #getDisplaySize(): number { if (this.hasAttributeNS(null, 'size')) { const size = parseInt(this.getAttribute('size')); if (!isNaN(size) && size >= 0) { diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts index 90793d6d4..ad3c912a7 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts @@ -9,7 +9,7 @@ import HTMLSelectElement from './HTMLSelectElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLSelectElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected _ownerElement: HTMLSelectElement; + protected __ownerElement__: HTMLSelectElement; /** * @override @@ -17,16 +17,16 @@ export default class HTMLSelectElementNamedNodeMap extends HTMLElementNamedNodeM public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if ((item.name === 'id' || item.name === 'name') && this._ownerElement._formNode) { + if ((item.name === 'id' || item.name === 'name') && this.__ownerElement__.__formNode__) { if (replacedItem && replacedItem.value) { - (this._ownerElement._formNode)._removeFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__removeFormControlItem__( + this.__ownerElement__, replacedItem.value ); } if (item.value) { - (this._ownerElement._formNode)._appendFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__appendFormControlItem__( + this.__ownerElement__, item.value ); } @@ -38,16 +38,16 @@ export default class HTMLSelectElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override _removeNamedItem(name: string): IAttr | null { - const removedItem = super._removeNamedItem(name); + public override __removeNamedItem__(name: string): IAttr | null { + const removedItem = super.__removeNamedItem__(name); if ( removedItem && (removedItem.name === 'id' || removedItem.name === 'name') && - this._ownerElement._formNode + this.__ownerElement__.__formNode__ ) { - (this._ownerElement._formNode)._removeFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__removeFormControlItem__( + this.__ownerElement__, removedItem.value ); } diff --git a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts index b7c754b29..853ae9c94 100644 --- a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts +++ b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts @@ -62,7 +62,7 @@ export default class HTMLSlotElement extends HTMLElement implements IHTMLSlotEle return this.assignedElements(options); } - return (host)._childNodes.slice(); + return (host).__childNodes__.slice(); } return []; @@ -86,7 +86,7 @@ export default class HTMLSlotElement extends HTMLElement implements IHTMLSlotEle if (name) { const assignedElements = []; - for (const child of (host)._children) { + for (const child of (host).__children__) { if (child.slot === name) { assignedElements.push(child); } @@ -95,7 +95,7 @@ export default class HTMLSlotElement extends HTMLElement implements IHTMLSlotEle return assignedElements; } - return (host)._children.slice(); + return (host).__children__.slice(); } return []; 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 982620789..7354c4135 100644 --- a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts +++ b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts @@ -9,7 +9,7 @@ import IHTMLStyleElement from './IHTMLStyleElement.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement. */ export default class HTMLStyleElement extends HTMLElement implements IHTMLStyleElement { - private _styleSheet: CSSStyleSheet | null = null; + private __styleSheet__: CSSStyleSheet | null = null; /** * Returns CSS style sheet. @@ -20,11 +20,11 @@ export default class HTMLStyleElement extends HTMLElement implements IHTMLStyleE if (!this.isConnected) { return null; } - if (!this._styleSheet) { - this._styleSheet = new CSSStyleSheet(); + if (!this.__styleSheet__) { + this.__styleSheet__ = new CSSStyleSheet(); } - this._styleSheet.replaceSync(this.textContent); - return this._styleSheet; + this.__styleSheet__.replaceSync(this.textContent); + return this.__styleSheet__; } /** diff --git a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts index 367be4cb3..98897a022 100644 --- a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts +++ b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts @@ -26,7 +26,7 @@ export default class HTMLTemplateElement extends HTMLElement implements IHTMLTem * @override */ public set innerHTML(html: string) { - for (const child of (this.content)._childNodes.slice()) { + for (const child of (this.content).__childNodes__.slice()) { this.content.removeChild(child); } @@ -56,7 +56,7 @@ export default class HTMLTemplateElement extends HTMLElement implements IHTMLTem escapeEntities: false }); let xml = ''; - for (const node of (this.content)._childNodes) { + for (const node of (this.content).__childNodes__) { xml += xmlSerializer.serializeToString(node); } return xml; diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index 2139289de..34e05912e 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -31,11 +31,11 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex public oninput: (event: Event) => void | null = null; public onselectionchange: (event: Event) => void | null = null; - public _value = null; - public _selectionStart = null; - public _selectionEnd = null; - public _selectionDirection = HTMLInputElementSelectionDirectionEnum.none; - public _textAreaNode: HTMLTextAreaElement = this; + public __value__ = null; + #selectionStart = null; + #selectionEnd = null; + #selectionDirection = HTMLInputElementSelectionDirectionEnum.none; + public __textAreaNode__: HTMLTextAreaElement = this; /** * Returns the default value. @@ -301,11 +301,11 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @returns Value. */ public get value(): string { - if (this._value === null) { + if (this.__value__ === null) { return this.textContent; } - return this._value; + return this.__value__; } /** @@ -314,13 +314,13 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @param value Value. */ public set value(value: string) { - const oldValue = this._value; - this._value = value; + const oldValue = this.__value__; + this.__value__ = value; - if (oldValue !== this._value) { - this._selectionStart = this._value.length; - this._selectionEnd = this._value.length; - this._selectionDirection = HTMLInputElementSelectionDirectionEnum.none; + if (oldValue !== this.__value__) { + this.#selectionStart = this.__value__.length; + this.#selectionEnd = this.__value__.length; + this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none; } } @@ -330,11 +330,11 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @returns Selection start. */ public get selectionStart(): number { - if (this._selectionStart === null) { + if (this.#selectionStart === null) { return this.value.length; } - return this._selectionStart; + return this.#selectionStart; } /** @@ -343,7 +343,7 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @param start Start. */ public set selectionStart(start: number) { - this.setSelectionRange(start, Math.max(start, this.selectionEnd), this._selectionDirection); + this.setSelectionRange(start, Math.max(start, this.selectionEnd), this.#selectionDirection); } /** @@ -352,11 +352,11 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @returns Selection end. */ public get selectionEnd(): number { - if (this._selectionEnd === null) { + if (this.#selectionEnd === null) { return this.value.length; } - return this._selectionEnd; + return this.#selectionEnd; } /** @@ -365,7 +365,7 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @param end End. */ public set selectionEnd(end: number) { - this.setSelectionRange(this.selectionStart, end, this._selectionDirection); + this.setSelectionRange(this.selectionStart, end, this.#selectionDirection); } /** @@ -374,7 +374,7 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @returns Selection direction. */ public get selectionDirection(): string { - return this._selectionDirection; + return this.#selectionDirection; } /** @@ -392,7 +392,7 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @returns Form. */ public get form(): IHTMLFormElement { - return this._formNode; + return this.__formNode__; } /** @@ -417,9 +417,9 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * Selects the text. */ public select(): void { - this._selectionStart = 0; - this._selectionEnd = this.value.length; - this._selectionDirection = HTMLInputElementSelectionDirectionEnum.none; + this.#selectionStart = 0; + this.#selectionEnd = this.value.length; + this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none; this.dispatchEvent(new Event('select', { bubbles: true, cancelable: true })); } @@ -432,9 +432,9 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @param [direction="none"] Direction. */ public setSelectionRange(start: number, end: number, direction = 'none'): void { - this._selectionEnd = Math.min(end, this.value.length); - this._selectionStart = Math.min(start, this.selectionEnd); - this._selectionDirection = + this.#selectionEnd = Math.min(end, this.value.length); + this.#selectionStart = Math.min(start, this.selectionEnd); + this.#selectionDirection = direction === HTMLInputElementSelectionDirectionEnum.forward || direction === HTMLInputElementSelectionDirectionEnum.backward ? direction @@ -458,10 +458,10 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex selectionMode = HTMLInputElementSelectionModeEnum.preserve ): void { if (start === null) { - start = this._selectionStart; + start = this.#selectionStart; } if (end === null) { - end = this._selectionEnd; + end = this.#selectionEnd; } if (start > end) { @@ -475,8 +475,8 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex end = Math.min(end, this.value.length); const val = this.value; - let selectionStart = this._selectionStart; - let selectionEnd = this._selectionEnd; + let selectionStart = this.#selectionStart; + let selectionEnd = this.#selectionEnd; this.value = val.slice(0, start) + replacement + val.slice(end); @@ -553,10 +553,10 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex public cloneNode(deep = false): IHTMLTextAreaElement { const clone = super.cloneNode(deep); - clone._value = this._value; - clone._selectionStart = this._selectionStart; - clone._selectionEnd = this._selectionEnd; - clone._selectionDirection = this._selectionDirection; + clone.__value__ = this.__value__; + clone.#selectionStart = this.#selectionStart; + clone.#selectionEnd = this.#selectionEnd; + clone.#selectionDirection = this.#selectionDirection; return clone; } @@ -564,30 +564,30 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex /** * Resets selection. */ - public _resetSelection(): void { - if (this._value === null) { - this._selectionStart = null; - this._selectionEnd = null; - this._selectionDirection = HTMLInputElementSelectionDirectionEnum.none; + public __resetSelection__(): void { + if (this.__value__ === null) { + this.#selectionStart = null; + this.#selectionEnd = null; + this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none; } } /** * @override */ - public override _connectToNode(parentNode: INode = null): void { - const oldFormNode = this._formNode; + public override __connectToNode__(parentNode: INode = null): void { + const oldFormNode = this.__formNode__; - super._connectToNode(parentNode); + super.__connectToNode__(parentNode); - if (oldFormNode !== this._formNode) { + if (oldFormNode !== this.__formNode__) { if (oldFormNode) { - oldFormNode._removeFormControlItem(this, this.name); - oldFormNode._removeFormControlItem(this, this.id); + oldFormNode.__removeFormControlItem__(this, this.name); + oldFormNode.__removeFormControlItem__(this, this.id); } - if (this._formNode) { - (this._formNode)._appendFormControlItem(this, this.name); - (this._formNode)._appendFormControlItem(this, this.id); + if (this.__formNode__) { + (this.__formNode__).__appendFormControlItem__(this, this.name); + (this.__formNode__).__appendFormControlItem__(this, this.id); } } } diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts index a829469c3..4ed117fb8 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts @@ -9,7 +9,7 @@ import HTMLTextAreaElement from './HTMLTextAreaElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLTextAreaElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected _ownerElement: HTMLTextAreaElement; + protected __ownerElement__: HTMLTextAreaElement; /** * @override @@ -17,16 +17,16 @@ export default class HTMLTextAreaElementNamedNodeMap extends HTMLElementNamedNod public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if ((item.name === 'id' || item.name === 'name') && this._ownerElement._formNode) { + if ((item.name === 'id' || item.name === 'name') && this.__ownerElement__.__formNode__) { if (replacedItem && replacedItem.value) { - (this._ownerElement._formNode)._removeFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__removeFormControlItem__( + this.__ownerElement__, replacedItem.value ); } if (item.value) { - (this._ownerElement._formNode)._appendFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__appendFormControlItem__( + this.__ownerElement__, item.value ); } @@ -38,16 +38,16 @@ export default class HTMLTextAreaElementNamedNodeMap extends HTMLElementNamedNod /** * @override */ - public override _removeNamedItem(name: string): IAttr | null { - const removedItem = super._removeNamedItem(name); + public override __removeNamedItem__(name: string): IAttr | null { + const removedItem = super.__removeNamedItem__(name); if ( removedItem && (removedItem.name === 'id' || removedItem.name === 'name') && - this._ownerElement._formNode + this.__ownerElement__.__formNode__ ) { - (this._ownerElement._formNode)._removeFormControlItem( - this._ownerElement, + (this.__ownerElement__.__formNode__).__removeFormControlItem__( + this.__ownerElement__, removedItem.value ); } diff --git a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts index 082fd1ff8..d44f3e785 100644 --- a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts +++ b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts @@ -15,61 +15,69 @@ import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js' * https://developer.mozilla.org/en-US/docs/Web/API/HTMLUnknownElement. */ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElement { - private _customElementDefineCallback: () => void = null; + #customElementDefineCallback: () => void = null; /** * Connects this element to another element. * * @param parentNode Parent node. */ - public _connectToNode(parentNode: INode = null): void { + public __connectToNode__(parentNode: INode = null): void { const tagName = this.tagName; // This element can potentially be a custom element that has not been defined yet // Therefore we need to register a callback for when it is defined in CustomElementRegistry and replace it with the registered element (see #404) - if (tagName.includes('-') && this.ownerDocument._defaultView.customElements._callbacks) { - const callbacks = this.ownerDocument._defaultView.customElements._callbacks; + if (tagName.includes('-') && this.ownerDocument.__defaultView__.customElements.__callbacks__) { + const callbacks = this.ownerDocument.__defaultView__.customElements.__callbacks__; - if (parentNode && !this._customElementDefineCallback) { + if (parentNode && !this.#customElementDefineCallback) { const callback = (): void => { if (this.parentNode) { const newElement = this.ownerDocument.createElement(tagName); - (>newElement._childNodes) = this._childNodes; - (>newElement._children) = this._children; + (>newElement.__childNodes__) = this.__childNodes__; + (>newElement.__children__) = this.__children__; (newElement.isConnected) = this.isConnected; - newElement._rootNode = this._rootNode; - newElement._formNode = this._formNode; - newElement._selectNode = this._selectNode; - newElement._textAreaNode = this._textAreaNode; - newElement._observers = this._observers; - newElement._isValue = this._isValue; + newElement.__rootNode__ = this.__rootNode__; + newElement.__formNode__ = this.__formNode__; + newElement.__selectNode__ = this.__selectNode__; + newElement.__textAreaNode__ = this.__textAreaNode__; + newElement.__observers__ = this.__observers__; + newElement.__isValue__ = this.__isValue__; for (let i = 0, max = this.attributes.length; i < max; i++) { newElement.attributes.setNamedItem(this.attributes[i]); } - (>this._childNodes) = new NodeList(); - (>this._children) = new HTMLCollection(); - this._rootNode = null; - this._formNode = null; - this._selectNode = null; - this._textAreaNode = null; - this._observers = []; - this._isValue = null; + (>this.__childNodes__) = new NodeList(); + (>this.__children__) = new HTMLCollection(); + this.__rootNode__ = null; + this.__formNode__ = null; + this.__selectNode__ = null; + this.__textAreaNode__ = null; + this.__observers__ = []; + this.__isValue__ = null; (this.attributes) = new HTMLElementNamedNodeMap(this); - for (let i = 0, max = (this.parentNode)._childNodes.length; i < max; i++) { - if ((this.parentNode)._childNodes[i] === this) { - (this.parentNode)._childNodes[i] = newElement; + for ( + let i = 0, max = (this.parentNode).__childNodes__.length; + i < max; + i++ + ) { + if ((this.parentNode).__childNodes__[i] === this) { + (this.parentNode).__childNodes__[i] = newElement; break; } } - if ((this.parentNode)._children) { - for (let i = 0, max = (this.parentNode)._children.length; i < max; i++) { - if ((this.parentNode)._children[i] === this) { - (this.parentNode)._children[i] = newElement; + if ((this.parentNode).__children__) { + for ( + let i = 0, max = (this.parentNode).__children__.length; + i < max; + i++ + ) { + if ((this.parentNode).__children__[i] === this) { + (this.parentNode).__children__[i] = newElement; break; } } @@ -80,24 +88,24 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem newElement.connectedCallback(); } - this._connectToNode(null); + this.__connectToNode__(null); } }; callbacks[tagName] = callbacks[tagName] || []; callbacks[tagName].push(callback); - this._customElementDefineCallback = callback; - } else if (!parentNode && callbacks[tagName] && this._customElementDefineCallback) { - const index = callbacks[tagName].indexOf(this._customElementDefineCallback); + this.#customElementDefineCallback = callback; + } else if (!parentNode && callbacks[tagName] && this.#customElementDefineCallback) { + const index = callbacks[tagName].indexOf(this.#customElementDefineCallback); if (index !== -1) { callbacks[tagName].splice(index, 1); } if (!callbacks[tagName].length) { delete callbacks[tagName]; } - this._customElementDefineCallback = null; + this.#customElementDefineCallback = null; } } - super._connectToNode(parentNode); + super.__connectToNode__(parentNode); } } diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index bf110e785..763e9a9ff 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -16,7 +16,7 @@ import INodeList from './INodeList.js'; */ export default class Node extends EventTarget implements INode { // Can be set before the Node is created. - public static _ownerDocument: IDocument | null = null; + public static __ownerDocument__: IDocument | null = null; // Public properties public static readonly ELEMENT_NODE = NodeTypeEnum.elementNode; @@ -56,12 +56,12 @@ export default class Node extends EventTarget implements INode { public readonly isConnected: boolean = false; // Custom Properties (not part of HTML standard) - public _rootNode: INode = null; - public _formNode: INode = null; - public _selectNode: INode = null; - public _textAreaNode: INode = null; - public _observers: MutationListener[] = []; - public readonly _childNodes: INodeList = new NodeList(); + public __rootNode__: INode = null; + public __formNode__: INode = null; + public __selectNode__: INode = null; + public __textAreaNode__: INode = null; + public __observers__: MutationListener[] = []; + public readonly __childNodes__: INodeList = new NodeList(); #ownerDocument: IDocument | null = null; /** @@ -70,8 +70,8 @@ export default class Node extends EventTarget implements INode { constructor() { super(); - if ((this.constructor)._ownerDocument) { - this.#ownerDocument = (this.constructor)._ownerDocument; + if ((this.constructor).__ownerDocument__) { + this.#ownerDocument = (this.constructor).__ownerDocument__; } } @@ -97,7 +97,7 @@ export default class Node extends EventTarget implements INode { * @returns Child nodes list. */ public get childNodes(): INodeList { - return this._childNodes; + return this.__childNodes__; } /** @@ -152,9 +152,9 @@ export default class Node extends EventTarget implements INode { */ public get previousSibling(): INode { if (this.parentNode) { - const index = (this.parentNode)._childNodes.indexOf(this); + const index = (this.parentNode).__childNodes__.indexOf(this); if (index > 0) { - return (this.parentNode)._childNodes[index - 1]; + return (this.parentNode).__childNodes__[index - 1]; } } return null; @@ -167,9 +167,9 @@ export default class Node extends EventTarget implements INode { */ public get nextSibling(): INode { if (this.parentNode) { - const index = (this.parentNode)._childNodes.indexOf(this); - if (index > -1 && index + 1 < (this.parentNode)._childNodes.length) { - return (this.parentNode)._childNodes[index + 1]; + const index = (this.parentNode).__childNodes__.indexOf(this); + if (index > -1 && index + 1 < (this.parentNode).__childNodes__.length) { + return (this.parentNode).__childNodes__[index + 1]; } } return null; @@ -181,8 +181,8 @@ export default class Node extends EventTarget implements INode { * @returns Node. */ public get firstChild(): INode { - if (this._childNodes.length > 0) { - return this._childNodes[0]; + if (this.__childNodes__.length > 0) { + return this.__childNodes__[0]; } return null; } @@ -193,8 +193,8 @@ export default class Node extends EventTarget implements INode { * @returns Node. */ public get lastChild(): INode { - if (this._childNodes.length > 0) { - return this._childNodes[this._childNodes.length - 1]; + if (this.__childNodes__.length > 0) { + return this.__childNodes__[this.__childNodes__.length - 1]; } return null; } @@ -222,7 +222,7 @@ export default class Node extends EventTarget implements INode { if (base) { return base.href; } - return this.ownerDocument._defaultView.location.href; + return this.ownerDocument.__defaultView__.location.href; } /** @@ -241,7 +241,7 @@ export default class Node extends EventTarget implements INode { * @returns "true" if the node has child nodes. */ public hasChildNodes(): boolean { - return this._childNodes.length > 0; + return this.__childNodes__.length > 0; } /** @@ -266,8 +266,8 @@ export default class Node extends EventTarget implements INode { return this; } - if (this._rootNode && !options?.composed) { - return this._rootNode; + if (this.__rootNode__ && !options?.composed) { + return this.__rootNode__; } return this.ownerDocument; @@ -280,22 +280,22 @@ export default class Node extends EventTarget implements INode { * @returns Cloned node. */ public cloneNode(deep = false): INode { - (this.constructor)._ownerDocument = this.ownerDocument; + (this.constructor).__ownerDocument__ = this.ownerDocument; const clone = new (this.constructor)(); - (this.constructor)._ownerDocument = null; + (this.constructor).__ownerDocument__ = null; // Document has childNodes directly when it is created - if (clone._childNodes.length) { - for (const node of clone._childNodes.slice()) { + if (clone.__childNodes__.length) { + for (const node of clone.__childNodes__.slice()) { node.parentNode.removeChild(node); } } if (deep) { - for (const childNode of this._childNodes) { + for (const childNode of this.__childNodes__) { const childClone = childNode.cloneNode(true); (childClone.parentNode) = clone; - clone._childNodes.push(childClone); + clone.__childNodes__.push(childClone); } } @@ -367,11 +367,11 @@ export default class Node extends EventTarget implements INode { * * @param listener Listener. */ - public _observe(listener: MutationListener): void { - this._observers.push(listener); + public __observe__(listener: MutationListener): void { + this.__observers__.push(listener); if (listener.options.subtree) { - for (const node of this._childNodes) { - (node)._observe(listener); + for (const node of this.__childNodes__) { + (node).__observe__(listener); } } } @@ -382,14 +382,14 @@ export default class Node extends EventTarget implements INode { * * @param listener Listener. */ - public _unobserve(listener: MutationListener): void { - const index = this._observers.indexOf(listener); + public __unobserve__(listener: MutationListener): void { + const index = this.__observers__.indexOf(listener); if (index !== -1) { - this._observers.splice(index, 1); + this.__observers__.splice(index, 1); } if (listener.options.subtree) { - for (const node of this._childNodes) { - (node)._unobserve(listener); + for (const node of this.__childNodes__) { + (node).__unobserve__(listener); } } } @@ -399,26 +399,27 @@ export default class Node extends EventTarget implements INode { * * @param parentNode Parent node. */ - public _connectToNode(parentNode: INode = null): void { + public __connectToNode__(parentNode: INode = null): void { const isConnected = !!parentNode && parentNode.isConnected; - const formNode = (this)._formNode; - const selectNode = (this)._selectNode; - const textAreaNode = (this)._textAreaNode; + const formNode = (this).__formNode__; + const selectNode = (this).__selectNode__; + const textAreaNode = (this).__textAreaNode__; if (this.nodeType !== NodeTypeEnum.documentFragmentNode) { (this.parentNode) = parentNode; - (this)._rootNode = isConnected && parentNode ? (parentNode)._rootNode : null; + (this).__rootNode__ = + isConnected && parentNode ? (parentNode).__rootNode__ : null; if (this['tagName'] !== 'FORM') { - (this)._formNode = parentNode ? (parentNode)._formNode : null; + (this).__formNode__ = parentNode ? (parentNode).__formNode__ : null; } if (this['tagName'] !== 'SELECT') { - (this)._selectNode = parentNode ? (parentNode)._selectNode : null; + (this).__selectNode__ = parentNode ? (parentNode).__selectNode__ : null; } if (this['tagName'] !== 'TEXTAREA') { - (this)._textAreaNode = parentNode ? (parentNode)._textAreaNode : null; + (this).__textAreaNode__ = parentNode ? (parentNode).__textAreaNode__ : null; } } @@ -426,8 +427,8 @@ export default class Node extends EventTarget implements INode { (this.isConnected) = isConnected; if (!isConnected) { - if (this.ownerDocument['_activeElement'] === this) { - this.ownerDocument['_activeElement'] = null; + if (this.ownerDocument['__activeElement__'] === this) { + this.ownerDocument['__activeElement__'] = null; } } @@ -437,22 +438,22 @@ export default class Node extends EventTarget implements INode { this.disconnectedCallback(); } - for (const child of this._childNodes) { - (child)._connectToNode(this); + for (const child of this.__childNodes__) { + (child).__connectToNode__(this); } // eslint-disable-next-line - if ((this)._shadowRoot) { + if ((this).__shadowRoot__) { // eslint-disable-next-line - (this)._shadowRoot._connectToNode(this); + (this).__shadowRoot__.__connectToNode__(this); } } else if ( - formNode !== this._formNode || - selectNode !== this._selectNode || - textAreaNode !== this._textAreaNode + formNode !== this.__formNode__ || + selectNode !== this.__selectNode__ || + textAreaNode !== this.__textAreaNode__ ) { - for (const child of this._childNodes) { - (child)._connectToNode(this); + for (const child of this.__childNodes__) { + (child).__connectToNode__(this); } } } @@ -603,7 +604,7 @@ export default class Node extends EventTarget implements INode { const computeNodeIndexes = (nodes: INode[]): void => { for (const childNode of nodes) { - computeNodeIndexes((childNode)._childNodes); + computeNodeIndexes((childNode).__childNodes__); if (childNode === node2Node) { node2Index = indexes; @@ -619,7 +620,7 @@ export default class Node extends EventTarget implements INode { } }; - computeNodeIndexes((commonAncestor)._childNodes); + computeNodeIndexes((commonAncestor).__childNodes__); /** * 9. If node1 is preceding node2, then return DOCUMENT_POSITION_PRECEDING. diff --git a/packages/happy-dom/src/nodes/node/NodeUtility.ts b/packages/happy-dom/src/nodes/node/NodeUtility.ts index 69d1a6a34..cd3ed067c 100644 --- a/packages/happy-dom/src/nodes/node/NodeUtility.ts +++ b/packages/happy-dom/src/nodes/node/NodeUtility.ts @@ -47,7 +47,7 @@ export default class NodeUtility { // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment if (node.nodeType === NodeTypeEnum.documentFragmentNode) { - for (const child of (node)._childNodes.slice()) { + for (const child of (node).__childNodes__.slice()) { ancestorNode.appendChild(child); } return node; @@ -55,30 +55,30 @@ export default class NodeUtility { // Remove the node from its previous parent if it has any. if (node.parentNode) { - const index = (node.parentNode)._childNodes.indexOf(node); + const index = (node.parentNode).__childNodes__.indexOf(node); if (index !== -1) { - (node.parentNode)._childNodes.splice(index, 1); + (node.parentNode).__childNodes__.splice(index, 1); } } if (ancestorNode.isConnected) { - (ancestorNode.ownerDocument || this)['_cacheID']++; + (ancestorNode.ownerDocument || this)['__cacheID__']++; } - (ancestorNode)._childNodes.push(node); + (ancestorNode).__childNodes__.push(node); - (node)._connectToNode(ancestorNode); + (node).__connectToNode__(ancestorNode); // MutationObserver - if ((ancestorNode)._observers.length > 0) { + if ((ancestorNode).__observers__.length > 0) { const record = new MutationRecord(); record.target = ancestorNode; record.type = MutationTypeEnum.childList; record.addedNodes = [node]; - for (const observer of (ancestorNode)._observers) { + for (const observer of (ancestorNode).__observers__) { if (observer.options.subtree) { - (node)._observe(observer); + (node).__observe__(observer); } if (observer.options.childList) { observer.callback([record], observer.observer); @@ -97,29 +97,29 @@ export default class NodeUtility { * @returns Removed node. */ public static removeChild(ancestorNode: INode, node: INode): INode { - const index = (ancestorNode)._childNodes.indexOf(node); + const index = (ancestorNode).__childNodes__.indexOf(node); if (index === -1) { throw new DOMException('Failed to remove node. Node is not child of parent.'); } if (ancestorNode.isConnected) { - (ancestorNode.ownerDocument || this)['_cacheID']++; + (ancestorNode.ownerDocument || this)['__cacheID__']++; } - (ancestorNode)._childNodes.splice(index, 1); + (ancestorNode).__childNodes__.splice(index, 1); - (node)._connectToNode(null); + (node).__connectToNode__(null); // MutationObserver - if ((ancestorNode)._observers.length > 0) { + if ((ancestorNode).__observers__.length > 0) { const record = new MutationRecord(); record.target = ancestorNode; record.type = MutationTypeEnum.childList; record.removedNodes = [node]; - for (const observer of (ancestorNode)._observers) { - (node)._unobserve(observer); + for (const observer of (ancestorNode).__observers__) { + (node).__unobserve__(observer); if (observer.options.childList) { observer.callback([record], observer.observer); } @@ -158,7 +158,7 @@ export default class NodeUtility { // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment if (newNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - for (const child of (newNode)._childNodes.slice()) { + for (const child of (newNode).__childNodes__.slice()) { ancestorNode.insertBefore(child, referenceNode); } return newNode; @@ -171,41 +171,41 @@ export default class NodeUtility { return newNode; } - if ((ancestorNode)._childNodes.indexOf(referenceNode) === -1) { + if ((ancestorNode).__childNodes__.indexOf(referenceNode) === -1) { throw new DOMException( "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node." ); } if (ancestorNode.isConnected) { - (ancestorNode.ownerDocument || this)['_cacheID']++; + (ancestorNode.ownerDocument || this)['__cacheID__']++; } if (newNode.parentNode) { - const index = (newNode.parentNode)._childNodes.indexOf(newNode); + const index = (newNode.parentNode).__childNodes__.indexOf(newNode); if (index !== -1) { - (newNode.parentNode)._childNodes.splice(index, 1); + (newNode.parentNode).__childNodes__.splice(index, 1); } } - (ancestorNode)._childNodes.splice( - (ancestorNode)._childNodes.indexOf(referenceNode), + (ancestorNode).__childNodes__.splice( + (ancestorNode).__childNodes__.indexOf(referenceNode), 0, newNode ); - (newNode)._connectToNode(ancestorNode); + (newNode).__connectToNode__(ancestorNode); // MutationObserver - if ((ancestorNode)._observers.length > 0) { + if ((ancestorNode).__observers__.length > 0) { const record = new MutationRecord(); record.target = ancestorNode; record.type = MutationTypeEnum.childList; record.addedNodes = [newNode]; - for (const observer of (ancestorNode)._observers) { + for (const observer of (ancestorNode).__observers__) { if (observer.options.subtree) { - (newNode)._observe(observer); + (newNode).__observe__(observer); } if (observer.options.childList) { observer.callback([record], observer.observer); @@ -251,7 +251,7 @@ export default class NodeUtility { return true; } - if (!(ancestorNode)._childNodes.length) { + if (!(ancestorNode).__childNodes__.length) { return false; } @@ -334,7 +334,7 @@ export default class NodeUtility { return (node).data.length; default: - return (node)._childNodes.length; + return (node).__childNodes__.length; } } @@ -492,13 +492,13 @@ export default class NodeUtility { return false; } - if ((nodeA)._childNodes.length !== (nodeB)._childNodes.length) { + if ((nodeA).__childNodes__.length !== (nodeB).__childNodes__.length) { return false; } - for (let i = 0; i < (nodeA)._childNodes.length; i++) { - const childNodeA = (nodeA)._childNodes[i]; - const childNodeB = (nodeB)._childNodes[i]; + for (let i = 0; i < (nodeA).__childNodes__.length; i++) { + const childNodeA = (nodeA).__childNodes__[i]; + const childNodeB = (nodeB).__childNodes__[i]; if (!NodeUtility.isEqualNode(childNodeA, childNodeB)) { return false; diff --git a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts index 1e6ecc215..e49042464 100644 --- a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts +++ b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts @@ -45,7 +45,7 @@ export default class ParentNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(parentNode.ownerDocument, node) - ))._childNodes.slice(); + )).__childNodes__.slice(); for (const newChildNode of newChildNodes) { parentNode.insertBefore(newChildNode, firstChild); } @@ -65,7 +65,7 @@ export default class ParentNodeUtility { parentNode: IElement | IDocument | IDocumentFragment, ...nodes: (string | INode)[] ): void { - for (const node of (parentNode)._childNodes.slice()) { + for (const node of (parentNode).__childNodes__.slice()) { parentNode.removeChild(node); } @@ -84,7 +84,7 @@ export default class ParentNodeUtility { ): IHTMLCollection { let matches = new HTMLCollection(); - for (const child of (parentNode)._children) { + for (const child of (parentNode).__children__) { if (child.className.split(' ').includes(className)) { matches.push(child); } @@ -111,7 +111,7 @@ export default class ParentNodeUtility { const includeAll = tagName === '*'; let matches = new HTMLCollection(); - for (const child of (parentNode)._children) { + for (const child of (parentNode).__children__) { if (includeAll || child.tagName === upperTagName) { matches.push(child); } @@ -140,7 +140,7 @@ export default class ParentNodeUtility { const includeAll = tagName === '*'; let matches = new HTMLCollection(); - for (const child of (parentNode)._children) { + for (const child of (parentNode).__children__) { if ((includeAll || child.tagName === upperTagName) && child.namespaceURI === namespaceURI) { matches.push(child); } @@ -166,7 +166,7 @@ export default class ParentNodeUtility { ): IElement { const upperTagName = tagName.toUpperCase(); - for (const child of (parentNode)._children) { + for (const child of (parentNode).__children__) { if (child.tagName === upperTagName) { return child; } @@ -191,7 +191,7 @@ export default class ParentNodeUtility { id: string ): IElement { id = String(id); - for (const child of (parentNode)._children) { + for (const child of (parentNode).__children__) { if (child.id === id) { return child; } diff --git a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts index 279bfc0c7..a7693f93b 100644 --- a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts +++ b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts @@ -28,7 +28,7 @@ export default class ShadowRoot extends DocumentFragment implements IShadowRoot escapeEntities: false }); let xml = ''; - for (const node of this._childNodes) { + for (const node of this.__childNodes__) { xml += xmlSerializer.serializeToString(node); } return xml; @@ -40,7 +40,7 @@ export default class ShadowRoot extends DocumentFragment implements IShadowRoot * @param html HTML. */ public set innerHTML(html: string) { - for (const child of this._childNodes.slice()) { + for (const child of this.__childNodes__.slice()) { this.removeChild(child); } @@ -53,7 +53,7 @@ export default class ShadowRoot extends DocumentFragment implements IShadowRoot * @returns Active element. */ public get activeElement(): IHTMLElement | null { - const activeElement: IHTMLElement = this.ownerDocument['_activeElement']; + const activeElement: IHTMLElement = this.ownerDocument['__activeElement__']; if (activeElement && activeElement.isConnected && activeElement.getRootNode() === this) { return activeElement; } diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts index 61fa568e3..772798ee3 100644 --- a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts +++ b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts @@ -26,8 +26,8 @@ export default class SVGElement extends Element implements ISVGElement { public onunload: (event: Event) => void | null = null; // Private properties - public _style: CSSStyleDeclaration = null; - private _dataset: Dataset = null; + public __style__: CSSStyleDeclaration = null; + #dataset: Dataset = null; /** * Returns viewport. @@ -61,7 +61,7 @@ export default class SVGElement extends Element implements ISVGElement { * @returns Data set. */ public get dataset(): { [key: string]: string } { - return (this._dataset ??= new Dataset(this)).proxy; + return (this.#dataset ??= new Dataset(this)).proxy; } /** @@ -70,10 +70,10 @@ export default class SVGElement extends Element implements ISVGElement { * @returns Style. */ public get style(): CSSStyleDeclaration { - if (!this._style) { - this._style = new CSSStyleDeclaration(this); + if (!this.__style__) { + this.__style__ = new CSSStyleDeclaration(this); } - return this._style; + return this.__style__; } /** diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts index e7573ae27..3e75195bd 100644 --- a/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts @@ -8,7 +8,7 @@ import SVGElement from './SVGElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class SVGElementNamedNodeMap extends ElementNamedNodeMap { - protected _ownerElement: SVGElement; + protected __ownerElement__: SVGElement; /** * @override @@ -16,8 +16,8 @@ export default class SVGElementNamedNodeMap extends ElementNamedNodeMap { public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if (item.name === 'style' && this._ownerElement._style) { - this._ownerElement._style.cssText = item.value; + if (item.name === 'style' && this.__ownerElement__.__style__) { + this.__ownerElement__.__style__.cssText = item.value; } return replacedItem || null; @@ -26,11 +26,11 @@ export default class SVGElementNamedNodeMap extends ElementNamedNodeMap { /** * @override */ - public override _removeNamedItem(name: string): IAttr | null { - const removedItem = super._removeNamedItem(name); + public override __removeNamedItem__(name: string): IAttr | null { + const removedItem = super.__removeNamedItem__(name); - if (removedItem && removedItem.name === 'style' && this._ownerElement._style) { - this._ownerElement._style.cssText = ''; + if (removedItem && removedItem.name === 'style' && this.__ownerElement__.__style__) { + this.__ownerElement__.__style__.cssText = ''; } return removedItem; diff --git a/packages/happy-dom/src/nodes/text/Text.ts b/packages/happy-dom/src/nodes/text/Text.ts index 46b4605da..1a482a3fb 100644 --- a/packages/happy-dom/src/nodes/text/Text.ts +++ b/packages/happy-dom/src/nodes/text/Text.ts @@ -25,7 +25,7 @@ export default class Text extends CharacterData implements IText { * @override */ public override get data(): string { - return this._data; + return this.__data__; } /** @@ -34,8 +34,8 @@ export default class Text extends CharacterData implements IText { public override set data(data: string) { super.data = data; - if (this._textAreaNode) { - (this._textAreaNode)._resetSelection(); + if (this.__textAreaNode__) { + (this.__textAreaNode__).__resetSelection__(); } } @@ -47,7 +47,7 @@ export default class Text extends CharacterData implements IText { * @returns New text node. */ public splitText(offset: number): IText { - const length = this._data.length; + const length = this.__data__.length; if (offset < 0 || offset > length) { throw new DOMException( @@ -92,17 +92,17 @@ export default class Text extends CharacterData implements IText { /** * @override */ - public override _connectToNode(parentNode: INode = null): void { - const oldTextAreaNode = this._textAreaNode; + public override __connectToNode__(parentNode: INode = null): void { + const oldTextAreaNode = this.__textAreaNode__; - super._connectToNode(parentNode); + super.__connectToNode__(parentNode); - if (oldTextAreaNode !== this._textAreaNode) { + if (oldTextAreaNode !== this.__textAreaNode__) { if (oldTextAreaNode) { - oldTextAreaNode._resetSelection(); + oldTextAreaNode.__resetSelection__(); } - if (this._textAreaNode) { - (this._textAreaNode)._resetSelection(); + if (this.__textAreaNode__) { + (this.__textAreaNode__).__resetSelection__(); } } } diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index e0bd1f163..b48a6c29d 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -49,7 +49,7 @@ export default class QuerySelector { matches = matches.concat( node.nodeType === NodeTypeEnum.elementNode ? this.findAll(node, [node], items) - : this.findAll(null, (node)._children, items) + : this.findAll(null, (node).__children__, items) ); } @@ -93,7 +93,7 @@ export default class QuerySelector { const match = node.nodeType === NodeTypeEnum.elementNode ? this.findFirst(node, [node], items) - : this.findFirst(null, (node)._children, items); + : this.findFirst(null, (node).__children__, items); if (match) { return match; @@ -251,7 +251,7 @@ export default class QuerySelector { matched = matched.concat( this.findAll( rootElement, - (child)._children, + (child).__children__, selectorItems.slice(1), position ) @@ -263,10 +263,10 @@ export default class QuerySelector { if ( selectorItem.combinator === SelectorCombinatorEnum.descendant && - (child)._children.length + (child).__children__.length ) { matched = matched.concat( - this.findAll(rootElement, (child)._children, selectorItems, position) + this.findAll(rootElement, (child).__children__, selectorItems, position) ); } } @@ -314,7 +314,7 @@ export default class QuerySelector { case SelectorCombinatorEnum.child: const match = this.findFirst( rootElement, - (child)._children, + (child).__children__, selectorItems.slice(1) ); if (match) { @@ -327,9 +327,9 @@ export default class QuerySelector { if ( selectorItem.combinator === SelectorCombinatorEnum.descendant && - (child)._children.length + (child).__children__.length ) { - const match = this.findFirst(rootElement, (child)._children, selectorItems); + const match = this.findFirst(rootElement, (child).__children__, selectorItems); if (match) { return match; diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts index 6b79a3d99..77f5a550b 100644 --- a/packages/happy-dom/src/query-selector/SelectorItem.ts +++ b/packages/happy-dom/src/query-selector/SelectorItem.ts @@ -112,7 +112,7 @@ export default class SelectorItem { */ private matchPsuedo(element: IElement): boolean { const parent = element.parentNode; - const parentChildren = element.parentNode ? (element.parentNode)._children : []; + const parentChildren = element.parentNode ? (element.parentNode).__children__ : []; if (!this.pseudos) { return true; @@ -185,7 +185,7 @@ export default class SelectorItem { case 'checked': return element.tagName === 'INPUT' && (element).checked; case 'empty': - return !(element)._children.length; + return !(element).__children__.length; case 'root': return element.tagName === 'HTML'; case 'not': diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts index 04e06156d..075ba707d 100644 --- a/packages/happy-dom/src/range/Range.ts +++ b/packages/happy-dom/src/range/Range.ts @@ -35,10 +35,10 @@ export default class Range { public readonly END_TO_START: number = RangeHowEnum.endToStart; public readonly START_TO_END: number = RangeHowEnum.startToEnd; public readonly START_TO_START: number = RangeHowEnum.startToStart; - public _start: IRangeBoundaryPoint | null = null; - public _end: IRangeBoundaryPoint | null = null; + public __start__: IRangeBoundaryPoint | null = null; + public __end__: IRangeBoundaryPoint | null = null; #window: IBrowserWindow; - public readonly _ownerDocument: IDocument; + public readonly __ownerDocument__: IDocument; /** * Constructor. @@ -47,9 +47,9 @@ export default class Range { */ constructor(window: IBrowserWindow) { this.#window = window; - this._ownerDocument = window.document; - this._start = { node: window.document, offset: 0 }; - this._end = { node: window.document, offset: 0 }; + this.__ownerDocument__ = window.document; + this.__start__ = { node: window.document, offset: 0 }; + this.__end__ = { node: window.document, offset: 0 }; } /** @@ -59,7 +59,7 @@ export default class Range { * @returns Start container. */ public get startContainer(): INode { - return this._start.node; + return this.__start__.node; } /** @@ -69,7 +69,7 @@ export default class Range { * @returns End container. */ public get endContainer(): INode { - return this._end.node; + return this.__end__.node; } /** @@ -79,14 +79,14 @@ export default class Range { * @returns Start offset. */ public get startOffset(): number { - if (this._start.offset > 0) { - const length = NodeUtility.getNodeLength(this._start.node); - if (this._start.offset > length) { - this._start.offset = length; + if (this.__start__.offset > 0) { + const length = NodeUtility.getNodeLength(this.__start__.node); + if (this.__start__.offset > length) { + this.__start__.offset = length; } } - return this._start.offset; + return this.__start__.offset; } /** @@ -96,14 +96,14 @@ export default class Range { * @returns End offset. */ public get endOffset(): number { - if (this._end.offset > 0) { - const length = NodeUtility.getNodeLength(this._end.node); - if (this._end.offset > length) { - this._end.offset = length; + if (this.__end__.offset > 0) { + const length = NodeUtility.getNodeLength(this.__end__.node); + if (this.__end__.offset > length) { + this.__end__.offset = length; } } - return this._end.offset; + return this.__end__.offset; } /** @@ -113,7 +113,7 @@ export default class Range { * @returns Collapsed. */ public get collapsed(): boolean { - return this._start.node === this._end.node && this.startOffset === this.endOffset; + return this.__start__.node === this.__end__.node && this.startOffset === this.endOffset; } /** @@ -123,10 +123,10 @@ export default class Range { * @returns Node. */ public get commonAncestorContainer(): INode { - let container = this._start.node; + let container = this.__start__.node; while (container) { - if (NodeUtility.isInclusiveAncestor(container, this._end.node)) { + if (NodeUtility.isInclusiveAncestor(container, this.__end__.node)) { return container; } container = container.parentNode; @@ -143,9 +143,9 @@ export default class Range { */ public collapse(toStart = false): void { if (toStart) { - this._end = Object.assign({}, this._start); + this.__end__ = Object.assign({}, this.__start__); } else { - this._start = Object.assign({}, this._end); + this.__start__ = Object.assign({}, this.__end__); } } @@ -170,7 +170,7 @@ export default class Range { ); } - if (this._ownerDocument !== sourceRange._ownerDocument) { + if (this.__ownerDocument__ !== sourceRange.__ownerDocument__) { throw new DOMException( `The two Ranges are not in the same tree.`, DOMExceptionNameEnum.wrongDocumentError @@ -188,27 +188,27 @@ export default class Range { switch (how) { case RangeHowEnum.startToStart: - thisPoint.node = this._start.node; + thisPoint.node = this.__start__.node; thisPoint.offset = this.startOffset; - sourcePoint.node = sourceRange._start.node; + sourcePoint.node = sourceRange.__start__.node; sourcePoint.offset = sourceRange.startOffset; break; case RangeHowEnum.startToEnd: - thisPoint.node = this._end.node; + thisPoint.node = this.__end__.node; thisPoint.offset = this.endOffset; - sourcePoint.node = sourceRange._start.node; + sourcePoint.node = sourceRange.__start__.node; sourcePoint.offset = sourceRange.startOffset; break; case RangeHowEnum.endToEnd: - thisPoint.node = this._end.node; + thisPoint.node = this.__end__.node; thisPoint.offset = this.endOffset; - sourcePoint.node = sourceRange._end.node; + sourcePoint.node = sourceRange.__end__.node; sourcePoint.offset = sourceRange.endOffset; break; case RangeHowEnum.endToStart: - thisPoint.node = this._start.node; + thisPoint.node = this.__start__.node; thisPoint.offset = this.startOffset; - sourcePoint.node = sourceRange._end.node; + sourcePoint.node = sourceRange.__end__.node; sourcePoint.offset = sourceRange.endOffset; break; } @@ -225,7 +225,7 @@ export default class Range { * @returns -1,0, or 1. */ public comparePoint(node: INode, offset): number { - if (node.ownerDocument !== this._ownerDocument) { + if (node.ownerDocument !== this.__ownerDocument__) { throw new DOMException( `The two Ranges are not in the same tree.`, DOMExceptionNameEnum.wrongDocumentError @@ -238,14 +238,14 @@ export default class Range { if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._start.node, + node: this.__start__.node, offset: this.startOffset }) === -1 ) { return -1; } else if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._end.node, + node: this.__end__.node, offset: this.endOffset }) === 1 ) { @@ -262,7 +262,7 @@ export default class Range { * @returns Document fragment. */ public cloneContents(): IDocumentFragment { - const fragment = this._ownerDocument.createDocumentFragment(); + const fragment = this.__ownerDocument__.createDocumentFragment(); const startOffset = this.startOffset; const endOffset = this.endOffset; @@ -271,24 +271,24 @@ export default class Range { } if ( - this._start.node === this._end.node && - (this._start.node.nodeType === NodeTypeEnum.textNode || - this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._start.node.nodeType === NodeTypeEnum.commentNode) + this.__start__.node === this.__end__.node && + (this.__start__.node.nodeType === NodeTypeEnum.textNode || + this.__start__.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.__start__.node.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this._start.node).cloneNode(false); - clone['_data'] = clone.substringData(startOffset, endOffset - startOffset); + const clone = (this.__start__.node).cloneNode(false); + clone['__data__'] = clone.substringData(startOffset, endOffset - startOffset); fragment.appendChild(clone); return fragment; } - let commonAncestor = this._start.node; - while (!NodeUtility.isInclusiveAncestor(commonAncestor, this._end.node)) { + let commonAncestor = this.__start__.node; + while (!NodeUtility.isInclusiveAncestor(commonAncestor, this.__end__.node)) { commonAncestor = commonAncestor.parentNode; } let firstPartialContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { + if (!NodeUtility.isInclusiveAncestor(this.__start__.node, this.__end__.node)) { let candidate = commonAncestor.firstChild; while (!firstPartialContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -300,7 +300,7 @@ export default class Range { } let lastPartiallyContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this._end.node, this._start.node)) { + if (!NodeUtility.isInclusiveAncestor(this.__end__.node, this.__start__.node)) { let candidate = commonAncestor.lastChild; while (!lastPartiallyContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -313,7 +313,7 @@ export default class Range { const containedChildren = []; - for (const node of (commonAncestor)._childNodes) { + for (const node of (commonAncestor).__childNodes__) { if (RangeUtility.isContained(node, this)) { if (node.nodeType === NodeTypeEnum.documentTypeNode) { throw new DOMException( @@ -331,10 +331,10 @@ export default class Range { firstPartialContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || firstPartialContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this._start.node).cloneNode(false); - clone['_data'] = clone.substringData( + const clone = (this.__start__.node).cloneNode(false); + clone['__data__'] = clone.substringData( startOffset, - NodeUtility.getNodeLength(this._start.node) - startOffset + NodeUtility.getNodeLength(this.__start__.node) - startOffset ); fragment.appendChild(clone); @@ -343,10 +343,10 @@ export default class Range { fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange._start.node = this._start.node; - subRange._start.offset = startOffset; - subRange._end.node = firstPartialContainedChild; - subRange._end.offset = NodeUtility.getNodeLength(firstPartialContainedChild); + subRange.__start__.node = this.__start__.node; + subRange.__start__.offset = startOffset; + subRange.__end__.node = firstPartialContainedChild; + subRange.__end__.offset = NodeUtility.getNodeLength(firstPartialContainedChild); const subDocumentFragment = subRange.cloneContents(); clone.appendChild(subDocumentFragment); @@ -363,8 +363,8 @@ export default class Range { lastPartiallyContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || lastPartiallyContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this._end.node).cloneNode(false); - clone['_data'] = clone.substringData(0, endOffset); + const clone = (this.__end__.node).cloneNode(false); + clone['__data__'] = clone.substringData(0, endOffset); fragment.appendChild(clone); } else if (lastPartiallyContainedChild !== null) { @@ -372,10 +372,10 @@ export default class Range { fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange._start.node = lastPartiallyContainedChild; - subRange._start.offset = 0; - subRange._end.node = this._end.node; - subRange._end.offset = endOffset; + subRange.__start__.node = lastPartiallyContainedChild; + subRange.__start__.offset = 0; + subRange.__end__.node = this.__end__.node; + subRange.__end__.offset = endOffset; const subFragment = subRange.cloneContents(); clone.appendChild(subFragment); @@ -393,10 +393,10 @@ export default class Range { public cloneRange(): Range { const clone = new this.#window.Range(); - clone._start.node = this._start.node; - clone._start.offset = this._start.offset; - clone._end.node = this._end.node; - clone._end.offset = this._end.offset; + clone.__start__.node = this.__start__.node; + clone.__start__.offset = this.__start__.offset; + clone.__end__.node = this.__end__.node; + clone.__end__.offset = this.__end__.offset; return clone; } @@ -410,7 +410,7 @@ export default class Range { */ public createContextualFragment(tagString: string): IDocumentFragment { // TODO: We only have support for HTML in the parser currently, so it is not necessary to check which context it is - return XMLParser.parse(this._ownerDocument, tagString); + return XMLParser.parse(this.__ownerDocument__, tagString); } /** @@ -427,18 +427,18 @@ export default class Range { } if ( - this._start.node === this._end.node && - (this._start.node.nodeType === NodeTypeEnum.textNode || - this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._start.node.nodeType === NodeTypeEnum.commentNode) + this.__start__.node === this.__end__.node && + (this.__start__.node.nodeType === NodeTypeEnum.textNode || + this.__start__.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.__start__.node.nodeType === NodeTypeEnum.commentNode) ) { - (this._start.node).replaceData(startOffset, endOffset - startOffset, ''); + (this.__start__.node).replaceData(startOffset, endOffset - startOffset, ''); return; } const nodesToRemove = []; - let currentNode = this._start.node; - const endNode = NodeUtility.nextDescendantNode(this._end.node); + let currentNode = this.__start__.node; + const endNode = NodeUtility.nextDescendantNode(this.__end__.node); while (currentNode && currentNode !== endNode) { if ( RangeUtility.isContained(currentNode, this) && @@ -452,31 +452,31 @@ export default class Range { let newNode; let newOffset; - if (NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { - newNode = this._start.node; + if (NodeUtility.isInclusiveAncestor(this.__start__.node, this.__end__.node)) { + newNode = this.__start__.node; newOffset = startOffset; } else { - let referenceNode = this._start.node; + let referenceNode = this.__start__.node; while ( referenceNode && - !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this._end.node) + !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this.__end__.node) ) { referenceNode = referenceNode.parentNode; } newNode = referenceNode.parentNode; - newOffset = (referenceNode.parentNode)._childNodes.indexOf(referenceNode) + 1; + newOffset = (referenceNode.parentNode).__childNodes__.indexOf(referenceNode) + 1; } if ( - this._start.node.nodeType === NodeTypeEnum.textNode || - this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._start.node.nodeType === NodeTypeEnum.commentNode + this.__start__.node.nodeType === NodeTypeEnum.textNode || + this.__start__.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.__start__.node.nodeType === NodeTypeEnum.commentNode ) { - (this._start.node).replaceData( + (this.__start__.node).replaceData( this.startOffset, - NodeUtility.getNodeLength(this._start.node) - this.startOffset, + NodeUtility.getNodeLength(this.__start__.node) - this.startOffset, '' ); } @@ -487,17 +487,17 @@ export default class Range { } if ( - this._end.node.nodeType === NodeTypeEnum.textNode || - this._end.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._end.node.nodeType === NodeTypeEnum.commentNode + this.__end__.node.nodeType === NodeTypeEnum.textNode || + this.__end__.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.__end__.node.nodeType === NodeTypeEnum.commentNode ) { - (this._end.node).replaceData(0, endOffset, ''); + (this.__end__.node).replaceData(0, endOffset, ''); } - this._start.node = newNode; - this._start.offset = newOffset; - this._end.node = newNode; - this._end.offset = newOffset; + this.__start__.node = newNode; + this.__start__.offset = newOffset; + this.__end__.node = newNode; + this.__end__.offset = newOffset; } /** @@ -516,7 +516,7 @@ export default class Range { * @returns Document fragment. */ public extractContents(): IDocumentFragment { - const fragment = this._ownerDocument.createDocumentFragment(); + const fragment = this.__ownerDocument__.createDocumentFragment(); const startOffset = this.startOffset; const endOffset = this.endOffset; @@ -525,28 +525,28 @@ export default class Range { } if ( - this._start.node === this._end.node && - (this._start.node.nodeType === NodeTypeEnum.textNode || - this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._start.node.nodeType === NodeTypeEnum.commentNode) + this.__start__.node === this.__end__.node && + (this.__start__.node.nodeType === NodeTypeEnum.textNode || + this.__start__.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.__start__.node.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this._start.node.cloneNode(false); - clone['_data'] = clone.substringData(startOffset, endOffset - startOffset); + const clone = this.__start__.node.cloneNode(false); + clone['__data__'] = clone.substringData(startOffset, endOffset - startOffset); fragment.appendChild(clone); - (this._start.node).replaceData(startOffset, endOffset - startOffset, ''); + (this.__start__.node).replaceData(startOffset, endOffset - startOffset, ''); return fragment; } - let commonAncestor = this._start.node; - while (!NodeUtility.isInclusiveAncestor(commonAncestor, this._end.node)) { + let commonAncestor = this.__start__.node; + while (!NodeUtility.isInclusiveAncestor(commonAncestor, this.__end__.node)) { commonAncestor = commonAncestor.parentNode; } let firstPartialContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { + if (!NodeUtility.isInclusiveAncestor(this.__start__.node, this.__end__.node)) { let candidate = commonAncestor.firstChild; while (!firstPartialContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -558,7 +558,7 @@ export default class Range { } let lastPartiallyContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this._end.node, this._start.node)) { + if (!NodeUtility.isInclusiveAncestor(this.__end__.node, this.__start__.node)) { let candidate = commonAncestor.lastChild; while (!lastPartiallyContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -571,7 +571,7 @@ export default class Range { const containedChildren = []; - for (const node of (commonAncestor)._childNodes) { + for (const node of (commonAncestor).__childNodes__) { if (RangeUtility.isContained(node, this)) { if (node.nodeType === NodeTypeEnum.documentTypeNode) { throw new DOMException( @@ -585,21 +585,21 @@ export default class Range { let newNode; let newOffset; - if (NodeUtility.isInclusiveAncestor(this._start.node, this._end.node)) { - newNode = this._start.node; + if (NodeUtility.isInclusiveAncestor(this.__start__.node, this.__end__.node)) { + newNode = this.__start__.node; newOffset = startOffset; } else { - let referenceNode = this._start.node; + let referenceNode = this.__start__.node; while ( referenceNode && - !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this._end.node) + !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this.__end__.node) ) { referenceNode = referenceNode.parentNode; } newNode = referenceNode.parentNode; - newOffset = (referenceNode.parentNode)._childNodes.indexOf(referenceNode) + 1; + newOffset = (referenceNode.parentNode).__childNodes__.indexOf(referenceNode) + 1; } if ( @@ -608,17 +608,17 @@ export default class Range { firstPartialContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || firstPartialContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this._start.node.cloneNode(false); - clone['_data'] = clone.substringData( + const clone = this.__start__.node.cloneNode(false); + clone['__data__'] = clone.substringData( startOffset, - NodeUtility.getNodeLength(this._start.node) - startOffset + NodeUtility.getNodeLength(this.__start__.node) - startOffset ); fragment.appendChild(clone); - (this._start.node).replaceData( + (this.__start__.node).replaceData( startOffset, - NodeUtility.getNodeLength(this._start.node) - startOffset, + NodeUtility.getNodeLength(this.__start__.node) - startOffset, '' ); } else if (firstPartialContainedChild !== null) { @@ -626,10 +626,10 @@ export default class Range { fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange._start.node = this._start.node; - subRange._start.offset = startOffset; - subRange._end.node = firstPartialContainedChild; - subRange._end.offset = NodeUtility.getNodeLength(firstPartialContainedChild); + subRange.__start__.node = this.__start__.node; + subRange.__start__.offset = startOffset; + subRange.__end__.node = firstPartialContainedChild; + subRange.__end__.offset = NodeUtility.getNodeLength(firstPartialContainedChild); const subFragment = subRange.extractContents(); clone.appendChild(subFragment); @@ -645,30 +645,30 @@ export default class Range { lastPartiallyContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || lastPartiallyContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this._end.node.cloneNode(false); - clone['_data'] = clone.substringData(0, endOffset); + const clone = this.__end__.node.cloneNode(false); + clone['__data__'] = clone.substringData(0, endOffset); fragment.appendChild(clone); - (this._end.node).replaceData(0, endOffset, ''); + (this.__end__.node).replaceData(0, endOffset, ''); } else if (lastPartiallyContainedChild !== null) { const clone = lastPartiallyContainedChild.cloneNode(false); fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange._start.node = lastPartiallyContainedChild; - subRange._start.offset = 0; - subRange._end.node = this._end.node; - subRange._end.offset = endOffset; + subRange.__start__.node = lastPartiallyContainedChild; + subRange.__start__.offset = 0; + subRange.__end__.node = this.__end__.node; + subRange.__end__.offset = endOffset; const subFragment = subRange.extractContents(); clone.appendChild(subFragment); } - this._start.node = newNode; - this._start.offset = newOffset; - this._end.node = newNode; - this._end.offset = newOffset; + this.__start__.node = newNode; + this.__start__.offset = newOffset; + this.__end__.node = newNode; + this.__end__.offset = newOffset; return fragment; } @@ -702,7 +702,7 @@ export default class Range { * @returns "true" if in range. */ public isPointInRange(node: INode, offset = 0): boolean { - if (node.ownerDocument !== this._ownerDocument) { + if (node.ownerDocument !== this.__ownerDocument__) { return false; } @@ -712,11 +712,11 @@ export default class Range { if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._start.node, + node: this.__start__.node, offset: this.startOffset }) === -1 || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._end.node, + node: this.__end__.node, offset: this.endOffset }) === 1 ) { @@ -734,22 +734,22 @@ export default class Range { */ public insertNode(newNode: INode): void { if ( - this._start.node.nodeType === NodeTypeEnum.processingInstructionNode || - this._start.node.nodeType === NodeTypeEnum.commentNode || - (this._start.node.nodeType === NodeTypeEnum.textNode && !this._start.node.parentNode) || - newNode === this._start.node + this.__start__.node.nodeType === NodeTypeEnum.processingInstructionNode || + this.__start__.node.nodeType === NodeTypeEnum.commentNode || + (this.__start__.node.nodeType === NodeTypeEnum.textNode && !this.__start__.node.parentNode) || + newNode === this.__start__.node ) { throw new DOMException('Invalid start node.', DOMExceptionNameEnum.hierarchyRequestError); } let referenceNode = - this._start.node.nodeType === NodeTypeEnum.textNode - ? this._start.node - : (this._start.node)._childNodes[this.startOffset] || null; - const parent = !referenceNode ? this._start.node : referenceNode.parentNode; + this.__start__.node.nodeType === NodeTypeEnum.textNode + ? this.__start__.node + : (this.__start__.node).__childNodes__[this.startOffset] || null; + const parent = !referenceNode ? this.__start__.node : referenceNode.parentNode; - if (this._start.node.nodeType === NodeTypeEnum.textNode) { - referenceNode = (this._start.node).splitText(this.startOffset); + if (this.__start__.node.nodeType === NodeTypeEnum.textNode) { + referenceNode = (this.__start__.node).splitText(this.startOffset); } if (newNode === referenceNode) { @@ -763,7 +763,7 @@ export default class Range { let newOffset = !referenceNode ? NodeUtility.getNodeLength(parent) - : (referenceNode.parentNode)._childNodes.indexOf(referenceNode); + : (referenceNode.parentNode).__childNodes__.indexOf(referenceNode); newOffset += newNode.nodeType === NodeTypeEnum.documentFragmentNode ? NodeUtility.getNodeLength(newNode) @@ -772,8 +772,8 @@ export default class Range { parent.insertBefore(newNode, referenceNode); if (this.collapsed) { - this._end.node = parent; - this._end.offset = newOffset; + this.__end__.node = parent; + this.__end__.offset = newOffset; } } @@ -785,7 +785,7 @@ export default class Range { * @returns "true" if it intersects. */ public intersectsNode(node: INode): boolean { - if (node.ownerDocument !== this._ownerDocument) { + if (node.ownerDocument !== this.__ownerDocument__) { return false; } @@ -795,16 +795,16 @@ export default class Range { return true; } - const offset = (parent)._childNodes.indexOf(node); + const offset = (parent).__childNodes__.indexOf(node); return ( RangeUtility.compareBoundaryPointsPosition( { node: parent, offset }, - { node: this._end.node, offset: this.endOffset } + { node: this.__end__.node, offset: this.endOffset } ) === -1 && RangeUtility.compareBoundaryPointsPosition( { node: parent, offset: offset + 1 }, - { node: this._start.node, offset: this.startOffset } + { node: this.__start__.node, offset: this.startOffset } ) === 1 ); } @@ -823,12 +823,12 @@ export default class Range { ); } - const index = (node.parentNode)._childNodes.indexOf(node); + const index = (node.parentNode).__childNodes__.indexOf(node); - this._start.node = node.parentNode; - this._start.offset = index; - this._end.node = node.parentNode; - this._end.offset = index + 1; + this.__start__.node = node.parentNode; + this.__start__.offset = index; + this.__end__.node = node.parentNode; + this.__end__.offset = index + 1; } /** @@ -845,10 +845,10 @@ export default class Range { ); } - this._start.node = node; - this._start.offset = 0; - this._end.node = node; - this._end.offset = NodeUtility.getNodeLength(node); + this.__start__.node = node; + this.__start__.offset = 0; + this.__end__.node = node; + this.__end__.offset = NodeUtility.getNodeLength(node); } /** @@ -864,18 +864,18 @@ export default class Range { const boundaryPoint = { node, offset }; if ( - node.ownerDocument !== this._ownerDocument || + node.ownerDocument !== this.__ownerDocument__ || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._start.node, + node: this.__start__.node, offset: this.startOffset }) === -1 ) { - this._start.node = node; - this._start.offset = offset; + this.__start__.node = node; + this.__start__.offset = offset; } - this._end.node = node; - this._end.offset = offset; + this.__end__.node = node; + this.__end__.offset = offset; } /** @@ -891,18 +891,18 @@ export default class Range { const boundaryPoint = { node, offset }; if ( - node.ownerDocument !== this._ownerDocument || + node.ownerDocument !== this.__ownerDocument__ || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this._end.node, + node: this.__end__.node, offset: this.endOffset }) === 1 ) { - this._end.node = node; - this._end.offset = offset; + this.__end__.node = node; + this.__end__.offset = offset; } - this._start.node = node; - this._start.offset = offset; + this.__start__.node = node; + this.__start__.offset = offset; } /** @@ -918,7 +918,7 @@ export default class Range { DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setEnd(node.parentNode, (node.parentNode)._childNodes.indexOf(node) + 1); + this.setEnd(node.parentNode, (node.parentNode).__childNodes__.indexOf(node) + 1); } /** @@ -934,7 +934,7 @@ export default class Range { DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setEnd(node.parentNode, (node.parentNode)._childNodes.indexOf(node)); + this.setEnd(node.parentNode, (node.parentNode).__childNodes__.indexOf(node)); } /** @@ -950,7 +950,7 @@ export default class Range { DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setStart(node.parentNode, (node.parentNode)._childNodes.indexOf(node) + 1); + this.setStart(node.parentNode, (node.parentNode).__childNodes__.indexOf(node) + 1); } /** @@ -966,7 +966,7 @@ export default class Range { DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setStart(node.parentNode, (node.parentNode)._childNodes.indexOf(node)); + this.setStart(node.parentNode, (node.parentNode).__childNodes__.indexOf(node)); } /** @@ -1024,18 +1024,18 @@ export default class Range { let string = ''; if ( - this._start.node === this._end.node && - this._start.node.nodeType === NodeTypeEnum.textNode + this.__start__.node === this.__end__.node && + this.__start__.node.nodeType === NodeTypeEnum.textNode ) { - return (this._start.node).data.slice(startOffset, endOffset); + return (this.__start__.node).data.slice(startOffset, endOffset); } - if (this._start.node.nodeType === NodeTypeEnum.textNode) { - string += (this._start.node).data.slice(startOffset); + if (this.__start__.node.nodeType === NodeTypeEnum.textNode) { + string += (this.__start__.node).data.slice(startOffset); } - const endNode = NodeUtility.nextDescendantNode(this._end.node); - let currentNode = this._start.node; + const endNode = NodeUtility.nextDescendantNode(this.__end__.node); + let currentNode = this.__start__.node; while (currentNode && currentNode !== endNode) { if ( @@ -1048,8 +1048,8 @@ export default class Range { currentNode = NodeUtility.following(currentNode); } - if (this._end.node.nodeType === NodeTypeEnum.textNode) { - string += (this._end.node).data.slice(0, endOffset); + if (this.__end__.node.nodeType === NodeTypeEnum.textNode) { + string += (this.__end__.node).data.slice(0, endOffset); } return string; diff --git a/packages/happy-dom/src/range/RangeUtility.ts b/packages/happy-dom/src/range/RangeUtility.ts index 6c5725eb8..1dbabcb9a 100644 --- a/packages/happy-dom/src/range/RangeUtility.ts +++ b/packages/happy-dom/src/range/RangeUtility.ts @@ -50,7 +50,7 @@ export default class RangeUtility { child = child.parentNode; } - if ((child.parentNode)._childNodes.indexOf(child) < pointA.offset) { + if ((child.parentNode).__childNodes__.indexOf(child) < pointA.offset) { return 1; } } diff --git a/packages/happy-dom/src/selection/Selection.ts b/packages/happy-dom/src/selection/Selection.ts index 2627cdfad..86adf6d0a 100644 --- a/packages/happy-dom/src/selection/Selection.ts +++ b/packages/happy-dom/src/selection/Selection.ts @@ -20,8 +20,8 @@ import SelectionDirectionEnum from './SelectionDirectionEnum.js'; */ export default class Selection { readonly #ownerDocument: IDocument = null; - public _range: Range = null; - public _direction: SelectionDirectionEnum = SelectionDirectionEnum.directionless; + #range: Range = null; + #direction: SelectionDirectionEnum = SelectionDirectionEnum.directionless; /** * Constructor. @@ -39,7 +39,7 @@ export default class Selection { * @returns Range count. */ public get rangeCount(): number { - return this._range ? 1 : 0; + return this.#range ? 1 : 0; } /** @@ -49,7 +49,7 @@ export default class Selection { * @returns "true" if collapsed. */ public get isCollapsed(): boolean { - return this._range === null || this._range.collapsed; + return this.#range === null || this.#range.collapsed; } /** @@ -59,9 +59,9 @@ export default class Selection { * @returns Type. */ public get type(): string { - if (!this._range) { + if (!this.#range) { return 'None'; - } else if (this._range.collapsed) { + } else if (this.#range.collapsed) { return 'Caret'; } @@ -75,12 +75,12 @@ export default class Selection { * @returns Node. */ public get anchorNode(): INode { - if (!this._range) { + if (!this.#range) { return null; } - return this._direction === SelectionDirectionEnum.forwards - ? this._range.startContainer - : this._range.endContainer; + return this.#direction === SelectionDirectionEnum.forwards + ? this.#range.startContainer + : this.#range.endContainer; } /** @@ -90,12 +90,12 @@ export default class Selection { * @returns Node. */ public get anchorOffset(): number { - if (!this._range) { + if (!this.#range) { return null; } - return this._direction === SelectionDirectionEnum.forwards - ? this._range.startOffset - : this._range.endOffset; + return this.#direction === SelectionDirectionEnum.forwards + ? this.#range.startOffset + : this.#range.endOffset; } /** @@ -172,8 +172,8 @@ export default class Selection { if (!newRange) { throw new Error('Failed to execute addRange on Selection. Parameter 1 is not of type Range.'); } - if (!this._range && newRange._ownerDocument === this.#ownerDocument) { - this._associateRange(newRange); + if (!this.#range && newRange.__ownerDocument__ === this.#ownerDocument) { + this.#associateRange(newRange); } } @@ -185,11 +185,11 @@ export default class Selection { * @returns Range. */ public getRangeAt(index: number): Range { - if (!this._range || index !== 0) { + if (!this.#range || index !== 0) { throw new DOMException('Invalid range index.', DOMExceptionNameEnum.indexSizeError); } - return this._range; + return this.#range; } /** @@ -199,17 +199,17 @@ export default class Selection { * @param range Range. */ public removeRange(range: Range): void { - if (this._range !== range) { + if (this.#range !== range) { throw new DOMException('Invalid range.', DOMExceptionNameEnum.notFoundError); } - this._associateRange(null); + this.#associateRange(null); } /** * Removes all ranges. */ public removeAllRanges(): void { - this._associateRange(null); + this.#associateRange(null); } /** @@ -249,14 +249,14 @@ export default class Selection { return; } - const newRange = new this.#ownerDocument._defaultView.Range(); + const newRange = new this.#ownerDocument.__defaultView__.Range(); - newRange._start.node = node; - newRange._start.offset = offset; - newRange._end.node = node; - newRange._end.offset = offset; + newRange.__start__.node = node; + newRange.__start__.offset = offset; + newRange.__end__.node = node; + newRange.__end__.offset = offset; - this._associateRange(newRange); + this.#associateRange(newRange); } /** @@ -277,22 +277,22 @@ export default class Selection { * @see https://w3c.github.io/selection-api/#dom-selection-collapsetoend */ public collapseToEnd(): void { - if (this._range === null) { + if (this.#range === null) { throw new DOMException( 'There is no selection to collapse.', DOMExceptionNameEnum.invalidStateError ); } - const { node, offset } = this._range._end; - const newRange = new this.#ownerDocument._defaultView.Range(); + const { node, offset } = this.#range.__end__; + const newRange = new this.#ownerDocument.__defaultView__.Range(); - newRange._start.node = node; - newRange._start.offset = offset; - newRange._end.node = node; - newRange._end.offset = offset; + newRange.__start__.node = node; + newRange.__start__.offset = offset; + newRange.__end__.node = node; + newRange.__end__.offset = offset; - this._associateRange(newRange); + this.#associateRange(newRange); } /** @@ -301,22 +301,22 @@ export default class Selection { * @see https://w3c.github.io/selection-api/#dom-selection-collapsetostart */ public collapseToStart(): void { - if (!this._range) { + if (!this.#range) { throw new DOMException( 'There is no selection to collapse.', DOMExceptionNameEnum.invalidStateError ); } - const { node, offset } = this._range._start; - const newRange = new this.#ownerDocument._defaultView.Range(); + const { node, offset } = this.#range.__start__; + const newRange = new this.#ownerDocument.__defaultView__.Range(); - newRange._start.node = node; - newRange._start.offset = offset; - newRange._end.node = node; - newRange._end.offset = offset; + newRange.__start__.node = node; + newRange.__start__.offset = offset; + newRange.__end__.node = node; + newRange.__end__.offset = offset; - this._associateRange(newRange); + this.#associateRange(newRange); } /** @@ -328,16 +328,14 @@ export default class Selection { * @returns Always returns "true" for now. */ public containsNode(node: INode, allowPartialContainment = false): boolean { - if (!this._range || node.ownerDocument !== this.#ownerDocument) { + if (!this.#range || node.ownerDocument !== this.#ownerDocument) { return false; } - const { _start, _end } = this._range; - const startIsBeforeNode = - RangeUtility.compareBoundaryPointsPosition(_start, { node, offset: 0 }) === -1; + RangeUtility.compareBoundaryPointsPosition(this.#range.__start__, { node, offset: 0 }) === -1; const endIsAfterNode = - RangeUtility.compareBoundaryPointsPosition(_end, { + RangeUtility.compareBoundaryPointsPosition(this.#range.__end__, { node, offset: NodeUtility.getNodeLength(node) }) === 1; @@ -353,8 +351,8 @@ export default class Selection { * @see https://w3c.github.io/selection-api/#dom-selection-deletefromdocument */ public deleteFromDocument(): void { - if (this._range) { - this._range.deleteContents(); + if (this.#range) { + this.#range.deleteContents(); } } @@ -370,7 +368,7 @@ export default class Selection { return; } - if (!this._range) { + if (!this.#range) { throw new DOMException( 'There is no selection to extend.', DOMExceptionNameEnum.invalidStateError @@ -379,34 +377,34 @@ export default class Selection { const anchorNode = this.anchorNode; const anchorOffset = this.anchorOffset; - const newRange = new this.#ownerDocument._defaultView.Range(); - newRange._start.node = node; - newRange._start.offset = 0; - newRange._end.node = node; - newRange._end.offset = 0; - - if (node.ownerDocument !== this._range._ownerDocument) { - newRange._start.offset = offset; - newRange._end.offset = offset; + const newRange = new this.#ownerDocument.__defaultView__.Range(); + newRange.__start__.node = node; + newRange.__start__.offset = 0; + newRange.__end__.node = node; + newRange.__end__.offset = 0; + + if (node.ownerDocument !== this.#range.__ownerDocument__) { + newRange.__start__.offset = offset; + newRange.__end__.offset = offset; } else if ( RangeUtility.compareBoundaryPointsPosition( { node: anchorNode, offset: anchorOffset }, { node, offset } ) <= 0 ) { - newRange._start.node = anchorNode; - newRange._start.offset = anchorOffset; - newRange._end.node = node; - newRange._end.offset = offset; + newRange.__start__.node = anchorNode; + newRange.__start__.offset = anchorOffset; + newRange.__end__.node = node; + newRange.__end__.offset = offset; } else { - newRange._start.node = node; - newRange._start.offset = offset; - newRange._end.node = anchorNode; - newRange._end.offset = anchorOffset; + newRange.__start__.node = node; + newRange.__start__.offset = offset; + newRange.__end__.node = anchorNode; + newRange.__end__.offset = anchorOffset; } - this._associateRange(newRange); - this._direction = + this.#associateRange(newRange); + this.#direction = RangeUtility.compareBoundaryPointsPosition( { node, offset }, { node: anchorNode, offset: anchorOffset } @@ -419,8 +417,7 @@ export default class Selection { * Selects all children. * * @see https://w3c.github.io/selection-api/#dom-selection-selectallchildren - * @param node - * @param _parentNode Parent node. + * @param node Node. */ public selectAllChildren(node: INode): void { if (node.nodeType === NodeTypeEnum.documentTypeNode) { @@ -435,14 +432,14 @@ export default class Selection { } const length = node.childNodes.length; - const newRange = new this.#ownerDocument._defaultView.Range(); + const newRange = new this.#ownerDocument.__defaultView__.Range(); - newRange._start.node = node; - newRange._start.offset = 0; - newRange._end.node = node; - newRange._end.offset = length; + newRange.__start__.node = node; + newRange.__start__.offset = 0; + newRange.__end__.node = node; + newRange.__end__.offset = length; - this._associateRange(newRange); + this.#associateRange(newRange); } /** @@ -479,18 +476,18 @@ export default class Selection { const anchor = { node: anchorNode, offset: anchorOffset }; const focus = { node: focusNode, offset: focusOffset }; - const newRange = new this.#ownerDocument._defaultView.Range(); + const newRange = new this.#ownerDocument.__defaultView__.Range(); if (RangeUtility.compareBoundaryPointsPosition(anchor, focus) === -1) { - newRange._start = anchor; - newRange._end = focus; + newRange.__start__ = anchor; + newRange.__end__ = focus; } else { - newRange._start = focus; - newRange._end = anchor; + newRange.__start__ = focus; + newRange.__end__ = anchor; } - this._associateRange(newRange); - this._direction = + this.#associateRange(newRange); + this.#direction = RangeUtility.compareBoundaryPointsPosition(focus, anchor) === -1 ? SelectionDirectionEnum.backwards : SelectionDirectionEnum.forwards; @@ -502,7 +499,7 @@ export default class Selection { * @returns Selection as string. */ public toString(): string { - return this._range ? this._range.toString() : ''; + return this.#range ? this.#range.toString() : ''; } /** @@ -510,13 +507,13 @@ export default class Selection { * * @param range Range. */ - protected _associateRange(range: Range): void { - const oldRange = this._range; - this._range = range; - this._direction = + #associateRange(range: Range): void { + const oldRange = this.#range; + this.#range = range; + this.#direction = range === null ? SelectionDirectionEnum.directionless : SelectionDirectionEnum.forwards; - if (oldRange !== this._range) { + if (oldRange !== this.#range) { // https://w3c.github.io/selection-api/#selectionchange-event this.#ownerDocument.dispatchEvent(new Event('selectionchange')); } diff --git a/packages/happy-dom/src/storage/Storage.ts b/packages/happy-dom/src/storage/Storage.ts index 4414681af..ef3ccba4a 100644 --- a/packages/happy-dom/src/storage/Storage.ts +++ b/packages/happy-dom/src/storage/Storage.ts @@ -2,7 +2,7 @@ * */ export default class Storage { - private _store: { [k: string]: string } = {}; + private #store: { [k: string]: string } = {}; /** * Returns length. @@ -10,7 +10,7 @@ export default class Storage { * @returns Length. */ public get length(): number { - return Object.keys(this._store).length; + return Object.keys(this.#store).length; } /** @@ -20,7 +20,7 @@ export default class Storage { * @returns Name. */ public key(index: number): string { - const name = Object.keys(this._store)[index]; + const name = Object.keys(this.#store)[index]; return name === undefined ? null : name; } @@ -31,7 +31,7 @@ export default class Storage { * @param item Item. */ public setItem(name: string, item: string): void { - this._store[name] = item; + this.#store[name] = item; } /** @@ -41,7 +41,7 @@ export default class Storage { * @returns Item. */ public getItem(name: string): string { - return this._store[name] === undefined ? null : this._store[name]; + return this.#store[name] === undefined ? null : this.#store[name]; } /** @@ -50,13 +50,13 @@ export default class Storage { * @param name Name. */ public removeItem(name: string): void { - delete this._store[name]; + delete this.#store[name]; } /** * Clears storage. */ public clear(): void { - this._store = {}; + this.#store = {}; } } diff --git a/packages/happy-dom/src/tree-walker/NodeIterator.ts b/packages/happy-dom/src/tree-walker/NodeIterator.ts index 82520507c..c533112f6 100644 --- a/packages/happy-dom/src/tree-walker/NodeIterator.ts +++ b/packages/happy-dom/src/tree-walker/NodeIterator.ts @@ -13,7 +13,7 @@ export default class NodeIterator { public whatToShow = -1; public filter: INodeFilter = null; - private readonly _walker: TreeWalker; + readonly #walker: TreeWalker; /** * Constructor. @@ -26,7 +26,7 @@ export default class NodeIterator { this.root = root; this.whatToShow = whatToShow; this.filter = filter; - this._walker = new TreeWalker(root, whatToShow, filter); + this.#walker = new TreeWalker(root, whatToShow, filter); } /** @@ -35,7 +35,7 @@ export default class NodeIterator { * @returns Current node. */ public nextNode(): INode { - return this._walker.nextNode(); + return this.#walker.nextNode(); } /** @@ -44,6 +44,6 @@ export default class NodeIterator { * @returns Current node. */ public previousNode(): INode { - return this._walker.previousNode(); + return this.#walker.previousNode(); } } diff --git a/packages/happy-dom/src/tree-walker/TreeWalker.ts b/packages/happy-dom/src/tree-walker/TreeWalker.ts index c77532e71..c0e5f8b09 100644 --- a/packages/happy-dom/src/tree-walker/TreeWalker.ts +++ b/packages/happy-dom/src/tree-walker/TreeWalker.ts @@ -83,7 +83,7 @@ export default class TreeWalker { * @returns Current node. */ public firstChild(): INode { - const childNodes = this.currentNode ? (this.currentNode)._childNodes : []; + const childNodes = this.currentNode ? (this.currentNode).__childNodes__ : []; if (childNodes.length > 0) { this.currentNode = childNodes[0]; @@ -104,7 +104,7 @@ export default class TreeWalker { * @returns Current node. */ public lastChild(): INode { - const childNodes = this.currentNode ? (this.currentNode)._childNodes : []; + const childNodes = this.currentNode ? (this.currentNode).__childNodes__ : []; if (childNodes.length > 0) { this.currentNode = childNodes[childNodes.length - 1]; @@ -126,7 +126,7 @@ export default class TreeWalker { */ public previousSibling(): INode { if (this.currentNode !== this.root && this.currentNode && this.currentNode.parentNode) { - const siblings = (this.currentNode.parentNode)._childNodes; + const siblings = (this.currentNode.parentNode).__childNodes__; const index = siblings.indexOf(this.currentNode); if (index > 0) { @@ -150,7 +150,7 @@ export default class TreeWalker { */ public nextSibling(): INode { if (this.currentNode !== this.root && this.currentNode && this.currentNode.parentNode) { - const siblings = (this.currentNode.parentNode)._childNodes; + const siblings = (this.currentNode.parentNode).__childNodes__; const index = siblings.indexOf(this.currentNode); if (index + 1 < siblings.length) { diff --git a/packages/happy-dom/src/url/URL.ts b/packages/happy-dom/src/url/URL.ts index 39ffae780..61536d44d 100644 --- a/packages/happy-dom/src/url/URL.ts +++ b/packages/happy-dom/src/url/URL.ts @@ -14,7 +14,7 @@ export default class URL extends NodeJSURL { */ public static override createObjectURL(object: NodeJSBlob | Blob): string { if (object instanceof Blob) { - const blob = new NodeJSBlob([object._buffer], { type: object.type }); + const blob = new NodeJSBlob([object.__buffer__], { type: object.type }); return super.createObjectURL(blob); } return super.createObjectURL(object); diff --git a/packages/happy-dom/src/validity-state/ValidityState.ts b/packages/happy-dom/src/validity-state/ValidityState.ts index 9e51667c8..d6b12a144 100644 --- a/packages/happy-dom/src/validity-state/ValidityState.ts +++ b/packages/happy-dom/src/validity-state/ValidityState.ts @@ -182,7 +182,7 @@ export default class ValidityState { return true; } const root = - this.element._formNode || this.element.getRootNode(); + this.element.__formNode__ || this.element.getRootNode(); return !root || !root.querySelector(`input[name="${this.element.name}"]:checked`); } } diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index c1ee65e00..c40d9911c 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -485,8 +485,8 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow // Used for tracking capture event listeners to improve performance when they are not used. // See EventTarget class. - public _captureEventListenerCount: { [eventType: string]: number } = {}; - public readonly _readyStateManager = new DocumentReadyStateManager(this); + public __captureEventListenerCount__: { [eventType: string]: number } = {}; + public readonly __readyStateManager__ = new DocumentReadyStateManager(this); // Private properties #setTimeout: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; @@ -556,7 +556,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow } } - this._setupVMContext(); + this.__setupVMContext__(); const classes = WindowClassFactory.getClasses({ window: this, @@ -684,7 +684,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow documentElement.appendChild(bodyElement); // Ready state manager - this._readyStateManager.whenComplete().then(() => { + this.__readyStateManager__.whenComplete().then(() => { (this.document.readyState) = DocumentReadyStateEnum.complete; this.document.dispatchEvent(new Event('readystatechange')); this.document.dispatchEvent(new Event('load', { bubbles: true })); @@ -752,8 +752,9 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow * @returns CSS style declaration. */ public getComputedStyle(element: IElement): CSSStyleDeclaration { - element['_computedStyle'] = element['_computedStyle'] || new CSSStyleDeclaration(element, true); - return element['_computedStyle']; + element['__computedStyle__'] = + element['__computedStyle__'] || new CSSStyleDeclaration(element, true); + return element['__computedStyle__']; } /** @@ -877,9 +878,9 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow } else { WindowErrorUtility.captureError(this, () => callback(...args)); } - this.#browserFrame._asyncTaskManager.endTimer(id); + this.#browserFrame.__asyncTaskManager__.endTimer(id); }, delay); - this.#browserFrame._asyncTaskManager.startTimer(id); + this.#browserFrame.__asyncTaskManager__.startTimer(id); return id; } @@ -890,7 +891,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow */ public clearTimeout(id: NodeJS.Timeout): void { this.#clearTimeout(id); - this.#browserFrame._asyncTaskManager.endTimer(id); + this.#browserFrame.__asyncTaskManager__.endTimer(id); } /** @@ -913,7 +914,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow ); } }, delay); - this.#browserFrame._asyncTaskManager.startTimer(id); + this.#browserFrame.__asyncTaskManager__.startTimer(id); return id; } @@ -924,7 +925,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow */ public clearInterval(id: NodeJS.Timeout): void { this.#clearInterval(id); - this.#browserFrame._asyncTaskManager.endTimer(id); + this.#browserFrame.__asyncTaskManager__.endTimer(id); } /** @@ -940,9 +941,9 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow } else { WindowErrorUtility.captureError(this, () => callback(this.performance.now())); } - this.#browserFrame._asyncTaskManager.endImmediate(id); + this.#browserFrame.__asyncTaskManager__.endImmediate(id); }); - this.#browserFrame._asyncTaskManager.startImmediate(id); + this.#browserFrame.__asyncTaskManager__.startImmediate(id); return id; } @@ -953,7 +954,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow */ public cancelAnimationFrame(id: NodeJS.Immediate): void { global.clearImmediate(id); - this.#browserFrame._asyncTaskManager.endImmediate(id); + this.#browserFrame.__asyncTaskManager__.endImmediate(id); } /** @@ -963,7 +964,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow */ public queueMicrotask(callback: Function): void { let isAborted = false; - const taskId = this.#browserFrame._asyncTaskManager.startTask(() => (isAborted = true)); + const taskId = this.#browserFrame.__asyncTaskManager__.startTask(() => (isAborted = true)); this.#queueMicrotask(() => { if (!isAborted) { if (this.#browserFrame.page.context.browser.settings.disableErrorCapturing) { @@ -971,7 +972,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow } else { WindowErrorUtility.captureError(this, <() => unknown>callback); } - this.#browserFrame._asyncTaskManager.endTask(taskId); + this.#browserFrame.__asyncTaskManager__.endTask(taskId); } }); } @@ -1061,7 +1062,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow /** * Setup of VM context. */ - protected _setupVMContext(): void { + protected __setupVMContext__(): void { if (!VM.isContext(this)) { VM.createContext(this); diff --git a/packages/happy-dom/src/window/GlobalWindow.ts b/packages/happy-dom/src/window/GlobalWindow.ts index ddde55fca..1c0e6f8c0 100644 --- a/packages/happy-dom/src/window/GlobalWindow.ts +++ b/packages/happy-dom/src/window/GlobalWindow.ts @@ -73,7 +73,7 @@ export default class GlobalWindow extends Window implements IWindow { /** * Setup of VM context. */ - protected override _setupVMContext(): void { + protected override __setupVMContext__(): void { // Do nothing } } diff --git a/packages/happy-dom/src/window/WindowClassFactory.ts b/packages/happy-dom/src/window/WindowClassFactory.ts index b8f894a6e..55a4926df 100644 --- a/packages/happy-dom/src/window/WindowClassFactory.ts +++ b/packages/happy-dom/src/window/WindowClassFactory.ts @@ -125,7 +125,7 @@ export default class WindowClassFactory { Audio: typeof AudioImplementation; } { const window = properties.window; - const asyncTaskManager = properties.browserFrame._asyncTaskManager; + const asyncTaskManager = properties.browserFrame.__asyncTaskManager__; /* eslint-disable jsdoc/require-jsdoc */ @@ -359,7 +359,7 @@ export default class WindowClassFactory { } } class Response extends ResponseImplementation { - protected static _window = window; + protected static __window__ = window; constructor(body?: IResponseBody, init?: IResponseInit) { super({ window, asyncTaskManager }, body, init); } diff --git a/packages/happy-dom/src/window/WindowErrorUtility.ts b/packages/happy-dom/src/window/WindowErrorUtility.ts index 54bbf6a61..d154cc385 100644 --- a/packages/happy-dom/src/window/WindowErrorUtility.ts +++ b/packages/happy-dom/src/window/WindowErrorUtility.ts @@ -56,7 +56,7 @@ export default class WindowErrorUtility { (elementOrWindow).console.error(error); elementOrWindow.dispatchEvent(new ErrorEvent('error', { message: error.message, error })); } else { - (elementOrWindow).ownerDocument._defaultView.console.error(error); + (elementOrWindow).ownerDocument.__defaultView__.console.error(error); (elementOrWindow).dispatchEvent( new ErrorEvent('error', { message: error.message, error }) ); diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index b5f8c63cc..334bd04fa 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -293,7 +293,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { password: password || null }; - this._setState(XMLHttpRequestReadyStateEnum.opened); + this.#setState(XMLHttpRequestReadyStateEnum.opened); } /** @@ -429,9 +429,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } if (this.#internal.settings.async) { - this._sendLocalAsyncRequest(url).catch((error) => this._onError(error)); + this.#sendLocalAsyncRequest(url).catch((error) => this.#onError(error)); } else { - this._sendLocalSyncRequest(url); + this.#sendLocalSyncRequest(url); } return; } @@ -488,7 +488,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { port: port, path: uri, method: this.#internal.settings.method, - headers: { ...this._getDefaultRequestHeaders(), ...this.#internal.state.requestHeaders }, + headers: { ...this.#getDefaultRequestHeaders(), ...this.#internal.state.requestHeaders }, agent: false, rejectUnauthorized: true }; @@ -503,9 +503,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { // Handle async requests if (this.#internal.settings.async) { - this._sendAsyncRequest(options, ssl, data).catch((error) => this._onError(error)); + this.#sendAsyncRequest(options, ssl, data).catch((error) => this.#onError(error)); } else { - this._sendSyncRequest(options, ssl, data); + this.#sendSyncRequest(options, ssl, data); } } @@ -532,12 +532,12 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.readyState !== XMLHttpRequestReadyStateEnum.done ) { this.#internal.state.send = false; - this._setState(XMLHttpRequestReadyStateEnum.done); + this.#setState(XMLHttpRequestReadyStateEnum.done); } this.#internal.state.readyState = XMLHttpRequestReadyStateEnum.unsent; if (this.#internal.state.asyncTaskID !== null) { - this.#browserFrame._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#browserFrame.__asyncTaskManager__.endTask(this.#internal.state.asyncTaskID); } } @@ -546,7 +546,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * * @param state */ - private _setState(state: XMLHttpRequestReadyStateEnum): void { + #setState(state: XMLHttpRequestReadyStateEnum): void { if ( this.readyState === state || (this.readyState === XMLHttpRequestReadyStateEnum.unsent && this.#internal.state.aborted) @@ -585,7 +585,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * * @returns Default request headers. */ - private _getDefaultRequestHeaders(): { [key: string]: string } { + #getDefaultRequestHeaders(): { [key: string]: string } { const headers: { [k: string]: string } = { accept: '*/*', referer: this.#window.location.href, @@ -609,7 +609,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param ssl * @param data */ - private _sendSyncRequest(options: HTTPS.RequestOptions, ssl: boolean, data?: string): void { + #sendSyncRequest(options: HTTPS.RequestOptions, ssl: boolean, data?: string): void { const scriptString = XMLHttpRequestSyncRequestScriptBuilder.getScript(options, ssl, data); // Start the other Node Process, executing this string @@ -626,7 +626,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { const { error, data: response } = JSON.parse(content.toString()); if (error) { - this._onError(error); + this.#onError(error); return; } @@ -639,9 +639,9 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.statusText = response.statusMessage; // Although it will immediately be set to loading, // According to the spec, the state should be headersRecieved first. - this._setState(XMLHttpRequestReadyStateEnum.headersRecieved); - this._setState(XMLHttpRequestReadyStateEnum.loading); - this.#internal.state.response = this._decodeResponseText( + this.#setState(XMLHttpRequestReadyStateEnum.headersRecieved); + this.#setState(XMLHttpRequestReadyStateEnum.loading); + this.#internal.state.response = this.#decodeResponseText( Buffer.from(response.data, 'base64') ); this.#internal.state.responseText = this.#internal.state.response; @@ -651,7 +651,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#window.location ).href; // Set Cookies. - this._setCookies(this.#internal.state.incommingMessage.headers); + this.#setCookies(this.#internal.state.incommingMessage.headers); // Redirect. if ( this.#internal.state.incommingMessage.statusCode === 301 || @@ -666,7 +666,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ssl = redirectUrl.protocol === 'https:'; this.#internal.settings.url = redirectUrl.href; // Recursive call. - this._sendSyncRequest( + this.#sendSyncRequest( Object.assign(options, { host: redirectUrl.host, path: redirectUrl.pathname + (redirectUrl.search ?? ''), @@ -685,7 +685,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ); } - this._setState(XMLHttpRequestReadyStateEnum.done); + this.#setState(XMLHttpRequestReadyStateEnum.done); } } @@ -696,14 +696,10 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param ssl * @param data */ - private _sendAsyncRequest( - options: HTTPS.RequestOptions, - ssl: boolean, - data?: string - ): Promise { + #sendAsyncRequest(options: HTTPS.RequestOptions, ssl: boolean, data?: string): Promise { return new Promise((resolve) => { // Starts async task in Happy DOM - this.#internal.state.asyncTaskID = this.#browserFrame._asyncTaskManager.startTask( + this.#internal.state.asyncTaskID = this.#browserFrame.__asyncTaskManager__.startTask( this.abort.bind(this) ); @@ -720,20 +716,20 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.asyncRequest = sendRequest( options, async (response: HTTP.IncomingMessage) => { - await this._onAsyncResponse(response, options, ssl, data); + await this.#onAsyncResponse(response, options, ssl, data); resolve(); // Ends async task in Happy DOM - this.#browserFrame._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#browserFrame.__asyncTaskManager__.endTask(this.#internal.state.asyncTaskID); } ); this.#internal.state.asyncRequest.on('error', (error: Error) => { - this._onError(error); + this.#onError(error); resolve(); // Ends async task in Happy DOM - this.#browserFrame._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#browserFrame.__asyncTaskManager__.endTask(this.#internal.state.asyncTaskID); }); // Node 0.4 and later won't accept empty data. Make sure it's needed. @@ -756,7 +752,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param response Response. * @returns Promise. */ - private _onAsyncResponse( + #onAsyncResponse( response: HTTP.IncomingMessage, options: HTTPS.RequestOptions, ssl: boolean, @@ -768,7 +764,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.incommingMessage = response; // Set Cookies - this._setCookies(this.#internal.state.incommingMessage.headers); + this.#setCookies(this.#internal.state.incommingMessage.headers); // Check for redirect // @TODO Prevent looped redirects @@ -786,7 +782,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.settings.url = redirectUrl.href; ssl = redirectUrl.protocol === 'https:'; // Issue the new request - this._sendAsyncRequest( + this.#sendAsyncRequest( { ...options, host: redirectUrl.hostname, @@ -807,7 +803,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.status = this.#internal.state.incommingMessage.statusCode; this.#internal.state.statusText = this.#internal.state.incommingMessage.statusMessage; - this._setState(XMLHttpRequestReadyStateEnum.headersRecieved); + this.#setState(XMLHttpRequestReadyStateEnum.headersRecieved); // Initialize response. let tempResponse = Buffer.from(new Uint8Array(0)); @@ -819,7 +815,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } // Don't emit state changes if the connection has been aborted. if (this.#internal.state.send) { - this._setState(XMLHttpRequestReadyStateEnum.loading); + this.#setState(XMLHttpRequestReadyStateEnum.loading); } const contentLength = Number( @@ -841,7 +837,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.send = false; // Set response according to responseType. - const { response, responseXML, responseText } = this._parseResponseData(tempResponse); + const { response, responseXML, responseText } = this.#parseResponseData(tempResponse); this.#internal.state.response = response; this.#internal.state.responseXML = responseXML; this.#internal.state.responseText = responseText; @@ -850,14 +846,14 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#window.location ).href; // Discard the 'end' event if the connection has been aborted - this._setState(XMLHttpRequestReadyStateEnum.done); + this.#setState(XMLHttpRequestReadyStateEnum.done); } resolve(); }); this.#internal.state.incommingMessage.on('error', (error) => { - this._onError(error); + this.#onError(error); resolve(); }); }); @@ -869,8 +865,8 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param url URL. * @returns Promise. */ - private async _sendLocalAsyncRequest(url: UrlObject): Promise { - this.#internal.state.asyncTaskID = this.#browserFrame._asyncTaskManager.startTask( + async #sendLocalAsyncRequest(url: UrlObject): Promise { + this.#internal.state.asyncTaskID = this.#browserFrame.__asyncTaskManager__.startTask( this.abort.bind(this) ); @@ -879,16 +875,16 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { try { data = await FS.promises.readFile(decodeURI(url.pathname.slice(1))); } catch (error) { - this._onError(error); + this.#onError(error); // Release async task. - this.#browserFrame._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#browserFrame.__asyncTaskManager__.endTask(this.#internal.state.asyncTaskID); return; } const dataLength = data.length; // @TODO: set state headersRecieved first. - this._setState(XMLHttpRequestReadyStateEnum.loading); + this.#setState(XMLHttpRequestReadyStateEnum.loading); this.dispatchEvent( new ProgressEvent('progress', { lengthComputable: true, @@ -898,11 +894,11 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { ); if (data) { - this._parseLocalRequestData(url, data); + this.#parseLocalRequestData(url, data); } - this._setState(XMLHttpRequestReadyStateEnum.done); - this.#browserFrame._asyncTaskManager.endTask(this.#internal.state.asyncTaskID); + this.#setState(XMLHttpRequestReadyStateEnum.done); + this.#browserFrame.__asyncTaskManager__.endTask(this.#internal.state.asyncTaskID); } /** @@ -910,23 +906,23 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * * @param url URL. */ - private _sendLocalSyncRequest(url: UrlObject): void { + #sendLocalSyncRequest(url: UrlObject): void { let data: Buffer; try { data = FS.readFileSync(decodeURI(url.pathname.slice(1))); } catch (error) { - this._onError(error); + this.#onError(error); return; } // @TODO: set state headersRecieved first. - this._setState(XMLHttpRequestReadyStateEnum.loading); + this.#setState(XMLHttpRequestReadyStateEnum.loading); if (data) { - this._parseLocalRequestData(url, data); + this.#parseLocalRequestData(url, data); } - this._setState(XMLHttpRequestReadyStateEnum.done); + this.#setState(XMLHttpRequestReadyStateEnum.done); } /** @@ -935,7 +931,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param url URL. * @param data Data. */ - private _parseLocalRequestData(url: UrlObject, data: Buffer): void { + #parseLocalRequestData(url: UrlObject, data: Buffer): void { // Manually set the response headers. this.#internal.state.incommingMessage = { statusCode: 200, @@ -949,7 +945,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#internal.state.status = this.#internal.state.incommingMessage.statusCode; this.#internal.state.statusText = 'OK'; - const { response, responseXML, responseText } = this._parseResponseData(data); + const { response, responseXML, responseText } = this.#parseResponseData(data); this.#internal.state.response = response; this.#internal.state.responseXML = responseXML; this.#internal.state.responseText = responseText; @@ -958,7 +954,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.#window.location ).href; - this._setState(XMLHttpRequestReadyStateEnum.done); + this.#setState(XMLHttpRequestReadyStateEnum.done); } /** @@ -967,7 +963,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param data Data. * @returns Parsed response. */ - private _parseResponseData(data: Buffer): { + #parseResponseData(data: Buffer): { response: ArrayBuffer | Blob | IDocument | object | string; responseText: string; responseXML: IDocument; @@ -1001,7 +997,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { let response: IDocument; try { - response = domParser.parseFromString(this._decodeResponseText(data), 'text/xml'); + response = domParser.parseFromString(this.#decodeResponseText(data), 'text/xml'); } catch (e) { return { response: null, responseText: null, responseXML: null }; } @@ -1010,7 +1006,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { case XMLHttpResponseTypeEnum.json: try { return { - response: JSON.parse(this._decodeResponseText(data)), + response: JSON.parse(this.#decodeResponseText(data)), responseText: null, responseXML: null }; @@ -1020,7 +1016,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { case XMLHttpResponseTypeEnum.text: case '': default: - const responseText = this._decodeResponseText(data); + const responseText = this.#decodeResponseText(data); return { response: responseText, responseText: responseText, @@ -1034,9 +1030,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * * @param headers Headers. */ - private _setCookies( - headers: { [name: string]: string | string[] } | HTTP.IncomingHttpHeaders - ): void { + #setCookies(headers: { [name: string]: string | string[] } | HTTP.IncomingHttpHeaders): void { const originURL = new URL(this.#internal.settings.url, this.#window.location); for (const header of ['set-cookie', 'set-cookie2']) { if (Array.isArray(headers[header])) { @@ -1058,12 +1052,12 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * * @param error Error. */ - private _onError(error: Error | string): void { + #onError(error: Error | string): void { this.#internal.state.status = 0; this.#internal.state.statusText = error.toString(); this.#internal.state.responseText = error instanceof Error ? error.stack : ''; this.#internal.state.error = true; - this._setState(XMLHttpRequestReadyStateEnum.done); + this.#setState(XMLHttpRequestReadyStateEnum.done); const errorObject = error instanceof Error ? error : new Error(error); const event = new ErrorEvent('error', { @@ -1082,7 +1076,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param data Data. * @returns Decoded text. **/ - private _decodeResponseText(data: Buffer): string { + #decodeResponseText(data: Buffer): string { const contextTypeEncodingRegexp = new RegExp(CONTENT_TYPE_ENCODING_REGEXP, 'gi'); // Compatibility with file:// protocol or unpredictable http request. const contentType = diff --git a/packages/happy-dom/src/xml-parser/XMLParser.ts b/packages/happy-dom/src/xml-parser/XMLParser.ts index 2d6fe68c2..b91d9f8b5 100755 --- a/packages/happy-dom/src/xml-parser/XMLParser.ts +++ b/packages/happy-dom/src/xml-parser/XMLParser.ts @@ -286,10 +286,10 @@ export default class XMLParser { // However, they are allowed to be executed when document.write() is used. // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement if (plainTextTagName === 'SCRIPT') { - (currentNode)._evaluateScript = evaluateScripts; + (currentNode).__evaluateScript__ = evaluateScripts; } else if (plainTextTagName === 'LINK') { // An assumption that the same rule should be applied for the HTMLLinkElement is made here. - (currentNode)._evaluateCSS = evaluateScripts; + (currentNode).__evaluateCSS__ = evaluateScripts; } // Plain text elements such as + `); + + await new Promise((resolve) => setTimeout(resolve, 2)); + + expect(errorEvent instanceof window.ErrorEvent).toBe(true); + expect(errorEvent.error.message).toBe('Test error'); + expect(errorEvent.message).toBe('Test error'); + + browser.close(); + }); + + it('Observes uncaught exceptions.', async () => { + const browser = new Browser({ + settings: { errorCapturing: BrowserErrorCapturingEnum.processLevel } + }); + const page = browser.newPage(); + const window = page.mainFrame.window; + const document = window.document; + let errorEvent = null; + + window.addEventListener('error', (event) => (errorEvent = event)); + window['customSetTimeout'] = setTimeout.bind(globalThis); + + document.write(` + + `); + + await new Promise((resolve) => setTimeout(resolve, 2)); + + const consoleOutput = page.virtualConsolePrinter.readAsString(); + + expect(consoleOutput.startsWith('Error: Test error\n at Timeout.eval')).toBe(true); + expect(errorEvent instanceof window.ErrorEvent).toBe(true); + expect(errorEvent.error.message).toBe('Test error'); + expect(errorEvent.message).toBe('Test error'); + + browser.close(); + }); + }); + + describe('disconnect()', () => { + it('Disconnects the observer.', async () => { + expect(process.listenerCount('uncaughtException')).toBe(0); + expect(process.listenerCount('unhandledRejection')).toBe(0); + }); + }); +}); diff --git a/packages/integration-test/test/tests/Fetch.test.js b/packages/integration-test/test/tests/Fetch.test.js index 49808cc89..1ed29da1e 100644 --- a/packages/integration-test/test/tests/Fetch.test.js +++ b/packages/integration-test/test/tests/Fetch.test.js @@ -4,7 +4,9 @@ import Express from 'express'; describe('Fetch', () => { it('Can perform a real fetch()', async () => { - const window = new Window(); + const window = new Window({ + url: 'http://localhost:3000' + }); const express = Express(); express.get('/get/json', (_req, res) => { @@ -31,7 +33,9 @@ describe('Fetch', () => { }); it('Can perform a real FormData post request using fetch()', async () => { - const window = new Window(); + const window = new Window({ + url: 'http://localhost:3000' + }); const express = Express(); express.post('/post/formdata', (req, res) => { diff --git a/packages/integration-test/test/tests/XMLHttpRequest.test.js b/packages/integration-test/test/tests/XMLHttpRequest.test.js index c72b4898e..1a895e581 100644 --- a/packages/integration-test/test/tests/XMLHttpRequest.test.js +++ b/packages/integration-test/test/tests/XMLHttpRequest.test.js @@ -5,7 +5,9 @@ import Express from 'express'; describe('XMLHttpRequest', () => { it('Can perform a real asynchronous XMLHttpRequest request', async () => { await new Promise((resolve) => { - const window = new Window(); + const window = new Window({ + url: 'http://localhost:3000/' + }); const express = Express(); express.get('/get/json', (_req, res) => { @@ -35,7 +37,9 @@ describe('XMLHttpRequest', () => { }); it('Can perform a real synchronous XMLHttpRequest request to Github.com', () => { - const window = new Window(); + const window = new Window({ + url: 'https://raw.githubusercontent.com/' + }); const request = new window.XMLHttpRequest(); request.open( diff --git a/packages/integration-test/test/utilities/TestFunctions.js b/packages/integration-test/test/utilities/TestFunctions.js index 945bf36fc..253be4955 100644 --- a/packages/integration-test/test/utilities/TestFunctions.js +++ b/packages/integration-test/test/utilities/TestFunctions.js @@ -1,3 +1,5 @@ +import Chalk from 'chalk'; + /* eslint-disable no-console*/ const tests = []; @@ -22,35 +24,56 @@ export function run(description, callback) { }); clearTimeout(timeout); timeout = setTimeout(async () => { + let hasError = false; for (const test of tests) { - console.log(test.description); - const result = test.callback(); + console.log(Chalk.blue(test.description)); + let result = null; + try { + result = test.callback(); + } catch (error) { + console.error(Chalk.red(error)); + hasError = true; + } if (result instanceof Promise) { - const testTimeout = setTimeout(() => { - console.error('Test timed out.'); - process.exit(1); - }, 2000); - try { - await result; - clearTimeout(testTimeout); - } catch (error) { - console.error(error); - process.exit(1); - } + await new Promise((resolve) => { + let hasTimedout = false; + const testTimeout = setTimeout(() => { + console.error(Chalk.red('Test timed out.')); + hasError = true; + hasTimedout = true; + resolve(); + }, 2000); + result + .then(() => { + if (!hasTimedout) { + clearTimeout(testTimeout); + resolve(); + } + }) + .catch((error) => { + console.error(Chalk.red(error)); + hasError = true; + }); + }); } } + if (hasError) { + console.log(''); + console.error(Chalk.red('❌ Some tests failed.')); + console.log(''); + process.exit(1); + } + + console.log(''); + console.log(Chalk.green('All tests passed.')); console.log(''); - console.log('All tests passed.'); }, 100); } export function expect(value) { return { toBe: (expected) => { - if (typeof value !== typeof expected) { - throw new Error(`Expected type "${typeof value}" to be "${typeof expected}".`); - } if (value !== expected) { throw new Error(`Expected value "${value}" to be "${expected}".`); } diff --git a/packages/uncaught-exception-observer/README.md b/packages/uncaught-exception-observer/README.md index 4ebb51d48..0c4c87747 100644 --- a/packages/uncaught-exception-observer/README.md +++ b/packages/uncaught-exception-observer/README.md @@ -1,3 +1,5 @@ +:warning: **This package is deprecated. Happy DOM now supports built in by setting "errorCapturing" to "processLevel".** :warning: + ![Happy DOM Logo](https://github.com/capricorn86/happy-dom/raw/master/docs/happy-dom-logo.jpg) # About From 575a6f01ea5dc04fc2a6bef8558b13b92b9f41ba Mon Sep 17 00:00:00 2001 From: David Ortner Date: Mon, 8 Jan 2024 01:54:53 +0100 Subject: [PATCH 50/63] #466@trivial: Continues on implementation. --- package-lock.json | 69 ++- package.json | 2 +- packages/happy-dom/package.json | 2 +- .../async-task-manager/AsyncTaskManager.ts | 13 +- packages/happy-dom/src/browser/Browser.ts | 21 +- .../happy-dom/src/browser/BrowserContext.ts | 39 +- .../happy-dom/src/browser/BrowserFrame.ts | 27 +- packages/happy-dom/src/browser/BrowserPage.ts | 8 +- .../detached-browser/DetachedBrowser.ts | 21 +- .../DetachedBrowserContext.ts | 31 +- .../detached-browser/DetachedBrowserFrame.ts | 25 +- .../detached-browser/DetachedBrowserPage.ts | 25 +- .../src/browser/types/IBrowserContext.ts | 4 +- .../src/browser/types/IBrowserFrame.ts | 4 +- .../src/browser/types/IBrowserPage.ts | 4 +- .../browser/utilities/BrowserFrameFactory.ts | 60 +- .../browser/utilities/BrowserPageUtility.ts | 39 +- .../dom-implementation/DOMImplementation.ts | 15 +- packages/happy-dom/src/event/EventTarget.ts | 12 +- packages/happy-dom/src/fetch/Fetch.ts | 43 +- packages/happy-dom/src/fetch/SyncFetch.ts | 28 +- .../preflight/IPreflightResponseCache.ts | 5 + .../cache/preflight/PreflightResponseCache.ts | 7 + .../src/fetch/cache/response/ResponseCache.ts | 8 +- .../happy-dom/src/nodes/document/Document.ts | 32 +- .../happy-dom/src/nodes/element/Element.ts | 2 + .../src/nodes/element/ElementNamedNodeMap.ts | 20 +- .../src/nodes/html-document/HTMLDocument.ts | 27 +- .../HTMLUnknownElement.ts | 1 - packages/happy-dom/src/nodes/node/Node.ts | 15 +- .../happy-dom/src/window/BrowserWindow.ts | 528 +++++++++--------- .../happy-dom/src/window/DetachedWindowAPI.ts | 10 +- .../src/window/WindowClassFactory.ts | 459 --------------- .../happy-dom/test/browser/Browser.test.ts | 24 +- .../test/browser/BrowserContext.test.ts | 8 +- .../test/browser/BrowserPage.test.ts | 4 +- .../detached-browser/DetachedBrowser.test.ts | 22 +- .../DetachedBrowserContext.test.ts | 8 +- .../DetachedBrowserPage.test.ts | 4 +- .../test/window/DetachedWindowAPI.test.ts | 6 +- .../xml-http-request/XMLHttpRequest.test.ts | 2 +- packages/integration-test/test/index.js | 1 + .../test/tests/Browser.test.js | 27 + .../BrowserFrameExceptionObserver.test.js | 4 +- .../test/utilities/TestFunctions.js | 8 +- packages/jest-environment/src/index.ts | 2 +- .../test/javascript/JavaScript.test.ts | 11 +- .../testing-library/TestingLibrary.test.tsx | 3 +- turbo.json | 10 +- 49 files changed, 739 insertions(+), 1011 deletions(-) delete mode 100644 packages/happy-dom/src/window/WindowClassFactory.ts create mode 100644 packages/integration-test/test/tests/Browser.test.js diff --git a/package-lock.json b/package-lock.json index 1d83fc0a2..7fbf496cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "husky": "2.3.0", "prettier": "^2.6.0", "semver": "^7.3.5", - "turbo": "^1.7.3", + "turbo": "^1.11.3", "typescript": "^5.0.4", "vitest": "^0.32.4" }, @@ -11001,26 +11001,26 @@ } }, "node_modules/turbo": { - "version": "1.10.14", - "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.14.tgz", - "integrity": "sha512-hr9wDNYcsee+vLkCDIm8qTtwhJ6+UAMJc3nIY6+PNgUTtXcQgHxCq8BGoL7gbABvNWv76CNbK5qL4Lp9G3ZYRA==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.11.3.tgz", + "integrity": "sha512-RCJOUFcFMQNIGKSjC9YmA5yVP1qtDiBA0Lv9VIgrXraI5Da1liVvl3VJPsoDNIR9eFMyA/aagx1iyj6UWem5hA==", "dev": true, "bin": { "turbo": "bin/turbo" }, "optionalDependencies": { - "turbo-darwin-64": "1.10.14", - "turbo-darwin-arm64": "1.10.14", - "turbo-linux-64": "1.10.14", - "turbo-linux-arm64": "1.10.14", - "turbo-windows-64": "1.10.14", - "turbo-windows-arm64": "1.10.14" + "turbo-darwin-64": "1.11.3", + "turbo-darwin-arm64": "1.11.3", + "turbo-linux-64": "1.11.3", + "turbo-linux-arm64": "1.11.3", + "turbo-windows-64": "1.11.3", + "turbo-windows-arm64": "1.11.3" } }, "node_modules/turbo-darwin-64": { - "version": "1.10.14", - "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.10.14.tgz", - "integrity": "sha512-I8RtFk1b9UILAExPdG/XRgGQz95nmXPE7OiGb6ytjtNIR5/UZBS/xVX/7HYpCdmfriKdVwBKhalCoV4oDvAGEg==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/turbo-darwin-64/-/turbo-darwin-64-1.11.3.tgz", + "integrity": "sha512-IsOOg2bVbIt3o/X8Ew9fbQp5t1hTHN3fGNQYrPQwMR2W1kIAC6RfbVD4A9OeibPGyEPUpwOH79hZ9ydFH5kifw==", "cpu": [ "x64" ], @@ -11031,9 +11031,9 @@ ] }, "node_modules/turbo-darwin-arm64": { - "version": "1.10.14", - "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.10.14.tgz", - "integrity": "sha512-KAdUWryJi/XX7OD0alOuOa0aJ5TLyd4DNIYkHPHYcM6/d7YAovYvxRNwmx9iv6Vx6IkzTnLeTiUB8zy69QkG9Q==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.11.3.tgz", + "integrity": "sha512-FsJL7k0SaPbJzI/KCnrf/fi3PgCDCjTliMc/kEFkuWVA6Httc3Q4lxyLIIinz69q6JTx8wzh6yznUMzJRI3+dg==", "cpu": [ "arm64" ], @@ -11044,9 +11044,9 @@ ] }, "node_modules/turbo-linux-64": { - "version": "1.10.14", - "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.10.14.tgz", - "integrity": "sha512-BOBzoREC2u4Vgpap/WDxM6wETVqVMRcM8OZw4hWzqCj2bqbQ6L0wxs1LCLWVrghQf93JBQtIGAdFFLyCSBXjWQ==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/turbo-linux-64/-/turbo-linux-64-1.11.3.tgz", + "integrity": "sha512-SvW7pvTVRGsqtSkII5w+wriZXvxqkluw5FO/MNAdFw0qmoov+PZ237+37/NgArqE3zVn1GX9P6nUx9VO+xcQAg==", "cpu": [ "x64" ], @@ -11057,9 +11057,9 @@ ] }, "node_modules/turbo-linux-arm64": { - "version": "1.10.14", - "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.10.14.tgz", - "integrity": "sha512-D8T6XxoTdN5D4V5qE2VZG+/lbZX/89BkAEHzXcsSUTRjrwfMepT3d2z8aT6hxv4yu8EDdooZq/2Bn/vjMI32xw==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/turbo-linux-arm64/-/turbo-linux-arm64-1.11.3.tgz", + "integrity": "sha512-YhUfBi1deB3m+3M55X458J6B7RsIS7UtM3P1z13cUIhF+pOt65BgnaSnkHLwETidmhRh8Dl3GelaQGrB3RdCDw==", "cpu": [ "arm64" ], @@ -11070,9 +11070,9 @@ ] }, "node_modules/turbo-windows-64": { - "version": "1.10.14", - "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.10.14.tgz", - "integrity": "sha512-zKNS3c1w4i6432N0cexZ20r/aIhV62g69opUn82FLVs/zk3Ie0GVkSB6h0rqIvMalCp7enIR87LkPSDGz9K4UA==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/turbo-windows-64/-/turbo-windows-64-1.11.3.tgz", + "integrity": "sha512-s+vEnuM2TiZuAUUUpmBHDr6vnNbJgj+5JYfnYmVklYs16kXh+EppafYQOAkcRIMAh7GjV3pLq5/uGqc7seZeHA==", "cpu": [ "x64" ], @@ -11083,9 +11083,9 @@ ] }, "node_modules/turbo-windows-arm64": { - "version": "1.10.14", - "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.10.14.tgz", - "integrity": "sha512-rkBwrTPTxNSOUF7of8eVvvM+BkfkhA2OvpHM94if8tVsU+khrjglilp8MTVPHlyS9byfemPAmFN90oRIPB05BA==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/turbo-windows-arm64/-/turbo-windows-arm64-1.11.3.tgz", + "integrity": "sha512-ZR5z5Zpc7cASwfdRAV5yNScCZBsgGSbcwiA/u3farCacbPiXsfoWUkz28iyrx21/TRW0bi6dbsB2v17swa8bjw==", "cpu": [ "arm64" ], @@ -11954,10 +11954,23 @@ "@types/node": "^16.11.7", "@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/parser": "^5.16.0", + "chalk": "^5.3.0", "express": "^4.18.2", "prettier": "^2.6.0" } }, + "packages/integration-test/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "packages/jest-environment": { "name": "@happy-dom/jest-environment", "version": "0.0.0", diff --git a/package.json b/package.json index db1c88459..251439d3e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "vitest": "^0.32.4", "prettier": "^2.6.0", "semver": "^7.3.5", - "turbo": "^1.7.3", + "turbo": "^1.11.3", "typescript": "^5.0.4" }, "engines": { diff --git a/packages/happy-dom/package.json b/packages/happy-dom/package.json index 68f92939b..1d43d30fe 100644 --- a/packages/happy-dom/package.json +++ b/packages/happy-dom/package.json @@ -65,7 +65,7 @@ "access": "public" }, "scripts": { - "compile": "tsc && tsc --moduleResolution Node --module CommonJS --outDir cjs && npm run change-cjs-file-extension && npm run build-version-file", + "compile": "tsc && rm -rf ./cjs && tsc --moduleResolution Node --module CommonJS --outDir cjs && npm run change-cjs-file-extension && npm run build-version-file", "change-cjs-file-extension": "node ./bin/change-file-extension.cjs --dir=./cjs --fromExt=.js --toExt=.cjs", "build-version-file": "node ./bin/build-version-file.cjs", "watch": "tsc -w --preserveWatchOutput", diff --git a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts index f70e8a0bd..574a97117 100644 --- a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts +++ b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts @@ -25,15 +25,15 @@ export default class AsyncTaskManager { /** * Aborts all tasks. */ - public abort(): void { - this.abortAll(false); + public abort(): Promise { + return this.abortAll(false); } /** * Destroys the manager. */ - public destroy(): void { - this.abortAll(true); + public destroy(): Promise { + return this.abortAll(true); } /** @@ -159,7 +159,7 @@ export default class AsyncTaskManager { * * @param destroy Destroy. */ - private abortAll(destroy: boolean): void { + private abortAll(destroy: boolean): Promise { const runningTimers = this.runningTimers; const runningImmediates = this.runningImmediates; const runningTasks = this.runningTasks; @@ -186,6 +186,7 @@ export default class AsyncTaskManager { runningTasks[key](destroy); } - this.resolveWhenComplete(); + // We need to wait for microtasks to complete before resolving. + return this.whenComplete(); } } diff --git a/packages/happy-dom/src/browser/Browser.ts b/packages/happy-dom/src/browser/Browser.ts index e31131b22..48886c7ea 100644 --- a/packages/happy-dom/src/browser/Browser.ts +++ b/packages/happy-dom/src/browser/Browser.ts @@ -44,10 +44,8 @@ export default class Browser implements IBrowser { /** * Aborts all ongoing operations and destroys the browser. */ - public close(): void { - for (const context of this.contexts) { - context.close(); - } + public async close(): Promise { + await Promise.all(this.contexts.slice().map((context) => context.close())); (this.contexts) = []; } @@ -66,10 +64,17 @@ export default class Browser implements IBrowser { /** * Aborts all ongoing operations. */ - public abort(): void { - for (const context of this.contexts.slice()) { - context.abort(); - } + public abort(): Promise { + // Using Promise instead of async/await to prevent microtask + return new Promise((resolve, reject) => { + if (!this.contexts.length) { + resolve(); + return; + } + Promise.all(this.contexts.slice().map((context) => context.abort())) + .then(() => resolve()) + .catch((error) => reject(error)); + }); } /** diff --git a/packages/happy-dom/src/browser/BrowserContext.ts b/packages/happy-dom/src/browser/BrowserContext.ts index c07837c15..a46cee49f 100644 --- a/packages/happy-dom/src/browser/BrowserContext.ts +++ b/packages/happy-dom/src/browser/BrowserContext.ts @@ -31,14 +31,25 @@ export default class BrowserContext implements IBrowserContext { /** * Aborts all ongoing operations and destroys the context. */ - public close(): void { - const index = this.browser.contexts.indexOf(this); - this.browser.contexts.splice(index, 1); - for (const page of this.pages.slice()) { - page.close(); + public async close(): Promise { + if (!this.browser) { + return; } - if (this.browser.contexts.length === 0) { - this.browser.close(); + await Promise.all(this.pages.slice().map((page) => page.close())); + const browser = this.browser; + const index = browser.contexts.indexOf(this); + if (index !== -1) { + browser.contexts.splice(index, 1); + } + (this.pages) = []; + (this.browser) = null; + (this.cookieContainer) = null; + this.responseCache.clear(); + this.preflightResponseCache.clear(); + (this.responseCache) = null; + (this.preflightResponseCache) = null; + if (browser.contexts.length === 0) { + browser.close(); } } @@ -54,10 +65,16 @@ export default class BrowserContext implements IBrowserContext { /** * Aborts all ongoing operations. */ - public abort(): void { - for (const page of this.pages) { - page.abort(); - } + public abort(): Promise { + return new Promise((resolve, reject) => { + if (!this.pages.length) { + resolve(); + return; + } + Promise.all(this.pages.slice().map((page) => page.abort())) + .then(() => resolve()) + .catch((error) => reject(error)); + }); } /** diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 0ce648932..0c4580f3c 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -12,6 +12,7 @@ import BrowserFrameNavigator from './utilities/BrowserFrameNavigator.js'; import IReloadOptions from './types/IReloadOptions.js'; import BrowserFrameExceptionObserver from './utilities/BrowserFrameExceptionObserver.js'; import BrowserErrorCapturingEnum from './enums/BrowserErrorCapturingEnum.js'; +import IDocument from '../nodes/document/IDocument.js'; /** * Browser frame. @@ -83,6 +84,15 @@ export default class BrowserFrame implements IBrowserFrame { ); } + /** + * Returns document. + * + * @returns Document. + */ + public get document(): IDocument { + return this.window?.document ?? null; + } + /** * Returns a promise that is resolved when all async tasks are complete. * @@ -97,14 +107,19 @@ export default class BrowserFrame implements IBrowserFrame { /** * Aborts all ongoing operations. - * - * @returns Promise. */ - public abort(): void { - for (const frame of this.childFrames) { - frame.abort(); + public abort(): Promise { + if (!this.childFrames.length) { + return this.__asyncTaskManager__.abort(); } - this.__asyncTaskManager__.abort(); + return new Promise((resolve, reject) => { + // Using Promise instead of async/await to prevent microtask + Promise.all( + this.childFrames.map((frame) => frame.abort()).concat([this.__asyncTaskManager__.abort()]) + ) + .then(() => resolve()) + .catch(reject); + }); } /** diff --git a/packages/happy-dom/src/browser/BrowserPage.ts b/packages/happy-dom/src/browser/BrowserPage.ts index 349b0f1af..d0d2eab78 100644 --- a/packages/happy-dom/src/browser/BrowserPage.ts +++ b/packages/happy-dom/src/browser/BrowserPage.ts @@ -74,8 +74,8 @@ export default class BrowserPage implements IBrowserPage { /** * Aborts all ongoing operations and destroys the page. */ - public close(): void { - BrowserPageUtility.closePage(this); + public close(): Promise { + return BrowserPageUtility.closePage(this); } /** @@ -90,8 +90,8 @@ export default class BrowserPage implements IBrowserPage { /** * Aborts all ongoing operations. */ - public abort(): void { - this.mainFrame.abort(); + public abort(): Promise { + return this.mainFrame.abort(); } /** diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts index c4f453565..d616adcd8 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowser.ts @@ -59,10 +59,8 @@ export default class DetachedBrowser implements IBrowser { /** * Aborts all ongoing operations and destroys the browser. */ - public close(): void { - for (const context of this.contexts.slice()) { - context.close(); - } + public async close(): Promise { + await Promise.all(this.contexts.slice().map((context) => context.close())); (this.contexts) = []; (this.console) = null; ( IBrowserWindow | null>this.windowClass) = null; @@ -80,10 +78,17 @@ export default class DetachedBrowser implements IBrowser { /** * Aborts all ongoing operations. */ - public abort(): void { - for (const context of this.contexts) { - context.abort(); - } + public abort(): Promise { + // Using Promise instead of async/await to prevent microtask + return new Promise((resolve, reject) => { + if (!this.contexts.length) { + resolve(); + return; + } + Promise.all(this.contexts.slice().map((context) => context.abort())) + .then(() => resolve()) + .catch((error) => reject(error)); + }); } /** diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts index b5e126859..42a4d56be 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserContext.ts @@ -33,17 +33,24 @@ export default class DetachedBrowserContext implements IBrowserContext { /** * Aborts all ongoing operations and destroys the context. */ - public close(): void { + public async close(): Promise { if (!this.browser) { return; } - for (const page of this.pages.slice()) { - page.close(); - } + await Promise.all(this.pages.slice().map((page) => page.close())); const browser = this.browser; + const index = browser.contexts.indexOf(this); + if (index !== -1) { + browser.contexts.splice(index, 1); + } (this.pages) = []; (this.browser) = null; - if (browser.defaultContext === this) { + (this.cookieContainer) = null; + this.responseCache.clear(); + this.preflightResponseCache.clear(); + (this.responseCache) = null; + (this.preflightResponseCache) = null; + if (browser.contexts.length === 0) { browser.close(); } } @@ -60,10 +67,16 @@ export default class DetachedBrowserContext implements IBrowserContext { /** * Aborts all ongoing operations. */ - public abort(): void { - for (const page of this.pages) { - page.abort(); - } + public abort(): Promise { + return new Promise((resolve, reject) => { + if (!this.pages.length) { + resolve(); + return; + } + Promise.all(this.pages.slice().map((page) => page.abort())) + .then(() => resolve()) + .catch((error) => reject(error)); + }); } /** diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index 68adfc67a..240f784ca 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -12,6 +12,7 @@ import IBrowserWindow from '../../window/IBrowserWindow.js'; import IReloadOptions from '../types/IReloadOptions.js'; import BrowserErrorCapturingEnum from '../enums/BrowserErrorCapturingEnum.js'; import BrowserFrameExceptionObserver from '../utilities/BrowserFrameExceptionObserver.js'; +import IDocument from '../../nodes/document/IDocument.js'; /** * Browser frame used when constructing a Window instance without a browser. @@ -99,6 +100,15 @@ export default class DetachedBrowserFrame implements IBrowserFrame { ); } + /** + * Returns document. + * + * @returns Document. + */ + public get document(): IDocument { + return this.window?.document ?? null; + } + /** * Returns a promise that is resolved when all async tasks are complete. * @@ -114,11 +124,18 @@ export default class DetachedBrowserFrame implements IBrowserFrame { /** * Aborts all ongoing operations. */ - public abort(): void { - for (const frame of this.childFrames) { - frame.abort(); + public abort(): Promise { + if (!this.childFrames.length) { + return this.__asyncTaskManager__.abort(); } - this.__asyncTaskManager__.abort(); + return new Promise((resolve, reject) => { + // Using Promise instead of async/await to prevent microtask + Promise.all( + this.childFrames.map((frame) => frame.abort()).concat([this.__asyncTaskManager__.abort()]) + ) + .then(() => resolve()) + .catch(reject); + }); } /** diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts index 5b959342f..77a4b01c5 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserPage.ts @@ -74,13 +74,20 @@ export default class DetachedBrowserPage implements IBrowserPage { /** * Aborts all ongoing operations and destroys the page. */ - public close(): void { - const context = this.context; - BrowserPageUtility.closePage(this); - // As we are in a detached page, a context or browser should not exist without a page as there are no references to them. - if (context.pages[0] === this) { - context.close(); - } + public close(): Promise { + // Using Promise instead of async/await to prevent microtask + return new Promise((resolve, reject) => { + const context = this.context; + BrowserPageUtility.closePage(this) + .then(() => { + // As we are in a detached page, a context or browser should not exist without a page as there are no references to them. + if (context.pages[0] === this) { + context.close(); + } + resolve(); + }) + .catch((error) => reject(error)); + }); } /** @@ -95,8 +102,8 @@ export default class DetachedBrowserPage implements IBrowserPage { /** * Aborts all ongoing operations. */ - public abort(): void { - this.mainFrame.abort(); + public abort(): Promise { + return this.mainFrame.abort(); } /** diff --git a/packages/happy-dom/src/browser/types/IBrowserContext.ts b/packages/happy-dom/src/browser/types/IBrowserContext.ts index b0805c5bc..b6d7618d0 100644 --- a/packages/happy-dom/src/browser/types/IBrowserContext.ts +++ b/packages/happy-dom/src/browser/types/IBrowserContext.ts @@ -18,7 +18,7 @@ export default interface IBrowserContext { /** * Aborts all ongoing operations and destroys the context. */ - close(): void; + close(): Promise; /** * Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all tasks such as timers are complete. @@ -30,7 +30,7 @@ export default interface IBrowserContext { /** * Aborts all ongoing operations. */ - abort(): void; + abort(): Promise; /** * Creates a new page. diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index 400978fb0..1a65d2025 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -1,5 +1,6 @@ import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; import IBrowserWindow from '../../window/IBrowserWindow.js'; +import IDocument from '../../nodes/document/IDocument.js'; import IBrowserPage from './IBrowserPage.js'; import IResponse from '../../fetch/types/IResponse.js'; import IGoToOptions from './IGoToOptions.js'; @@ -13,6 +14,7 @@ import BrowserFrameExceptionObserver from '../utilities/BrowserFrameExceptionObs export default interface IBrowserFrame { readonly childFrames: IBrowserFrame[]; readonly window: IBrowserWindow; + readonly document: IDocument; content: string; url: string; readonly parentFrame: IBrowserFrame | null; @@ -31,7 +33,7 @@ export default interface IBrowserFrame { /** * Aborts all ongoing operations. */ - abort(): void; + abort(): Promise; /** * Evaluates code or a VM Script in the page's context. diff --git a/packages/happy-dom/src/browser/types/IBrowserPage.ts b/packages/happy-dom/src/browser/types/IBrowserPage.ts index 6e29757af..8c5ae7c5f 100644 --- a/packages/happy-dom/src/browser/types/IBrowserPage.ts +++ b/packages/happy-dom/src/browser/types/IBrowserPage.ts @@ -22,7 +22,7 @@ export default interface IBrowserPage { /** * Aborts all ongoing operations and destroys the page. */ - close(): void; + close(): Promise; /** * Returns a promise that is resolved when all async tasks are complete. @@ -34,7 +34,7 @@ export default interface IBrowserPage { /** * Aborts all ongoing operations. */ - abort(): void; + abort(): Promise; /** * Evaluates code or a VM Script in the page's context. diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts index 9eac938a8..2132b6d3d 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts @@ -26,29 +26,49 @@ export default class BrowserFrameFactory { * * @param frame Frame. */ - public static destroyFrame(frame: IBrowserFrame): void { - if (!frame.window) { - return; - } + public static destroyFrame(frame: IBrowserFrame): Promise { + // Using Promise instead of async/await to prevent microtask + return new Promise((resolve, reject) => { + if (!frame.window) { + resolve(); + return; + } - if (frame.parentFrame) { - const index = frame.parentFrame.childFrames.indexOf(frame); - if (index !== -1) { - frame.parentFrame.childFrames.splice(index, 1); + if (frame.parentFrame) { + const index = frame.parentFrame.childFrames.indexOf(frame); + if (index !== -1) { + frame.parentFrame.childFrames.splice(index, 1); + } } - } - for (const childFrame of frame.childFrames.slice()) { - this.destroyFrame(childFrame); - } + (frame.window.closed) = true; + + if (!frame.childFrames.length) { + const window = frame.window; + WindowBrowserSettingsReader.removeSettings(frame.window); + (frame.page) = null; + (frame.window) = null; + (frame.opener) = null; + window.close(); + frame.__exceptionObserver__?.disconnect(); + resolve(); + return; + } - (frame.window.closed) = true; - frame.__asyncTaskManager__.destroy(); - frame.__exceptionObserver__?.disconnect(); - WindowBrowserSettingsReader.removeSettings(frame.window); - // TODO: Setting page to null causes error when window tries to access the console - // (frame.page) = null; - (frame.window) = null; - (frame.opener) = null; + Promise.all(frame.childFrames.slice().map((childFrame) => this.destroyFrame(childFrame))) + .then(() => { + return frame.__asyncTaskManager__.destroy().then(() => { + const window = frame.window; + WindowBrowserSettingsReader.removeSettings(frame.window); + (frame.page) = null; + (frame.window) = null; + (frame.opener) = null; + window.close(); + frame.__exceptionObserver__?.disconnect(); + resolve(); + }); + }) + .catch((error) => reject(error)); + }); } } diff --git a/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts b/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts index 595200108..c71f109c2 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserPageUtility.ts @@ -54,27 +54,28 @@ export default class BrowserPageUtility { * * @param page Page. */ - public static closePage(page: IBrowserPage): void { - if (!page.mainFrame) { - return; - } - - BrowserFrameFactory.destroyFrame(page.mainFrame); - - const index = page.context.pages.indexOf(page); - if (index !== -1) { - page.context.pages.splice(index, 1); - } - - const context = page.context; + public static closePage(page: IBrowserPage): Promise { + // Using Promise instead of async/await to prevent microtask + return new Promise((resolve, reject) => { + if (!page.mainFrame) { + resolve(); + return; + } - (page.virtualConsolePrinter) = null; - (page.mainFrame) = null; - (page.context) = null; + const index = page.context.pages.indexOf(page); + if (index !== -1) { + page.context.pages.splice(index, 1); + } - if (context.pages[0] === page) { - context.close(); - } + BrowserFrameFactory.destroyFrame(page.mainFrame) + .then(() => { + (page.virtualConsolePrinter) = null; + (page.mainFrame) = null; + (page.context) = null; + resolve(); + }) + .catch((error) => reject(error)); + }); } /** diff --git a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts index 610206d7f..d9c03fa32 100644 --- a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts +++ b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts @@ -1,4 +1,3 @@ -import { IBrowserWindow } from '../index.js'; import DocumentType from '../nodes/document-type/DocumentType.js'; import IDocument from '../nodes/document/IDocument.js'; @@ -6,15 +5,15 @@ import IDocument from '../nodes/document/IDocument.js'; * The DOMImplementation interface represents an object providing methods which are not dependent on any particular document. Such an object is returned by the. */ export default class DOMImplementation { - #window: IBrowserWindow; + #document: IDocument; /** * Constructor. * * @param window Window. */ - constructor(window: IBrowserWindow) { - this.#window = window; + constructor(window: IDocument) { + this.#document = window; } /** @@ -23,14 +22,14 @@ export default class DOMImplementation { * TODO: Not fully implemented. */ public createDocument(): IDocument { - return new this.#window.XMLDocument(); + return new this.#document.__defaultView__.HTMLDocument(); } /** * Creates and returns an HTML Document. */ public createHTMLDocument(): IDocument { - return new this.#window.HTMLDocument(); + return new this.#document.__defaultView__.HTMLDocument(); } /** @@ -45,7 +44,9 @@ export default class DOMImplementation { publicId: string, systemId: string ): DocumentType { - const documentType = new this.#window.DocumentType(); + this.#document.__defaultView__.DocumentType.__ownerDocument__ = this.#document; + const documentType = new this.#document.__defaultView__.DocumentType(); + this.#document.__defaultView__.DocumentType.__ownerDocument__ = null; documentType.name = qualifiedName; documentType.publicId = publicId; documentType.systemId = systemId; diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts index 098a5bcc2..b29c5b650 100644 --- a/packages/happy-dom/src/event/EventTarget.ts +++ b/packages/happy-dom/src/event/EventTarget.ts @@ -151,6 +151,8 @@ export default abstract class EventTarget implements IEventTarget { event.__currentTarget__ = this; + const browserSettings = window ? WindowBrowserSettingsReader.getSettings(window) : null; + if (event.eventPhase !== EventPhaseEnum.capturing) { const onEventName = 'on' + event.type.toLowerCase(); @@ -159,9 +161,8 @@ export default abstract class EventTarget implements IEventTarget { if ( window && (this !== window || event.type !== 'error') && - (!WindowBrowserSettingsReader.getSettings(window).disableErrorCapturing || - WindowBrowserSettingsReader.getSettings(window).errorCapturing === - BrowserErrorCapturingEnum.tryAndCatch) + !browserSettings?.disableErrorCapturing && + browserSettings?.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch ) { WindowErrorUtility.captureError(window, this[onEventName].bind(this, event)); } else { @@ -194,9 +195,8 @@ export default abstract class EventTarget implements IEventTarget { if ( window && (this !== window || event.type !== 'error') && - (!WindowBrowserSettingsReader.getSettings(window).disableErrorCapturing || - WindowBrowserSettingsReader.getSettings(window).errorCapturing === - BrowserErrorCapturingEnum.tryAndCatch) + !browserSettings?.disableErrorCapturing && + browserSettings?.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch ) { if ((listener).handleEvent) { WindowErrorUtility.captureError( diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index 5dcca90eb..ee02dd339 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -125,22 +125,26 @@ export default class Fetch { ); } - const cachedResponse = await this.getCachedResponse(); + if (!this.disableCache) { + const cachedResponse = await this.getCachedResponse(); - if (cachedResponse) { - return cachedResponse; + if (cachedResponse) { + return cachedResponse; + } } - const compliesWithCrossOriginPolicy = await this.compliesWithCrossOriginPolicy(); + if (!this.disableCrossOriginPolicy) { + const compliesWithCrossOriginPolicy = await this.compliesWithCrossOriginPolicy(); - if (!compliesWithCrossOriginPolicy) { - this.#window.console.warn( - `Cross-Origin Request Blocked: The Same Origin Policy dissallows reading the remote resource at "${this.request.url}".` - ); - throw new DOMException( - `Cross-Origin Request Blocked: The Same Origin Policy dissallows reading the remote resource at "${this.request.url}".`, - DOMExceptionNameEnum.networkError - ); + if (!compliesWithCrossOriginPolicy) { + this.#window.console.warn( + `Cross-Origin Request Blocked: The Same Origin Policy dissallows reading the remote resource at "${this.request.url}".` + ); + throw new DOMException( + `Cross-Origin Request Blocked: The Same Origin Policy dissallows reading the remote resource at "${this.request.url}".`, + DOMExceptionNameEnum.networkError + ); + } } return await this.sendRequest(); @@ -162,15 +166,15 @@ export default class Fetch { return null; } - if ( - cachedResponse.etag || - (cachedResponse.state === CachedResponseStateEnum.stale && cachedResponse.lastModified) - ) { + if (cachedResponse.state === CachedResponseStateEnum.stale) { const headers = new Headers(cachedResponse.request.headers); if (cachedResponse.etag) { headers.set('If-None-Match', cachedResponse.etag); } else { + if (!cachedResponse.lastModified) { + return null; + } headers.set('If-Modified-Since', new Date(cachedResponse.lastModified).toUTCString()); } @@ -179,7 +183,8 @@ export default class Fetch { window: this.#window, url: this.request.url, init: { headers, method: cachedResponse.request.method }, - disableCache: true + disableCache: true, + disableCrossOriginPolicy: true }); if (cachedResponse.etag || !cachedResponse.staleWhileRevalidate) { @@ -339,7 +344,9 @@ export default class Fetch { } this.resolve = (response: IResponse | Promise): void => { - if (!this.disableCache && response instanceof Response) { + // We can end up here when closing down the browser frame and there is an ongoing request. + // Therefore we need to check if browserFrame.page.context is still available. + if (!this.disableCache && response instanceof Response && this.#browserFrame.page.context) { response.__cachedResponse__ = this.#browserFrame.page.context.responseCache.add( this.request, { diff --git a/packages/happy-dom/src/fetch/SyncFetch.ts b/packages/happy-dom/src/fetch/SyncFetch.ts index 751588b38..2da36ed44 100644 --- a/packages/happy-dom/src/fetch/SyncFetch.ts +++ b/packages/happy-dom/src/fetch/SyncFetch.ts @@ -152,25 +152,26 @@ export default class SyncFetch { return null; } - if ( - cachedResponse.etag || - (cachedResponse.state === CachedResponseStateEnum.stale && cachedResponse.lastModified) - ) { - if (cachedResponse.etag || !cachedResponse.staleWhileRevalidate) { - const headers = new Headers(cachedResponse.request.headers); + if (cachedResponse.state === CachedResponseStateEnum.stale) { + const headers = new Headers(cachedResponse.request.headers); - if (cachedResponse.etag) { - headers.set('If-None-Match', cachedResponse.etag); - } else { - headers.set('If-Modified-Since', new Date(cachedResponse.lastModified).toUTCString()); + if (cachedResponse.etag) { + headers.set('If-None-Match', cachedResponse.etag); + } else { + if (!cachedResponse.lastModified) { + return null; } + headers.set('If-Modified-Since', new Date(cachedResponse.lastModified).toUTCString()); + } + if (cachedResponse.etag || !cachedResponse.staleWhileRevalidate) { const fetch = new SyncFetch({ browserFrame: this.#browserFrame, window: this.#window, url: this.request.url, init: { headers, method: cachedResponse.request.method }, - disableCache: true + disableCache: true, + disableCrossOriginPolicy: true }); const validateResponse = fetch.send(); @@ -190,8 +191,9 @@ export default class SyncFetch { browserFrame: this.#browserFrame, window: this.#window, url: this.request.url, - init: { method: cachedResponse.request.method }, - disableCache: true + init: { headers, method: cachedResponse.request.method }, + disableCache: true, + disableCrossOriginPolicy: true }); fetch.send().then((response) => { response.buffer().then((body: Buffer) => { diff --git a/packages/happy-dom/src/fetch/cache/preflight/IPreflightResponseCache.ts b/packages/happy-dom/src/fetch/cache/preflight/IPreflightResponseCache.ts index 5d791547f..129995610 100644 --- a/packages/happy-dom/src/fetch/cache/preflight/IPreflightResponseCache.ts +++ b/packages/happy-dom/src/fetch/cache/preflight/IPreflightResponseCache.ts @@ -25,4 +25,9 @@ export default interface IPreflightResponseCache { request: ICachablePreflightRequest, response: ICachablePreflightResponse ): ICachedPreflightResponse | null; + + /** + * Clears the cache. + */ + clear(): void; } diff --git a/packages/happy-dom/src/fetch/cache/preflight/PreflightResponseCache.ts b/packages/happy-dom/src/fetch/cache/preflight/PreflightResponseCache.ts index 7f9ae4e90..0e8bd4065 100644 --- a/packages/happy-dom/src/fetch/cache/preflight/PreflightResponseCache.ts +++ b/packages/happy-dom/src/fetch/cache/preflight/PreflightResponseCache.ts @@ -84,4 +84,11 @@ export default class PreflightResponseCache implements IPreflightResponseCache { return cachedResponse; } + + /** + * Clears the cache. + */ + public clear(): void { + this.#entries = {}; + } } diff --git a/packages/happy-dom/src/fetch/cache/response/ResponseCache.ts b/packages/happy-dom/src/fetch/cache/response/ResponseCache.ts index fac9b55c7..213490c0d 100644 --- a/packages/happy-dom/src/fetch/cache/response/ResponseCache.ts +++ b/packages/happy-dom/src/fetch/cache/response/ResponseCache.ts @@ -28,23 +28,25 @@ export default class ResponseCache implements IResponseCache { } const url = request.url; + if (this.#entries[url]) { for (let i = 0, max = this.#entries[url].length; i < max; i++) { const entry = this.#entries[url][i]; let isMatch = entry.request.method === request.method; if (isMatch) { for (const header of Object.keys(entry.vary)) { - if (entry.vary[header] !== request.headers.get(header)) { + const requestHeader = request.headers.get(header); + if (requestHeader !== null && entry.vary[header] !== requestHeader) { isMatch = false; break; } } } if (isMatch) { - if (!entry.etag && entry.expires && entry.expires < Date.now()) { + if (entry.expires && entry.expires < Date.now()) { if (entry.lastModified) { entry.state = CachedResponseStateEnum.stale; - } else { + } else if (!entry.etag) { this.#entries[url].splice(i, 1); return null; } diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 180409115..1f2e50014 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -195,17 +195,8 @@ export default class Document extends Node implements IDocument { constructor(injected: { browserFrame: IBrowserFrame; window: IBrowserWindow }) { super(); this.#browserFrame = injected.browserFrame; - this.implementation = new DOMImplementation(injected.window); this.__defaultView__ = injected.window; - } - - /** - * Returns owner document. - * - * @returns Owner document. - */ - public get ownerDocument(): IDocument { - return null; + this.implementation = new DOMImplementation(this); } /** @@ -858,7 +849,10 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createTextNode(data?: string): IText { - return new this.__defaultView__.Text(data); + this.__defaultView__.Text.__ownerDocument__ = this; + const node = new this.__defaultView__.Text(data); + this.__defaultView__.Text.__ownerDocument__ = null; + return node; } /** @@ -868,7 +862,10 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createComment(data?: string): IComment { - return new this.__defaultView__.Comment(data); + this.__defaultView__.Comment.__ownerDocument__ = this; + const node = new this.__defaultView__.Comment(data); + this.__defaultView__.Comment.__ownerDocument__ = null; + return node; } /** @@ -877,7 +874,10 @@ export default class Document extends Node implements IDocument { * @returns Document fragment. */ public createDocumentFragment(): IDocumentFragment { - return new this.__defaultView__.DocumentFragment(); + this.__defaultView__.DocumentFragment.__ownerDocument__ = this; + const node = new this.__defaultView__.DocumentFragment(); + this.__defaultView__.DocumentFragment.__ownerDocument__ = null; + return node; } /** @@ -938,7 +938,9 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public createAttributeNS(namespaceURI: string, qualifiedName: string): IAttr { + this.__defaultView__.Attr.__ownerDocument__ = this; const attribute = new this.__defaultView__.Attr(); + this.__defaultView__.Attr.__ownerDocument__ = null; attribute.namespaceURI = namespaceURI; attribute.name = qualifiedName; return attribute; @@ -982,7 +984,7 @@ export default class Document extends Node implements IDocument { const adopted = node.parentNode ? node.parentNode.removeChild(node) : node; const document = this; - Object.defineProperty(adopted, 'ownerDocument', { get: () => document }); + Object.defineProperty(adopted, 'ownerDocument', { value: document }); return adopted; } @@ -1025,7 +1027,9 @@ export default class Document extends Node implements IDocument { `Failed to execute 'createProcessingInstruction' on 'Document': The data provided ('?>') contains '?>'` ); } + this.__defaultView__.ProcessingInstruction.__ownerDocument__ = this; const processingInstruction = new this.__defaultView__.ProcessingInstruction(data); + this.__defaultView__.ProcessingInstruction.__ownerDocument__ = null; processingInstruction.target = target; return processingInstruction; } diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 348a0391d..aad01cd3f 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -693,7 +693,9 @@ export default class Element extends Node implements IElement { throw new DOMException('Shadow root has already been attached.'); } + this.ownerDocument.__defaultView__.ShadowRoot.__ownerDocument__ = this.ownerDocument; (this.__shadowRoot__) = new this.ownerDocument.__defaultView__.ShadowRoot(); + this.ownerDocument.__defaultView__.ShadowRoot.__ownerDocument__ = null; (this.__shadowRoot__.host) = this; (this.__shadowRoot__.mode) = shadowRootInit.mode; (this.__shadowRoot__).__connectToNode__(this); diff --git a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts index 599171b36..3b10e67a1 100644 --- a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts @@ -13,14 +13,14 @@ import IElement from './IElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class ElementNamedNodeMap extends NamedNodeMap { - protected __ownerElement__: Element; + protected __ownerElement__: IElement; /** * Constructor. * * @param ownerElement Owner element. */ - constructor(ownerElement: Element) { + constructor(ownerElement: IElement) { super(); this.__ownerElement__ = ownerElement; } @@ -57,8 +57,8 @@ export default class ElementNamedNodeMap extends NamedNodeMap { this.__ownerElement__.ownerDocument['__cacheID__']++; } - if (item.name === 'class' && this.__ownerElement__.__classList__) { - this.__ownerElement__.__classList__.__updateIndices__(); + if (item.name === 'class' && this.__ownerElement__['__classList__']) { + this.__ownerElement__['__classList__'].__updateIndices__(); } if (item.name === 'id' || item.name === 'name') { @@ -89,8 +89,8 @@ export default class ElementNamedNodeMap extends NamedNodeMap { } // MutationObserver - if (this.__ownerElement__.__observers__.length > 0) { - for (const observer of this.__ownerElement__.__observers__) { + if (this.__ownerElement__['__observers__'].length > 0) { + for (const observer of this.__ownerElement__['__observers__']) { if ( observer.options.attributes && (!observer.options.attributeFilter || @@ -123,8 +123,8 @@ export default class ElementNamedNodeMap extends NamedNodeMap { this.__ownerElement__.ownerDocument['__cacheID__']++; } - if (removedItem.name === 'class' && this.__ownerElement__.__classList__) { - this.__ownerElement__.__classList__.__updateIndices__(); + if (removedItem.name === 'class' && this.__ownerElement__['__classList__']) { + this.__ownerElement__['__classList__'].__updateIndices__(); } if (removedItem.name === 'id' || removedItem.name === 'name') { @@ -150,8 +150,8 @@ export default class ElementNamedNodeMap extends NamedNodeMap { } // MutationObserver - if (this.__ownerElement__.__observers__.length > 0) { - for (const observer of this.__ownerElement__.__observers__) { + if (this.__ownerElement__['__observers__'].length > 0) { + for (const observer of this.__ownerElement__['__observers__']) { if ( observer.options.attributes && (!observer.options.attributeFilter || diff --git a/packages/happy-dom/src/nodes/html-document/HTMLDocument.ts b/packages/happy-dom/src/nodes/html-document/HTMLDocument.ts index df203ccc9..871c3e0b4 100644 --- a/packages/happy-dom/src/nodes/html-document/HTMLDocument.ts +++ b/packages/happy-dom/src/nodes/html-document/HTMLDocument.ts @@ -1,6 +1,31 @@ +import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; +import IBrowserWindow from '../../window/IBrowserWindow.js'; import Document from '../document/Document.js'; /** * Document. */ -export default class HTMLDocument extends Document {} +export default class HTMLDocument extends Document { + /** + * Constructor. + * + * @param injected Injected properties. + * @param injected.browserFrame Browser frame. + * @param injected.window Window. + */ + constructor(injected: { browserFrame: IBrowserFrame; window: IBrowserWindow }) { + super(injected); + + // Default document elements + const doctype = this.implementation.createDocumentType('html', '', ''); + const documentElement = this.createElement('html'); + const bodyElement = this.createElement('body'); + const headElement = this.createElement('head'); + + this.appendChild(doctype); + this.appendChild(documentElement); + + documentElement.appendChild(headElement); + documentElement.appendChild(bodyElement); + } +} diff --git a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts index d44f3e785..dcce059b3 100644 --- a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts +++ b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts @@ -84,7 +84,6 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem } if (newElement.isConnected && newElement.connectedCallback) { - debugger; newElement.connectedCallback(); } diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 763e9a9ff..7315b9677 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -62,16 +62,15 @@ export default class Node extends EventTarget implements INode { public __textAreaNode__: INode = null; public __observers__: MutationListener[] = []; public readonly __childNodes__: INodeList = new NodeList(); - #ownerDocument: IDocument | null = null; + public readonly ownerDocument: IDocument | null = null; /** * Constructor. */ constructor() { super(); - if ((this.constructor).__ownerDocument__) { - this.#ownerDocument = (this.constructor).__ownerDocument__; + this.ownerDocument = (this.constructor).__ownerDocument__; } } @@ -84,13 +83,6 @@ export default class Node extends EventTarget implements INode { return this.constructor.name; } - /** - * Returns owner document. - */ - public get ownerDocument(): IDocument { - return this.#ownerDocument; - } - /** * Get child nodes. * @@ -427,6 +419,9 @@ export default class Node extends EventTarget implements INode { (this.isConnected) = isConnected; if (!isConnected) { + if (!this.ownerDocument) { + debugger; + } if (this.ownerDocument['__activeElement__'] === this) { this.ownerDocument['__activeElement__'] = null; } diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index ba63f28a6..3464ffb12 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -1,8 +1,8 @@ import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; -import Document from '../nodes/document/Document.js'; -import HTMLDocument from '../nodes/html-document/HTMLDocument.js'; -import XMLDocument from '../nodes/xml-document/XMLDocument.js'; -import SVGDocument from '../nodes/svg-document/SVGDocument.js'; +import DocumentImplementation from '../nodes/document/Document.js'; +import HTMLDocumentImplementation from '../nodes/html-document/HTMLDocument.js'; +import XMLDocumentImplementation from '../nodes/xml-document/XMLDocument.js'; +import SVGDocumentImplementation from '../nodes/svg-document/SVGDocument.js'; import Node from '../nodes/node/Node.js'; import NodeFilter from '../tree-walker/NodeFilter.js'; import Text from '../nodes/text/Text.js'; @@ -16,7 +16,7 @@ import HTMLUnknownElement from '../nodes/html-unknown-element/HTMLUnknownElement import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; import HTMLSelectElement from '../nodes/html-select-element/HTMLSelectElement.js'; import HTMLTextAreaElement from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; -import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement.js'; +import HTMLLinkElementImplementation from '../nodes/html-link-element/HTMLLinkElement.js'; import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement.js'; import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement.js'; import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement.js'; @@ -25,12 +25,12 @@ import HTMLMediaElement from '../nodes/html-media-element/HTMLMediaElement.js'; import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement.js'; import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement.js'; import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement.js'; -import HTMLIFrameElement from '../nodes/html-iframe-element/HTMLIFrameElement.js'; +import HTMLIFrameElementImplementation from '../nodes/html-iframe-element/HTMLIFrameElement.js'; import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement.js'; import SVGSVGElement from '../nodes/svg-element/SVGSVGElement.js'; import SVGElement from '../nodes/svg-element/SVGElement.js'; import SVGGraphicsElement from '../nodes/svg-element/SVGGraphicsElement.js'; -import HTMLScriptElement from '../nodes/html-script-element/HTMLScriptElement.js'; +import HTMLScriptElementImplementation from '../nodes/html-script-element/HTMLScriptElement.js'; import HTMLImageElement from '../nodes/html-image-element/HTMLImageElement.js'; import CharacterData from '../nodes/character-data/CharacterData.js'; import DocumentType from '../nodes/document-type/DocumentType.js'; @@ -81,7 +81,6 @@ import ErrorEvent from '../event/events/ErrorEvent.js'; import StorageEvent from '../event/events/StorageEvent.js'; import SubmitEvent from '../event/events/SubmitEvent.js'; import Screen from '../screen/Screen.js'; -import Response from '../fetch/Response.js'; import IResponse from '../fetch/types/IResponse.js'; import IRequestInit from '../fetch/types/IRequestInit.js'; import Storage from '../storage/Storage.js'; @@ -126,15 +125,7 @@ import Clipboard from '../clipboard/Clipboard.js'; import ClipboardItem from '../clipboard/ClipboardItem.js'; import ClipboardEvent from '../event/events/ClipboardEvent.js'; import Headers from '../fetch/Headers.js'; -import WindowClassFactory from './WindowClassFactory.js'; -import Audio from '../nodes/html-audio-element/Audio.js'; -import Image from '../nodes/html-image-element/Image.js'; -import DocumentFragment from '../nodes/document-fragment/DocumentFragment.js'; -import DOMParser from '../dom-parser/DOMParser.js'; -import FileReader from '../file/FileReader.js'; -import Request from '../fetch/Request.js'; -import Range from '../range/Range.js'; -import XMLHttpRequest from '../xml-http-request/XMLHttpRequest.js'; +import XMLHttpRequestImplementation from '../xml-http-request/XMLHttpRequest.js'; import WindowBrowserSettingsReader from './WindowBrowserSettingsReader.js'; import DocumentReadyStateManager from '../nodes/document/DocumentReadyStateManager.js'; import DocumentReadyStateEnum from '../nodes/document/DocumentReadyStateEnum.js'; @@ -149,6 +140,14 @@ import IResponseInit from '../fetch/types/IResponseInit.js'; import IRequestInfo from '../fetch/types/IRequestInfo.js'; import IBrowserWindow from './IBrowserWindow.js'; import BrowserErrorCapturingEnum from '../browser/enums/BrowserErrorCapturingEnum.js'; +import AudioImplementation from '../nodes/html-audio-element/Audio.js'; +import ImageImplementation from '../nodes/html-image-element/Image.js'; +import DocumentFragmentImplementation from '../nodes/document-fragment/DocumentFragment.js'; +import DOMParserImplementation from '../dom-parser/DOMParser.js'; +import FileReaderImplementation from '../file/FileReader.js'; +import RequestImplementation from '../fetch/Request.js'; +import ResponseImplementation from '../fetch/Response.js'; +import RangeImplementation from '../range/Range.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -164,97 +163,97 @@ const ORIGINAL_QUEUE_MICROTASK = queueMicrotask; */ export default class BrowserWindow extends EventTarget implements IBrowserWindow { // Nodes - public readonly Node: typeof Node; - public readonly Attr: typeof Attr; - public readonly SVGSVGElement: typeof SVGSVGElement; - public readonly SVGElement: typeof SVGElement; - public readonly SVGGraphicsElement: typeof SVGGraphicsElement; - public readonly Text: typeof Text; - public readonly Comment: typeof Comment; - public readonly ShadowRoot: typeof ShadowRoot; - public readonly ProcessingInstruction: typeof ProcessingInstruction; - public readonly Element: typeof Element; - public readonly CharacterData: typeof CharacterData; - public readonly Document: new () => Document; - public readonly HTMLDocument: new () => HTMLDocument; - public readonly XMLDocument: new () => XMLDocument; - public readonly SVGDocument: new () => SVGDocument; - public readonly DocumentType: typeof DocumentType; + public readonly Node: typeof Node = Node; + public readonly Attr: typeof Attr = Attr; + public readonly SVGSVGElement: typeof SVGSVGElement = SVGSVGElement; + public readonly SVGElement: typeof SVGElement = SVGElement; + public readonly SVGGraphicsElement: typeof SVGGraphicsElement = SVGGraphicsElement; + public readonly Text: typeof Text = Text; + public readonly Comment: typeof Comment = Comment; + public readonly ShadowRoot: typeof ShadowRoot = ShadowRoot; + public readonly ProcessingInstruction: typeof ProcessingInstruction = ProcessingInstruction; + public readonly Element: typeof Element = Element; + public readonly CharacterData: typeof CharacterData = CharacterData; + public readonly DocumentType: typeof DocumentType = DocumentType; + public readonly Document: new () => DocumentImplementation; + public readonly HTMLDocument: new () => HTMLDocumentImplementation; + public readonly XMLDocument: new () => XMLDocumentImplementation; + public readonly SVGDocument: new () => SVGDocumentImplementation; // Element classes - public readonly HTMLAnchorElement: typeof HTMLAnchorElement; - public readonly HTMLButtonElement: typeof HTMLButtonElement; - public readonly HTMLOptGroupElement: typeof HTMLOptGroupElement; - public readonly HTMLOptionElement: typeof HTMLOptionElement; - public readonly HTMLElement: typeof HTMLElement; - public readonly HTMLUnknownElement: typeof HTMLUnknownElement; - public readonly HTMLTemplateElement: typeof HTMLTemplateElement; - public readonly HTMLFormElement: typeof HTMLFormElement; - public readonly HTMLInputElement: typeof HTMLInputElement; - public readonly HTMLSelectElement: typeof HTMLSelectElement; - public readonly HTMLTextAreaElement: typeof HTMLTextAreaElement; - public readonly HTMLImageElement: typeof HTMLImageElement; - public readonly HTMLScriptElement: typeof HTMLScriptElement; - public readonly HTMLLinkElement: typeof HTMLLinkElement; - public readonly HTMLStyleElement: typeof HTMLStyleElement; - public readonly HTMLLabelElement: typeof HTMLLabelElement; - public readonly HTMLSlotElement: typeof HTMLSlotElement; - public readonly HTMLMetaElement: typeof HTMLMetaElement; - public readonly HTMLMediaElement: typeof HTMLMediaElement; - public readonly HTMLAudioElement: typeof HTMLAudioElement; - public readonly HTMLVideoElement: typeof HTMLVideoElement; - public readonly HTMLBaseElement: typeof HTMLBaseElement; - public readonly HTMLIFrameElement: typeof HTMLIFrameElement; - public readonly HTMLDialogElement: typeof HTMLDialogElement; + public readonly HTMLAnchorElement: typeof HTMLAnchorElement = HTMLAnchorElement; + public readonly HTMLButtonElement: typeof HTMLButtonElement = HTMLButtonElement; + public readonly HTMLOptGroupElement: typeof HTMLOptGroupElement = HTMLOptGroupElement; + public readonly HTMLOptionElement: typeof HTMLOptionElement = HTMLOptionElement; + public readonly HTMLElement: typeof HTMLElement = HTMLElement; + public readonly HTMLUnknownElement: typeof HTMLUnknownElement = HTMLUnknownElement; + public readonly HTMLTemplateElement: typeof HTMLTemplateElement = HTMLTemplateElement; + public readonly HTMLFormElement: typeof HTMLFormElement = HTMLFormElement; + public readonly HTMLInputElement: typeof HTMLInputElement = HTMLInputElement; + public readonly HTMLSelectElement: typeof HTMLSelectElement = HTMLSelectElement; + public readonly HTMLTextAreaElement: typeof HTMLTextAreaElement = HTMLTextAreaElement; + public readonly HTMLImageElement: typeof HTMLImageElement = HTMLImageElement; + public readonly HTMLStyleElement: typeof HTMLStyleElement = HTMLStyleElement; + public readonly HTMLLabelElement: typeof HTMLLabelElement = HTMLLabelElement; + public readonly HTMLSlotElement: typeof HTMLSlotElement = HTMLSlotElement; + public readonly HTMLMetaElement: typeof HTMLMetaElement = HTMLMetaElement; + public readonly HTMLMediaElement: typeof HTMLMediaElement = HTMLMediaElement; + public readonly HTMLAudioElement: typeof HTMLAudioElement = HTMLAudioElement; + public readonly HTMLVideoElement: typeof HTMLVideoElement = HTMLVideoElement; + public readonly HTMLBaseElement: typeof HTMLBaseElement = HTMLBaseElement; + public readonly HTMLDialogElement: typeof HTMLDialogElement = HTMLDialogElement; + public readonly HTMLScriptElement: typeof HTMLScriptElementImplementation; + public readonly HTMLLinkElement: typeof HTMLLinkElementImplementation; + public readonly HTMLIFrameElement: typeof HTMLIFrameElementImplementation; // Non-implemented element classes - public readonly HTMLHeadElement: typeof HTMLElement; - public readonly HTMLTitleElement: typeof HTMLElement; - public readonly HTMLBodyElement: typeof HTMLElement; - public readonly HTMLHeadingElement: typeof HTMLElement; - public readonly HTMLParagraphElement: typeof HTMLElement; - public readonly HTMLHRElement: typeof HTMLElement; - public readonly HTMLPreElement: typeof HTMLElement; - public readonly HTMLUListElement: typeof HTMLElement; - public readonly HTMLOListElement: typeof HTMLElement; - public readonly HTMLLIElement: typeof HTMLElement; - public readonly HTMLMenuElement: typeof HTMLElement; - public readonly HTMLDListElement: typeof HTMLElement; - public readonly HTMLDivElement: typeof HTMLElement; - public readonly HTMLAreaElement: typeof HTMLElement; - public readonly HTMLBRElement: typeof HTMLElement; - public readonly HTMLCanvasElement: typeof HTMLElement; - public readonly HTMLDataElement: typeof HTMLElement; - public readonly HTMLDataListElement: typeof HTMLElement; - public readonly HTMLDetailsElement: typeof HTMLElement; - public readonly HTMLDirectoryElement: typeof HTMLElement; - public readonly HTMLFieldSetElement: typeof HTMLElement; - public readonly HTMLFontElement: typeof HTMLElement; - public readonly HTMLHtmlElement: typeof HTMLElement; - public readonly HTMLLegendElement: typeof HTMLElement; - public readonly HTMLMapElement: typeof HTMLElement; - public readonly HTMLMarqueeElement: typeof HTMLElement; - public readonly HTMLMeterElement: typeof HTMLElement; - public readonly HTMLModElement: typeof HTMLElement; - public readonly HTMLOutputElement: typeof HTMLElement; - public readonly HTMLPictureElement: typeof HTMLElement; - public readonly HTMLProgressElement: typeof HTMLElement; - public readonly HTMLQuoteElement: typeof HTMLElement; - public readonly HTMLSourceElement: typeof HTMLElement; - public readonly HTMLSpanElement: typeof HTMLElement; - public readonly HTMLTableCaptionElement: typeof HTMLElement; - public readonly HTMLTableCellElement: typeof HTMLElement; - public readonly HTMLTableColElement: typeof HTMLElement; - public readonly HTMLTableElement: typeof HTMLElement; - public readonly HTMLTimeElement: typeof HTMLElement; - public readonly HTMLTableRowElement: typeof HTMLElement; - public readonly HTMLTableSectionElement: typeof HTMLElement; - public readonly HTMLFrameElement: typeof HTMLElement; - public readonly HTMLFrameSetElement: typeof HTMLElement; - public readonly HTMLEmbedElement: typeof HTMLElement; - public readonly HTMLObjectElement: typeof HTMLElement; - public readonly HTMLParamElement: typeof HTMLElement; - public readonly HTMLTrackElement: typeof HTMLElement; + public readonly HTMLHeadElement: typeof HTMLElement = HTMLElement; + public readonly HTMLTitleElement: typeof HTMLElement = HTMLElement; + public readonly HTMLBodyElement: typeof HTMLElement = HTMLElement; + public readonly HTMLHeadingElement: typeof HTMLElement = HTMLElement; + public readonly HTMLParagraphElement: typeof HTMLElement = HTMLElement; + public readonly HTMLHRElement: typeof HTMLElement = HTMLElement; + public readonly HTMLPreElement: typeof HTMLElement = HTMLElement; + public readonly HTMLUListElement: typeof HTMLElement = HTMLElement; + public readonly HTMLOListElement: typeof HTMLElement = HTMLElement; + public readonly HTMLLIElement: typeof HTMLElement = HTMLElement; + public readonly HTMLMenuElement: typeof HTMLElement = HTMLElement; + public readonly HTMLDListElement: typeof HTMLElement = HTMLElement; + public readonly HTMLDivElement: typeof HTMLElement = HTMLElement; + public readonly HTMLAreaElement: typeof HTMLElement = HTMLElement; + public readonly HTMLBRElement: typeof HTMLElement = HTMLElement; + public readonly HTMLCanvasElement: typeof HTMLElement = HTMLElement; + public readonly HTMLDataElement: typeof HTMLElement = HTMLElement; + public readonly HTMLDataListElement: typeof HTMLElement = HTMLElement; + public readonly HTMLDetailsElement: typeof HTMLElement = HTMLElement; + public readonly HTMLDirectoryElement: typeof HTMLElement = HTMLElement; + public readonly HTMLFieldSetElement: typeof HTMLElement = HTMLElement; + public readonly HTMLFontElement: typeof HTMLElement = HTMLElement; + public readonly HTMLHtmlElement: typeof HTMLElement = HTMLElement; + public readonly HTMLLegendElement: typeof HTMLElement = HTMLElement; + public readonly HTMLMapElement: typeof HTMLElement = HTMLElement; + public readonly HTMLMarqueeElement: typeof HTMLElement = HTMLElement; + public readonly HTMLMeterElement: typeof HTMLElement = HTMLElement; + public readonly HTMLModElement: typeof HTMLElement = HTMLElement; + public readonly HTMLOutputElement: typeof HTMLElement = HTMLElement; + public readonly HTMLPictureElement: typeof HTMLElement = HTMLElement; + public readonly HTMLProgressElement: typeof HTMLElement = HTMLElement; + public readonly HTMLQuoteElement: typeof HTMLElement = HTMLElement; + public readonly HTMLSourceElement: typeof HTMLElement = HTMLElement; + public readonly HTMLSpanElement: typeof HTMLElement = HTMLElement; + public readonly HTMLTableCaptionElement: typeof HTMLElement = HTMLElement; + public readonly HTMLTableCellElement: typeof HTMLElement = HTMLElement; + public readonly HTMLTableColElement: typeof HTMLElement = HTMLElement; + public readonly HTMLTableElement: typeof HTMLElement = HTMLElement; + public readonly HTMLTimeElement: typeof HTMLElement = HTMLElement; + public readonly HTMLTableRowElement: typeof HTMLElement = HTMLElement; + public readonly HTMLTableSectionElement: typeof HTMLElement = HTMLElement; + public readonly HTMLFrameElement: typeof HTMLElement = HTMLElement; + public readonly HTMLFrameSetElement: typeof HTMLElement = HTMLElement; + public readonly HTMLEmbedElement: typeof HTMLElement = HTMLElement; + public readonly HTMLObjectElement: typeof HTMLElement = HTMLElement; + public readonly HTMLParamElement: typeof HTMLElement = HTMLElement; + public readonly HTMLTrackElement: typeof HTMLElement = HTMLElement; // Events classes public readonly Event = Event; @@ -366,12 +365,12 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow public readonly RadioNodeList = RadioNodeList; public readonly ValidityState = ValidityState; public readonly Headers = Headers; - public readonly Request: new (input: IRequestInfo, init?: IRequestInit) => Request; + public readonly Request: new (input: IRequestInfo, init?: IRequestInit) => RequestImplementation; public readonly Response: { - new (body?: IResponseBody, init?: IResponseInit): Response; - redirect: (url: string, status?: number) => Response; - error: () => Response; - json: (data: object, init?: IResponseInit) => Response; + new (body?: IResponseBody, init?: IResponseInit): ResponseImplementation; + redirect: (url: string, status?: number) => ResponseImplementation; + error: () => ResponseImplementation; + json: (data: object, init?: IResponseInit) => ResponseImplementation; }; public readonly XMLHttpRequestUpload = XMLHttpRequestUpload; public readonly XMLHttpRequestEventTarget = XMLHttpRequestEventTarget; @@ -385,24 +384,25 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow public readonly PermissionStatus = PermissionStatus; public readonly Clipboard = Clipboard; public readonly ClipboardItem = ClipboardItem; - public readonly XMLHttpRequest: new () => XMLHttpRequest; - public readonly DOMParser: new () => DOMParser; - public readonly Range: new () => Range; - public readonly FileReader: new () => FileReader; - public readonly Image: typeof Image; - public readonly DocumentFragment: typeof DocumentFragment; - public readonly Audio: typeof Audio; + public readonly XMLHttpRequest: new () => XMLHttpRequestImplementation; + public readonly DOMParser: new () => DOMParserImplementation; + public readonly Range: new () => RangeImplementation; + public readonly FileReader: new () => FileReaderImplementation; + public readonly Image: typeof ImageImplementation; + public readonly DocumentFragment: typeof DocumentFragmentImplementation; + public readonly Audio: typeof AudioImplementation; // Events public onload: ((event: Event) => void) | null = null; public onerror: ((event: ErrorEvent) => void) | null = null; // Public properties. - public readonly document: Document; + public readonly document: DocumentImplementation; public readonly customElements: CustomElementRegistry; public readonly location: Location; public readonly history: History; public readonly navigator: Navigator; + public readonly console: Console; public readonly opener: IBrowserWindow | null = null; public readonly self: IBrowserWindow = this; public readonly top: IBrowserWindow = this; @@ -531,6 +531,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow this.sessionStorage = new Storage(); this.localStorage = new Storage(); this.location = new Location(this.#browserFrame, options?.url ?? 'about:blank'); + this.console = browserFrame.page.console; WindowBrowserSettingsReader.setSettings(this, this.#browserFrame.page.context.browser.settings); @@ -562,132 +563,115 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow } } - this.__setupVMContext__(); - - const classes = WindowClassFactory.getClasses({ - window: this, - browserFrame: this.#browserFrame - }); + const window = this; + const asyncTaskManager = this.#browserFrame.__asyncTaskManager__; - // Classes that require the window to be injected - this.Response = classes.Response; - this.Request = classes.Request; - this.Image = classes.Image; - this.DocumentFragment = classes.DocumentFragment; - this.FileReader = classes.FileReader; - this.DOMParser = classes.DOMParser; - this.XMLHttpRequest = classes.XMLHttpRequest; - this.Range = classes.Range; - this.Audio = classes.Audio; - - // Nodes - this.Node = classes.Node; - this.Attr = classes.Attr; - this.SVGSVGElement = classes.SVGSVGElement; - this.SVGElement = classes.SVGElement; - this.SVGGraphicsElement = classes.SVGGraphicsElement; - this.Text = classes.Text; - this.Comment = classes.Comment; - this.ShadowRoot = classes.ShadowRoot; - this.ProcessingInstruction = classes.ProcessingInstruction; - this.Element = classes.Element; - this.CharacterData = classes.CharacterData; - this.Document = classes.Document; - this.HTMLDocument = classes.HTMLDocument; - this.XMLDocument = classes.XMLDocument; - this.SVGDocument = classes.SVGDocument; - this.DocumentType = classes.DocumentType; + this.__setupVMContext__(); - // HTML Element classes - this.HTMLAnchorElement = classes.HTMLAnchorElement; - this.HTMLButtonElement = classes.HTMLButtonElement; - this.HTMLOptGroupElement = classes.HTMLOptGroupElement; - this.HTMLOptionElement = classes.HTMLOptionElement; - this.HTMLElement = classes.HTMLElement; - this.HTMLUnknownElement = classes.HTMLUnknownElement; - this.HTMLTemplateElement = classes.HTMLTemplateElement; - this.HTMLFormElement = classes.HTMLFormElement; - this.HTMLInputElement = classes.HTMLInputElement; - this.HTMLSelectElement = classes.HTMLSelectElement; - this.HTMLTextAreaElement = classes.HTMLTextAreaElement; - this.HTMLImageElement = classes.HTMLImageElement; - this.HTMLScriptElement = classes.HTMLScriptElement; - this.HTMLLinkElement = classes.HTMLLinkElement; - this.HTMLStyleElement = classes.HTMLStyleElement; - this.HTMLLabelElement = classes.HTMLLabelElement; - this.HTMLSlotElement = classes.HTMLSlotElement; - this.HTMLMetaElement = classes.HTMLMetaElement; - this.HTMLMediaElement = classes.HTMLMediaElement; - this.HTMLAudioElement = classes.HTMLAudioElement; - this.HTMLVideoElement = classes.HTMLVideoElement; - this.HTMLBaseElement = classes.HTMLBaseElement; - this.HTMLIFrameElement = classes.HTMLIFrameElement; - this.HTMLDialogElement = classes.HTMLDialogElement; + // Class overrides + // For classes that need to be bound to the correct context. - // Non-implemented HTML element classes - this.HTMLHeadElement = classes.HTMLElement; - this.HTMLTitleElement = classes.HTMLElement; - this.HTMLBodyElement = classes.HTMLElement; - this.HTMLHeadingElement = classes.HTMLElement; - this.HTMLParagraphElement = classes.HTMLElement; - this.HTMLHRElement = classes.HTMLElement; - this.HTMLPreElement = classes.HTMLElement; - this.HTMLUListElement = classes.HTMLElement; - this.HTMLOListElement = classes.HTMLElement; - this.HTMLLIElement = classes.HTMLElement; - this.HTMLMenuElement = classes.HTMLElement; - this.HTMLDListElement = classes.HTMLElement; - this.HTMLDivElement = classes.HTMLElement; - this.HTMLAreaElement = classes.HTMLElement; - this.HTMLBRElement = classes.HTMLElement; - this.HTMLCanvasElement = classes.HTMLElement; - this.HTMLDataElement = classes.HTMLElement; - this.HTMLDataListElement = classes.HTMLElement; - this.HTMLDetailsElement = classes.HTMLElement; - this.HTMLDirectoryElement = classes.HTMLElement; - this.HTMLFieldSetElement = classes.HTMLElement; - this.HTMLFontElement = classes.HTMLElement; - this.HTMLHtmlElement = classes.HTMLElement; - this.HTMLLegendElement = classes.HTMLElement; - this.HTMLMapElement = classes.HTMLElement; - this.HTMLMarqueeElement = classes.HTMLElement; - this.HTMLMeterElement = classes.HTMLElement; - this.HTMLModElement = classes.HTMLElement; - this.HTMLOutputElement = classes.HTMLElement; - this.HTMLPictureElement = classes.HTMLElement; - this.HTMLProgressElement = classes.HTMLElement; - this.HTMLQuoteElement = classes.HTMLElement; - this.HTMLSourceElement = classes.HTMLElement; - this.HTMLSpanElement = classes.HTMLElement; - this.HTMLTableCaptionElement = classes.HTMLElement; - this.HTMLTableCellElement = classes.HTMLElement; - this.HTMLTableColElement = classes.HTMLElement; - this.HTMLTableElement = classes.HTMLElement; - this.HTMLTimeElement = classes.HTMLElement; - this.HTMLTableRowElement = classes.HTMLElement; - this.HTMLTableSectionElement = classes.HTMLElement; - this.HTMLFrameElement = classes.HTMLElement; - this.HTMLFrameSetElement = classes.HTMLElement; - this.HTMLEmbedElement = classes.HTMLElement; - this.HTMLObjectElement = classes.HTMLElement; - this.HTMLParamElement = classes.HTMLElement; - this.HTMLTrackElement = classes.HTMLElement; + /* eslint-disable jsdoc/require-jsdoc */ + class Document extends DocumentImplementation { + constructor() { + super({ window, browserFrame }); + } + } + class HTMLDocument extends HTMLDocumentImplementation { + constructor() { + super({ window, browserFrame }); + } + } + class XMLDocument extends XMLDocumentImplementation { + constructor() { + super({ window, browserFrame }); + } + } + class SVGDocument extends SVGDocumentImplementation { + constructor() { + super({ window, browserFrame }); + } + } - // Document - this.document = new this.HTMLDocument(); + this.document = new HTMLDocument(); (this.document.defaultView) = this; - // Default document elements - const doctype = this.document.implementation.createDocumentType('html', '', ''); - const documentElement = this.document.createElement('html'); - const bodyElement = this.document.createElement('body'); - const headElement = this.document.createElement('head'); + class Audio extends AudioImplementation { + public static __ownerDocument__ = window.document; + } + class Image extends ImageImplementation { + public static __ownerDocument__ = window.document; + } + class DocumentFragment extends DocumentFragmentImplementation { + public static __ownerDocument__ = window.document; + } - this.document.appendChild(doctype); - this.document.appendChild(documentElement); + // Other Classes + class Request extends RequestImplementation { + constructor(input: IRequestInfo, init?: IRequestInit) { + super({ window, asyncTaskManager }, input, init); + } + } + class Response extends ResponseImplementation { + protected static __window__ = window; + constructor(body?: IResponseBody, init?: IResponseInit) { + super({ window, browserFrame }, body, init); + } + } + class XMLHttpRequest extends XMLHttpRequestImplementation { + constructor() { + super({ window, browserFrame }); + } + } + class FileReader extends FileReaderImplementation { + constructor() { + super(window); + } + } + class DOMParser extends DOMParserImplementation { + constructor() { + super(window); + } + } + class Range extends RangeImplementation { + constructor() { + super(window); + } + } + class HTMLScriptElement extends HTMLScriptElementImplementation { + constructor() { + super(browserFrame); + } + } + class HTMLLinkElement extends HTMLLinkElementImplementation { + constructor() { + super(browserFrame); + } + } + class HTMLIFrameElement extends HTMLIFrameElementImplementation { + constructor() { + super(browserFrame); + } + } + + /* eslint-enable jsdoc/require-jsdoc */ - documentElement.appendChild(headElement); - documentElement.appendChild(bodyElement); + this.Response = Response; + this.Request = Request; + this.Image = Image; + this.DocumentFragment = DocumentFragment; + this.FileReader = FileReader; + this.DOMParser = DOMParser; + this.XMLHttpRequest = XMLHttpRequest; + this.Range = Range; + this.Audio = Audio; + this.HTMLScriptElement = HTMLScriptElement; + this.HTMLLinkElement = HTMLLinkElement; + this.HTMLIFrameElement = HTMLIFrameElement; + this.Document = Document; + this.HTMLDocument = HTMLDocument; + this.XMLDocument = XMLDocument; + this.SVGDocument = SVGDocument; // Ready state manager this.__readyStateManager__.whenComplete().then(() => { @@ -697,15 +681,6 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow }); } - /** - * Returns the console. - * - * @returns Console. - */ - public get console(): Console { - return this.#browserFrame.page.console; - } - /** * The number of pixels that the document is currently scrolled horizontally. * @@ -854,7 +829,10 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow * Closes the window. */ public close(): void { - if (this.#browserFrame.page.mainFrame === this.#browserFrame) { + this.Audio.__ownerDocument__ = null; + this.Image.__ownerDocument__ = null; + this.DocumentFragment.__ownerDocument__ = null; + if (this.#browserFrame.page?.mainFrame === this.#browserFrame) { this.#browserFrame.page.close(); } } @@ -878,15 +856,16 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow * @returns Timeout ID. */ public setTimeout(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { + const settings = this.#browserFrame.page?.context?.browser?.settings; + const useTryCatch = + !settings || + !settings.disableErrorCapturing || + settings.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch; const id = this.#setTimeout(() => { - if ( - this.#browserFrame.page.context.browser.settings.disableErrorCapturing || - this.#browserFrame.page.context.browser.settings.errorCapturing !== - BrowserErrorCapturingEnum.tryAndCatch - ) { - callback(...args); - } else { + if (useTryCatch) { WindowErrorUtility.captureError(this, () => callback(...args)); + } else { + callback(...args); } this.#browserFrame.__asyncTaskManager__.endTimer(id); }, delay); @@ -913,19 +892,20 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow * @returns Interval ID. */ public setInterval(callback: Function, delay = 0, ...args: unknown[]): NodeJS.Timeout { + const settings = this.#browserFrame.page?.context?.browser?.settings; + const useTryCatch = + !settings || + !settings.disableErrorCapturing || + settings.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch; const id = this.#setInterval(() => { - if ( - this.#browserFrame.page.context.browser.settings.disableErrorCapturing || - this.#browserFrame.page.context.browser.settings.errorCapturing !== - BrowserErrorCapturingEnum.tryAndCatch - ) { - callback(...args); - } else { + if (useTryCatch) { WindowErrorUtility.captureError( this, () => callback(...args), () => this.clearInterval(id) ); + } else { + callback(...args); } }, delay); this.#browserFrame.__asyncTaskManager__.startTimer(id); @@ -949,15 +929,16 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow * @returns ID. */ public requestAnimationFrame(callback: (timestamp: number) => void): NodeJS.Immediate { + const settings = this.#browserFrame.page?.context?.browser?.settings; + const useTryCatch = + !settings || + !settings.disableErrorCapturing || + settings.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch; const id = global.setImmediate(() => { - if ( - this.#browserFrame.page.context.browser.settings.disableErrorCapturing || - this.#browserFrame.page.context.browser.settings.errorCapturing !== - BrowserErrorCapturingEnum.tryAndCatch - ) { - callback(this.performance.now()); - } else { + if (useTryCatch) { WindowErrorUtility.captureError(this, () => callback(this.performance.now())); + } else { + callback(this.performance.now()); } this.#browserFrame.__asyncTaskManager__.endImmediate(id); }); @@ -983,16 +964,17 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow public queueMicrotask(callback: Function): void { let isAborted = false; const taskId = this.#browserFrame.__asyncTaskManager__.startTask(() => (isAborted = true)); + const settings = this.#browserFrame.page?.context?.browser?.settings; + const useTryCatch = + !settings || + !settings.disableErrorCapturing || + settings.errorCapturing === BrowserErrorCapturingEnum.tryAndCatch; this.#queueMicrotask(() => { if (!isAborted) { - if ( - this.#browserFrame.page.context.browser.settings.disableErrorCapturing || - this.#browserFrame.page.context.browser.settings.errorCapturing !== - BrowserErrorCapturingEnum.tryAndCatch - ) { - callback(); - } else { + if (useTryCatch) { WindowErrorUtility.captureError(this, <() => unknown>callback); + } else { + callback(); } this.#browserFrame.__asyncTaskManager__.endTask(taskId); } diff --git a/packages/happy-dom/src/window/DetachedWindowAPI.ts b/packages/happy-dom/src/window/DetachedWindowAPI.ts index 899a33094..3fa867eb5 100644 --- a/packages/happy-dom/src/window/DetachedWindowAPI.ts +++ b/packages/happy-dom/src/window/DetachedWindowAPI.ts @@ -21,7 +21,6 @@ export default class DetachedWindowAPI { /** * Returns settings. * - * @deprecated Depreacted for security reasons and will be removed in the future. Use Browser API instead to access settings (e.g. new Browser()). * @returns Settings. */ public get settings(): IBrowserSettings { @@ -59,8 +58,8 @@ export default class DetachedWindowAPI { /** * Aborts all async tasks. */ - public abort(): void { - this.#browserFrame.abort(); + public abort(): Promise { + return this.#browserFrame.abort(); } /** @@ -68,14 +67,13 @@ export default class DetachedWindowAPI { * * @deprecated Use abort() instead. */ - public cancelAsync(): void { - this.abort(); + public cancelAsync(): Promise { + return this.abort(); } /** * Sets the URL without navigating the browser. * - * @deprecated Depreacted for security reasons and will be removed in the future. Use Browser API instead to change URL (e.g. new Browser()). * @param url URL. */ public setURL(url: string): void { diff --git a/packages/happy-dom/src/window/WindowClassFactory.ts b/packages/happy-dom/src/window/WindowClassFactory.ts deleted file mode 100644 index 000c37656..000000000 --- a/packages/happy-dom/src/window/WindowClassFactory.ts +++ /dev/null @@ -1,459 +0,0 @@ -import AudioImplementation from '../nodes/html-audio-element/Audio.js'; -import ImageImplementation from '../nodes/html-image-element/Image.js'; -import DocumentFragmentImplementation from '../nodes/document-fragment/DocumentFragment.js'; -import DOMParserImplementation from '../dom-parser/DOMParser.js'; -import FileReaderImplementation from '../file/FileReader.js'; -import RequestImplementation from '../fetch/Request.js'; -import ResponseImplementation from '../fetch/Response.js'; -import RangeImplementation from '../range/Range.js'; -import XMLHttpRequestImplementation from '../xml-http-request/XMLHttpRequest.js'; -import IBrowserWindow from './IBrowserWindow.js'; -import IDocument from '../nodes/document/IDocument.js'; -import HTMLElementImplementation from '../nodes/html-element/HTMLElement.js'; -import HTMLUnknownElementImplementation from '../nodes/html-unknown-element/HTMLUnknownElement.js'; -import HTMLTemplateElementImplementation from '../nodes/html-template-element/HTMLTemplateElement.js'; -import HTMLFormElementImplementation from '../nodes/html-form-element/HTMLFormElement.js'; -import HTMLInputElementImplementation from '../nodes/html-input-element/HTMLInputElement.js'; -import HTMLSelectElementImplementation from '../nodes/html-select-element/HTMLSelectElement.js'; -import HTMLTextAreaElementImplementation from '../nodes/html-text-area-element/HTMLTextAreaElement.js'; -import HTMLImageElementImplementation from '../nodes/html-image-element/HTMLImageElement.js'; -import HTMLScriptElementImplementation from '../nodes/html-script-element/HTMLScriptElement.js'; -import HTMLLinkElementImplementation from '../nodes/html-link-element/HTMLLinkElement.js'; -import HTMLStyleElementImplementation from '../nodes/html-style-element/HTMLStyleElement.js'; -import HTMLLabelElementImplementation from '../nodes/html-label-element/HTMLLabelElement.js'; -import HTMLSlotElementImplementation from '../nodes/html-slot-element/HTMLSlotElement.js'; -import HTMLMetaElementImplementation from '../nodes/html-meta-element/HTMLMetaElement.js'; -import HTMLMediaElementImplementation from '../nodes/html-media-element/HTMLMediaElement.js'; -import HTMLAudioElementImplementation from '../nodes/html-audio-element/HTMLAudioElement.js'; -import HTMLVideoElementImplementation from '../nodes/html-video-element/HTMLVideoElement.js'; -import HTMLBaseElementImplementation from '../nodes/html-base-element/HTMLBaseElement.js'; -import HTMLIFrameElementImplementation from '../nodes/html-iframe-element/HTMLIFrameElement.js'; -import HTMLDialogElementImplementation from '../nodes/html-dialog-element/HTMLDialogElement.js'; -import NodeImplementation from '../nodes/node/Node.js'; -import AttrImplementation from '../nodes/attr/Attr.js'; -import SVGSVGElementImplementation from '../nodes/svg-element/SVGSVGElement.js'; -import SVGElementImplementation from '../nodes/svg-element/SVGElement.js'; -import SVGGraphicsElementImplementation from '../nodes/svg-element/SVGGraphicsElement.js'; -import TextImplementation from '../nodes/text/Text.js'; -import CommentImplementation from '../nodes/comment/Comment.js'; -import ShadowRootImplementation from '../nodes/shadow-root/ShadowRoot.js'; -import ProcessingInstructionImplementation from '../nodes/processing-instruction/ProcessingInstruction.js'; -import ElementImplementation from '../nodes/element/Element.js'; -import CharacterDataImplementation from '../nodes/character-data/CharacterData.js'; -import DocumentImplementation from '../nodes/document/Document.js'; -import HTMLDocumentImplementation from '../nodes/html-document/HTMLDocument.js'; -import XMLDocumentImplementation from '../nodes/xml-document/XMLDocument.js'; -import SVGDocumentImplementation from '../nodes/svg-document/SVGDocument.js'; -import DocumentTypeImplementation from '../nodes/document-type/DocumentType.js'; -import HTMLAnchorElementImplementation from '../nodes/html-anchor-element/HTMLAnchorElement.js'; -import HTMLButtonElementImplementation from '../nodes/html-button-element/HTMLButtonElement.js'; -import HTMLOptGroupElementImplementation from '../nodes/html-opt-group-element/HTMLOptGroupElement.js'; -import HTMLOptionElementImplementation from '../nodes/html-option-element/HTMLOptionElement.js'; -import IBrowserFrame from '../browser/types/IBrowserFrame.js'; -import IRequestInfo from '../fetch/types/IRequestInfo.js'; -import IRequestInit from '../fetch/types/IRequestInit.js'; -import IResponseInit from '../fetch/types/IResponseInit.js'; -import IResponseBody from '../fetch/types/IResponseBody.js'; - -/** - * Some classes need to get access to the window object without having a reference to the window in the constructor. - * This factory will extend classes with a class that has a reference to the window. - */ -export default class WindowClassFactory { - /** - * Returns classes for the given window. - * - * @param properties Properties. - * @param properties.window Window. - * @param properties.browserFrame Browser frame. - * @returns Classes. - */ - public static getClasses(properties: { window: IBrowserWindow; browserFrame: IBrowserFrame }): { - // Nodes - Node: typeof NodeImplementation; - Attr: typeof AttrImplementation; - SVGSVGElement: typeof SVGSVGElementImplementation; - SVGElement: typeof SVGElementImplementation; - SVGGraphicsElement: typeof SVGGraphicsElementImplementation; - Text: typeof TextImplementation; - Comment: typeof CommentImplementation; - ShadowRoot: typeof ShadowRootImplementation; - ProcessingInstruction: typeof ProcessingInstructionImplementation; - Element: typeof ElementImplementation; - CharacterData: typeof CharacterDataImplementation; - Document: new () => DocumentImplementation; - HTMLDocument: new () => HTMLDocumentImplementation; - XMLDocument: new () => XMLDocumentImplementation; - SVGDocument: new () => SVGDocumentImplementation; - DocumentType: typeof DocumentTypeImplementation; - - // HTML Elements - HTMLAnchorElement: typeof HTMLAnchorElementImplementation; - HTMLButtonElement: typeof HTMLButtonElementImplementation; - HTMLOptGroupElement: typeof HTMLOptGroupElementImplementation; - HTMLOptionElement: typeof HTMLOptionElementImplementation; - HTMLElement: typeof HTMLElementImplementation; - HTMLUnknownElement: typeof HTMLUnknownElementImplementation; - HTMLTemplateElement: typeof HTMLTemplateElementImplementation; - HTMLFormElement: typeof HTMLFormElementImplementation; - HTMLInputElement: typeof HTMLInputElementImplementation; - HTMLSelectElement: typeof HTMLSelectElementImplementation; - HTMLTextAreaElement: typeof HTMLTextAreaElementImplementation; - HTMLImageElement: typeof HTMLImageElementImplementation; - HTMLScriptElement: typeof HTMLScriptElementImplementation; - HTMLLinkElement: typeof HTMLLinkElementImplementation; - HTMLStyleElement: typeof HTMLStyleElementImplementation; - HTMLLabelElement: typeof HTMLLabelElementImplementation; - HTMLSlotElement: typeof HTMLSlotElementImplementation; - HTMLMetaElement: typeof HTMLMetaElementImplementation; - HTMLMediaElement: typeof HTMLMediaElementImplementation; - HTMLAudioElement: typeof HTMLAudioElementImplementation; - HTMLVideoElement: typeof HTMLVideoElementImplementation; - HTMLBaseElement: typeof HTMLBaseElementImplementation; - HTMLIFrameElement: typeof HTMLIFrameElementImplementation; - HTMLDialogElement: typeof HTMLDialogElementImplementation; - - // Other Classes - Request: new (input: IRequestInfo, init?: IRequestInit) => RequestImplementation; - Response: { - new (body?: IResponseBody, init?: IResponseInit): ResponseImplementation; - redirect: (url: string, status?: number) => ResponseImplementation; - error: () => ResponseImplementation; - json: (data: object, init?: IResponseInit) => ResponseImplementation; - }; - XMLHttpRequest: new () => XMLHttpRequestImplementation; - Image: typeof ImageImplementation; - DocumentFragment: typeof DocumentFragmentImplementation; - FileReader: new () => FileReaderImplementation; - DOMParser: new () => DOMParserImplementation; - Range: new () => RangeImplementation; - Audio: typeof AudioImplementation; - } { - const window = properties.window; - const browserFrame = properties.browserFrame; - const asyncTaskManager = properties.browserFrame.__asyncTaskManager__; - - /* eslint-disable jsdoc/require-jsdoc */ - - // Nodes - class Node extends NodeImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class Attr extends AttrImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class SVGSVGElement extends SVGSVGElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class SVGElement extends SVGElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class SVGGraphicsElement extends SVGGraphicsElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class Text extends TextImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class Comment extends CommentImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class ShadowRoot extends ShadowRootImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class ProcessingInstruction extends ProcessingInstructionImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class Element extends ElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class CharacterData extends CharacterDataImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class Document extends DocumentImplementation { - constructor() { - super(properties); - } - } - - class HTMLDocument extends HTMLDocumentImplementation { - constructor() { - super(properties); - } - } - class XMLDocument extends XMLDocumentImplementation { - constructor() { - super(properties); - } - } - class SVGDocument extends SVGDocumentImplementation { - constructor() { - super(properties); - } - } - class DocumentType extends DocumentTypeImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - - // HTML Elements - class HTMLAnchorElement extends HTMLAnchorElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLButtonElement extends HTMLButtonElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLOptGroupElement extends HTMLOptGroupElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLOptionElement extends HTMLOptionElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class Audio extends AudioImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class Image extends ImageImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class DocumentFragment extends DocumentFragmentImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLElement extends HTMLElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLUnknownElement extends HTMLUnknownElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLTemplateElement extends HTMLTemplateElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLFormElement extends HTMLFormElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLInputElement extends HTMLInputElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLSelectElement extends HTMLSelectElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLTextAreaElement extends HTMLTextAreaElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLImageElement extends HTMLImageElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLScriptElement extends HTMLScriptElementImplementation { - constructor() { - super(properties.browserFrame); - } - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLLinkElement extends HTMLLinkElementImplementation { - constructor() { - super(properties.browserFrame); - } - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLStyleElement extends HTMLStyleElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLLabelElement extends HTMLLabelElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLSlotElement extends HTMLSlotElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLMetaElement extends HTMLMetaElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLMediaElement extends HTMLMediaElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLAudioElement extends HTMLAudioElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLVideoElement extends HTMLVideoElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLBaseElement extends HTMLBaseElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLIFrameElement extends HTMLIFrameElementImplementation { - constructor() { - super(properties.browserFrame); - } - public get ownerDocument(): IDocument { - return window.document; - } - } - class HTMLDialogElement extends HTMLDialogElementImplementation { - public get ownerDocument(): IDocument { - return window.document; - } - } - - // Other Classes - class Request extends RequestImplementation { - constructor(input: IRequestInfo, init?: IRequestInit) { - super({ window, asyncTaskManager }, input, init); - } - } - class Response extends ResponseImplementation { - protected static __window__ = window; - constructor(body?: IResponseBody, init?: IResponseInit) { - super({ window, browserFrame }, body, init); - } - } - class XMLHttpRequest extends XMLHttpRequestImplementation { - constructor() { - super(properties); - } - } - class FileReader extends FileReaderImplementation { - constructor() { - super(properties.window); - } - } - class DOMParser extends DOMParserImplementation { - constructor() { - super(properties.window); - } - } - class Range extends RangeImplementation { - constructor() { - super(properties.window); - } - } - - /* eslint-enable jsdoc/require-jsdoc */ - - return { - // Nodes - Node, - Attr, - SVGSVGElement, - SVGElement, - SVGGraphicsElement, - Text, - Comment, - ShadowRoot, - ProcessingInstruction, - Element, - CharacterData, - Document, - HTMLDocument, - XMLDocument, - SVGDocument, - DocumentType, - - // HTML Elements - HTMLAnchorElement, - HTMLButtonElement, - HTMLOptGroupElement, - HTMLOptionElement, - HTMLElement, - HTMLUnknownElement, - HTMLTemplateElement, - HTMLFormElement, - HTMLInputElement, - HTMLSelectElement, - HTMLTextAreaElement, - HTMLImageElement, - HTMLScriptElement, - HTMLLinkElement, - HTMLStyleElement, - HTMLLabelElement, - HTMLSlotElement, - HTMLMetaElement, - HTMLMediaElement, - HTMLAudioElement, - HTMLVideoElement, - HTMLBaseElement, - HTMLIFrameElement, - HTMLDialogElement, - - // Other Classes - Response, - Request, - Image, - DocumentFragment, - FileReader, - DOMParser, - XMLHttpRequest, - Range, - Audio - }; - } -} diff --git a/packages/happy-dom/test/browser/Browser.test.ts b/packages/happy-dom/test/browser/Browser.test.ts index 69a46474b..9e6e6a6e7 100644 --- a/packages/happy-dom/test/browser/Browser.test.ts +++ b/packages/happy-dom/test/browser/Browser.test.ts @@ -10,7 +10,7 @@ describe('Browser', () => { }); describe('get contexts()', () => { - it('Returns the contexts.', () => { + it('Returns the contexts.', async () => { const browser = new Browser(); expect(browser.contexts.length).toBe(1); expect(browser.contexts[0]).toBe(browser.defaultContext); @@ -20,12 +20,12 @@ describe('Browser', () => { expect(browser.contexts[0]).toBe(browser.defaultContext); expect(browser.contexts[1]).toBe(incognitoContext); - incognitoContext.close(); + await incognitoContext.close(); expect(browser.contexts.length).toBe(1); expect(browser.contexts[0]).toBe(browser.defaultContext); - browser.defaultContext.close(); + await browser.defaultContext.close(); expect(browser.contexts.length).toBe(0); }); @@ -71,9 +71,9 @@ describe('Browser', () => { expect(browser.contexts[0]).toBe(browser.defaultContext); }); - it('Throws an error if the browser has been closed.', () => { + it('Throws an error if the browser has been closed.', async () => { const browser = new Browser(); - browser.close(); + await browser.close(); expect(() => browser.defaultContext).toThrow( 'No default context. The browser has been closed.' ); @@ -81,17 +81,17 @@ describe('Browser', () => { }); describe('close()', () => { - it('Closes the browser.', () => { + it('Closes the browser.', async () => { const browser = new Browser(); const originalClose = browser.defaultContext.close; let isContextClosed = false; vi.spyOn(browser.defaultContext, 'close').mockImplementation(() => { isContextClosed = true; - originalClose.call(browser.defaultContext); + return originalClose.call(browser.defaultContext); }); - browser.close(); + await browser.close(); expect(browser.contexts.length).toBe(0); expect(isContextClosed).toBe(true); }); @@ -139,9 +139,9 @@ describe('Browser', () => { expect(browser.contexts[1]).toBe(context); }); - it('Throws an error if the browser has been closed.', () => { + it('Throws an error if the browser has been closed.', async () => { const browser = new Browser(); - browser.close(); + await browser.close(); expect(() => browser.newIncognitoContext()).toThrow( 'No default context. The browser has been closed.' ); @@ -158,9 +158,9 @@ describe('Browser', () => { expect(browser.contexts[0].pages[0]).toBe(page); }); - it('Throws an error if the browser has been closed.', () => { + it('Throws an error if the browser has been closed.', async () => { const browser = new Browser(); - browser.close(); + await browser.close(); expect(() => browser.newPage()).toThrow('No default context. The browser has been closed.'); }); diff --git a/packages/happy-dom/test/browser/BrowserContext.test.ts b/packages/happy-dom/test/browser/BrowserContext.test.ts index 7610a99bb..5db952d7d 100644 --- a/packages/happy-dom/test/browser/BrowserContext.test.ts +++ b/packages/happy-dom/test/browser/BrowserContext.test.ts @@ -25,7 +25,7 @@ describe('BrowserContext', () => { }); describe('close()', () => { - it('Closes the context.', () => { + it('Closes the context.', async () => { const browser = new Browser(); const context = browser.defaultContext; const page1 = context.newPage(); @@ -35,14 +35,14 @@ describe('BrowserContext', () => { let pagesClosed = 0; vi.spyOn(page1, 'close').mockImplementation(() => { pagesClosed++; - originalClose1.call(page1); + return originalClose1.call(page1); }); vi.spyOn(page2, 'close').mockImplementation(() => { pagesClosed++; - originalClose2.call(page2); + return originalClose2.call(page2); }); expect(browser.contexts.length).toBe(1); - context.close(); + await context.close(); expect(browser.contexts.length).toBe(0); expect(pagesClosed).toBe(2); }); diff --git a/packages/happy-dom/test/browser/BrowserPage.test.ts b/packages/happy-dom/test/browser/BrowserPage.test.ts index 1375bbb47..1a6efe3e3 100644 --- a/packages/happy-dom/test/browser/BrowserPage.test.ts +++ b/packages/happy-dom/test/browser/BrowserPage.test.ts @@ -103,14 +103,14 @@ describe('BrowserPage', () => { }); describe('close()', () => { - it('Closes the page.', () => { + it('Closes the page.', async () => { const browser = new Browser(); const page = browser.defaultContext.newPage(); const mainFrame = BrowserFrameFactory.newChildFrame(page.mainFrame); const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); - page.close(); + await page.close(); expect(browser.defaultContext.pages.length).toBe(0); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts index a016f01f7..d1e35b293 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts @@ -12,7 +12,7 @@ describe('DetachedBrowser', () => { }); describe('get contexts()', () => { - it('Returns the contexts.', () => { + it('Returns the contexts.', async () => { const browser = new DetachedBrowser(BrowserWindow); expect(browser.contexts.length).toBe(1); @@ -21,7 +21,7 @@ describe('DetachedBrowser', () => { expect(browser.contexts.length).toBe(1); expect(browser.contexts[0]).toBe(browser.defaultContext); - browser.defaultContext.close(); + await browser.defaultContext.close(); expect(browser.contexts.length).toBe(0); }); @@ -67,9 +67,9 @@ describe('DetachedBrowser', () => { expect(browser.contexts[0]).toBe(browser.defaultContext); }); - it('Throws an error if the browser has been closed.', () => { + it('Throws an error if the browser has been closed.', async () => { const browser = new DetachedBrowser(BrowserWindow); - browser.close(); + await browser.close(); expect(() => browser.defaultContext).toThrow( 'No default context. The browser has been closed.' ); @@ -77,7 +77,7 @@ describe('DetachedBrowser', () => { }); describe('close()', () => { - it('Closes the browser.', () => { + it('Closes the browser.', async () => { const browser = new DetachedBrowser(BrowserWindow); const originalClose = browser.defaultContext.close; let isContextClosed = false; @@ -86,10 +86,10 @@ describe('DetachedBrowser', () => { vi.spyOn(browser.defaultContext, 'close').mockImplementation(() => { isContextClosed = true; - originalClose.call(browser.defaultContext); + return originalClose.call(browser.defaultContext); }); - browser.close(); + await browser.close(); expect(browser.contexts.length).toBe(0); expect(isContextClosed).toBe(true); }); @@ -123,10 +123,10 @@ describe('DetachedBrowser', () => { }); describe('newIncognitoContext()', () => { - it('Throws an error as it is not possible to create a new incognito context inside a detached browser.', () => { + it('Throws an error as it is not possible to create a new incognito context inside a detached browser.', async () => { const browser = new DetachedBrowser(BrowserWindow); browser.defaultContext.pages[0].mainFrame.window = new Window(); - browser.close(); + await browser.close(); expect(() => browser.newIncognitoContext()).toThrow( 'Not possible to create a new context on a detached browser.' ); @@ -146,10 +146,10 @@ describe('DetachedBrowser', () => { expect(browser.contexts[0].pages[1]).toBe(page); }); - it('Throws an error if the browser has been closed.', () => { + it('Throws an error if the browser has been closed.', async () => { const browser = new DetachedBrowser(BrowserWindow); browser.defaultContext.pages[0].mainFrame.window = new Window(); - browser.close(); + await browser.close(); expect(() => browser.newPage()).toThrow('No default context. The browser has been closed.'); }); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts index aacfc2e54..6da6b21aa 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserContext.test.ts @@ -31,7 +31,7 @@ describe('DetachedBrowserContext', () => { }); describe('close()', () => { - it('Closes the context.', () => { + it('Closes the context.', async () => { const browser = new DetachedBrowser(BrowserWindow); browser.defaultContext.pages[0].mainFrame.window = new Window(); const context = browser.defaultContext; @@ -42,14 +42,14 @@ describe('DetachedBrowserContext', () => { let pagesClosed = 0; vi.spyOn(page1, 'close').mockImplementation(() => { pagesClosed++; - originalClose1.call(page1); + return originalClose1.call(page1); }); vi.spyOn(page2, 'close').mockImplementation(() => { pagesClosed++; - originalClose2.call(page2); + return originalClose2.call(page2); }); expect(browser.contexts.length).toBe(1); - context.close(); + await context.close(); expect(browser.contexts.length).toBe(0); expect(pagesClosed).toBe(2); }); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts index b45aeb020..6c9ada717 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts @@ -110,7 +110,7 @@ describe('DetachedBrowserPage', () => { }); describe('close()', () => { - it('Closes the page.', () => { + it('Closes the page.', async () => { const browser = new DetachedBrowser(BrowserWindow); browser.defaultContext.pages[0].mainFrame.window = new Window(); const page = browser.defaultContext.newPage(); @@ -118,7 +118,7 @@ describe('DetachedBrowserPage', () => { const frame1 = BrowserFrameFactory.newChildFrame(page.mainFrame); const frame2 = BrowserFrameFactory.newChildFrame(page.mainFrame); - page.close(); + await page.close(); // There is always one page in a detached browser context. expect(browser.defaultContext.pages.length).toBe(1); diff --git a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts index 03b37cdb9..bae7bbe0c 100644 --- a/packages/happy-dom/test/window/DetachedWindowAPI.test.ts +++ b/packages/happy-dom/test/window/DetachedWindowAPI.test.ts @@ -6,6 +6,7 @@ import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; import DetachedWindowAPI from '../../src/window/DetachedWindowAPI.js'; import VirtualConsolePrinter from '../../src/console/VirtualConsolePrinter.js'; import DefaultBrowserSettings from '../../src/browser/DefaultBrowserSettings.js'; +import '../types.d.js'; describe('DetachedWindowAPI', () => { let window: IWindow; @@ -185,11 +186,11 @@ describe('DetachedWindowAPI', () => { expect(tasksDone).toBe(0); - window.setTimeout(() => { + setTimeout(() => { expect(isFirstWhenAsyncCompleteCalled).toBe(true); expect(isSecondWhenAsyncCompleteCalled).toBe(true); resolve(null); - }, 1); + }, 10); }); }); }); @@ -199,6 +200,7 @@ describe('DetachedWindowAPI', () => { let isCalled = false; vi.spyOn(window.happyDOM, 'abort').mockImplementation(() => { isCalled = true; + return Promise.resolve(); }); window.happyDOM?.abort(); expect(isCalled).toBe(true); diff --git a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts index c660246f6..f3bc9b4c0 100644 --- a/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts +++ b/packages/happy-dom/test/xml-http-request/XMLHttpRequest.test.ts @@ -1170,7 +1170,7 @@ describe('XMLHttpRequest', () => { isLoadEndTriggered = true; }); - window.happyDOM?.abort(); + await window.happyDOM?.abort(); expect(isAbortTriggered).toBe(true); expect(isErrorTriggered).toBe(false); diff --git a/packages/integration-test/test/index.js b/packages/integration-test/test/index.js index d0889494c..4c354bfd1 100644 --- a/packages/integration-test/test/index.js +++ b/packages/integration-test/test/index.js @@ -3,5 +3,6 @@ await Promise.all([ import('./tests/XMLHttpRequest.test.js'), import('./tests/WindowGlobals.test.js'), import('./tests/BrowserFrameExceptionObserver.test.js'), + import('./tests/Browser.test.js'), import('./tests/CommonJS.test.cjs') ]); diff --git a/packages/integration-test/test/tests/Browser.test.js b/packages/integration-test/test/tests/Browser.test.js new file mode 100644 index 000000000..531ccc2af --- /dev/null +++ b/packages/integration-test/test/tests/Browser.test.js @@ -0,0 +1,27 @@ +import { describe, it, expect } from '../utilities/TestFunctions.js'; +import { Browser, BrowserErrorCapturingEnum } from 'happy-dom'; + +describe('Browser', () => { + it('Goes to a real page.', async () => { + const browser = new Browser({ + settings: { errorCapturing: BrowserErrorCapturingEnum.processLevel } + }); + const page = browser.newPage(); + + await page.goto('https://github.com/capricorn86'); + await page.whenComplete(); + + page.mainFrame.document.querySelector('a[href="/capricorn86/happy-dom"]').click(); + await page.whenComplete(); + + expect(page.mainFrame.url).toBe('https://github.com/capricorn86/happy-dom'); + expect( + page.mainFrame.document.title.startsWith('GitHub - capricorn86/happy-dom: Happy DOM') + ).toBe(true); + expect( + page.mainFrame.document.querySelector('a[href="/capricorn86/happy-dom"]').textContent.trim() + ).toBe('happy-dom'); + + await browser.close(); + }); +}); diff --git a/packages/integration-test/test/tests/BrowserFrameExceptionObserver.test.js b/packages/integration-test/test/tests/BrowserFrameExceptionObserver.test.js index 890b466f0..8b6a1f342 100644 --- a/packages/integration-test/test/tests/BrowserFrameExceptionObserver.test.js +++ b/packages/integration-test/test/tests/BrowserFrameExceptionObserver.test.js @@ -37,7 +37,7 @@ describe('BrowserFrameExceptionObserver', () => { expect(errorEvent.error.message).toBe('Test error'); expect(errorEvent.message).toBe('Test error'); - browser.close(); + await browser.close(); }); it('Observes uncaught exceptions.', async () => { @@ -75,7 +75,7 @@ describe('BrowserFrameExceptionObserver', () => { expect(errorEvent.error.message).toBe('Test error'); expect(errorEvent.message).toBe('Test error'); - browser.close(); + await browser.close(); }); }); diff --git a/packages/integration-test/test/utilities/TestFunctions.js b/packages/integration-test/test/utilities/TestFunctions.js index 253be4955..4cf33c519 100644 --- a/packages/integration-test/test/utilities/TestFunctions.js +++ b/packages/integration-test/test/utilities/TestFunctions.js @@ -26,8 +26,9 @@ export function run(description, callback) { timeout = setTimeout(async () => { let hasError = false; for (const test of tests) { - console.log(Chalk.blue(test.description)); + process.stdout.write(Chalk.blue(test.description)); let result = null; + const startTime = performance.now(); try { result = test.callback(); } catch (error) { @@ -42,7 +43,7 @@ export function run(description, callback) { hasError = true; hasTimedout = true; resolve(); - }, 2000); + }, 100000); result .then(() => { if (!hasTimedout) { @@ -56,6 +57,9 @@ export function run(description, callback) { }); }); } + process.stdout.write( + Chalk.blue(` (${Math.round((performance.now() - startTime) * 100) / 100}ms)\n`) + ); } if (hasError) { diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index b24cff803..5bce2c074 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -117,7 +117,7 @@ export default class HappyDOMEnvironment implements JestEnvironment { this.fakeTimers.dispose(); this.fakeTimersModern.dispose(); - ((this.global)).happyDOM.cancelAsync(); + ((this.global)).close(); this.global = null; this.moduleMocker = null; diff --git a/packages/jest-environment/test/javascript/JavaScript.test.ts b/packages/jest-environment/test/javascript/JavaScript.test.ts index bea3351e8..311b56ecf 100644 --- a/packages/jest-environment/test/javascript/JavaScript.test.ts +++ b/packages/jest-environment/test/javascript/JavaScript.test.ts @@ -14,6 +14,8 @@ describe('JavaScript', () => { }); it('Can perform a real fetch()', async () => { + location.href = 'http://localhost:3000/'; + const express = Express(); express.get('/get/json', (_req, res) => { @@ -22,7 +24,6 @@ describe('JavaScript', () => { }); const server = express.listen(3000); - const response = await fetch('http://localhost:3000/get/json'); server.close(); @@ -40,6 +41,8 @@ describe('JavaScript', () => { }); it('Can perform a real FormData post request using fetch()', async () => { + location.href = 'http://localhost:3000/'; + const express = Express(); express.post('/post/formdata', (req, res) => { @@ -54,7 +57,6 @@ describe('JavaScript', () => { }); const server = express.listen(3000); - const requestFormData = new FormData(); requestFormData.append('key1', 'value1'); @@ -87,6 +89,8 @@ describe('JavaScript', () => { }); it('Can perform a real asynchronous XMLHttpRequest request', (done) => { + location.href = 'http://localhost:3000/'; + const express = Express(); express.get('/get/json', (_req, res) => { @@ -95,7 +99,6 @@ describe('JavaScript', () => { }); const server = express.listen(3000); - const request = new XMLHttpRequest(); request.open('GET', 'http://localhost:3000/get/json', true); @@ -116,6 +119,8 @@ describe('JavaScript', () => { }); it('Can perform a real synchronous XMLHttpRequest request to Github.com', () => { + location.href = 'https://raw.githubusercontent.com/'; + const request = new XMLHttpRequest(); request.open( diff --git a/packages/jest-environment/test/testing-library/TestingLibrary.test.tsx b/packages/jest-environment/test/testing-library/TestingLibrary.test.tsx index 04e2aeeba..38e3216b6 100644 --- a/packages/jest-environment/test/testing-library/TestingLibrary.test.tsx +++ b/packages/jest-environment/test/testing-library/TestingLibrary.test.tsx @@ -39,7 +39,8 @@ describe('TestingLibrary', () => { render(); - await user.click(screen.getByRole('checkbox')); + const checkbox = screen.getByRole('checkbox'); + await user.click(checkbox); expect(changeHandler).toHaveBeenCalledTimes(1); }); diff --git a/turbo.json b/turbo.json index 47e8ba1b1..5c5dd920b 100644 --- a/turbo.json +++ b/turbo.json @@ -3,7 +3,7 @@ "pipeline": { "compile": { "dependsOn": ["^compile"], - "inputs": ["src/**", "src/tsconfig.json", "src/package.json"], + "inputs": ["src/**", "tsconfig.json", "package.json"], "outputs": ["lib/**", "cjs/**", "tmp/**"] }, "global-registrator#compile": { @@ -19,17 +19,19 @@ "dependsOn": ["happy-dom#compile"] }, "happy-dom#test": { + "dependsOn": ["^compile"], + "inputs": ["vitest.config.ts", "package.json"], "outputs": ["node_modules/vitest/**"] }, "global-registrator#test": { - "dependsOn": ["happy-dom#compile", "global-registrator#compile"], + "dependsOn": ["happy-dom#compile", "^compile"], "outputs": ["tmp/**"] }, "jest-environment#test": { - "dependsOn": ["happy-dom#compile", "jest-environment#compile"] + "dependsOn": ["happy-dom#compile", "^compile"] }, "integration-test#test": { - "dependsOn": ["happy-dom#compile", "integration-test#compile"] + "dependsOn": ["happy-dom#compile", "^compile"] }, "uncaught-exception-observer#test": { "dependsOn": ["happy-dom#compile", "uncaught-exception-observer#compile"], From e5518458e16bcd08e6e33e6267b6475626eb15e9 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 9 Jan 2024 00:19:56 +0100 Subject: [PATCH 51/63] #466@trivial: Continues on implementation. --- .../happy-dom/src/dom-parser/DOMParser.ts | 3 + .../happy-dom/src/nodes/document/Document.ts | 21 ++++-- packages/happy-dom/src/nodes/node/Node.ts | 2 +- .../happy-dom/src/window/BrowserWindow.ts | 67 +++++++++---------- .../DOMImplementation.test.ts | 2 +- .../test/window/BrowserWindow.test.ts | 1 + packages/happy-dom/test/window/Window.test.ts | 19 ++---- .../testing-library/TestingLibrary.test.tsx | 3 + 8 files changed, 64 insertions(+), 54 deletions(-) diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index 65d5aeb18..799ae806c 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -37,6 +37,9 @@ export default class DOMParser { const newDocument = this.#createDocument(mimeType); + newDocument['__childNodes__'].length = 0; + newDocument['__children__'].length = 0; + const root = XMLParser.parse(newDocument, string, { evaluateScripts: true }); let documentElement = null; let documentTypeNode = null; diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 1f2e50014..32cb76219 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -874,10 +874,7 @@ export default class Document extends Node implements IDocument { * @returns Document fragment. */ public createDocumentFragment(): IDocumentFragment { - this.__defaultView__.DocumentFragment.__ownerDocument__ = this; - const node = new this.__defaultView__.DocumentFragment(); - this.__defaultView__.DocumentFragment.__ownerDocument__ = null; - return node; + return new this.__defaultView__.DocumentFragment(); } /** @@ -957,8 +954,7 @@ export default class Document extends Node implements IDocument { throw new DOMException('Parameter 1 was not of type Node.'); } const clone = node.cloneNode(deep); - const document = this; - Object.defineProperty(clone, 'ownerDocument', { get: () => document }); + this.#importNode(clone); return clone; } @@ -1033,4 +1029,17 @@ export default class Document extends Node implements IDocument { processingInstruction.target = target; return processingInstruction; } + + /** + * Imports a node. + * + * @param node Node. + */ + #importNode(node: INode): void { + (node.ownerDocument) = this; + + for (const child of node['__childNodes__']) { + this.#importNode(child); + } + } } diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 7315b9677..6682dc99f 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -62,7 +62,7 @@ export default class Node extends EventTarget implements INode { public __textAreaNode__: INode = null; public __observers__: MutationListener[] = []; public readonly __childNodes__: INodeList = new NodeList(); - public readonly ownerDocument: IDocument | null = null; + public readonly ownerDocument: IDocument = null; /** * Constructor. diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index 3464ffb12..aedbed864 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -572,41 +572,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow // For classes that need to be bound to the correct context. /* eslint-disable jsdoc/require-jsdoc */ - class Document extends DocumentImplementation { - constructor() { - super({ window, browserFrame }); - } - } - class HTMLDocument extends HTMLDocumentImplementation { - constructor() { - super({ window, browserFrame }); - } - } - class XMLDocument extends XMLDocumentImplementation { - constructor() { - super({ window, browserFrame }); - } - } - class SVGDocument extends SVGDocumentImplementation { - constructor() { - super({ window, browserFrame }); - } - } - - this.document = new HTMLDocument(); - (this.document.defaultView) = this; - - class Audio extends AudioImplementation { - public static __ownerDocument__ = window.document; - } - class Image extends ImageImplementation { - public static __ownerDocument__ = window.document; - } - class DocumentFragment extends DocumentFragmentImplementation { - public static __ownerDocument__ = window.document; - } - // Other Classes class Request extends RequestImplementation { constructor(input: IRequestInfo, init?: IRequestInit) { super({ window, asyncTaskManager }, input, init); @@ -653,6 +619,30 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow super(browserFrame); } } + class Document extends DocumentImplementation { + constructor() { + super({ window, browserFrame }); + } + } + class HTMLDocument extends HTMLDocumentImplementation { + constructor() { + super({ window, browserFrame }); + } + } + class XMLDocument extends XMLDocumentImplementation { + constructor() { + super({ window, browserFrame }); + } + } + class SVGDocument extends SVGDocumentImplementation { + constructor() { + super({ window, browserFrame }); + } + } + + class Audio extends AudioImplementation {} + class Image extends ImageImplementation {} + class DocumentFragment extends DocumentFragmentImplementation {} /* eslint-enable jsdoc/require-jsdoc */ @@ -673,6 +663,15 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow this.XMLDocument = XMLDocument; this.SVGDocument = SVGDocument; + // Document + this.document = new HTMLDocument(); + (this.document.defaultView) = this; + + // Override owner document + this.Audio.__ownerDocument__ = this.document; + this.Image.__ownerDocument__ = this.document; + this.DocumentFragment.__ownerDocument__ = this.document; + // Ready state manager this.__readyStateManager__.whenComplete().then(() => { (this.document.readyState) = DocumentReadyStateEnum.complete; diff --git a/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts b/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts index 6e669648b..1b373fac5 100644 --- a/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts +++ b/packages/happy-dom/test/dom-implementation/DOMImplementation.test.ts @@ -14,7 +14,7 @@ describe('DOMImplementation', () => { describe('createDocument()', () => { it('Returns a new XMLDocument.', () => { const document = window.document.implementation.createDocument(); - expect(document instanceof XMLDocument).toBe(true); + expect(document instanceof HTMLDocument).toBe(true); expect(document.defaultView).toBe(null); }); }); diff --git a/packages/happy-dom/test/window/BrowserWindow.test.ts b/packages/happy-dom/test/window/BrowserWindow.test.ts index d9179c828..8efac6ba9 100644 --- a/packages/happy-dom/test/window/BrowserWindow.test.ts +++ b/packages/happy-dom/test/window/BrowserWindow.test.ts @@ -62,6 +62,7 @@ describe('BrowserWindow', () => { resetMockedModules(); vi.restoreAllMocks(); }); + describe('get happyDOM()', () => { it('Returns "undefined" for an attached browser.', () => { expect(browserFrame.window['happyDOM']).toBeUndefined(); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index a501c2414..cf20d6707 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -75,18 +75,13 @@ describe('Window', () => { const domParser2 = new window2.DOMParser(); const domParser3 = new window3.DOMParser(); - expect( - domParser1.parseFromString('', 'text/html').childNodes[0].ownerDocument === - window1.document - ).toBe(true); - expect( - domParser2.parseFromString('', 'text/html').childNodes[0].ownerDocument === - window2.document - ).toBe(true); - expect( - domParser3.parseFromString('', 'text/html').childNodes[0].ownerDocument === - window3.document - ).toBe(true); + const document1 = domParser1.parseFromString('', 'text/html'); + const document2 = domParser2.parseFromString('', 'text/html'); + const document3 = domParser3.parseFromString('', 'text/html'); + + expect(document1.childNodes[0].ownerDocument === document1).toBe(true); + expect(document2.childNodes[0].ownerDocument === document2).toBe(true); + expect(document3.childNodes[0].ownerDocument === document3).toBe(true); // Range const range1 = window1.document.createRange(); diff --git a/packages/jest-environment/test/testing-library/TestingLibrary.test.tsx b/packages/jest-environment/test/testing-library/TestingLibrary.test.tsx index 38e3216b6..761edd292 100644 --- a/packages/jest-environment/test/testing-library/TestingLibrary.test.tsx +++ b/packages/jest-environment/test/testing-library/TestingLibrary.test.tsx @@ -10,6 +10,7 @@ describe('TestingLibrary', () => { render( onChange(event.target.value)} />); await user.type(screen.getByRole('textbox'), 'hello'); + expect(onChange).toHaveBeenCalledWith('hello'); }); @@ -29,6 +30,7 @@ describe('TestingLibrary', () => { ); await user.click(screen.getByRole('button')); + expect(handleSubmit).toHaveBeenCalledTimes(1); expect(clickHandler).toHaveBeenCalledTimes(1); }); @@ -40,6 +42,7 @@ describe('TestingLibrary', () => { render(); const checkbox = screen.getByRole('checkbox'); + await user.click(checkbox); expect(changeHandler).toHaveBeenCalledTimes(1); From d4403e39529b7dddfc086f4fa226d8bbd4af5389 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 9 Jan 2024 00:57:26 +0100 Subject: [PATCH 52/63] #466@trivial: Continues on implementation. --- packages/happy-dom/bin/property-symbol.js | 100 ++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 packages/happy-dom/bin/property-symbol.js diff --git a/packages/happy-dom/bin/property-symbol.js b/packages/happy-dom/bin/property-symbol.js new file mode 100644 index 000000000..9caf6b8b8 --- /dev/null +++ b/packages/happy-dom/bin/property-symbol.js @@ -0,0 +1,100 @@ +/* eslint-disable no-console*/ +/* eslint-disable @typescript-eslint/no-var-requires*/ + +const Path = require('path'); +const FS = require('fs'); + +process.on('unhandledRejection', (reason) => { + console.error(reason); + process.exit(1); +}); + +main(); + +function getArguments() { + const args = { + dir: null + }; + + for (const arg of process.argv) { + if (arg.startsWith('--dir=')) { + args.dir = arg.split('=')[1]; + } + } + + return args; +} + +async function readDirectory(directory) { + const files = await FS.promises.readdir(directory); + const statsPromises = []; + let allFiles = []; + + for (const file of files) { + const filePath = Path.join(directory, file); + statsPromises.push( + FS.promises.stat(filePath).then((stats) => { + if (stats.isDirectory()) { + return readDirectory(filePath).then((files) => (allFiles = allFiles.concat(files))); + } + allFiles.push(filePath); + }) + ); + } + + await Promise.all(statsPromises); + + return allFiles; +} + +async function renameFiles(directory, files) { + const writePromises = []; + + const symbols = {}; + + for (const file of files) { + writePromises.push( + FS.promises.readFile(file).then((content) => { + const oldContent = content.toString(); + const regexp = /\__([a-zA-Z]+)__/gm; + let match; + while ((match = regexp.exec(oldContent)) !== null) { + symbols[match[1]] = true; + } + const newContent = oldContent + .replace( + /import.+;/gs, + `$0\nimport { PropertySymbol } from '${Path.join( + Path.relative(Path.dirname(path), directory), + 'PropertySymbol.js' + )}';` + ) + .replace(/\.__([a-zA-Z]+)__/gm, `[PropertySymbol.$1]`) + .replace(/\['__([a-zA-Z]+)__'\]/gm, `[PropertySymbol.$1]`) + .replace(/\__([a-zA-Z]+)__/gm, `[PropertySymbol.$1]`); + return FS.promises.writeFile(file, newContent); + }) + ); + } + + await Promise.all(writePromises); + + const keys = Object.keys(symbols); + + keys.sort(); + + const content = keys.map((key) => `export const ${key} = Symbol('${key}');`).join('\n'); + const path = Path.join(directory, 'PropertySymbol.ts'); + + await FS.promises.writeFile(path, content); +} + +async function main() { + const args = getArguments(); + if (!args.dir) { + throw new Error('Invalid arguments'); + } + const directory = Path.resolve(args.dir); + const files = await readDirectory(directory); + await renameFiles(directory, files); +} From f5de1bdadf6497b43bc9788f2a4a651de579593d Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 9 Jan 2024 01:07:11 +0100 Subject: [PATCH 53/63] #466@trivial: Use symbols instead of __ for internal properties. --- packages/happy-dom/bin/property-symbol.cjs | 105 +++++ packages/happy-dom/src/PropertySymbol.ts | 79 ++++ .../happy-dom/src/browser/BrowserFrame.ts | 19 +- .../detached-browser/DetachedBrowserFrame.ts | 19 +- .../src/browser/types/IBrowserFrame.ts | 5 +- .../browser/utilities/BrowserFrameFactory.ts | 7 +- .../utilities/BrowserFrameNavigator.ts | 13 +- packages/happy-dom/src/css/CSS.ts | 5 +- packages/happy-dom/src/css/CSSParser.ts | 3 +- .../AbstractCSSStyleDeclaration.ts | 19 +- .../CSSStyleDeclarationElementStyle.ts | 15 +- .../src/css/rules/CSSFontFaceRule.ts | 5 +- .../src/css/rules/CSSKeyframeRule.ts | 5 +- .../happy-dom/src/css/rules/CSSStyleRule.ts | 5 +- .../custom-element/CustomElementRegistry.ts | 23 +- .../dom-implementation/DOMImplementation.ts | 11 +- .../happy-dom/src/dom-parser/DOMParser.ts | 13 +- .../src/dom-token-list/DOMTokenList.ts | 5 +- packages/happy-dom/src/event/Event.ts | 29 +- packages/happy-dom/src/event/EventTarget.ts | 69 ++-- .../happy-dom/src/fetch/AbortController.ts | 3 +- packages/happy-dom/src/fetch/AbortSignal.ts | 3 +- packages/happy-dom/src/fetch/Fetch.ts | 39 +- packages/happy-dom/src/fetch/Headers.ts | 29 +- packages/happy-dom/src/fetch/Request.ts | 59 +-- packages/happy-dom/src/fetch/Response.ts | 53 +-- packages/happy-dom/src/fetch/SyncFetch.ts | 21 +- .../multipart/MultipartFormDataParser.ts | 3 +- .../src/fetch/utilities/FetchBodyUtility.ts | 3 +- .../utilities/FetchRequestHeaderUtility.ts | 21 +- .../utilities/FetchRequestReferrerUtility.ts | 5 +- .../FetchRequestValidationUtility.ts | 5 +- packages/happy-dom/src/file/Blob.ts | 17 +- packages/happy-dom/src/file/FileReader.ts | 3 +- packages/happy-dom/src/form-data/FormData.ts | 9 +- .../src/match-media/MediaQueryList.ts | 7 +- .../src/mutation-observer/MutationObserver.ts | 5 +- .../src/named-node-map/NamedNodeMap.ts | 35 +- .../src/nodes/character-data/CharacterData.ts | 25 +- .../src/nodes/child-node/ChildNodeUtility.ts | 7 +- .../document-fragment/DocumentFragment.ts | 21 +- .../happy-dom/src/nodes/document/Document.ts | 143 +++---- .../happy-dom/src/nodes/document/IDocument.ts | 3 +- .../happy-dom/src/nodes/element/Dataset.ts | 3 +- .../happy-dom/src/nodes/element/Element.ts | 81 ++-- .../src/nodes/element/ElementNamedNodeMap.ts | 87 ++--- .../src/nodes/element/ElementUtility.ts | 23 +- .../src/nodes/element/HTMLCollection.ts | 39 +- .../happy-dom/src/nodes/element/IElement.ts | 5 +- .../html-anchor-element/HTMLAnchorElement.ts | 113 +++--- .../HTMLAnchorElementNamedNodeMap.ts | 21 +- .../html-button-element/HTMLButtonElement.ts | 25 +- .../HTMLButtonElementNamedNodeMap.ts | 23 +- .../src/nodes/html-element/HTMLElement.ts | 23 +- .../html-element/HTMLElementNamedNodeMap.ts | 15 +- .../nodes/html-element/HTMLElementUtility.ts | 21 +- .../HTMLFormControlsCollection.ts | 51 +-- .../html-form-element/HTMLFormElement.ts | 17 +- .../html-iframe-element/HTMLIFrameElement.ts | 5 +- .../HTMLIFrameElementPageLoader.ts | 3 +- .../html-input-element/HTMLInputElement.ts | 71 ++-- .../HTMLInputElementNamedNodeMap.ts | 23 +- .../html-label-element/HTMLLabelElement.ts | 3 +- .../html-link-element/HTMLLinkElement.ts | 17 +- .../HTMLLinkElementNamedNodeMap.ts | 19 +- .../HTMLLinkElementStyleSheetLoader.ts | 11 +- .../html-option-element/HTMLOptionElement.ts | 35 +- .../HTMLOptionElementNamedNodeMap.ts | 23 +- .../html-script-element/HTMLScriptElement.ts | 23 +- .../HTMLScriptElementNamedNodeMap.ts | 5 +- .../HTMLScriptElementScriptLoader.ts | 21 +- .../html-select-element/HTMLSelectElement.ts | 49 +-- .../HTMLSelectElementNamedNodeMap.ts | 23 +- .../html-slot-element/HTMLSlotElement.ts | 7 +- .../html-style-element/HTMLStyleElement.ts | 11 +- .../HTMLTemplateElement.ts | 5 +- .../HTMLTextAreaElement.ts | 45 +-- .../HTMLTextAreaElementNamedNodeMap.ts | 23 +- .../HTMLUnknownElement.ts | 57 +-- packages/happy-dom/src/nodes/node/Node.ts | 121 +++--- .../happy-dom/src/nodes/node/NodeUtility.ts | 67 ++-- .../nodes/parent-node/ParentNodeUtility.ts | 15 +- .../src/nodes/shadow-root/ShadowRoot.ts | 7 +- .../src/nodes/svg-element/SVGElement.ts | 9 +- .../svg-element/SVGElementNamedNodeMap.ts | 15 +- packages/happy-dom/src/nodes/text/Text.ts | 23 +- .../src/query-selector/QuerySelector.ts | 17 +- .../src/query-selector/SelectorItem.ts | 5 +- packages/happy-dom/src/range/Range.ts | 365 +++++++++--------- packages/happy-dom/src/range/RangeUtility.ts | 3 +- packages/happy-dom/src/selection/Selection.ts | 95 ++--- .../happy-dom/src/tree-walker/TreeWalker.ts | 9 +- packages/happy-dom/src/url/URL.ts | 3 +- .../src/validity-state/ValidityState.ts | 3 +- .../happy-dom/src/window/BrowserWindow.ts | 53 +-- packages/happy-dom/src/window/GlobalWindow.ts | 3 +- .../src/window/WindowBrowserSettingsReader.ts | 11 +- .../src/window/WindowErrorUtility.ts | 3 +- .../src/xml-http-request/XMLHttpRequest.ts | 9 +- .../happy-dom/src/xml-parser/XMLParser.ts | 5 +- .../src/xml-serializer/XMLSerializer.ts | 13 +- 101 files changed, 1568 insertions(+), 1285 deletions(-) create mode 100644 packages/happy-dom/bin/property-symbol.cjs create mode 100644 packages/happy-dom/src/PropertySymbol.ts diff --git a/packages/happy-dom/bin/property-symbol.cjs b/packages/happy-dom/bin/property-symbol.cjs new file mode 100644 index 000000000..96405bde1 --- /dev/null +++ b/packages/happy-dom/bin/property-symbol.cjs @@ -0,0 +1,105 @@ +/* eslint-disable no-console*/ +/* eslint-disable @typescript-eslint/no-var-requires*/ + +const Path = require('path'); +const FS = require('fs'); + +process.on('unhandledRejection', (reason) => { + console.error(reason); + process.exit(1); +}); + +main(); + +function getArguments() { + const args = { + dir: null + }; + + for (const arg of process.argv) { + if (arg.startsWith('--dir=')) { + args.dir = arg.split('=')[1]; + } + } + + return args; +} + +async function readDirectory(directory) { + const files = await FS.promises.readdir(directory); + const statsPromises = []; + let allFiles = []; + + for (const file of files) { + const filePath = Path.join(directory, file); + statsPromises.push( + FS.promises.stat(filePath).then((stats) => { + if (stats.isDirectory()) { + return readDirectory(filePath).then((files) => (allFiles = allFiles.concat(files))); + } + allFiles.push(filePath); + }) + ); + } + + await Promise.all(statsPromises); + + return allFiles; +} + +async function renameFiles(directory, files) { + const writePromises = []; + + const symbols = {}; + + for (const file of files) { + writePromises.push( + FS.promises.readFile(file).then((content) => { + const oldContent = content.toString(); + const regexp = /\__([a-zA-Z]+)__/gm; + let match; + let hasSymbols = false; + while ((match = regexp.exec(oldContent)) !== null) { + symbols[match[1]] = true; + hasSymbols = true; + } + if (!hasSymbols) { + return; + } + const newContent = oldContent + .replace( + /(import.+;)/, + `$1\nimport * as PropertySymbol from '${Path.join( + Path.relative(Path.dirname(file), directory), + 'PropertySymbol.js' + )}';` + ) + .replace(/\.__([a-zA-Z]+)__/gm, `[PropertySymbol.$1]`) + .replace(/\['__([a-zA-Z]+)__'\]/gm, `[PropertySymbol.$1]`) + .replace(/\__([a-zA-Z]+)__/gm, `[PropertySymbol.$1]`); + return FS.promises.writeFile(file, newContent); + }) + ); + } + + await Promise.all(writePromises); + + const keys = Object.keys(symbols); + + keys.sort(); + + const content = keys.map((key) => `export const ${key} = Symbol('${key}');`).join('\n'); + const path = Path.join(directory, 'PropertySymbol.ts'); + + await FS.promises.writeFile(path, content); +} + +async function main() { + const args = getArguments(); + if (!args.dir) { + throw new Error('Invalid arguments'); + } + const directory = Path.resolve(args.dir); + const files = await readDirectory(directory); + await renameFiles(directory, files); +} diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts new file mode 100644 index 000000000..f1fe541d8 --- /dev/null +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -0,0 +1,79 @@ +export const abort = Symbol('abort'); +export const activeElement = Symbol('activeElement'); +export const appendFormControlItem = Symbol('appendFormControlItem'); +export const appendNamedItem = Symbol('appendNamedItem'); +export const asyncTaskManager = Symbol('asyncTaskManager'); +export const bodyBuffer = Symbol('bodyBuffer'); +export const buffer = Symbol('buffer'); +export const cacheID = Symbol('cacheID'); +export const cachedResponse = Symbol('cachedResponse'); +export const callbacks = Symbol('callbacks'); +export const captureEventListenerCount = Symbol('captureEventListenerCount'); +export const checked = Symbol('checked'); +export const childNodes = Symbol('childNodes'); +export const children = Symbol('children'); +export const classList = Symbol('classList'); +export const computedStyle = Symbol('computedStyle'); +export const connectToNode = Symbol('connectToNode'); +export const contentLength = Symbol('contentLength'); +export const contentType = Symbol('contentType'); +export const cssText = Symbol('cssText'); +export const currentScript = Symbol('currentScript'); +export const currentTarget = Symbol('currentTarget'); +export const data = Symbol('data'); +export const defaultView = Symbol('defaultView'); +export const dirtyness = Symbol('dirtyness'); +export const end = Symbol('end'); +export const entries = Symbol('entries'); +export const evaluateCSS = Symbol('evaluateCSS'); +export const evaluateScript = Symbol('evaluateScript'); +export const exceptionObserver = Symbol('exceptionObserver'); +export const formNode = Symbol('formNode'); +export const getAttributeName = Symbol('getAttributeName'); +export const happyDOMSettingsID = Symbol('happyDOMSettingsID'); +export const height = Symbol('height'); +export const immediatePropagationStopped = Symbol('immediatePropagationStopped'); +export const isFirstWrite = Symbol('isFirstWrite'); +export const isFirstWriteAfterOpen = Symbol('isFirstWriteAfterOpen'); +export const isInPassiveEventListener = Symbol('isInPassiveEventListener'); +export const isValidPropertyName = Symbol('isValidPropertyName'); +export const isValue = Symbol('isValue'); +export const listenerOptions = Symbol('listenerOptions'); +export const listeners = Symbol('listeners'); +export const namedItems = Symbol('namedItems'); +export const nextActiveElement = Symbol('nextActiveElement'); +export const observe = Symbol('observe'); +export const observedAttributes = Symbol('observedAttributes'); +export const observers = Symbol('observers'); +export const ownerDocument = Symbol('ownerDocument'); +export const ownerElement = Symbol('ownerElement'); +export const propagationStopped = Symbol('propagationStopped'); +export const readyStateManager = Symbol('readyStateManager'); +export const referrer = Symbol('referrer'); +export const registry = Symbol('registry'); +export const relList = Symbol('relList'); +export const removeFormControlItem = Symbol('removeFormControlItem'); +export const removeNamedItem = Symbol('removeNamedItem'); +export const removeNamedItemIndex = Symbol('removeNamedItemIndex'); +export const removeNamedItemWithoutConsequences = Symbol('removeNamedItemWithoutConsequences'); +export const resetSelection = Symbol('resetSelection'); +export const rootNode = Symbol('rootNode'); +export const selectNode = Symbol('selectNode'); +export const selectedness = Symbol('selectedness'); +export const selection = Symbol('selection'); +export const setNamedItemWithoutConsequences = Symbol('setNamedItemWithoutConsequences'); +export const setupVMContext = Symbol('setupVMContext'); +export const shadowRoot = Symbol('shadowRoot'); +export const start = Symbol('start'); +export const style = Symbol('style'); +export const styleSheet = Symbol('styleSheet'); +export const target = Symbol('target'); +export const textAreaNode = Symbol('textAreaNode'); +export const unobserve = Symbol('unobserve'); +export const updateIndices = Symbol('updateIndices'); +export const updateOptionItems = Symbol('updateOptionItems'); +export const url = Symbol('url'); +export const value = Symbol('value'); +export const width = Symbol('width'); +export const window = Symbol('window'); +export const windowResizeListener = Symbol('windowResizeListener'); \ No newline at end of file diff --git a/packages/happy-dom/src/browser/BrowserFrame.ts b/packages/happy-dom/src/browser/BrowserFrame.ts index 0c4580f3c..21ce6d41d 100644 --- a/packages/happy-dom/src/browser/BrowserFrame.ts +++ b/packages/happy-dom/src/browser/BrowserFrame.ts @@ -1,4 +1,5 @@ import BrowserPage from './BrowserPage.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import AsyncTaskManager from '../async-task-manager/AsyncTaskManager.js'; import IBrowserFrame from './types/IBrowserFrame.js'; import BrowserWindow from '../window/BrowserWindow.js'; @@ -23,8 +24,8 @@ export default class BrowserFrame implements IBrowserFrame { public readonly opener: BrowserFrame | null = null; public readonly page: BrowserPage; public readonly window: BrowserWindow; - public __asyncTaskManager__ = new AsyncTaskManager(); - public __exceptionObserver__: BrowserFrameExceptionObserver | null = null; + public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager(); + public [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null = null; /** * Constructor. @@ -37,8 +38,8 @@ export default class BrowserFrame implements IBrowserFrame { // Attach process level error capturing. if (page.context.browser.settings.errorCapturing === BrowserErrorCapturingEnum.processLevel) { - this.__exceptionObserver__ = new BrowserFrameExceptionObserver(); - this.__exceptionObserver__.observe(this); + this[PropertySymbol.exceptionObserver] = new BrowserFrameExceptionObserver(); + this[PropertySymbol.exceptionObserver].observe(this); } } @@ -57,8 +58,8 @@ export default class BrowserFrame implements IBrowserFrame { * @param content Content. */ public set content(content) { - this.window.document['__isFirstWrite__'] = true; - this.window.document['__isFirstWriteAfterOpen__'] = false; + this.window.document[PropertySymbol.isFirstWrite] = true; + this.window.document[PropertySymbol.isFirstWriteAfterOpen] = false; this.window.document.open(); this.window.document.write(content); } @@ -100,7 +101,7 @@ export default class BrowserFrame implements IBrowserFrame { */ public async whenComplete(): Promise { await Promise.all([ - this.__asyncTaskManager__.whenComplete(), + this[PropertySymbol.asyncTaskManager].whenComplete(), ...this.childFrames.map((frame) => frame.whenComplete()) ]); } @@ -110,12 +111,12 @@ export default class BrowserFrame implements IBrowserFrame { */ public abort(): Promise { if (!this.childFrames.length) { - return this.__asyncTaskManager__.abort(); + return this[PropertySymbol.asyncTaskManager].abort(); } return new Promise((resolve, reject) => { // Using Promise instead of async/await to prevent microtask Promise.all( - this.childFrames.map((frame) => frame.abort()).concat([this.__asyncTaskManager__.abort()]) + this.childFrames.map((frame) => frame.abort()).concat([this[PropertySymbol.asyncTaskManager].abort()]) ) .then(() => resolve()) .catch(reject); diff --git a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts index 240f784ca..c1c09a7b4 100644 --- a/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts +++ b/packages/happy-dom/src/browser/detached-browser/DetachedBrowserFrame.ts @@ -1,4 +1,5 @@ import DetachedBrowserPage from './DetachedBrowserPage.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; import IBrowserFrame from '../types/IBrowserFrame.js'; import Location from '../../location/Location.js'; @@ -24,8 +25,8 @@ export default class DetachedBrowserFrame implements IBrowserFrame { public readonly page: DetachedBrowserPage; // Needs to be injected from the outside when the browser frame is constructed. public window: IBrowserWindow; - public __asyncTaskManager__ = new AsyncTaskManager(); - public __exceptionObserver__: BrowserFrameExceptionObserver | null = null; + public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager(); + public [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null = null; /** * Constructor. @@ -41,8 +42,8 @@ export default class DetachedBrowserFrame implements IBrowserFrame { // Attach process level error capturing. if (page.context.browser.settings.errorCapturing === BrowserErrorCapturingEnum.processLevel) { - this.__exceptionObserver__ = new BrowserFrameExceptionObserver(); - this.__exceptionObserver__.observe(this); + this[PropertySymbol.exceptionObserver] = new BrowserFrameExceptionObserver(); + this[PropertySymbol.exceptionObserver].observe(this); } } @@ -67,8 +68,8 @@ export default class DetachedBrowserFrame implements IBrowserFrame { if (!this.window) { throw new Error('The frame has been destroyed, the "window" property is not set.'); } - this.window.document['__isFirstWrite__'] = true; - this.window.document['__isFirstWriteAfterOpen__'] = false; + this.window.document[PropertySymbol.isFirstWrite] = true; + this.window.document[PropertySymbol.isFirstWriteAfterOpen] = false; this.window.document.open(); this.window.document.write(content); } @@ -116,7 +117,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame { */ public async whenComplete(): Promise { await Promise.all([ - this.__asyncTaskManager__.whenComplete(), + this[PropertySymbol.asyncTaskManager].whenComplete(), ...this.childFrames.map((frame) => frame.whenComplete()) ]); } @@ -126,12 +127,12 @@ export default class DetachedBrowserFrame implements IBrowserFrame { */ public abort(): Promise { if (!this.childFrames.length) { - return this.__asyncTaskManager__.abort(); + return this[PropertySymbol.asyncTaskManager].abort(); } return new Promise((resolve, reject) => { // Using Promise instead of async/await to prevent microtask Promise.all( - this.childFrames.map((frame) => frame.abort()).concat([this.__asyncTaskManager__.abort()]) + this.childFrames.map((frame) => frame.abort()).concat([this[PropertySymbol.asyncTaskManager].abort()]) ) .then(() => resolve()) .catch(reject); diff --git a/packages/happy-dom/src/browser/types/IBrowserFrame.ts b/packages/happy-dom/src/browser/types/IBrowserFrame.ts index 1a65d2025..98507c41a 100644 --- a/packages/happy-dom/src/browser/types/IBrowserFrame.ts +++ b/packages/happy-dom/src/browser/types/IBrowserFrame.ts @@ -1,4 +1,5 @@ import AsyncTaskManager from '../../async-task-manager/AsyncTaskManager.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IBrowserWindow from '../../window/IBrowserWindow.js'; import IDocument from '../../nodes/document/IDocument.js'; import IBrowserPage from './IBrowserPage.js'; @@ -19,8 +20,8 @@ export default interface IBrowserFrame { url: string; readonly parentFrame: IBrowserFrame | null; readonly opener: IBrowserFrame | null; - __asyncTaskManager__: AsyncTaskManager; - __exceptionObserver__: BrowserFrameExceptionObserver | null; + [PropertySymbol.asyncTaskManager]: AsyncTaskManager; + [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null; readonly page: IBrowserPage; /** diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts index 2132b6d3d..b9e228510 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameFactory.ts @@ -1,4 +1,5 @@ import IBrowserFrame from '../types/IBrowserFrame.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IBrowserWindow from '../../window/IBrowserWindow.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; import IBrowserPage from '../types/IBrowserPage.js'; @@ -50,21 +51,21 @@ export default class BrowserFrameFactory { (frame.window) = null; (frame.opener) = null; window.close(); - frame.__exceptionObserver__?.disconnect(); + frame[PropertySymbol.exceptionObserver]?.disconnect(); resolve(); return; } Promise.all(frame.childFrames.slice().map((childFrame) => this.destroyFrame(childFrame))) .then(() => { - return frame.__asyncTaskManager__.destroy().then(() => { + return frame[PropertySymbol.asyncTaskManager].destroy().then(() => { const window = frame.window; WindowBrowserSettingsReader.removeSettings(frame.window); (frame.page) = null; (frame.window) = null; (frame.opener) = null; window.close(); - frame.__exceptionObserver__?.disconnect(); + frame[PropertySymbol.exceptionObserver]?.disconnect(); resolve(); }); }) diff --git a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts index 009470e06..4e594730f 100644 --- a/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts +++ b/packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts @@ -1,4 +1,5 @@ import IBrowserFrame from '../types/IBrowserFrame.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import WindowBrowserSettingsReader from '../../window/WindowBrowserSettingsReader.js'; import IGoToOptions from '../types/IGoToOptions.js'; import IResponse from '../../fetch/types/IResponse.js'; @@ -44,9 +45,9 @@ export default class BrowserFrameNavigator { if (targetURL.protocol === 'javascript:') { if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { - const readyStateManager = (<{ __readyStateManager__: DocumentReadyStateManager }>( + const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>( (frame.window) - )).__readyStateManager__; + ))[PropertySymbol.readyStateManager]; readyStateManager.startTask(); @@ -94,8 +95,8 @@ export default class BrowserFrameNavigator { (frame.childFrames) = []; (frame.window.closed) = true; - frame.__asyncTaskManager__.destroy(); - frame.__asyncTaskManager__ = new AsyncTaskManager(); + frame[PropertySymbol.asyncTaskManager].destroy(); + frame[PropertySymbol.asyncTaskManager] = new AsyncTaskManager(); WindowBrowserSettingsReader.removeSettings(frame.window); (frame.window) = new windowClass(frame, { url: targetURL.href, width, height }); @@ -109,9 +110,9 @@ export default class BrowserFrameNavigator { return null; } - const readyStateManager = (<{ __readyStateManager__: DocumentReadyStateManager }>( + const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>( (frame.window) - )).__readyStateManager__; + ))[PropertySymbol.readyStateManager]; readyStateManager.startTask(); diff --git a/packages/happy-dom/src/css/CSS.ts b/packages/happy-dom/src/css/CSS.ts index f91274dfd..a9e0b5cc4 100644 --- a/packages/happy-dom/src/css/CSS.ts +++ b/packages/happy-dom/src/css/CSS.ts @@ -1,4 +1,5 @@ import CSSUnitValue from './CSSUnitValue.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import CSSUnits from './CSSUnits.js'; import CSSEscape from 'css.escape'; @@ -24,10 +25,10 @@ export default class CSS { * TODO: Always returns "true" for now, but it should probably be improved in the future. * * @param _condition Property name or condition. - * @param [__value__] Value when using property name. + * @param [[PropertySymbol.value]] Value when using property name. * @returns "true" if supported. */ - public supports(_condition: string, __value__?: string): boolean { + public supports(_condition: string, [PropertySymbol.value]?: string): boolean { return true; } diff --git a/packages/happy-dom/src/css/CSSParser.ts b/packages/happy-dom/src/css/CSSParser.ts index 71bc7b97b..e08e8a00d 100644 --- a/packages/happy-dom/src/css/CSSParser.ts +++ b/packages/happy-dom/src/css/CSSParser.ts @@ -1,4 +1,5 @@ import CSSRule from './CSSRule.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import CSSStyleSheet from './CSSStyleSheet.js'; import CSSStyleRule from './rules/CSSStyleRule.js'; import CSSKeyframeRule from './rules/CSSKeyframeRule.js'; @@ -131,7 +132,7 @@ export default class CSSParser { case CSSRule.FONT_FACE_RULE: case CSSRule.KEYFRAME_RULE: case CSSRule.STYLE_RULE: - (parentRule).__cssText__ = cssText; + (parentRule)[PropertySymbol.cssText] = cssText; break; } } diff --git a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts index 2128cb130..331e8feab 100644 --- a/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts +++ b/packages/happy-dom/src/css/declaration/AbstractCSSStyleDeclaration.ts @@ -1,4 +1,5 @@ import IElement from '../../nodes/element/IElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IAttr from '../../nodes/attr/IAttr.js'; import CSSRule from '../CSSRule.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; @@ -82,14 +83,14 @@ export default abstract class AbstractCSSStyleDeclaration { if (!styleAttribute) { styleAttribute = this.#ownerElement.ownerDocument.createAttribute('style'); - // We use "__setNamedItemWithoutConsequences__" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. - (this.#ownerElement.attributes).__setNamedItemWithoutConsequences__( + // We use "[PropertySymbol.setNamedItemWithoutConsequences]" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. + (this.#ownerElement.attributes)[PropertySymbol.setNamedItemWithoutConsequences]( styleAttribute ); } if (this.#ownerElement.isConnected) { - this.#ownerElement.ownerDocument['__cacheID__']++; + this.#ownerElement.ownerDocument[PropertySymbol.cacheID]++; } styleAttribute.value = style.toString(); @@ -140,14 +141,14 @@ export default abstract class AbstractCSSStyleDeclaration { if (!styleAttribute) { styleAttribute = this.#ownerElement.ownerDocument.createAttribute('style'); - // We use "__setNamedItemWithoutConsequences__" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. - (this.#ownerElement.attributes).__setNamedItemWithoutConsequences__( + // We use "[PropertySymbol.setNamedItemWithoutConsequences]" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. + (this.#ownerElement.attributes)[PropertySymbol.setNamedItemWithoutConsequences]( styleAttribute ); } if (this.#ownerElement.isConnected) { - this.#ownerElement.ownerDocument['__cacheID__']++; + this.#ownerElement.ownerDocument[PropertySymbol.cacheID]++; } const style = this.#elementStyle.getElementStyle(); @@ -180,14 +181,14 @@ export default abstract class AbstractCSSStyleDeclaration { const newCSSText = style.toString(); if (this.#ownerElement.isConnected) { - this.#ownerElement.ownerDocument['__cacheID__']++; + this.#ownerElement.ownerDocument[PropertySymbol.cacheID]++; } if (newCSSText) { (this.#ownerElement.attributes['style']).value = newCSSText; } else { - // We use "__removeNamedItemWithoutConsequences__" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. - (this.#ownerElement.attributes).__removeNamedItemWithoutConsequences__( + // We use "[PropertySymbol.removeNamedItemWithoutConsequences]" here to avoid triggering setting "Element.style.cssText" when setting the "style" attribute. + (this.#ownerElement.attributes)[PropertySymbol.removeNamedItemWithoutConsequences]( 'style' ); } diff --git a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts index 4ed52936c..433b97059 100644 --- a/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts +++ b/packages/happy-dom/src/css/declaration/element-style/CSSStyleDeclarationElementStyle.ts @@ -1,4 +1,5 @@ import IShadowRoot from '../../../nodes/shadow-root/IShadowRoot.js'; +import * as PropertySymbol from '../../../PropertySymbol.js'; import IElement from '../../../nodes/element/IElement.js'; import IDocument from '../../../nodes/document/IDocument.js'; import IHTMLStyleElement from '../../../nodes/html-style-element/IHTMLStyleElement.js'; @@ -99,12 +100,12 @@ export default class CSSStyleDeclarationElementStyle { if ( this.cache.propertyManager && - this.cache.documentCacheID === this.element.ownerDocument['__cacheID__'] + this.cache.documentCacheID === this.element.ownerDocument[PropertySymbol.cacheID] ) { return this.cache.propertyManager; } - this.cache.documentCacheID = this.element.ownerDocument['__cacheID__']; + this.cache.documentCacheID = this.element.ownerDocument[PropertySymbol.cacheID]; // Walks through all parent elements and stores them in an array with element and matching CSS text. while (styleAndElement.element) { @@ -276,7 +277,7 @@ export default class CSSStyleDeclarationElementStyle { return; } - const ownerWindow = this.element.ownerDocument.__defaultView__; + const ownerWindow = this.element.ownerDocument[PropertySymbol.defaultView]; for (const rule of options.cssRules) { if (rule.type === CSSRuleTypeEnum.styleRule) { @@ -285,7 +286,7 @@ export default class CSSStyleDeclarationElementStyle { if (selectorText.startsWith(':host')) { if (options.hostElement) { options.hostElement.cssTexts.push({ - cssText: (rule).__cssText__, + cssText: (rule)[PropertySymbol.cssText], priorityWeight: 0 }); } @@ -294,7 +295,7 @@ export default class CSSStyleDeclarationElementStyle { const matchResult = QuerySelector.match(element.element, selectorText); if (matchResult) { element.cssTexts.push({ - cssText: (rule).__cssText__, + cssText: (rule)[PropertySymbol.cssText], priorityWeight: matchResult.priorityWeight }); } @@ -355,7 +356,7 @@ export default class CSSStyleDeclarationElementStyle { parentSize: string | number | null; }): string { if ( - WindowBrowserSettingsReader.getSettings(this.element.ownerDocument.__defaultView__) + WindowBrowserSettingsReader.getSettings(this.element.ownerDocument[PropertySymbol.defaultView]) .disableComputedStyleRendering ) { return options.value; @@ -368,7 +369,7 @@ export default class CSSStyleDeclarationElementStyle { while ((match = regexp.exec(options.value)) !== null) { if (match[1] !== 'px') { const valueInPixels = CSSMeasurementConverter.toPixels({ - ownerWindow: this.element.ownerDocument.__defaultView__, + ownerWindow: this.element.ownerDocument[PropertySymbol.defaultView], value: match[0], rootFontSize: options.rootFontSize, parentFontSize: options.parentFontSize, diff --git a/packages/happy-dom/src/css/rules/CSSFontFaceRule.ts b/packages/happy-dom/src/css/rules/CSSFontFaceRule.ts index 62f5dc2db..3ed7013b5 100644 --- a/packages/happy-dom/src/css/rules/CSSFontFaceRule.ts +++ b/packages/happy-dom/src/css/rules/CSSFontFaceRule.ts @@ -1,4 +1,5 @@ import CSSRule from '../CSSRule.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import CSSStyleDeclaration from '../declaration/CSSStyleDeclaration.js'; /** @@ -6,7 +7,7 @@ import CSSStyleDeclaration from '../declaration/CSSStyleDeclaration.js'; */ export default class CSSFontFaceRule extends CSSRule { public readonly type = CSSRule.FONT_FACE_RULE; - public __cssText__ = ''; + public [PropertySymbol.cssText] = ''; #style: CSSStyleDeclaration = null; /** @@ -18,7 +19,7 @@ export default class CSSFontFaceRule extends CSSRule { if (!this.#style) { this.#style = new CSSStyleDeclaration(); (this.#style.parentRule) = this; - this.#style.cssText = this.__cssText__; + this.#style.cssText = this[PropertySymbol.cssText]; } return this.#style; } diff --git a/packages/happy-dom/src/css/rules/CSSKeyframeRule.ts b/packages/happy-dom/src/css/rules/CSSKeyframeRule.ts index ab254b20b..2b3a7b53f 100644 --- a/packages/happy-dom/src/css/rules/CSSKeyframeRule.ts +++ b/packages/happy-dom/src/css/rules/CSSKeyframeRule.ts @@ -1,4 +1,5 @@ import CSSRule from '../CSSRule.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import CSSStyleDeclaration from '../declaration/CSSStyleDeclaration.js'; /** @@ -7,7 +8,7 @@ import CSSStyleDeclaration from '../declaration/CSSStyleDeclaration.js'; export default class CSSKeyframeRule extends CSSRule { public readonly type = CSSRule.KEYFRAME_RULE; public readonly keyText: string; - public __cssText__ = ''; + public [PropertySymbol.cssText] = ''; #style: CSSStyleDeclaration = null; /** @@ -19,7 +20,7 @@ export default class CSSKeyframeRule extends CSSRule { if (!this.#style) { this.#style = new CSSStyleDeclaration(); (this.#style.parentRule) = this; - this.#style.cssText = this.__cssText__; + this.#style.cssText = this[PropertySymbol.cssText]; } return this.#style; } diff --git a/packages/happy-dom/src/css/rules/CSSStyleRule.ts b/packages/happy-dom/src/css/rules/CSSStyleRule.ts index 6f69d64fa..94d38da59 100644 --- a/packages/happy-dom/src/css/rules/CSSStyleRule.ts +++ b/packages/happy-dom/src/css/rules/CSSStyleRule.ts @@ -1,4 +1,5 @@ import CSSRule from '../CSSRule.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import CSSStyleDeclaration from '../declaration/CSSStyleDeclaration.js'; /** @@ -8,7 +9,7 @@ export default class CSSStyleRule extends CSSRule { public readonly type = CSSRule.STYLE_RULE; public readonly selectorText = ''; public readonly styleMap = new Map(); - public __cssText__ = ''; + public [PropertySymbol.cssText] = ''; #style: CSSStyleDeclaration = null; /** @@ -20,7 +21,7 @@ export default class CSSStyleRule extends CSSRule { if (!this.#style) { this.#style = new CSSStyleDeclaration(); (this.#style.parentRule) = this; - this.#style.cssText = this.__cssText__; + this.#style.cssText = this[PropertySymbol.cssText]; } return this.#style; } diff --git a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts index 7b35dc7d7..41f1358e4 100644 --- a/packages/happy-dom/src/custom-element/CustomElementRegistry.ts +++ b/packages/happy-dom/src/custom-element/CustomElementRegistry.ts @@ -1,4 +1,5 @@ import DOMException from '../exception/DOMException.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import HTMLElement from '../nodes/html-element/HTMLElement.js'; import Node from '../nodes/node/Node.js'; @@ -6,8 +7,8 @@ import Node from '../nodes/node/Node.js'; * Custom elements registry. */ export default class CustomElementRegistry { - public __registry__: { [k: string]: { elementClass: typeof HTMLElement; extends: string } } = {}; - public __callbacks__: { [k: string]: (() => void)[] } = {}; + public [PropertySymbol.registry]: { [k: string]: { elementClass: typeof HTMLElement; extends: string } } = {}; + public [PropertySymbol.callbacks]: { [k: string]: (() => void)[] } = {}; /** * Defines a custom element class. @@ -32,19 +33,19 @@ export default class CustomElementRegistry { ); } - this.__registry__[upperTagName] = { + this[PropertySymbol.registry][upperTagName] = { elementClass, extends: options && options.extends ? options.extends.toLowerCase() : null }; // ObservedAttributes should only be called once by CustomElementRegistry (see #117) if (elementClass.prototype.attributeChangedCallback) { - elementClass.__observedAttributes__ = elementClass.observedAttributes; + elementClass[PropertySymbol.observedAttributes] = elementClass.observedAttributes; } - if (this.__callbacks__[upperTagName]) { - const callbacks = this.__callbacks__[upperTagName]; - delete this.__callbacks__[upperTagName]; + if (this[PropertySymbol.callbacks][upperTagName]) { + const callbacks = this[PropertySymbol.callbacks][upperTagName]; + delete this[PropertySymbol.callbacks][upperTagName]; for (const callback of callbacks) { callback(); } @@ -59,8 +60,8 @@ export default class CustomElementRegistry { */ public get(tagName: string): typeof HTMLElement { const upperTagName = tagName.toUpperCase(); - return this.__registry__[upperTagName] - ? this.__registry__[upperTagName].elementClass + return this[PropertySymbol.registry][upperTagName] + ? this[PropertySymbol.registry][upperTagName].elementClass : undefined; } @@ -87,8 +88,8 @@ export default class CustomElementRegistry { return Promise.resolve(); } return new Promise((resolve) => { - this.__callbacks__[upperTagName] = this.__callbacks__[upperTagName] || []; - this.__callbacks__[upperTagName].push(resolve); + this[PropertySymbol.callbacks][upperTagName] = this[PropertySymbol.callbacks][upperTagName] || []; + this[PropertySymbol.callbacks][upperTagName].push(resolve); }); } } diff --git a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts index d9c03fa32..da46f9243 100644 --- a/packages/happy-dom/src/dom-implementation/DOMImplementation.ts +++ b/packages/happy-dom/src/dom-implementation/DOMImplementation.ts @@ -1,4 +1,5 @@ import DocumentType from '../nodes/document-type/DocumentType.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import IDocument from '../nodes/document/IDocument.js'; /** @@ -22,14 +23,14 @@ export default class DOMImplementation { * TODO: Not fully implemented. */ public createDocument(): IDocument { - return new this.#document.__defaultView__.HTMLDocument(); + return new this.#document[PropertySymbol.defaultView].HTMLDocument(); } /** * Creates and returns an HTML Document. */ public createHTMLDocument(): IDocument { - return new this.#document.__defaultView__.HTMLDocument(); + return new this.#document[PropertySymbol.defaultView].HTMLDocument(); } /** @@ -44,9 +45,9 @@ export default class DOMImplementation { publicId: string, systemId: string ): DocumentType { - this.#document.__defaultView__.DocumentType.__ownerDocument__ = this.#document; - const documentType = new this.#document.__defaultView__.DocumentType(); - this.#document.__defaultView__.DocumentType.__ownerDocument__ = null; + this.#document[PropertySymbol.defaultView].DocumentType[PropertySymbol.ownerDocument] = this.#document; + const documentType = new this.#document[PropertySymbol.defaultView].DocumentType(); + this.#document[PropertySymbol.defaultView].DocumentType[PropertySymbol.ownerDocument] = null; documentType.name = qualifiedName; documentType.publicId = publicId; documentType.systemId = systemId; diff --git a/packages/happy-dom/src/dom-parser/DOMParser.ts b/packages/happy-dom/src/dom-parser/DOMParser.ts index 799ae806c..05019b006 100644 --- a/packages/happy-dom/src/dom-parser/DOMParser.ts +++ b/packages/happy-dom/src/dom-parser/DOMParser.ts @@ -1,4 +1,5 @@ import IDocument from '../nodes/document/IDocument.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import XMLParser from '../xml-parser/XMLParser.js'; import Node from '../nodes/node/Node.js'; import DOMException from '../exception/DOMException.js'; @@ -37,14 +38,14 @@ export default class DOMParser { const newDocument = this.#createDocument(mimeType); - newDocument['__childNodes__'].length = 0; - newDocument['__children__'].length = 0; + newDocument[PropertySymbol.childNodes].length = 0; + newDocument[PropertySymbol.children].length = 0; const root = XMLParser.parse(newDocument, string, { evaluateScripts: true }); let documentElement = null; let documentTypeNode = null; - for (const node of root.__childNodes__) { + for (const node of root[PropertySymbol.childNodes]) { if (node['tagName'] === 'HTML') { documentElement = node; } else if (node.nodeType === Node.DOCUMENT_TYPE_NODE) { @@ -63,7 +64,7 @@ export default class DOMParser { newDocument.appendChild(documentElement); const body = newDocument.body; if (body) { - for (const child of root.__childNodes__.slice()) { + for (const child of root[PropertySymbol.childNodes].slice()) { body.appendChild(child); } } @@ -71,7 +72,7 @@ export default class DOMParser { switch (mimeType) { case 'image/svg+xml': { - for (const node of root.__childNodes__.slice()) { + for (const node of root[PropertySymbol.childNodes].slice()) { newDocument.appendChild(node); } } @@ -87,7 +88,7 @@ export default class DOMParser { documentElement.appendChild(bodyElement); newDocument.appendChild(documentElement); - for (const node of root.__childNodes__.slice()) { + for (const node of root[PropertySymbol.childNodes].slice()) { bodyElement.appendChild(node); } } diff --git a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts index e38bf91cd..04b40e89c 100644 --- a/packages/happy-dom/src/dom-token-list/DOMTokenList.ts +++ b/packages/happy-dom/src/dom-token-list/DOMTokenList.ts @@ -1,4 +1,5 @@ import Element from '../nodes/element/Element.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import IDOMTokenList from './IDOMTokenList.js'; /** @@ -21,7 +22,7 @@ export default class DOMTokenList implements IDOMTokenList { constructor(ownerElement: Element, attributeName) { this.#ownerElement = ownerElement; this.#attributeName = attributeName; - this.__updateIndices__(); + this[PropertySymbol.updateIndices](); } /** @@ -197,7 +198,7 @@ export default class DOMTokenList implements IDOMTokenList { /** * Updates indices. */ - public __updateIndices__(): void { + public [PropertySymbol.updateIndices](): void { const attr = this.#ownerElement.getAttribute(this.#attributeName); const list = attr ? Array.from(new Set(attr.split(' '))) : []; diff --git a/packages/happy-dom/src/event/Event.ts b/packages/happy-dom/src/event/Event.ts index db69df9c6..28d9476a7 100644 --- a/packages/happy-dom/src/event/Event.ts +++ b/packages/happy-dom/src/event/Event.ts @@ -1,4 +1,5 @@ import IEventInit from './IEventInit.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import INode from '../nodes/node/INode.js'; import IBrowserWindow from '../window/IBrowserWindow.js'; import IShadowRoot from '../nodes/shadow-root/IShadowRoot.js'; @@ -24,11 +25,11 @@ export default class Event { public AT_TARGET = EventPhaseEnum.atTarget; public BUBBLING_PHASE = EventPhaseEnum.bubbling; - public __immediatePropagationStopped__ = false; - public __propagationStopped__ = false; - public __target__: IEventTarget = null; - public __currentTarget__: IEventTarget = null; - public __isInPassiveEventListener__ = false; + public [PropertySymbol.immediatePropagationStopped] = false; + public [PropertySymbol.propagationStopped] = false; + public [PropertySymbol.target]: IEventTarget = null; + public [PropertySymbol.currentTarget]: IEventTarget = null; + public [PropertySymbol.isInPassiveEventListener] = false; /** * Constructor. @@ -50,7 +51,7 @@ export default class Event { * @returns Target. */ public get target(): IEventTarget { - return this.__target__; + return this[PropertySymbol.target]; } /** @@ -59,7 +60,7 @@ export default class Event { * @returns Target. */ public get currentTarget(): IEventTarget { - return this.__currentTarget__; + return this[PropertySymbol.currentTarget]; } /** @@ -68,7 +69,7 @@ export default class Event { * @returns "true" if propagation has been stopped. */ public get cancelBubble(): boolean { - return this.__propagationStopped__; + return this[PropertySymbol.propagationStopped]; } /** @@ -77,13 +78,13 @@ export default class Event { * @returns Composed path. */ public composedPath(): IEventTarget[] { - if (!this.__target__) { + if (!this[PropertySymbol.target]) { return []; } const composedPath = []; let eventTarget: INode | IShadowRoot | IBrowserWindow = ( - (this.__target__) + (this[PropertySymbol.target]) ); while (eventTarget) { @@ -98,7 +99,7 @@ export default class Event { ) { eventTarget = (eventTarget).host; } else if ((eventTarget).nodeType === NodeTypeEnum.documentNode) { - eventTarget = ((eventTarget)).__defaultView__; + eventTarget = ((eventTarget))[PropertySymbol.defaultView]; } else { break; } @@ -125,7 +126,7 @@ export default class Event { * Prevents default. */ public preventDefault(): void { - if (!this.__isInPassiveEventListener__) { + if (!this[PropertySymbol.isInPassiveEventListener]) { this.defaultPrevented = true; } } @@ -134,13 +135,13 @@ export default class Event { * Stops immediate propagation. */ public stopImmediatePropagation(): void { - this.__immediatePropagationStopped__ = true; + this[PropertySymbol.immediatePropagationStopped] = true; } /** * Stops propagation. */ public stopPropagation(): void { - this.__propagationStopped__ = true; + this[PropertySymbol.propagationStopped] = true; } } diff --git a/packages/happy-dom/src/event/EventTarget.ts b/packages/happy-dom/src/event/EventTarget.ts index b29c5b650..10da23cc0 100644 --- a/packages/happy-dom/src/event/EventTarget.ts +++ b/packages/happy-dom/src/event/EventTarget.ts @@ -1,4 +1,5 @@ import IEventListener from './IEventListener.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import Event from './Event.js'; import IEventTarget from './IEventTarget.js'; import IEventListenerOptions from './IEventListenerOptions.js'; @@ -14,10 +15,10 @@ import BrowserErrorCapturingEnum from '../browser/enums/BrowserErrorCapturingEnu * Handles events. */ export default abstract class EventTarget implements IEventTarget { - public readonly __listeners__: { + public readonly [PropertySymbol.listeners]: { [k: string]: (((event: Event) => void) | IEventListener)[]; } = {}; - public readonly __listenerOptions__: { + public readonly [PropertySymbol.listenerOptions]: { [k: string]: (IEventListenerOptions | null)[]; } = {}; @@ -43,21 +44,21 @@ export default abstract class EventTarget implements IEventTarget { ): void { const listenerOptions = typeof options === 'boolean' ? { capture: options } : options || null; - this.__listeners__[type] = this.__listeners__[type] || []; - this.__listenerOptions__[type] = this.__listenerOptions__[type] || []; - if (this.__listeners__[type].includes(listener)) { + this[PropertySymbol.listeners][type] = this[PropertySymbol.listeners][type] || []; + this[PropertySymbol.listenerOptions][type] = this[PropertySymbol.listenerOptions][type] || []; + if (this[PropertySymbol.listeners][type].includes(listener)) { return; } - this.__listeners__[type].push(listener); - this.__listenerOptions__[type].push(listenerOptions); + this[PropertySymbol.listeners][type].push(listener); + this[PropertySymbol.listenerOptions][type].push(listenerOptions); // Tracks the amount of capture event listeners to improve performance when they are not used. if (listenerOptions && listenerOptions.capture) { const window = this.#getWindow(); if (window) { - window['__captureEventListenerCount__'][type] = - window['__captureEventListenerCount__'][type] ?? 0; - window['__captureEventListenerCount__'][type]++; + window[PropertySymbol.captureEventListenerCount][type] = + window[PropertySymbol.captureEventListenerCount][type] ?? 0; + window[PropertySymbol.captureEventListenerCount][type]++; } } } @@ -72,22 +73,22 @@ export default abstract class EventTarget implements IEventTarget { type: string, listener: ((event: Event) => void) | IEventListener ): void { - if (this.__listeners__[type]) { - const index = this.__listeners__[type].indexOf(listener); + if (this[PropertySymbol.listeners][type]) { + const index = this[PropertySymbol.listeners][type].indexOf(listener); if (index !== -1) { // Tracks the amount of capture event listeners to improve performance when they are not used. if ( - this.__listenerOptions__[type][index] && - this.__listenerOptions__[type][index].capture + this[PropertySymbol.listenerOptions][type][index] && + this[PropertySymbol.listenerOptions][type][index].capture ) { const window = this.#getWindow(); - if (window && window['__captureEventListenerCount__'][type]) { - window['__captureEventListenerCount__'][type]--; + if (window && window[PropertySymbol.captureEventListenerCount][type]) { + window[PropertySymbol.captureEventListenerCount][type]--; } } - this.__listeners__[type].splice(index, 1); - this.__listenerOptions__[type].splice(index, 1); + this[PropertySymbol.listeners][type].splice(index, 1); + this[PropertySymbol.listenerOptions][type].splice(index, 1); } } } @@ -104,19 +105,19 @@ export default abstract class EventTarget implements IEventTarget { const window = this.#getWindow(); if (event.eventPhase === EventPhaseEnum.none) { - event.__target__ = this; + event[PropertySymbol.target] = this; const composedPath = event.composedPath(); // Capturing phase // We only need to iterate over the composed path if there are capture event listeners. - if (window && window['__captureEventListenerCount__'][event.type]) { + if (window && window[PropertySymbol.captureEventListenerCount][event.type]) { event.eventPhase = EventPhaseEnum.capturing; for (let i = composedPath.length - 1; i >= 0; i--) { composedPath[i].dispatchEvent(event); - if (event.__propagationStopped__ || event.__immediatePropagationStopped__) { + if (event[PropertySymbol.propagationStopped] || event[PropertySymbol.immediatePropagationStopped]) { break; } } @@ -130,14 +131,14 @@ export default abstract class EventTarget implements IEventTarget { // Bubbling phase if ( event.bubbles && - !event.__propagationStopped__ && - !event.__immediatePropagationStopped__ + !event[PropertySymbol.propagationStopped] && + !event[PropertySymbol.immediatePropagationStopped] ) { event.eventPhase = EventPhaseEnum.bubbling; for (let i = 1; i < composedPath.length; i++) { composedPath[i].dispatchEvent(event); - if (event.__propagationStopped__ || event.__immediatePropagationStopped__) { + if (event[PropertySymbol.propagationStopped] || event[PropertySymbol.immediatePropagationStopped]) { break; } } @@ -149,7 +150,7 @@ export default abstract class EventTarget implements IEventTarget { return !(event.cancelable && event.defaultPrevented); } - event.__currentTarget__ = this; + event[PropertySymbol.currentTarget] = this; const browserSettings = window ? WindowBrowserSettingsReader.getSettings(window) : null; @@ -171,10 +172,10 @@ export default abstract class EventTarget implements IEventTarget { } } - if (this.__listeners__[event.type]) { + if (this[PropertySymbol.listeners][event.type]) { // We need to clone the arrays because the listeners may remove themselves while we are iterating. - const listeners = this.__listeners__[event.type].slice(); - const listenerOptions = this.__listenerOptions__[event.type].slice(); + const listeners = this[PropertySymbol.listeners][event.type].slice(); + const listenerOptions = this[PropertySymbol.listenerOptions][event.type].slice(); for (let i = 0, max = listeners.length; i < max; i++) { const listener = listeners[i]; @@ -188,7 +189,7 @@ export default abstract class EventTarget implements IEventTarget { } if (options?.passive) { - event.__isInPassiveEventListener__ = true; + event[PropertySymbol.isInPassiveEventListener] = true; } // We can end up in a never ending loop if the listener for the error event on Window also throws an error. @@ -217,7 +218,7 @@ export default abstract class EventTarget implements IEventTarget { } } - event.__isInPassiveEventListener__ = false; + event[PropertySymbol.isInPassiveEventListener] = false; if (options?.once) { // At this time, listeners and listenersOptions are cloned arrays. When the original value is deleted, @@ -229,7 +230,7 @@ export default abstract class EventTarget implements IEventTarget { max--; } - if (event.__immediatePropagationStopped__) { + if (event[PropertySymbol.immediatePropagationStopped]) { return !(event.cancelable && event.defaultPrevented); } } @@ -273,10 +274,10 @@ export default abstract class EventTarget implements IEventTarget { */ #getWindow(): IBrowserWindow | null { if (((this)).ownerDocument) { - return ((this)).ownerDocument.__defaultView__; + return ((this)).ownerDocument[PropertySymbol.defaultView]; } - if (((this)).__defaultView__) { - return ((this)).__defaultView__; + if (((this))[PropertySymbol.defaultView]) { + return ((this))[PropertySymbol.defaultView]; } if (((this)).document) { return (this); diff --git a/packages/happy-dom/src/fetch/AbortController.ts b/packages/happy-dom/src/fetch/AbortController.ts index 3a73994b7..d4bf6a428 100644 --- a/packages/happy-dom/src/fetch/AbortController.ts +++ b/packages/happy-dom/src/fetch/AbortController.ts @@ -1,4 +1,5 @@ import AbortSignal from './AbortSignal.js'; +import * as PropertySymbol from '../PropertySymbol.js'; /** * AbortController. @@ -21,6 +22,6 @@ export default class AbortController { * @param [reason] Reason. */ public abort(reason?: string): void { - this.signal.__abort__(reason); + this.signal[PropertySymbol.abort](reason); } } diff --git a/packages/happy-dom/src/fetch/AbortSignal.ts b/packages/happy-dom/src/fetch/AbortSignal.ts index 269fb23f0..70e0cb502 100644 --- a/packages/happy-dom/src/fetch/AbortSignal.ts +++ b/packages/happy-dom/src/fetch/AbortSignal.ts @@ -1,4 +1,5 @@ import EventTarget from '../event/EventTarget.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import Event from '../event/Event.js'; /** @@ -23,7 +24,7 @@ export default class AbortSignal extends EventTarget { * * @param [reason] Reason. */ - public __abort__(reason?: string): void { + public [PropertySymbol.abort](reason?: string): void { if (this.aborted) { return; } diff --git a/packages/happy-dom/src/fetch/Fetch.ts b/packages/happy-dom/src/fetch/Fetch.ts index ee02dd339..38b5134ab 100644 --- a/packages/happy-dom/src/fetch/Fetch.ts +++ b/packages/happy-dom/src/fetch/Fetch.ts @@ -1,4 +1,5 @@ import IRequestInit from './types/IRequestInit.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import IResponse from './types/IResponse.js'; import IRequestInfo from './types/IRequestInfo.js'; import Headers from './Headers.js'; @@ -85,7 +86,7 @@ export default class Fetch { ? new options.browserFrame.window.Request(options.url, options.init) : options.url; if (options.contentType) { - (this.request.__contentType__) = options.contentType; + (this.request[PropertySymbol.contentType]) = options.contentType; } this.redirectCount = options.redirectCount ?? 0; this.disableCache = options.disableCache ?? false; @@ -105,7 +106,7 @@ export default class Fetch { throw new DOMException('The operation was aborted.', DOMExceptionNameEnum.abortError); } - if (this.request.__url__.protocol === 'data:') { + if (this.request[PropertySymbol.url].protocol === 'data:') { const result = DataURIParser.parse(this.request.url); this.response = new this.#window.Response(result.buffer, { headers: { 'Content-Type': result.type } @@ -114,7 +115,7 @@ export default class Fetch { } // Security check for "https" to "http" requests. - if (this.request.__url__.protocol === 'http:' && this.#window.location.protocol === 'https:') { + if (this.request[PropertySymbol.url].protocol === 'http:' && this.#window.location.protocol === 'https:') { throw new DOMException( `Mixed Content: The page at '${ this.#window.location.href @@ -204,7 +205,7 @@ export default class Fetch { headers: validateResponse.headers }); (response.url) = validateResponse.url; - response.__cachedResponse__ = cachedResponse; + response[PropertySymbol.cachedResponse] = cachedResponse; return response; } @@ -231,7 +232,7 @@ export default class Fetch { headers: cachedResponse.response.headers }); (response.url) = cachedResponse.response.url; - response.__cachedResponse__ = cachedResponse; + response[PropertySymbol.cachedResponse] = cachedResponse; return response; } @@ -244,7 +245,7 @@ export default class Fetch { private async compliesWithCrossOriginPolicy(): Promise { if ( this.disableCrossOriginPolicy || - !FetchCORSUtility.isCORS(this.#window.location, this.request.__url__) + !FetchCORSUtility.isCORS(this.#window.location, this.request[PropertySymbol.url]) ) { return true; } @@ -335,7 +336,7 @@ export default class Fetch { */ private sendRequest(): Promise { return new Promise((resolve, reject) => { - const taskID = this.#browserFrame.__asyncTaskManager__.startTask(() => + const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(() => this.onAsyncTaskManagerAbort() ); @@ -347,29 +348,29 @@ export default class Fetch { // We can end up here when closing down the browser frame and there is an ongoing request. // Therefore we need to check if browserFrame.page.context is still available. if (!this.disableCache && response instanceof Response && this.#browserFrame.page.context) { - response.__cachedResponse__ = this.#browserFrame.page.context.responseCache.add( + response[PropertySymbol.cachedResponse] = this.#browserFrame.page.context.responseCache.add( this.request, { ...response, headers: this.responseHeaders, - body: response.__buffer__, - waitingForBody: !response.__buffer__ && !!response.body + body: response[PropertySymbol.buffer], + waitingForBody: !response[PropertySymbol.buffer] && !!response.body } ); } - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); resolve(response); }; this.reject = (error: Error): void => { - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); reject(error); }; this.request.signal.addEventListener('abort', this.listeners.onSignalAbort); - const send = (this.request.__url__.protocol === 'https:' ? HTTPS : HTTP).request; + const send = (this.request[PropertySymbol.url].protocol === 'https:' ? HTTPS : HTTP).request; - this.nodeRequest = send(this.request.__url__.href, { + this.nodeRequest = send(this.request[PropertySymbol.url].href, { method: this.request.method, headers: FetchRequestHeaderUtility.getRequestHeaders({ browserFrame: this.#browserFrame, @@ -378,8 +379,8 @@ export default class Fetch { }), agent: false, rejectUnauthorized: true, - key: this.request.__url__.protocol === 'https:' ? FetchHTTPSCertificate.key : undefined, - cert: this.request.__url__.protocol === 'https:' ? FetchHTTPSCertificate.cert : undefined + key: this.request[PropertySymbol.url].protocol === 'https:' ? FetchHTTPSCertificate.key : undefined, + cert: this.request[PropertySymbol.url].protocol === 'https:' ? FetchHTTPSCertificate.cert : undefined }); this.nodeRequest.on('error', this.onError.bind(this)); @@ -493,7 +494,7 @@ export default class Fetch { this.nodeRequest.setTimeout(0); this.responseHeaders = FetchResponseHeaderUtility.parseResponseHeaders({ browserFrame: this.#browserFrame, - requestURL: this.request.__url__, + requestURL: this.request[PropertySymbol.url], rawHeaders: nodeResponse.rawHeaders }); @@ -688,7 +689,7 @@ export default class Fetch { referrerPolicy: this.request.referrerPolicy, credentials: this.request.credentials, headers, - body: this.request.__bodyBuffer__ + body: this.request[PropertySymbol.bodyBuffer] }; if ( @@ -726,7 +727,7 @@ export default class Fetch { url: locationURL, init: requestInit, redirectCount: this.redirectCount + 1, - contentType: !shouldBecomeGetRequest ? this.request.__contentType__ : undefined + contentType: !shouldBecomeGetRequest ? this.request[PropertySymbol.contentType] : undefined }); this.finalizeRequest(); diff --git a/packages/happy-dom/src/fetch/Headers.ts b/packages/happy-dom/src/fetch/Headers.ts index 493be92d2..0cc8ec207 100644 --- a/packages/happy-dom/src/fetch/Headers.ts +++ b/packages/happy-dom/src/fetch/Headers.ts @@ -1,4 +1,5 @@ import DOMException from '../exception/DOMException.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import IHeaders from './types/IHeaders.js'; import IHeadersInit from './types/IHeadersInit.js'; @@ -9,7 +10,7 @@ import IHeadersInit from './types/IHeadersInit.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/Headers */ export default class Headers implements IHeaders { - public __entries__: { [k: string]: { name: string; value: string } } = {}; + public [PropertySymbol.entries]: { [k: string]: { name: string; value: string } } = {}; /** * Constructor. @@ -19,7 +20,7 @@ export default class Headers implements IHeaders { constructor(init?: IHeadersInit) { if (init) { if (init instanceof Headers) { - this.__entries__ = JSON.parse(JSON.stringify(init.__entries__)); + this[PropertySymbol.entries] = JSON.parse(JSON.stringify(init[PropertySymbol.entries])); } else if (Array.isArray(init)) { for (const entry of init) { if (entry.length !== 2) { @@ -46,10 +47,10 @@ export default class Headers implements IHeaders { */ public append(name: string, value: string): void { const lowerName = name.toLowerCase(); - if (this.__entries__[lowerName]) { - this.__entries__[lowerName].value += `, ${value}`; + if (this[PropertySymbol.entries][lowerName]) { + this[PropertySymbol.entries][lowerName].value += `, ${value}`; } else { - this.__entries__[lowerName] = { + this[PropertySymbol.entries][lowerName] = { name, value }; @@ -62,7 +63,7 @@ export default class Headers implements IHeaders { * @param name Name. */ public delete(name: string): void { - delete this.__entries__[name.toLowerCase()]; + delete this[PropertySymbol.entries][name.toLowerCase()]; } /** @@ -72,7 +73,7 @@ export default class Headers implements IHeaders { * @returns Value. */ public get(name: string): string | null { - return this.__entries__[name.toLowerCase()]?.value || null; + return this[PropertySymbol.entries][name.toLowerCase()]?.value || null; } /** @@ -82,7 +83,7 @@ export default class Headers implements IHeaders { * @param value Value. */ public set(name: string, value: string): void { - this.__entries__[name.toLowerCase()] = { + this[PropertySymbol.entries][name.toLowerCase()] = { name, value }; @@ -95,7 +96,7 @@ export default class Headers implements IHeaders { * @returns "true" if the Headers object contains the key. */ public has(name: string): boolean { - return !!this.__entries__[name.toLowerCase()]; + return !!this[PropertySymbol.entries][name.toLowerCase()]; } /** @@ -104,7 +105,7 @@ export default class Headers implements IHeaders { * @param callback Callback. */ public forEach(callback: (name: string, value: string, thisArg: IHeaders) => void): void { - for (const header of Object.values(this.__entries__)) { + for (const header of Object.values(this[PropertySymbol.entries])) { callback(header.value, header.name, this); } } @@ -115,7 +116,7 @@ export default class Headers implements IHeaders { * @returns Iterator. */ public *keys(): IterableIterator { - for (const header of Object.values(this.__entries__)) { + for (const header of Object.values(this[PropertySymbol.entries])) { yield header.name; } } @@ -126,7 +127,7 @@ export default class Headers implements IHeaders { * @returns Iterator. */ public *values(): IterableIterator { - for (const header of Object.values(this.__entries__)) { + for (const header of Object.values(this[PropertySymbol.entries])) { yield header.value; } } @@ -137,7 +138,7 @@ export default class Headers implements IHeaders { * @returns Iterator. */ public *entries(): IterableIterator<[string, string]> { - for (const header of Object.values(this.__entries__)) { + for (const header of Object.values(this[PropertySymbol.entries])) { yield [header.name, header.value]; } } @@ -148,7 +149,7 @@ export default class Headers implements IHeaders { * @returns Iterator. */ public *[Symbol.iterator](): IterableIterator<[string, string]> { - for (const header of Object.values(this.__entries__)) { + for (const header of Object.values(this[PropertySymbol.entries])) { yield [header.name, header.value]; } } diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts index 871429565..39f7f035e 100644 --- a/packages/happy-dom/src/fetch/Request.ts +++ b/packages/happy-dom/src/fetch/Request.ts @@ -1,4 +1,5 @@ import IBlob from '../file/IBlob.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import IDocument from '../nodes/document/IDocument.js'; import IRequestInit from './types/IRequestInit.js'; import URL from '../url/URL.js'; @@ -43,11 +44,11 @@ export default class Request implements IRequest { public readonly credentials: IRequestCredentials; // Internal properties - public readonly __contentLength__: number | null = null; - public readonly __contentType__: string | null = null; - public __referrer__: '' | 'no-referrer' | 'client' | URL = 'client'; - public readonly __url__: URL; - public readonly __bodyBuffer__: Buffer | null; + public readonly [PropertySymbol.contentLength]: number | null = null; + public readonly [PropertySymbol.contentType]: string | null = null; + public [PropertySymbol.referrer]: '' | 'no-referrer' | 'client' | URL = 'client'; + public readonly [PropertySymbol.url]: URL; + public readonly [PropertySymbol.bodyBuffer]: Buffer | null; readonly #window: IBrowserWindow; readonly #asyncTaskManager: AsyncTaskManager; @@ -73,12 +74,12 @@ export default class Request implements IRequest { this.method = (init?.method || (input).method || 'GET').toUpperCase(); const { stream, buffer, contentType, contentLength } = FetchBodyUtility.getBodyStream( - input instanceof Request && (input.__bodyBuffer__ || input.body) - ? input.__bodyBuffer__ || FetchBodyUtility.cloneBodyStream(input) + input instanceof Request && (input[PropertySymbol.bodyBuffer] || input.body) + ? input[PropertySymbol.bodyBuffer] || FetchBodyUtility.cloneBodyStream(input) : init?.body ); - this.__bodyBuffer__ = buffer; + this[PropertySymbol.bodyBuffer] = buffer; this.body = stream; this.credentials = init?.credentials || (input).credentials || 'same-origin'; this.headers = new Headers(init?.headers || (input).headers || {}); @@ -86,18 +87,18 @@ export default class Request implements IRequest { FetchRequestHeaderUtility.removeForbiddenHeaders(this.headers); if (contentLength) { - this.__contentLength__ = contentLength; + this[PropertySymbol.contentLength] = contentLength; } else if (!this.body && (this.method === 'POST' || this.method === 'PUT')) { - this.__contentLength__ = 0; + this[PropertySymbol.contentLength] = 0; } if (contentType) { if (!this.headers.has('Content-Type')) { this.headers.set('Content-Type', contentType); } - this.__contentType__ = contentType; - } else if (input instanceof Request && input.__contentType__) { - this.__contentType__ = input.__contentType__; + this[PropertySymbol.contentType] = contentType; + } else if (input instanceof Request && input[PropertySymbol.contentType]) { + this[PropertySymbol.contentType] = input[PropertySymbol.contentType]; } this.redirect = init?.redirect || (input).redirect || 'follow'; @@ -105,7 +106,7 @@ export default class Request implements IRequest { (init?.referrerPolicy || (input).referrerPolicy || '').toLowerCase() ); this.signal = init?.signal || (input).signal || new AbortSignal(); - this.__referrer__ = FetchRequestReferrerUtility.getInitialReferrer( + this[PropertySymbol.referrer] = FetchRequestReferrerUtility.getInitialReferrer( injected.window, init?.referrer !== null && init?.referrer !== undefined ? init?.referrer @@ -113,13 +114,13 @@ export default class Request implements IRequest { ); if (input instanceof URL) { - this.__url__ = input; + this[PropertySymbol.url] = input; } else { try { if (input instanceof Request && input.url) { - this.__url__ = new URL(input.url, injected.window.location); + this[PropertySymbol.url] = new URL(input.url, injected.window.location); } else { - this.__url__ = new URL(input, injected.window.location); + this[PropertySymbol.url] = new URL(input, injected.window.location); } } catch (error) { throw new DOMException( @@ -137,7 +138,7 @@ export default class Request implements IRequest { FetchRequestValidationUtility.validateMethod(this); FetchRequestValidationUtility.validateBody(this); - FetchRequestValidationUtility.validateURL(this.__url__); + FetchRequestValidationUtility.validateURL(this[PropertySymbol.url]); FetchRequestValidationUtility.validateReferrerPolicy(this.referrerPolicy); FetchRequestValidationUtility.validateRedirect(this.redirect); } @@ -145,8 +146,8 @@ export default class Request implements IRequest { /** * Returns owner document. */ - protected get __ownerDocument__(): IDocument { - throw new Error('__ownerDocument__ needs to be implemented by sub-class.'); + protected get [PropertySymbol.ownerDocument](): IDocument { + throw new Error('[PropertySymbol.ownerDocument] needs to be implemented by sub-class.'); } /** @@ -155,15 +156,15 @@ export default class Request implements IRequest { * @returns Referrer. */ public get referrer(): string { - if (!this.__referrer__ || this.__referrer__ === 'no-referrer') { + if (!this[PropertySymbol.referrer] || this[PropertySymbol.referrer] === 'no-referrer') { return ''; } - if (this.__referrer__ === 'client') { + if (this[PropertySymbol.referrer] === 'client') { return 'about:client'; } - return this.__referrer__.toString(); + return this[PropertySymbol.referrer].toString(); } /** @@ -172,7 +173,7 @@ export default class Request implements IRequest { * @returns URL. */ public get url(): string { - return this.__url__.href; + return this[PropertySymbol.url].href; } /** @@ -199,7 +200,7 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this.#asyncTaskManager.startTask(() => this.signal.__abort__()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]()); let buffer: Buffer; try { @@ -241,7 +242,7 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this.#asyncTaskManager.startTask(() => this.signal.__abort__()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]()); let buffer: Buffer; try { @@ -271,7 +272,7 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this.#asyncTaskManager.startTask(() => this.signal.__abort__()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]()); let buffer: Buffer; try { @@ -311,11 +312,11 @@ export default class Request implements IRequest { (this.bodyUsed) = true; - const taskID = this.#asyncTaskManager.startTask(() => this.signal.__abort__()); + const taskID = this.#asyncTaskManager.startTask(() => this.signal[PropertySymbol.abort]()); let formData: FormData; try { - const type = this.__contentType__; + const type = this[PropertySymbol.contentType]; formData = (await MultipartFormDataParser.streamToFormData(this.body, type)).formData; } catch (error) { this.#asyncTaskManager.endTask(taskID); diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts index 43172fad4..8c15e0ba3 100644 --- a/packages/happy-dom/src/fetch/Response.ts +++ b/packages/happy-dom/src/fetch/Response.ts @@ -1,4 +1,5 @@ import IResponse from './types/IResponse.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import IBlob from '../file/IBlob.js'; import IResponseInit from './types/IResponseInit.js'; import IResponseBody from './types/IResponseBody.js'; @@ -30,7 +31,7 @@ const REDIRECT_STATUS_CODES = [301, 302, 303, 307, 308]; */ export default class Response implements IResponse { // Needs to be injected by sub-class. - protected static __window__: IBrowserWindow; + protected static [PropertySymbol.window]: IBrowserWindow; // Public properties public readonly body: Stream.Readable | null = null; @@ -43,8 +44,8 @@ export default class Response implements IResponse { public readonly statusText: string; public readonly ok: boolean; public readonly headers: IHeaders; - public __cachedResponse__: ICachedResponse | null = null; - public readonly __buffer__: Buffer | null = null; + public [PropertySymbol.cachedResponse]: ICachedResponse | null = null; + public readonly [PropertySymbol.buffer]: Buffer | null = null; readonly #window: IBrowserWindow; readonly #browserFrame: IBrowserFrame; @@ -77,7 +78,7 @@ export default class Response implements IResponse { this.body = stream; if (buffer) { - this.__buffer__ = buffer; + this[PropertySymbol.buffer] = buffer; } if (contentType && !this.headers.has('Content-Type')) { @@ -110,19 +111,19 @@ export default class Response implements IResponse { (this.bodyUsed) = true; - let buffer: Buffer | null = this.__buffer__; + let buffer: Buffer | null = this[PropertySymbol.buffer]; if (!buffer) { - const taskID = this.#browserFrame.__asyncTaskManager__.startTask(); + const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(); try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); throw error; } - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); } this.#storeBodyInCache(buffer); @@ -157,17 +158,17 @@ export default class Response implements IResponse { (this.bodyUsed) = true; - let buffer: Buffer | null = this.__buffer__; + let buffer: Buffer | null = this[PropertySymbol.buffer]; if (!buffer) { - const taskID = this.#browserFrame.__asyncTaskManager__.startTask(); + const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(); try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); throw error; } - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); } this.#storeBodyInCache(buffer); @@ -190,17 +191,17 @@ export default class Response implements IResponse { (this.bodyUsed) = true; - let buffer: Buffer | null = this.__buffer__; + let buffer: Buffer | null = this[PropertySymbol.buffer]; if (!buffer) { - const taskID = this.#browserFrame.__asyncTaskManager__.startTask(); + const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(); try { buffer = await FetchBodyUtility.consumeBodyStream(this.body); } catch (error) { - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); throw error; } - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); } this.#storeBodyInCache(buffer); @@ -238,7 +239,7 @@ export default class Response implements IResponse { return formData; } - const taskID = this.#browserFrame.__asyncTaskManager__.startTask(); + const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(); let formData: FormData; let buffer: Buffer; @@ -247,13 +248,13 @@ export default class Response implements IResponse { formData = result.formData; buffer = result.buffer; } catch (error) { - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); throw error; } this.#storeBodyInCache(buffer); - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); return formData; } @@ -287,9 +288,9 @@ export default class Response implements IResponse { * @param buffer Buffer. */ #storeBodyInCache(buffer: Buffer): void { - if (this.__cachedResponse__?.response?.waitingForBody) { - this.__cachedResponse__.response.body = buffer; - this.__cachedResponse__.response.waitingForBody = false; + if (this[PropertySymbol.cachedResponse]?.response?.waitingForBody) { + this[PropertySymbol.cachedResponse].response.body = buffer; + this[PropertySymbol.cachedResponse].response.waitingForBody = false; } } @@ -308,7 +309,7 @@ export default class Response implements IResponse { ); } - return new this.__window__.Response(null, { + return new this[PropertySymbol.window].Response(null, { headers: { location: new URL(url).toString() }, @@ -324,7 +325,7 @@ export default class Response implements IResponse { * @returns Response. */ public static error(): Response { - const response = new this.__window__.Response(null, { status: 0, statusText: '' }); + const response = new this[PropertySymbol.window].Response(null, { status: 0, statusText: '' }); (response.type) = 'error'; return response; } @@ -344,13 +345,13 @@ export default class Response implements IResponse { throw new TypeError('data is not JSON serializable'); } - const headers = new this.__window__.Headers(init && init.headers); + const headers = new this[PropertySymbol.window].Headers(init && init.headers); if (!headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } - return new this.__window__.Response(body, { + return new this[PropertySymbol.window].Response(body, { status: 200, ...init, headers diff --git a/packages/happy-dom/src/fetch/SyncFetch.ts b/packages/happy-dom/src/fetch/SyncFetch.ts index 2da36ed44..7a61b81d8 100644 --- a/packages/happy-dom/src/fetch/SyncFetch.ts +++ b/packages/happy-dom/src/fetch/SyncFetch.ts @@ -1,4 +1,5 @@ import IRequestInit from './types/IRequestInit.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import IRequestInfo from './types/IRequestInfo.js'; import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; @@ -72,7 +73,7 @@ export default class SyncFetch { ? new options.browserFrame.window.Request(options.url, options.init) : options.url; if (options.contentType) { - (this.request.__contentType__) = options.contentType; + (this.request[PropertySymbol.contentType]) = options.contentType; } this.redirectCount = options.redirectCount ?? 0; this.disableCache = options.disableCache ?? false; @@ -92,7 +93,7 @@ export default class SyncFetch { throw new DOMException('The operation was aborted.', DOMExceptionNameEnum.abortError); } - if (this.request.__url__.protocol === 'data:') { + if (this.request[PropertySymbol.url].protocol === 'data:') { const result = DataURIParser.parse(this.request.url); return { status: 200, @@ -106,7 +107,7 @@ export default class SyncFetch { } // Security check for "https" to "http" requests. - if (this.request.__url__.protocol === 'http:' && this.#window.location.protocol === 'https:') { + if (this.request[PropertySymbol.url].protocol === 'http:' && this.#window.location.protocol === 'https:') { throw new DOMException( `Mixed Content: The page at '${ this.#window.location.href @@ -231,7 +232,7 @@ export default class SyncFetch { private compliesWithCrossOriginPolicy(): boolean { if ( this.disableCrossOriginPolicy || - !FetchCORSUtility.isCORS(this.#window.location, this.request.__url__) + !FetchCORSUtility.isCORS(this.#window.location, this.request[PropertySymbol.url]) ) { return true; } @@ -321,7 +322,7 @@ export default class SyncFetch { * @returns Response. */ public sendRequest(): ISyncResponse { - if (!this.request.__bodyBuffer__ && this.request.body) { + if (!this.request[PropertySymbol.bodyBuffer] && this.request.body) { throw new DOMException( `Streams are not supported as request body for synchrounous requests.`, DOMExceptionNameEnum.notSupportedError @@ -329,14 +330,14 @@ export default class SyncFetch { } const script = SyncFetchScriptBuilder.getScript({ - url: this.request.__url__, + url: this.request[PropertySymbol.url], method: this.request.method, headers: FetchRequestHeaderUtility.getRequestHeaders({ browserFrame: this.#browserFrame, window: this.#window, request: this.request }), - body: this.request.__bodyBuffer__ + body: this.request[PropertySymbol.bodyBuffer] }); // Start the other Node Process, executing this string @@ -364,7 +365,7 @@ export default class SyncFetch { const headers = FetchResponseHeaderUtility.parseResponseHeaders({ browserFrame: this.#browserFrame, - requestURL: this.request.__url__, + requestURL: this.request[PropertySymbol.url], rawHeaders: incomingMessage.rawHeaders }); @@ -506,7 +507,7 @@ export default class SyncFetch { referrerPolicy: this.request.referrerPolicy, credentials: this.request.credentials, headers, - body: this.request.__bodyBuffer__ + body: this.request[PropertySymbol.bodyBuffer] }; if ( @@ -539,7 +540,7 @@ export default class SyncFetch { url: locationURL, init: requestInit, redirectCount: this.redirectCount + 1, - contentType: !shouldBecomeGetRequest ? this.request.__contentType__ : undefined + contentType: !shouldBecomeGetRequest ? this.request[PropertySymbol.contentType] : undefined }); return fetch.send(); diff --git a/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts b/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts index 92d9865f3..43bcdd0f8 100644 --- a/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts +++ b/packages/happy-dom/src/fetch/multipart/MultipartFormDataParser.ts @@ -1,4 +1,5 @@ import FormData from '../../form-data/FormData.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import Stream from 'stream'; import MultipartReader from './MultipartReader.js'; import DOMException from '../../exception/DOMException.js'; @@ -120,7 +121,7 @@ export default class MultipartFormDataParser { )}"\r\nContent-Type: ${value.type || 'application/octet-stream'}\r\n\r\n` ) ); - chunks.push(value.__buffer__); + chunks.push(value[PropertySymbol.buffer]); chunks.push(Buffer.from('\r\n')); } } diff --git a/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts index 94b2ae140..1fda7f04e 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchBodyUtility.ts @@ -1,4 +1,5 @@ import MultipartFormDataParser from '../multipart/MultipartFormDataParser.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import Stream from 'stream'; import { URLSearchParams } from 'url'; import FormData from '../../form-data/FormData.js'; @@ -38,7 +39,7 @@ export default class FetchBodyUtility { contentLength: buffer.length }; } else if (body instanceof Blob) { - const buffer = (body).__buffer__; + const buffer = (body)[PropertySymbol.buffer]; return { buffer, stream: Stream.Readable.from(buffer), diff --git a/packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts index 4512dec8d..084e521c5 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchRequestHeaderUtility.ts @@ -1,4 +1,5 @@ import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IBrowserWindow from '../../window/IBrowserWindow.js'; import CookieStringUtility from '../../cookie/urilities/CookieStringUtility.js'; import Headers from '../Headers.js'; @@ -40,13 +41,13 @@ export default class FetchRequestHeaderUtility { * @param headers Headers. */ public static removeForbiddenHeaders(headers: IHeaders): void { - for (const key of Object.keys((headers).__entries__)) { + for (const key of Object.keys((headers)[PropertySymbol.entries])) { if ( FORBIDDEN_HEADER_NAMES.includes(key) || key.startsWith('proxy-') || key.startsWith('sec-') ) { - delete (headers).__entries__[key]; + delete (headers)[PropertySymbol.entries][key]; } } } @@ -76,7 +77,7 @@ export default class FetchRequestHeaderUtility { request: Request; }): { [key: string]: string } { const headers = new Headers(options.request.headers); - const isCORS = FetchCORSUtility.isCORS(options.window.location, options.request.__url__); + const isCORS = FetchCORSUtility.isCORS(options.window.location, options.request[PropertySymbol.url]); // TODO: Maybe we need to add support for OPTIONS request with 'Access-Control-Allow-*' headers? if ( @@ -94,8 +95,8 @@ export default class FetchRequestHeaderUtility { headers.set('User-Agent', options.window.navigator.userAgent); } - if (options.request.__referrer__ instanceof URL) { - headers.set('Referer', options.request.__referrer__.href); + if (options.request[PropertySymbol.referrer] instanceof URL) { + headers.set('Referer', options.request[PropertySymbol.referrer].href); } if ( @@ -115,18 +116,18 @@ export default class FetchRequestHeaderUtility { headers.set('Accept', '*/*'); } - if (!headers.has('Content-Length') && options.request.__contentLength__ !== null) { - headers.set('Content-Length', String(options.request.__contentLength__)); + if (!headers.has('Content-Length') && options.request[PropertySymbol.contentLength] !== null) { + headers.set('Content-Length', String(options.request[PropertySymbol.contentLength])); } - if (!headers.has('Content-Type') && options.request.__contentType__) { - headers.set('Content-Type', options.request.__contentType__); + if (!headers.has('Content-Type') && options.request[PropertySymbol.contentType]) { + headers.set('Content-Type', options.request[PropertySymbol.contentType]); } // We need to convert the headers to Node request headers. const httpRequestHeaders = {}; - for (const header of Object.values(headers.__entries__)) { + for (const header of Object.values(headers[PropertySymbol.entries])) { httpRequestHeaders[header.name] = header.value; } diff --git a/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts index 437ca8c0c..79c861d96 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchRequestReferrerUtility.ts @@ -1,4 +1,5 @@ import URL from '../../url/URL.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IBrowserWindow from '../../window/IBrowserWindow.js'; import { isIP } from 'net'; import Headers from '../Headers.js'; @@ -34,9 +35,9 @@ export default class FetchRequestReferrerUtility { } if (request.referrer && request.referrer !== 'no-referrer') { - request.__referrer__ = this.getSentReferrer(originURL, request); + request[PropertySymbol.referrer] = this.getSentReferrer(originURL, request); } else { - request.__referrer__ = 'no-referrer'; + request[PropertySymbol.referrer] = 'no-referrer'; } } diff --git a/packages/happy-dom/src/fetch/utilities/FetchRequestValidationUtility.ts b/packages/happy-dom/src/fetch/utilities/FetchRequestValidationUtility.ts index 40b5ebb96..8a7d10b9f 100644 --- a/packages/happy-dom/src/fetch/utilities/FetchRequestValidationUtility.ts +++ b/packages/happy-dom/src/fetch/utilities/FetchRequestValidationUtility.ts @@ -1,4 +1,5 @@ import DOMException from '../../exception/DOMException.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import IRequestReferrerPolicy from '../types/IRequestReferrerPolicy.js'; import IRequestRedirect from '../types/IRequestRedirect.js'; @@ -114,9 +115,9 @@ export default class FetchRequestValidationUtility { * @param redirect Redirect. */ public static validateSchema(request: Request): void { - if (!SUPPORTED_SCHEMAS.includes(request.__url__.protocol)) { + if (!SUPPORTED_SCHEMAS.includes(request[PropertySymbol.url].protocol)) { throw new DOMException( - `Failed to fetch from "${request.url}": URL scheme "${request.__url__.protocol.replace( + `Failed to fetch from "${request.url}": URL scheme "${request[PropertySymbol.url].protocol.replace( /:$/, '' )}" is not supported.`, diff --git a/packages/happy-dom/src/file/Blob.ts b/packages/happy-dom/src/file/Blob.ts index f47d04136..db07d9445 100644 --- a/packages/happy-dom/src/file/Blob.ts +++ b/packages/happy-dom/src/file/Blob.ts @@ -1,4 +1,5 @@ import IBlob from './IBlob.js'; +import * as PropertySymbol from '../PropertySymbol.js'; /** * Reference: @@ -9,7 +10,7 @@ import IBlob from './IBlob.js'; */ export default class Blob implements IBlob { public readonly type: string = ''; - public __buffer__: Buffer = null; + public [PropertySymbol.buffer]: Buffer = null; /** * Constructor. @@ -31,7 +32,7 @@ export default class Blob implements IBlob { if (bit instanceof ArrayBuffer) { buffer = Buffer.from(new Uint8Array(bit)); } else if (bit instanceof Blob) { - buffer = bit.__buffer__; + buffer = bit[PropertySymbol.buffer]; } else if (bit instanceof Buffer) { buffer = bit; } else if (ArrayBuffer.isView(bit)) { @@ -44,7 +45,7 @@ export default class Blob implements IBlob { } } - this.__buffer__ = Buffer.concat(buffers); + this[PropertySymbol.buffer] = Buffer.concat(buffers); if (options && options.type && options.type.match(/^[\u0020-\u007E]*$/)) { this.type = String(options.type).toLowerCase(); @@ -57,7 +58,7 @@ export default class Blob implements IBlob { * @returns Size. */ public get size(): number { - return this.__buffer__.length; + return this[PropertySymbol.buffer].length; } /** @@ -100,12 +101,12 @@ export default class Blob implements IBlob { const span = Math.max(relativeEnd - relativeStart, 0); - const buffer = this.__buffer__; + const buffer = this[PropertySymbol.buffer]; const slicedBuffer = buffer.slice(relativeStart, relativeStart + span); const blob = new Blob([], { type: relativeContentType }); - (blob.__buffer__) = slicedBuffer; + (blob[PropertySymbol.buffer]) = slicedBuffer; return blob; } @@ -123,7 +124,7 @@ export default class Blob implements IBlob { * */ public async arrayBuffer(): Promise { - return new Uint8Array(this.__buffer__).buffer; + return new Uint8Array(this[PropertySymbol.buffer]).buffer; } /** @@ -132,7 +133,7 @@ export default class Blob implements IBlob { * @returns Text. */ public async text(): Promise { - return this.__buffer__.toString(); + return this[PropertySymbol.buffer].toString(); } /** diff --git a/packages/happy-dom/src/file/FileReader.ts b/packages/happy-dom/src/file/FileReader.ts index 29ac47e8b..0b95bbc81 100644 --- a/packages/happy-dom/src/file/FileReader.ts +++ b/packages/happy-dom/src/file/FileReader.ts @@ -1,4 +1,5 @@ import WhatwgMIMEType from 'whatwg-mimetype'; +import * as PropertySymbol from '../PropertySymbol.js'; import WhatwgEncoding from 'whatwg-encoding'; import IBrowserWindow from '../window/IBrowserWindow.js'; import ProgressEvent from '../event/events/ProgressEvent.js'; @@ -133,7 +134,7 @@ export default class FileReader extends EventTarget { this.dispatchEvent(new ProgressEvent(FileReaderEventTypeEnum.loadstart)); - let data = blob.__buffer__; + let data = blob[PropertySymbol.buffer]; if (!data) { data = Buffer.alloc(0); } diff --git a/packages/happy-dom/src/form-data/FormData.ts b/packages/happy-dom/src/form-data/FormData.ts index 484766299..7a72610b5 100644 --- a/packages/happy-dom/src/form-data/FormData.ts +++ b/packages/happy-dom/src/form-data/FormData.ts @@ -1,4 +1,5 @@ import Blob from '../file/Blob.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import File from '../file/File.js'; import IHTMLInputElement from '../nodes/html-input-element/IHTMLInputElement.js'; import IHTMLFormElement from '../nodes/html-form-element/IHTMLFormElement.js'; @@ -27,8 +28,8 @@ export default class FormData implements Iterable<[string, string | File]> { */ constructor(form?: IHTMLFormElement) { if (form) { - for (const name of Object.keys((form.elements).__namedItems__)) { - let radioNodeList = (form.elements).__namedItems__[name]; + for (const name of Object.keys((form.elements)[PropertySymbol.namedItems])) { + let radioNodeList = (form.elements)[PropertySymbol.namedItems][name]; if ( radioNodeList[0].tagName === 'INPUT' && @@ -220,14 +221,14 @@ export default class FormData implements Iterable<[string, string | File]> { #parseValue(value: string | Blob | File, filename?: string): string | File { if (value instanceof Blob && !(value instanceof File)) { const file = new File([], 'blob', { type: value.type }); - file.__buffer__ = value.__buffer__; + file[PropertySymbol.buffer] = value[PropertySymbol.buffer]; return file; } if (value instanceof File) { if (filename) { const file = new File([], filename, { type: value.type, lastModified: value.lastModified }); - file.__buffer__ = value.__buffer__; + file[PropertySymbol.buffer] = value[PropertySymbol.buffer]; return file; } return value; diff --git a/packages/happy-dom/src/match-media/MediaQueryList.ts b/packages/happy-dom/src/match-media/MediaQueryList.ts index 43cabc333..9ef81ca61 100644 --- a/packages/happy-dom/src/match-media/MediaQueryList.ts +++ b/packages/happy-dom/src/match-media/MediaQueryList.ts @@ -1,4 +1,5 @@ import EventTarget from '../event/EventTarget.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import Event from '../event/Event.js'; import IBrowserWindow from '../window/IBrowserWindow.js'; import IEventListener from '../event/IEventListener.js'; @@ -112,7 +113,7 @@ export default class MediaQueryList extends EventTarget { this.dispatchEvent(new MediaQueryListEvent('change', { matches, media: this.media })); } }; - listener['__windowResizeListener__'] = resizeListener; + listener[PropertySymbol.windowResizeListener] = resizeListener; this.#ownerWindow.addEventListener('resize', resizeListener); } } @@ -125,8 +126,8 @@ export default class MediaQueryList extends EventTarget { listener: IEventListener | ((event: Event) => void) ): void { super.removeEventListener(type, listener); - if (type === 'change' && listener['__windowResizeListener__']) { - this.#ownerWindow.removeEventListener('resize', listener['__windowResizeListener__']); + if (type === 'change' && listener[PropertySymbol.windowResizeListener]) { + this.#ownerWindow.removeEventListener('resize', listener[PropertySymbol.windowResizeListener]); } } } diff --git a/packages/happy-dom/src/mutation-observer/MutationObserver.ts b/packages/happy-dom/src/mutation-observer/MutationObserver.ts index 7894447b4..2eee6dd33 100644 --- a/packages/happy-dom/src/mutation-observer/MutationObserver.ts +++ b/packages/happy-dom/src/mutation-observer/MutationObserver.ts @@ -1,4 +1,5 @@ import DOMException from '../exception/DOMException.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import INode from '../nodes/node/INode.js'; import Node from '../nodes/node/Node.js'; import IMutationObserverInit from './IMutationObserverInit.js'; @@ -49,7 +50,7 @@ export default class MutationObserver { this.listener.callback = this.callback.bind(this); this.listener.observer = this; - (target).__observe__(this.listener); + (target)[PropertySymbol.observe](this.listener); } /** @@ -57,7 +58,7 @@ export default class MutationObserver { */ public disconnect(): void { if (this.target) { - (this.target).__unobserve__(this.listener); + (this.target)[PropertySymbol.unobserve](this.listener); this.target = null; } } diff --git a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts index 4548177cf..9fe4c4f5a 100644 --- a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts +++ b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts @@ -1,4 +1,5 @@ import INamedNodeMap from './INamedNodeMap.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import IAttr from '../nodes/attr/IAttr.js'; import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; @@ -11,7 +12,7 @@ import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; export default class NamedNodeMap implements INamedNodeMap { [index: number]: IAttr; public length = 0; - protected __namedItems__: { [k: string]: IAttr } = {}; + protected [PropertySymbol.namedItems]: { [k: string]: IAttr } = {}; /** * Returns string. @@ -49,7 +50,7 @@ export default class NamedNodeMap implements INamedNodeMap { * @returns Itme. */ public getNamedItem(name: string): IAttr | null { - return this.__namedItems__[name] || null; + return this[PropertySymbol.namedItems][name] || null; } /** @@ -82,7 +83,7 @@ export default class NamedNodeMap implements INamedNodeMap { * @returns Replaced item. */ public setNamedItem(item: IAttr): IAttr | null { - return this.__setNamedItemWithoutConsequences__(item); + return this[PropertySymbol.setNamedItemWithoutConsequences](item); } /** @@ -104,7 +105,7 @@ export default class NamedNodeMap implements INamedNodeMap { * @returns Removed item. */ public removeNamedItem(name: string): IAttr { - const item = this.__removeNamedItem__(name); + const item = this[PropertySymbol.removeNamedItem](name); if (!item) { throw new DOMException( `Failed to execute 'removeNamedItem' on 'NamedNodeMap': No item with name '${name}' was found.`, @@ -135,20 +136,20 @@ export default class NamedNodeMap implements INamedNodeMap { * @param item Item. * @returns Replaced item. */ - public __setNamedItemWithoutConsequences__(item: IAttr): IAttr | null { + public [PropertySymbol.setNamedItemWithoutConsequences](item: IAttr): IAttr | null { if (item.name) { - const replacedItem = this.__namedItems__[item.name] || null; + const replacedItem = this[PropertySymbol.namedItems][item.name] || null; - this.__namedItems__[item.name] = item; + this[PropertySymbol.namedItems][item.name] = item; if (replacedItem) { - this.__removeNamedItemIndex__(replacedItem); + this[PropertySymbol.removeNamedItemIndex](replacedItem); } this[this.length] = item; this.length++; - if (this.__isValidPropertyName__(item.name)) { + if (this[PropertySymbol.isValidPropertyName](item.name)) { this[item.name] = item; } @@ -163,8 +164,8 @@ export default class NamedNodeMap implements INamedNodeMap { * @param name Name of item. * @returns Removed item, or null if it didn't exist. */ - public __removeNamedItem__(name: string): IAttr | null { - return this.__removeNamedItemWithoutConsequences__(name); + public [PropertySymbol.removeNamedItem](name: string): IAttr | null { + return this[PropertySymbol.removeNamedItemWithoutConsequences](name); } /** @@ -173,20 +174,20 @@ export default class NamedNodeMap implements INamedNodeMap { * @param name Name of item. * @returns Removed item, or null if it didn't exist. */ - public __removeNamedItemWithoutConsequences__(name: string): IAttr | null { - const removedItem = this.__namedItems__[name] || null; + public [PropertySymbol.removeNamedItemWithoutConsequences](name: string): IAttr | null { + const removedItem = this[PropertySymbol.namedItems][name] || null; if (!removedItem) { return null; } - this.__removeNamedItemIndex__(removedItem); + this[PropertySymbol.removeNamedItemIndex](removedItem); if (this[name] === removedItem) { delete this[name]; } - delete this.__namedItems__[name]; + delete this[PropertySymbol.namedItems][name]; return removedItem; } @@ -196,7 +197,7 @@ export default class NamedNodeMap implements INamedNodeMap { * * @param item Item. */ - protected __removeNamedItemIndex__(item: IAttr): void { + protected [PropertySymbol.removeNamedItemIndex](item: IAttr): void { for (let i = 0; i < this.length; i++) { if (this[i] === item) { for (let b = i; b < this.length; b++) { @@ -218,7 +219,7 @@ export default class NamedNodeMap implements INamedNodeMap { * @param name Name. * @returns True if the property name is valid. */ - protected __isValidPropertyName__(name: string): boolean { + protected [PropertySymbol.isValidPropertyName](name: string): boolean { return ( !this.constructor.prototype.hasOwnProperty(name) && (isNaN(Number(name)) || name.includes('.')) diff --git a/packages/happy-dom/src/nodes/character-data/CharacterData.ts b/packages/happy-dom/src/nodes/character-data/CharacterData.ts index 50d6b39c8..a80aaeba0 100644 --- a/packages/happy-dom/src/nodes/character-data/CharacterData.ts +++ b/packages/happy-dom/src/nodes/character-data/CharacterData.ts @@ -1,4 +1,5 @@ import Node from '../node/Node.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import CharacterDataUtility from './CharacterDataUtility.js'; import ICharacterData from './ICharacterData.js'; import IElement from '../element/IElement.js'; @@ -14,7 +15,7 @@ import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; * https://developer.mozilla.org/en-US/docs/Web/API/CharacterData. */ export default abstract class CharacterData extends Node implements ICharacterData { - public __data__ = ''; + public [PropertySymbol.data] = ''; /** * Constructor. @@ -25,7 +26,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa super(); if (data) { - this.__data__ = data; + this[PropertySymbol.data] = data; } } @@ -35,7 +36,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa * @returns Text content. */ public get length(): number { - return this.__data__.length; + return this[PropertySymbol.data].length; } /** @@ -44,7 +45,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa * @returns Text content. */ public get data(): string { - return this.__data__; + return this[PropertySymbol.data]; } /** @@ -53,16 +54,16 @@ export default abstract class CharacterData extends Node implements ICharacterDa * @param textContent Text content. */ public set data(data: string) { - const oldValue = this.__data__; - this.__data__ = String(data); + const oldValue = this[PropertySymbol.data]; + this[PropertySymbol.data] = String(data); if (this.isConnected) { - this.ownerDocument['__cacheID__']++; + this.ownerDocument[PropertySymbol.cacheID]++; } // MutationObserver - if (this.__observers__.length > 0) { - for (const observer of this.__observers__) { + if (this[PropertySymbol.observers].length > 0) { + for (const observer of this[PropertySymbol.observers]) { if (observer.options.characterData) { const record = new MutationRecord(); record.target = this; @@ -80,7 +81,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa * @returns Text content. */ public get textContent(): string { - return this.__data__; + return this[PropertySymbol.data]; } /** @@ -98,7 +99,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa * @returns Node value. */ public get nodeValue(): string { - return this.__data__; + return this[PropertySymbol.data]; } /** @@ -221,7 +222,7 @@ export default abstract class CharacterData extends Node implements ICharacterDa */ public cloneNode(deep = false): ICharacterData { const clone = super.cloneNode(deep); - clone.__data__ = this.__data__; + clone[PropertySymbol.data] = this[PropertySymbol.data]; return clone; } } diff --git a/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts b/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts index c706c898c..2f494caa6 100644 --- a/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts +++ b/packages/happy-dom/src/nodes/child-node/ChildNodeUtility.ts @@ -1,4 +1,5 @@ import DOMException from '../../exception/DOMException.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import XMLParser from '../../xml-parser/XMLParser.js'; import DocumentFragment from '../document-fragment/DocumentFragment.js'; import Document from '../document/Document.js'; @@ -38,7 +39,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode.ownerDocument, node) - )).__childNodes__.slice(); + ))[PropertySymbol.childNodes].slice(); for (const newChildNode of newChildNodes) { parent.insertBefore(newChildNode, childNode); } @@ -67,7 +68,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode.ownerDocument, node) - )).__childNodes__.slice(); + ))[PropertySymbol.childNodes].slice(); for (const newChildNode of newChildNodes) { parent.insertBefore(newChildNode, childNode); } @@ -96,7 +97,7 @@ export default class ChildNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(childNode.ownerDocument, node) - )).__childNodes__.slice(); + ))[PropertySymbol.childNodes].slice(); for (const newChildNode of newChildNodes) { if (!nextSibling) { parent.appendChild(newChildNode); diff --git a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts index 4182c5616..920136b14 100644 --- a/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts +++ b/packages/happy-dom/src/nodes/document-fragment/DocumentFragment.ts @@ -1,4 +1,5 @@ import Node from '../node/Node.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IElement from '../element/IElement.js'; import QuerySelector from '../../query-selector/QuerySelector.js'; import ParentNodeUtility from '../parent-node/ParentNodeUtility.js'; @@ -14,14 +15,14 @@ import INodeList from '../node/INodeList.js'; */ export default class DocumentFragment extends Node implements IDocumentFragment { public nodeType = Node.DOCUMENT_FRAGMENT_NODE; - public readonly __children__: IHTMLCollection = new HTMLCollection(); - public __rootNode__: INode = this; + public readonly [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); + public [PropertySymbol.rootNode]: INode = this; /** * Returns the document fragment children. */ public get children(): IHTMLCollection { - return this.__children__; + return this[PropertySymbol.children]; } /** @@ -30,7 +31,7 @@ export default class DocumentFragment extends Node implements IDocumentFragment * @returns Element. */ public get childElementCount(): number { - return this.__children__.length; + return this[PropertySymbol.children].length; } /** @@ -39,7 +40,7 @@ export default class DocumentFragment extends Node implements IDocumentFragment * @returns Element. */ public get firstElementChild(): IElement { - return this.__children__[0] ?? null; + return this[PropertySymbol.children][0] ?? null; } /** @@ -48,7 +49,7 @@ export default class DocumentFragment extends Node implements IDocumentFragment * @returns Element. */ public get lastElementChild(): IElement { - return this.__children__[this.__children__.length - 1] ?? null; + return this[PropertySymbol.children][this[PropertySymbol.children].length - 1] ?? null; } /** @@ -58,7 +59,7 @@ export default class DocumentFragment extends Node implements IDocumentFragment */ public get textContent(): string { let result = ''; - for (const childNode of this.__childNodes__) { + for (const childNode of this[PropertySymbol.childNodes]) { if (childNode.nodeType === Node.ELEMENT_NODE || childNode.nodeType === Node.TEXT_NODE) { result += childNode.textContent; } @@ -72,7 +73,7 @@ export default class DocumentFragment extends Node implements IDocumentFragment * @param textContent Text content. */ public set textContent(textContent: string) { - for (const child of this.__childNodes__.slice()) { + for (const child of this[PropertySymbol.childNodes].slice()) { this.removeChild(child); } if (textContent) { @@ -148,9 +149,9 @@ export default class DocumentFragment extends Node implements IDocumentFragment const clone = super.cloneNode(deep); if (deep) { - for (const node of clone.__childNodes__) { + for (const node of clone[PropertySymbol.childNodes]) { if (node.nodeType === Node.ELEMENT_NODE) { - clone.__children__.push(node); + clone[PropertySymbol.children].push(node); } } } diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 32cb76219..145ed9c70 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -1,4 +1,5 @@ import Element from '../element/Element.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLUnknownElement from '../html-unknown-element/HTMLUnknownElement.js'; import IBrowserWindow from '../../window/IBrowserWindow.js'; import Node from '../node/Node.js'; @@ -57,21 +58,21 @@ export default class Document extends Node implements IDocument { public readonly readyState = DocumentReadyStateEnum.interactive; public readonly isConnected: boolean = true; public readonly defaultView: IBrowserWindow | null = null; - public readonly __defaultView__: IBrowserWindow; + public readonly [PropertySymbol.defaultView]: IBrowserWindow; public readonly referrer = ''; - public readonly __children__: IHTMLCollection = new HTMLCollection(); - public __activeElement__: IHTMLElement = null; - public __nextActiveElement__: IHTMLElement = null; - public __currentScript__: IHTMLScriptElement = null; - public __rootNode__ = this; + public readonly [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); + public [PropertySymbol.activeElement]: IHTMLElement = null; + public [PropertySymbol.nextActiveElement]: IHTMLElement = null; + public [PropertySymbol.currentScript]: IHTMLScriptElement = null; + public [PropertySymbol.rootNode] = this; // Used as an unique identifier which is updated whenever the DOM gets modified. - public __cacheID__ = 0; + public [PropertySymbol.cacheID] = 0; - public __isFirstWrite__ = true; - public __isFirstWriteAfterOpen__ = false; + public [PropertySymbol.isFirstWrite] = true; + public [PropertySymbol.isFirstWriteAfterOpen] = false; - private __selection__: Selection = null; + private [PropertySymbol.selection]: Selection = null; #browserFrame: IBrowserFrame; // Events @@ -195,7 +196,7 @@ export default class Document extends Node implements IDocument { constructor(injected: { browserFrame: IBrowserFrame; window: IBrowserWindow }) { super(); this.#browserFrame = injected.browserFrame; - this.__defaultView__ = injected.window; + this[PropertySymbol.defaultView] = injected.window; this.implementation = new DOMImplementation(this); } @@ -203,7 +204,7 @@ export default class Document extends Node implements IDocument { * Returns document children. */ public get children(): IHTMLCollection { - return this.__children__; + return this[PropertySymbol.children]; } /** @@ -267,7 +268,7 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public get childElementCount(): number { - return this.__children__.length; + return this[PropertySymbol.children].length; } /** @@ -276,7 +277,7 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public get firstElementChild(): IElement { - return this.__children__[0] ?? null; + return this[PropertySymbol.children][0] ?? null; } /** @@ -285,7 +286,7 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public get lastElementChild(): IElement { - return this.__children__[this.__children__.length - 1] ?? null; + return this[PropertySymbol.children][this[PropertySymbol.children].length - 1] ?? null; } /** @@ -296,7 +297,7 @@ export default class Document extends Node implements IDocument { public get cookie(): string { return CookieStringUtility.cookiesToString( this.#browserFrame.page.context.cookieContainer.getCookies( - this.__defaultView__.location, + this[PropertySymbol.defaultView].location, true ) ); @@ -309,7 +310,7 @@ export default class Document extends Node implements IDocument { */ public set cookie(cookie: string) { this.#browserFrame.page.context.cookieContainer.addCookies([ - CookieStringUtility.stringToCookie(this.__defaultView__.location, cookie) + CookieStringUtility.stringToCookie(this[PropertySymbol.defaultView].location, cookie) ]); } @@ -337,7 +338,7 @@ export default class Document extends Node implements IDocument { * @returns Document type. */ public get doctype(): IDocumentType { - for (const node of this.__childNodes__) { + for (const node of this[PropertySymbol.childNodes]) { if (node instanceof DocumentType) { return node; } @@ -388,22 +389,22 @@ export default class Document extends Node implements IDocument { * @returns Active element. */ public get activeElement(): IHTMLElement { - if (this.__activeElement__ && !this.__activeElement__.isConnected) { - this.__activeElement__ = null; + if (this[PropertySymbol.activeElement] && !this[PropertySymbol.activeElement].isConnected) { + this[PropertySymbol.activeElement] = null; } - if (this.__activeElement__ && this.__activeElement__ instanceof Element) { + if (this[PropertySymbol.activeElement] && this[PropertySymbol.activeElement] instanceof Element) { let rootNode: IShadowRoot | IDocument = ( - this.__activeElement__.getRootNode() + this[PropertySymbol.activeElement].getRootNode() ); - let activeElement: IHTMLElement = this.__activeElement__; + let activeElement: IHTMLElement = this[PropertySymbol.activeElement]; while (rootNode !== this) { activeElement = (rootNode).host; rootNode = activeElement ? activeElement.getRootNode() : this; } return activeElement; } - return this.__activeElement__ || this.body || this.documentElement || null; + return this[PropertySymbol.activeElement] || this.body || this.documentElement || null; } /** @@ -421,7 +422,7 @@ export default class Document extends Node implements IDocument { * @returns Location. */ public get location(): Location { - return this.__defaultView__.location; + return this[PropertySymbol.defaultView].location; } /** @@ -444,7 +445,7 @@ export default class Document extends Node implements IDocument { if (element) { return element.href; } - return this.__defaultView__.location.href; + return this[PropertySymbol.defaultView].location.href; } /** @@ -453,7 +454,7 @@ export default class Document extends Node implements IDocument { * @returns the URL of the current document. * */ public get URL(): string { - return this.__defaultView__.location.href; + return this[PropertySymbol.defaultView].location.href; } /** @@ -497,7 +498,7 @@ export default class Document extends Node implements IDocument { * @returns the currently executing script element. */ public get currentScript(): IHTMLScriptElement { - return this.__currentScript__; + return this[PropertySymbol.currentScript]; } /** @@ -600,7 +601,7 @@ export default class Document extends Node implements IDocument { name: string ): INodeList => { const matches = new NodeList(); - for (const child of (parentNode).__children__) { + for (const child of (parentNode)[PropertySymbol.children]) { if (child.getAttributeNS(null, 'name') === name) { matches.push(child); } @@ -624,9 +625,9 @@ export default class Document extends Node implements IDocument { const clone = super.cloneNode(deep); if (deep) { - for (const node of clone.__childNodes__) { + for (const node of clone[PropertySymbol.childNodes]) { if (node.nodeType === Node.ELEMENT_NODE) { - clone.__children__.push(node); + clone[PropertySymbol.children].push(node); } } } @@ -672,20 +673,20 @@ export default class Document extends Node implements IDocument { public write(html: string): void { const root = XMLParser.parse(this, html, { evaluateScripts: true }); - if (this.__isFirstWrite__ || this.__isFirstWriteAfterOpen__) { - if (this.__isFirstWrite__) { - if (!this.__isFirstWriteAfterOpen__) { + if (this[PropertySymbol.isFirstWrite] || this[PropertySymbol.isFirstWriteAfterOpen]) { + if (this[PropertySymbol.isFirstWrite]) { + if (!this[PropertySymbol.isFirstWriteAfterOpen]) { this.open(); } - this.__isFirstWrite__ = false; + this[PropertySymbol.isFirstWrite] = false; } - this.__isFirstWriteAfterOpen__ = false; + this[PropertySymbol.isFirstWriteAfterOpen] = false; let documentElement = null; let documentTypeNode = null; - for (const node of root.__childNodes__) { + for (const node of root[PropertySymbol.childNodes]) { if (node['tagName'] === 'HTML') { documentElement = node; } else if (node.nodeType === NodeTypeEnum.documentTypeNode) { @@ -720,7 +721,7 @@ export default class Document extends Node implements IDocument { const rootBody = ParentNodeUtility.getElementByTagName(root, 'body'); const body = ParentNodeUtility.getElementByTagName(this, 'body'); if (rootBody && body) { - for (const child of rootBody.__childNodes__.slice()) { + for (const child of rootBody[PropertySymbol.childNodes].slice()) { body.appendChild(child); } } @@ -729,7 +730,7 @@ export default class Document extends Node implements IDocument { // Remaining nodes outside the element are added to the element. const body = ParentNodeUtility.getElementByTagName(this, 'body'); if (body) { - for (const child of root.__childNodes__.slice()) { + for (const child of root[PropertySymbol.childNodes].slice()) { if (child['tagName'] !== 'HTML' && child.nodeType !== NodeTypeEnum.documentTypeNode) { body.appendChild(child); } @@ -740,7 +741,7 @@ export default class Document extends Node implements IDocument { const bodyElement = this.createElement('body'); const headElement = this.createElement('head'); - for (const child of root.__childNodes__.slice()) { + for (const child of root[PropertySymbol.childNodes].slice()) { bodyElement.appendChild(child); } @@ -752,7 +753,7 @@ export default class Document extends Node implements IDocument { } else { const bodyNode = ParentNodeUtility.getElementByTagName(root, 'body'); const body = ParentNodeUtility.getElementByTagName(this, 'body'); - for (const child of ((bodyNode || root)).__childNodes__.slice()) { + for (const child of ((bodyNode || root))[PropertySymbol.childNodes].slice()) { body.appendChild(child); } } @@ -764,10 +765,10 @@ export default class Document extends Node implements IDocument { * @returns Document. */ public open(): IDocument { - this.__isFirstWriteAfterOpen__ = true; + this[PropertySymbol.isFirstWriteAfterOpen] = true; - for (const eventType of Object.keys(this.__listeners__)) { - const listeners = this.__listeners__[eventType]; + for (const eventType of Object.keys(this[PropertySymbol.listeners])) { + const listeners = this[PropertySymbol.listeners][eventType]; if (listeners) { for (const listener of listeners) { this.removeEventListener(eventType, listener); @@ -775,7 +776,7 @@ export default class Document extends Node implements IDocument { } } - for (const child of this.__childNodes__.slice()) { + for (const child of this[PropertySymbol.childNodes].slice()) { this.removeChild(child); } @@ -819,22 +820,22 @@ export default class Document extends Node implements IDocument { let customElementClass; if (options && options.is) { - customElementClass = this.__defaultView__.customElements.get(String(options.is)); + customElementClass = this[PropertySymbol.defaultView].customElements.get(String(options.is)); } else { - customElementClass = this.__defaultView__.customElements.get(tagName); + customElementClass = this[PropertySymbol.defaultView].customElements.get(tagName); } const elementClass: typeof Element = - customElementClass || this.__defaultView__[ElementTag[tagName]] || HTMLUnknownElement; + customElementClass || this[PropertySymbol.defaultView][ElementTag[tagName]] || HTMLUnknownElement; - elementClass.__ownerDocument__ = this; + elementClass[PropertySymbol.ownerDocument] = this; const element = new elementClass(); - elementClass.__ownerDocument__ = null; + elementClass[PropertySymbol.ownerDocument] = null; element.tagName = tagName; (element.namespaceURI) = namespaceURI; if (element instanceof Element && options && options.is) { - element.__isValue__ = String(options.is); + element[PropertySymbol.isValue] = String(options.is); } return element; @@ -849,9 +850,9 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createTextNode(data?: string): IText { - this.__defaultView__.Text.__ownerDocument__ = this; - const node = new this.__defaultView__.Text(data); - this.__defaultView__.Text.__ownerDocument__ = null; + this[PropertySymbol.defaultView].Text[PropertySymbol.ownerDocument] = this; + const node = new this[PropertySymbol.defaultView].Text(data); + this[PropertySymbol.defaultView].Text[PropertySymbol.ownerDocument] = null; return node; } @@ -862,9 +863,9 @@ export default class Document extends Node implements IDocument { * @returns Text node. */ public createComment(data?: string): IComment { - this.__defaultView__.Comment.__ownerDocument__ = this; - const node = new this.__defaultView__.Comment(data); - this.__defaultView__.Comment.__ownerDocument__ = null; + this[PropertySymbol.defaultView].Comment[PropertySymbol.ownerDocument] = this; + const node = new this[PropertySymbol.defaultView].Comment(data); + this[PropertySymbol.defaultView].Comment[PropertySymbol.ownerDocument] = null; return node; } @@ -874,7 +875,7 @@ export default class Document extends Node implements IDocument { * @returns Document fragment. */ public createDocumentFragment(): IDocumentFragment { - return new this.__defaultView__.DocumentFragment(); + return new this[PropertySymbol.defaultView].DocumentFragment(); } /** @@ -911,8 +912,8 @@ export default class Document extends Node implements IDocument { * @returns Event. */ public createEvent(type: string): Event { - if (typeof this.__defaultView__[type] === 'function') { - return new this.__defaultView__[type]('init'); + if (typeof this[PropertySymbol.defaultView][type] === 'function') { + return new this[PropertySymbol.defaultView][type]('init'); } return new Event('init'); } @@ -935,9 +936,9 @@ export default class Document extends Node implements IDocument { * @returns Element. */ public createAttributeNS(namespaceURI: string, qualifiedName: string): IAttr { - this.__defaultView__.Attr.__ownerDocument__ = this; - const attribute = new this.__defaultView__.Attr(); - this.__defaultView__.Attr.__ownerDocument__ = null; + this[PropertySymbol.defaultView].Attr[PropertySymbol.ownerDocument] = this; + const attribute = new this[PropertySymbol.defaultView].Attr(); + this[PropertySymbol.defaultView].Attr[PropertySymbol.ownerDocument] = null; attribute.namespaceURI = namespaceURI; attribute.name = qualifiedName; return attribute; @@ -964,7 +965,7 @@ export default class Document extends Node implements IDocument { * @returns Range. */ public createRange(): Range { - return new this.__defaultView__.Range(); + return new this[PropertySymbol.defaultView].Range(); } /** @@ -990,10 +991,10 @@ export default class Document extends Node implements IDocument { * @returns Selection. */ public getSelection(): Selection { - if (!this.__selection__) { - this.__selection__ = new Selection(this); + if (!this[PropertySymbol.selection]) { + this[PropertySymbol.selection] = new Selection(this); } - return this.__selection__; + return this[PropertySymbol.selection]; } /** @@ -1023,9 +1024,9 @@ export default class Document extends Node implements IDocument { `Failed to execute 'createProcessingInstruction' on 'Document': The data provided ('?>') contains '?>'` ); } - this.__defaultView__.ProcessingInstruction.__ownerDocument__ = this; - const processingInstruction = new this.__defaultView__.ProcessingInstruction(data); - this.__defaultView__.ProcessingInstruction.__ownerDocument__ = null; + this[PropertySymbol.defaultView].ProcessingInstruction[PropertySymbol.ownerDocument] = this; + const processingInstruction = new this[PropertySymbol.defaultView].ProcessingInstruction(data); + this[PropertySymbol.defaultView].ProcessingInstruction[PropertySymbol.ownerDocument] = null; processingInstruction.target = target; return processingInstruction; } @@ -1038,7 +1039,7 @@ export default class Document extends Node implements IDocument { #importNode(node: INode): void { (node.ownerDocument) = this; - for (const child of node['__childNodes__']) { + for (const child of node[PropertySymbol.childNodes]) { this.#importNode(child); } } diff --git a/packages/happy-dom/src/nodes/document/IDocument.ts b/packages/happy-dom/src/nodes/document/IDocument.ts index 7518eeca9..5b417247f 100644 --- a/packages/happy-dom/src/nodes/document/IDocument.ts +++ b/packages/happy-dom/src/nodes/document/IDocument.ts @@ -1,4 +1,5 @@ import IElement from '../element/IElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IHTMLElement from '../html-element/IHTMLElement.js'; import IBrowserWindow from '../../window/IBrowserWindow.js'; import NodeIterator from '../../tree-walker/NodeIterator.js'; @@ -28,7 +29,7 @@ import VisibilityStateEnum from './VisibilityStateEnum.js'; */ export default interface IDocument extends IParentNode { readonly defaultView: IBrowserWindow | null; - readonly __defaultView__: IBrowserWindow; + readonly [PropertySymbol.defaultView]: IBrowserWindow; readonly implementation: DOMImplementation; readonly documentElement: IHTMLElement; readonly doctype: IDocumentType; diff --git a/packages/happy-dom/src/nodes/element/Dataset.ts b/packages/happy-dom/src/nodes/element/Dataset.ts index c163e5a33..b8e59d335 100644 --- a/packages/happy-dom/src/nodes/element/Dataset.ts +++ b/packages/happy-dom/src/nodes/element/Dataset.ts @@ -1,4 +1,5 @@ import Element from '../element/Element.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; /** @@ -47,7 +48,7 @@ export default class Dataset { return true; }, deleteProperty(dataset: DatasetRecord, key: string): boolean { - (element.attributes).__removeNamedItem__( + (element.attributes)[PropertySymbol.removeNamedItem]( 'data-' + Dataset.camelCaseToKebab(key) ); return delete dataset[key]; diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index aad01cd3f..b0fb2440e 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -1,4 +1,5 @@ import Node from '../node/Node.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import ShadowRoot from '../shadow-root/ShadowRoot.js'; import DOMRect from './DOMRect.js'; import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; @@ -38,8 +39,8 @@ import BrowserErrorCapturingEnum from '../../browser/enums/BrowserErrorCapturing */ export default class Element extends Node implements IElement { // ObservedAttributes should only be called once by CustomElementRegistry (see #117) - // CustomElementRegistry will therefore populate "__observedAttributes__" when CustomElementRegistry.define() is called - public static __observedAttributes__: string[]; + // CustomElementRegistry will therefore populate "[PropertySymbol.observedAttributes]" when CustomElementRegistry.define() is called + public static [PropertySymbol.observedAttributes]: string[]; public static observedAttributes: string[]; public tagName: string = null; public nodeType = Node.ELEMENT_NODE; @@ -88,21 +89,21 @@ export default class Element extends Node implements IElement { public ontouchmove: (event: Event) => void | null = null; public ontouchstart: (event: Event) => void | null = null; - public readonly __children__: IHTMLCollection = new HTMLCollection(); + public readonly [PropertySymbol.children]: IHTMLCollection = new HTMLCollection(); // Used for being able to access closed shadow roots public readonly attributes: INamedNodeMap = new ElementNamedNodeMap(this); - public __shadowRoot__: IShadowRoot = null; - public __classList__: DOMTokenList = null; - public __isValue__: string | null = null; - public __computedStyle__: CSSStyleDeclaration | null = null; + public [PropertySymbol.shadowRoot]: IShadowRoot = null; + public [PropertySymbol.classList]: DOMTokenList = null; + public [PropertySymbol.isValue]: string | null = null; + public [PropertySymbol.computedStyle]: CSSStyleDeclaration | null = null; /** * Returns element children. */ public get children(): IHTMLCollection { - return this.__children__; + return this[PropertySymbol.children]; } /** @@ -111,10 +112,10 @@ export default class Element extends Node implements IElement { * @returns Class list. */ public get classList(): IDOMTokenList { - if (!this.__classList__) { - this.__classList__ = new DOMTokenList(this, 'class'); + if (!this[PropertySymbol.classList]) { + this[PropertySymbol.classList] = new DOMTokenList(this, 'class'); } - return this.__classList__; + return this[PropertySymbol.classList]; } /** @@ -214,7 +215,7 @@ export default class Element extends Node implements IElement { */ public get textContent(): string { let result = ''; - for (const childNode of this.__childNodes__) { + for (const childNode of this[PropertySymbol.childNodes]) { if (childNode.nodeType === Node.ELEMENT_NODE || childNode.nodeType === Node.TEXT_NODE) { result += childNode.textContent; } @@ -228,7 +229,7 @@ export default class Element extends Node implements IElement { * @param textContent Text content. */ public set textContent(textContent: string) { - for (const child of this.__childNodes__.slice()) { + for (const child of this[PropertySymbol.childNodes].slice()) { this.removeChild(child); } if (textContent) { @@ -251,7 +252,7 @@ export default class Element extends Node implements IElement { * @param html HTML. */ public set innerHTML(html: string) { - for (const child of this.__childNodes__.slice()) { + for (const child of this[PropertySymbol.childNodes].slice()) { this.removeChild(child); } @@ -282,7 +283,7 @@ export default class Element extends Node implements IElement { * @returns Element. */ public get firstElementChild(): IElement { - return this.__children__[0] ?? null; + return this[PropertySymbol.children][0] ?? null; } /** @@ -291,7 +292,7 @@ export default class Element extends Node implements IElement { * @returns Element. */ public get lastElementChild(): IElement { - return this.__children__[this.__children__.length - 1] ?? null; + return this[PropertySymbol.children][this[PropertySymbol.children].length - 1] ?? null; } /** @@ -300,7 +301,7 @@ export default class Element extends Node implements IElement { * @returns Element. */ public get childElementCount(): number { - return this.__children__.length; + return this[PropertySymbol.children].length; } /** @@ -347,7 +348,7 @@ export default class Element extends Node implements IElement { escapeEntities: false }); let xml = ''; - for (const node of this.__childNodes__) { + for (const node of this[PropertySymbol.childNodes]) { xml += xmlSerializer.serializeToString(node); } return xml; @@ -374,9 +375,9 @@ export default class Element extends Node implements IElement { } if (deep) { - for (const node of clone.__childNodes__) { + for (const node of clone[PropertySymbol.childNodes]) { if (node.nodeType === Node.ELEMENT_NODE) { - clone.__children__.push(node); + clone[PropertySymbol.children].push(node); } } } @@ -520,7 +521,7 @@ export default class Element extends Node implements IElement { public insertAdjacentHTML(position: TInsertAdjacentPositions, text: string): void { for (const node of (( XMLParser.parse(this.ownerDocument, text) - )).__childNodes__.slice()) { + ))[PropertySymbol.childNodes].slice()) { this.insertAdjacentElement(position, node); } } @@ -689,22 +690,22 @@ export default class Element extends Node implements IElement { * @returns Shadow root. */ public attachShadow(shadowRootInit: { mode: string }): IShadowRoot { - if (this.__shadowRoot__) { + if (this[PropertySymbol.shadowRoot]) { throw new DOMException('Shadow root has already been attached.'); } - this.ownerDocument.__defaultView__.ShadowRoot.__ownerDocument__ = this.ownerDocument; - (this.__shadowRoot__) = new this.ownerDocument.__defaultView__.ShadowRoot(); - this.ownerDocument.__defaultView__.ShadowRoot.__ownerDocument__ = null; - (this.__shadowRoot__.host) = this; - (this.__shadowRoot__.mode) = shadowRootInit.mode; - (this.__shadowRoot__).__connectToNode__(this); + this.ownerDocument[PropertySymbol.defaultView].ShadowRoot[PropertySymbol.ownerDocument] = this.ownerDocument; + (this[PropertySymbol.shadowRoot]) = new this.ownerDocument[PropertySymbol.defaultView].ShadowRoot(); + this.ownerDocument[PropertySymbol.defaultView].ShadowRoot[PropertySymbol.ownerDocument] = null; + (this[PropertySymbol.shadowRoot].host) = this; + (this[PropertySymbol.shadowRoot].mode) = shadowRootInit.mode; + (this[PropertySymbol.shadowRoot])[PropertySymbol.connectToNode](this); - if (this.__shadowRoot__.mode === 'open') { - (this.shadowRoot) = this.__shadowRoot__; + if (this[PropertySymbol.shadowRoot].mode === 'open') { + (this.shadowRoot) = this[PropertySymbol.shadowRoot]; } - return this.__shadowRoot__; + return this[PropertySymbol.shadowRoot]; } /** @@ -888,7 +889,7 @@ export default class Element extends Node implements IElement { public scroll(x: { top?: number; left?: number; behavior?: string } | number, y?: number): void { if (typeof x === 'object') { if (x.behavior === 'smooth') { - this.ownerDocument.__defaultView__.setTimeout(() => { + this.ownerDocument[PropertySymbol.defaultView].setTimeout(() => { if (x.top !== undefined) { (this.scrollTop) = x.top; } @@ -929,7 +930,7 @@ export default class Element extends Node implements IElement { public override dispatchEvent(event: Event): boolean { const returnValue = super.dispatchEvent(event); const browserSettings = WindowBrowserSettingsReader.getSettings( - this.ownerDocument.__defaultView__ + this.ownerDocument[PropertySymbol.defaultView] ); if ( @@ -937,21 +938,21 @@ export default class Element extends Node implements IElement { !browserSettings.disableJavaScriptEvaluation && (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && - !event.__immediatePropagationStopped__ + !event[PropertySymbol.immediatePropagationStopped] ) { const attribute = this.getAttribute('on' + event.type); - if (attribute && !event.__immediatePropagationStopped__) { - const code = `//# sourceURL=${this.ownerDocument.__defaultView__.location.href}\n${attribute}`; + if (attribute && !event[PropertySymbol.immediatePropagationStopped]) { + const code = `//# sourceURL=${this.ownerDocument[PropertySymbol.defaultView].location.href}\n${attribute}`; if ( browserSettings.disableErrorCapturing || browserSettings.errorCapturing !== BrowserErrorCapturingEnum.tryAndCatch ) { - this.ownerDocument.__defaultView__.eval(code); + this.ownerDocument[PropertySymbol.defaultView].eval(code); } else { - WindowErrorUtility.captureError(this.ownerDocument.__defaultView__, () => - this.ownerDocument.__defaultView__.eval(code) + WindowErrorUtility.captureError(this.ownerDocument[PropertySymbol.defaultView], () => + this.ownerDocument[PropertySymbol.defaultView].eval(code) ); } } @@ -966,7 +967,7 @@ export default class Element extends Node implements IElement { * @param name Name. * @returns Attribute name based on namespace. */ - protected __getAttributeName__(name): string { + protected [PropertySymbol.getAttributeName](name): string { if (this.namespaceURI === NamespaceURI.svg) { return name; } diff --git a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts index 3b10e67a1..b2c21d965 100644 --- a/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/element/ElementNamedNodeMap.ts @@ -1,4 +1,5 @@ import NamespaceURI from '../../config/NamespaceURI.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import MutationRecord from '../../mutation-observer/MutationRecord.js'; import MutationTypeEnum from '../../mutation-observer/MutationTypeEnum.js'; import NamedNodeMap from '../../named-node-map/NamedNodeMap.js'; @@ -13,7 +14,7 @@ import IElement from './IElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class ElementNamedNodeMap extends NamedNodeMap { - protected __ownerElement__: IElement; + protected [PropertySymbol.ownerElement]: IElement; /** * Constructor. @@ -22,21 +23,21 @@ export default class ElementNamedNodeMap extends NamedNodeMap { */ constructor(ownerElement: IElement) { super(); - this.__ownerElement__ = ownerElement; + this[PropertySymbol.ownerElement] = ownerElement; } /** * @override */ public override getNamedItem(name: string): IAttr | null { - return this.__namedItems__[this.__getAttributeName__(name)] || null; + return this[PropertySymbol.namedItems][this[PropertySymbol.getAttributeName](name)] || null; } /** * @override */ public override getNamedItemNS(namespace: string, localName: string): IAttr | null { - return super.getNamedItemNS(namespace, this.__getAttributeName__(localName)); + return super.getNamedItemNS(namespace, this[PropertySymbol.getAttributeName](localName)); } /** @@ -47,57 +48,57 @@ export default class ElementNamedNodeMap extends NamedNodeMap { return null; } - item.name = this.__getAttributeName__(item.name); - (item.ownerElement) = this.__ownerElement__; + item.name = this[PropertySymbol.getAttributeName](item.name); + (item.ownerElement) = this[PropertySymbol.ownerElement]; const replacedItem = super.setNamedItem(item); const oldValue = replacedItem ? replacedItem.value : null; - if (this.__ownerElement__.isConnected) { - this.__ownerElement__.ownerDocument['__cacheID__']++; + if (this[PropertySymbol.ownerElement].isConnected) { + this[PropertySymbol.ownerElement].ownerDocument[PropertySymbol.cacheID]++; } - if (item.name === 'class' && this.__ownerElement__['__classList__']) { - this.__ownerElement__['__classList__'].__updateIndices__(); + if (item.name === 'class' && this[PropertySymbol.ownerElement][PropertySymbol.classList]) { + this[PropertySymbol.ownerElement][PropertySymbol.classList][PropertySymbol.updateIndices](); } if (item.name === 'id' || item.name === 'name') { if ( - this.__ownerElement__.parentNode && - (this.__ownerElement__.parentNode).__children__ && + this[PropertySymbol.ownerElement].parentNode && + (this[PropertySymbol.ownerElement].parentNode)[PropertySymbol.children] && item.value !== oldValue ) { if (oldValue) { (>( - (this.__ownerElement__.parentNode).__children__ - )).__removeNamedItem__(this.__ownerElement__, oldValue); + (this[PropertySymbol.ownerElement].parentNode)[PropertySymbol.children] + ))[PropertySymbol.removeNamedItem](this[PropertySymbol.ownerElement], oldValue); } if (item.value) { (>( - (this.__ownerElement__.parentNode).__children__ - )).__appendNamedItem__(this.__ownerElement__, item.value); + (this[PropertySymbol.ownerElement].parentNode)[PropertySymbol.children] + ))[PropertySymbol.appendNamedItem](this[PropertySymbol.ownerElement], item.value); } } } if ( - this.__ownerElement__.attributeChangedCallback && - (this.__ownerElement__.constructor).__observedAttributes__ && - (this.__ownerElement__.constructor).__observedAttributes__.includes(item.name) + this[PropertySymbol.ownerElement].attributeChangedCallback && + (this[PropertySymbol.ownerElement].constructor)[PropertySymbol.observedAttributes] && + (this[PropertySymbol.ownerElement].constructor)[PropertySymbol.observedAttributes].includes(item.name) ) { - this.__ownerElement__.attributeChangedCallback(item.name, oldValue, item.value); + this[PropertySymbol.ownerElement].attributeChangedCallback(item.name, oldValue, item.value); } // MutationObserver - if (this.__ownerElement__['__observers__'].length > 0) { - for (const observer of this.__ownerElement__['__observers__']) { + if (this[PropertySymbol.ownerElement][PropertySymbol.observers].length > 0) { + for (const observer of this[PropertySymbol.ownerElement][PropertySymbol.observers]) { if ( observer.options.attributes && (!observer.options.attributeFilter || observer.options.attributeFilter.includes(item.name)) ) { const record = new MutationRecord(); - record.target = this.__ownerElement__; + record.target = this[PropertySymbol.ownerElement]; record.type = MutationTypeEnum.attributes; record.attributeName = item.name; record.oldValue = observer.options.attributeOldValue ? oldValue : null; @@ -112,53 +113,53 @@ export default class ElementNamedNodeMap extends NamedNodeMap { /** * @override */ - public override __removeNamedItem__(name: string): IAttr | null { - const removedItem = super.__removeNamedItem__(this.__getAttributeName__(name)); + public override [PropertySymbol.removeNamedItem](name: string): IAttr | null { + const removedItem = super[PropertySymbol.removeNamedItem](this[PropertySymbol.getAttributeName](name)); if (!removedItem) { return null; } - if (this.__ownerElement__.isConnected) { - this.__ownerElement__.ownerDocument['__cacheID__']++; + if (this[PropertySymbol.ownerElement].isConnected) { + this[PropertySymbol.ownerElement].ownerDocument[PropertySymbol.cacheID]++; } - if (removedItem.name === 'class' && this.__ownerElement__['__classList__']) { - this.__ownerElement__['__classList__'].__updateIndices__(); + if (removedItem.name === 'class' && this[PropertySymbol.ownerElement][PropertySymbol.classList]) { + this[PropertySymbol.ownerElement][PropertySymbol.classList][PropertySymbol.updateIndices](); } if (removedItem.name === 'id' || removedItem.name === 'name') { if ( - this.__ownerElement__.parentNode && - (this.__ownerElement__.parentNode).__children__ && + this[PropertySymbol.ownerElement].parentNode && + (this[PropertySymbol.ownerElement].parentNode)[PropertySymbol.children] && removedItem.value ) { (>( - (this.__ownerElement__.parentNode).__children__ - )).__removeNamedItem__(this.__ownerElement__, removedItem.value); + (this[PropertySymbol.ownerElement].parentNode)[PropertySymbol.children] + ))[PropertySymbol.removeNamedItem](this[PropertySymbol.ownerElement], removedItem.value); } } if ( - this.__ownerElement__.attributeChangedCallback && - (this.__ownerElement__.constructor).__observedAttributes__ && - (this.__ownerElement__.constructor).__observedAttributes__.includes( + this[PropertySymbol.ownerElement].attributeChangedCallback && + (this[PropertySymbol.ownerElement].constructor)[PropertySymbol.observedAttributes] && + (this[PropertySymbol.ownerElement].constructor)[PropertySymbol.observedAttributes].includes( removedItem.name ) ) { - this.__ownerElement__.attributeChangedCallback(removedItem.name, removedItem.value, null); + this[PropertySymbol.ownerElement].attributeChangedCallback(removedItem.name, removedItem.value, null); } // MutationObserver - if (this.__ownerElement__['__observers__'].length > 0) { - for (const observer of this.__ownerElement__['__observers__']) { + if (this[PropertySymbol.ownerElement][PropertySymbol.observers].length > 0) { + for (const observer of this[PropertySymbol.ownerElement][PropertySymbol.observers]) { if ( observer.options.attributes && (!observer.options.attributeFilter || observer.options.attributeFilter.includes(removedItem.name)) ) { const record = new MutationRecord(); - record.target = this.__ownerElement__; + record.target = this[PropertySymbol.ownerElement]; record.type = MutationTypeEnum.attributes; record.attributeName = removedItem.name; record.oldValue = observer.options.attributeOldValue ? removedItem.value : null; @@ -174,7 +175,7 @@ export default class ElementNamedNodeMap extends NamedNodeMap { * @override */ public override removeNamedItemNS(namespace: string, localName: string): IAttr | null { - return super.removeNamedItemNS(namespace, this.__getAttributeName__(localName)); + return super.removeNamedItemNS(namespace, this[PropertySymbol.getAttributeName](localName)); } /** @@ -183,8 +184,8 @@ export default class ElementNamedNodeMap extends NamedNodeMap { * @param name Name. * @returns Attribute name based on namespace. */ - protected __getAttributeName__(name): string { - if (this.__ownerElement__.namespaceURI === NamespaceURI.svg) { + protected [PropertySymbol.getAttributeName](name): string { + if (this[PropertySymbol.ownerElement].namespaceURI === NamespaceURI.svg) { return name; } return name.toLowerCase(); diff --git a/packages/happy-dom/src/nodes/element/ElementUtility.ts b/packages/happy-dom/src/nodes/element/ElementUtility.ts index b1d308530..8fdb1ac1c 100644 --- a/packages/happy-dom/src/nodes/element/ElementUtility.ts +++ b/packages/happy-dom/src/nodes/element/ElementUtility.ts @@ -1,4 +1,5 @@ import NodeTypeEnum from '../node/NodeTypeEnum.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IElement from './IElement.js'; import INode from '../node/INode.js'; import HTMLCollection from './HTMLCollection.js'; @@ -42,7 +43,7 @@ export default class ElementUtility { } if (node.parentNode) { const parentNodeChildren = >( - (node.parentNode).__children__ + (node.parentNode)[PropertySymbol.children] ); if (parentNodeChildren) { @@ -51,7 +52,7 @@ export default class ElementUtility { for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const attribute = (node).attributes.getNamedItem(attributeName); if (attribute) { - parentNodeChildren.__removeNamedItem__(node, attribute.value); + parentNodeChildren[PropertySymbol.removeNamedItem](node, attribute.value); } } @@ -60,13 +61,13 @@ export default class ElementUtility { } } const ancestorNodeChildren = >( - (ancestorNode).__children__ + (ancestorNode)[PropertySymbol.children] ); for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const attribute = (node).attributes.getNamedItem(attributeName); if (attribute) { - ancestorNodeChildren.__appendNamedItem__(node, attribute.value); + ancestorNodeChildren[PropertySymbol.appendNamedItem](node, attribute.value); } } @@ -93,14 +94,14 @@ export default class ElementUtility { ): INode { if (node.nodeType === NodeTypeEnum.elementNode) { const ancestorNodeChildren = >( - (ancestorNode).__children__ + (ancestorNode)[PropertySymbol.children] ); const index = ancestorNodeChildren.indexOf(node); if (index !== -1) { for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const attribute = (node).attributes.getNamedItem(attributeName); if (attribute) { - ancestorNodeChildren.__removeNamedItem__(node, attribute.value); + ancestorNodeChildren[PropertySymbol.removeNamedItem](node, attribute.value); } } ancestorNodeChildren.splice(index, 1); @@ -142,7 +143,7 @@ export default class ElementUtility { } if (newNode.parentNode) { const parentNodeChildren = >( - (newNode.parentNode).__children__ + (newNode.parentNode)[PropertySymbol.children] ); if (parentNodeChildren) { @@ -151,7 +152,7 @@ export default class ElementUtility { for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const attribute = (newNode).attributes.getNamedItem(attributeName); if (attribute) { - parentNodeChildren.__removeNamedItem__(newNode, attribute.value); + parentNodeChildren[PropertySymbol.removeNamedItem](newNode, attribute.value); } } @@ -161,7 +162,7 @@ export default class ElementUtility { } const ancestorNodeChildren = >( - (ancestorNode).__children__ + (ancestorNode)[PropertySymbol.children] ); if (referenceNode.nodeType === NodeTypeEnum.elementNode) { @@ -172,7 +173,7 @@ export default class ElementUtility { } else { ancestorNodeChildren.length = 0; - for (const node of (ancestorNode).__childNodes__) { + for (const node of (ancestorNode)[PropertySymbol.childNodes]) { if (node === referenceNode) { ancestorNodeChildren.push(newNode); } @@ -185,7 +186,7 @@ export default class ElementUtility { for (const attributeName of NAMED_ITEM_ATTRIBUTES) { const attribute = (newNode).attributes.getNamedItem(attributeName); if (attribute) { - ancestorNodeChildren.__appendNamedItem__(newNode, attribute.value); + ancestorNodeChildren[PropertySymbol.appendNamedItem](newNode, attribute.value); } } diff --git a/packages/happy-dom/src/nodes/element/HTMLCollection.ts b/packages/happy-dom/src/nodes/element/HTMLCollection.ts index 0937c8b9d..99a7e6fc2 100644 --- a/packages/happy-dom/src/nodes/element/HTMLCollection.ts +++ b/packages/happy-dom/src/nodes/element/HTMLCollection.ts @@ -1,10 +1,11 @@ import IHTMLCollection from './IHTMLCollection.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; /** * HTML collection. */ export default class HTMLCollection extends Array implements IHTMLCollection { - protected __namedItems__: { [k: string]: T[] } = {}; + protected [PropertySymbol.namedItems]: { [k: string]: T[] } = {}; /** * Returns item by index. @@ -22,8 +23,8 @@ export default class HTMLCollection extends Array implements IHTMLCollection< * @returns Node. */ public namedItem(name: string): T | null { - return this.__namedItems__[name] && this.__namedItems__[name].length - ? this.__namedItems__[name][0] + return this[PropertySymbol.namedItems][name] && this[PropertySymbol.namedItems][name].length + ? this[PropertySymbol.namedItems][name][0] : null; } @@ -33,16 +34,16 @@ export default class HTMLCollection extends Array implements IHTMLCollection< * @param node Node. * @param name Name. */ - public __appendNamedItem__(node: T, name: string): void { + public [PropertySymbol.appendNamedItem](node: T, name: string): void { if (name) { - this.__namedItems__[name] = this.__namedItems__[name] || []; + this[PropertySymbol.namedItems][name] = this[PropertySymbol.namedItems][name] || []; - if (!this.__namedItems__[name].includes(node)) { - this.__namedItems__[name].push(node); + if (!this[PropertySymbol.namedItems][name].includes(node)) { + this[PropertySymbol.namedItems][name].push(node); } - if (!this.hasOwnProperty(name) && this.__isValidPropertyName__(name)) { - this[name] = this.__namedItems__[name][0]; + if (!this.hasOwnProperty(name) && this[PropertySymbol.isValidPropertyName](name)) { + this[name] = this[PropertySymbol.namedItems][name][0]; } } } @@ -53,20 +54,20 @@ export default class HTMLCollection extends Array implements IHTMLCollection< * @param node Node. * @param name Name. */ - public __removeNamedItem__(node: T, name: string): void { - if (name && this.__namedItems__[name]) { - const index = this.__namedItems__[name].indexOf(node); + public [PropertySymbol.removeNamedItem](node: T, name: string): void { + if (name && this[PropertySymbol.namedItems][name]) { + const index = this[PropertySymbol.namedItems][name].indexOf(node); if (index > -1) { - this.__namedItems__[name].splice(index, 1); + this[PropertySymbol.namedItems][name].splice(index, 1); - if (this.__namedItems__[name].length === 0) { - delete this.__namedItems__[name]; - if (this.hasOwnProperty(name) && this.__isValidPropertyName__(name)) { + if (this[PropertySymbol.namedItems][name].length === 0) { + delete this[PropertySymbol.namedItems][name]; + if (this.hasOwnProperty(name) && this[PropertySymbol.isValidPropertyName](name)) { delete this[name]; } - } else if (this.__isValidPropertyName__(name)) { - this[name] = this.__namedItems__[name][0]; + } else if (this[PropertySymbol.isValidPropertyName](name)) { + this[name] = this[PropertySymbol.namedItems][name][0]; } } } @@ -78,7 +79,7 @@ export default class HTMLCollection extends Array implements IHTMLCollection< * @param name Name. * @returns True if the property name is valid. */ - protected __isValidPropertyName__(name: string): boolean { + protected [PropertySymbol.isValidPropertyName](name: string): boolean { return ( !this.constructor.prototype.hasOwnProperty(name) && !Array.prototype.hasOwnProperty(name) && diff --git a/packages/happy-dom/src/nodes/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts index e49c7379f..dd20b0a26 100644 --- a/packages/happy-dom/src/nodes/element/IElement.ts +++ b/packages/happy-dom/src/nodes/element/IElement.ts @@ -1,4 +1,5 @@ import IShadowRoot from '../shadow-root/IShadowRoot.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IAttr from '../attr/IAttr.js'; import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import DOMRect from './DOMRect.js'; @@ -182,10 +183,10 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode, /** * Attaches a shadow root. * - * @param __shadowRoot__Init Shadow root init. + * @param [PropertySymbol.shadowRoot]Init Shadow root init. * @returns Shadow root. */ - attachShadow(__shadowRoot__Init: { mode: string }): IShadowRoot; + attachShadow([PropertySymbol.shadowRoot]Init: { mode: string }): IShadowRoot; /** * Scrolls to a particular set of coordinates. diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts index e6990bc8a..f53ccf27c 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -1,4 +1,5 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; import IDOMTokenList from '../../dom-token-list/IDOMTokenList.js'; import IHTMLAnchorElement from './IHTMLAnchorElement.js'; @@ -17,8 +18,8 @@ import EventPhaseEnum from '../../event/EventPhaseEnum.js'; */ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAnchorElement { public override readonly attributes: INamedNodeMap = new HTMLAnchorElementNamedNodeMap(this); - public __relList__: DOMTokenList = null; - public __url__: URL | null = null; + public [PropertySymbol.relList]: DOMTokenList = null; + public [PropertySymbol.url]: URL | null = null; /** * Returns download. @@ -44,7 +45,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Hash. */ public get hash(): string { - return this.__url__?.hash ?? ''; + return this[PropertySymbol.url]?.hash ?? ''; } /** @@ -53,9 +54,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param hash Hash. */ public set hash(hash: string) { - if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { - this.__url__.hash = hash; - this.setAttribute('href', this.__url__.toString()); + if (this[PropertySymbol.url] && !HTMLAnchorElementUtility.isBlobURL(this[PropertySymbol.url])) { + this[PropertySymbol.url].hash = hash; + this.setAttribute('href', this[PropertySymbol.url].toString()); } } @@ -65,8 +66,8 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Href. */ public get href(): string | null { - if (this.__url__) { - return this.__url__.toString(); + if (this[PropertySymbol.url]) { + return this[PropertySymbol.url].toString(); } return this.getAttribute('href') || ''; @@ -105,7 +106,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Origin. */ public get origin(): string { - return this.__url__?.origin ?? ''; + return this[PropertySymbol.url]?.origin ?? ''; } /** @@ -132,7 +133,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Protocol. */ public get protocol(): string { - return this.__url__?.protocol ?? ''; + return this[PropertySymbol.url]?.protocol ?? ''; } /** @@ -141,9 +142,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param protocol Protocol. */ public set protocol(protocol: string) { - if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { - this.__url__.protocol = protocol; - this.setAttribute('href', this.__url__.toString()); + if (this[PropertySymbol.url] && !HTMLAnchorElementUtility.isBlobURL(this[PropertySymbol.url])) { + this[PropertySymbol.url].protocol = protocol; + this.setAttribute('href', this[PropertySymbol.url].toString()); } } @@ -153,7 +154,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Username. */ public get username(): string { - return this.__url__?.username ?? ''; + return this[PropertySymbol.url]?.username ?? ''; } /** @@ -163,13 +164,13 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho */ public set username(username: string) { if ( - this.__url__ && - !HTMLAnchorElementUtility.isBlobURL(this.__url__) && - this.__url__.host && - this.__url__.protocol != 'file' + this[PropertySymbol.url] && + !HTMLAnchorElementUtility.isBlobURL(this[PropertySymbol.url]) && + this[PropertySymbol.url].host && + this[PropertySymbol.url].protocol != 'file' ) { - this.__url__.username = username; - this.setAttribute('href', this.__url__.toString()); + this[PropertySymbol.url].username = username; + this.setAttribute('href', this[PropertySymbol.url].toString()); } } @@ -179,7 +180,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Password. */ public get password(): string { - return this.__url__?.password ?? ''; + return this[PropertySymbol.url]?.password ?? ''; } /** @@ -189,13 +190,13 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho */ public set password(password: string) { if ( - this.__url__ && - !HTMLAnchorElementUtility.isBlobURL(this.__url__) && - this.__url__.host && - this.__url__.protocol != 'file' + this[PropertySymbol.url] && + !HTMLAnchorElementUtility.isBlobURL(this[PropertySymbol.url]) && + this[PropertySymbol.url].host && + this[PropertySymbol.url].protocol != 'file' ) { - this.__url__.password = password; - this.setAttribute('href', this.__url__.toString()); + this[PropertySymbol.url].password = password; + this.setAttribute('href', this[PropertySymbol.url].toString()); } } @@ -205,7 +206,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Pathname. */ public get pathname(): string { - return this.__url__?.pathname ?? ''; + return this[PropertySymbol.url]?.pathname ?? ''; } /** @@ -214,9 +215,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param pathname Pathname. */ public set pathname(pathname: string) { - if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { - this.__url__.pathname = pathname; - this.setAttribute('href', this.__url__.toString()); + if (this[PropertySymbol.url] && !HTMLAnchorElementUtility.isBlobURL(this[PropertySymbol.url])) { + this[PropertySymbol.url].pathname = pathname; + this.setAttribute('href', this[PropertySymbol.url].toString()); } } @@ -226,7 +227,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Port. */ public get port(): string { - return this.__url__?.port ?? ''; + return this[PropertySymbol.url]?.port ?? ''; } /** @@ -236,13 +237,13 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho */ public set port(port: string) { if ( - this.__url__ && - !HTMLAnchorElementUtility.isBlobURL(this.__url__) && - this.__url__.host && - this.__url__.protocol != 'file' + this[PropertySymbol.url] && + !HTMLAnchorElementUtility.isBlobURL(this[PropertySymbol.url]) && + this[PropertySymbol.url].host && + this[PropertySymbol.url].protocol != 'file' ) { - this.__url__.port = port; - this.setAttribute('href', this.__url__.toString()); + this[PropertySymbol.url].port = port; + this.setAttribute('href', this[PropertySymbol.url].toString()); } } @@ -252,7 +253,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Host. */ public get host(): string { - return this.__url__?.host ?? ''; + return this[PropertySymbol.url]?.host ?? ''; } /** @@ -261,9 +262,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param host Host. */ public set host(host: string) { - if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { - this.__url__.host = host; - this.setAttribute('href', this.__url__.toString()); + if (this[PropertySymbol.url] && !HTMLAnchorElementUtility.isBlobURL(this[PropertySymbol.url])) { + this[PropertySymbol.url].host = host; + this.setAttribute('href', this[PropertySymbol.url].toString()); } } @@ -273,7 +274,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Hostname. */ public get hostname(): string { - return this.__url__?.hostname ?? ''; + return this[PropertySymbol.url]?.hostname ?? ''; } /** @@ -282,9 +283,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param hostname Hostname. */ public set hostname(hostname: string) { - if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { - this.__url__.hostname = hostname; - this.setAttribute('href', this.__url__.toString()); + if (this[PropertySymbol.url] && !HTMLAnchorElementUtility.isBlobURL(this[PropertySymbol.url])) { + this[PropertySymbol.url].hostname = hostname; + this.setAttribute('href', this[PropertySymbol.url].toString()); } } @@ -330,10 +331,10 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Rel list. */ public get relList(): IDOMTokenList { - if (!this.__relList__) { - this.__relList__ = new DOMTokenList(this, 'rel'); + if (!this[PropertySymbol.relList]) { + this[PropertySymbol.relList] = new DOMTokenList(this, 'rel'); } - return this.__relList__; + return this[PropertySymbol.relList]; } /** @@ -342,7 +343,7 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @returns Search. */ public get search(): string { - return this.__url__?.search ?? ''; + return this[PropertySymbol.url]?.search ?? ''; } /** @@ -351,9 +352,9 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho * @param search Search. */ public set search(search: string) { - if (this.__url__ && !HTMLAnchorElementUtility.isBlobURL(this.__url__)) { - this.__url__.search = search; - this.setAttribute('href', this.__url__.toString()); + if (this[PropertySymbol.url] && !HTMLAnchorElementUtility.isBlobURL(this[PropertySymbol.url])) { + this[PropertySymbol.url].search = search; + this.setAttribute('href', this[PropertySymbol.url].toString()); } } @@ -429,10 +430,10 @@ export default class HTMLAnchorElement extends HTMLElement implements IHTMLAncho (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && !event.defaultPrevented && - this.__url__ + this[PropertySymbol.url] ) { - this.ownerDocument.__defaultView__.open(this.__url__.toString(), this.target || '_self'); - if (this.ownerDocument.__defaultView__.closed) { + this.ownerDocument[PropertySymbol.defaultView].open(this[PropertySymbol.url].toString(), this.target || '_self'); + if (this.ownerDocument[PropertySymbol.defaultView].closed) { event.stopImmediatePropagation(); } } diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts index 0115d2977..7f7ccb0b5 100644 --- a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElementNamedNodeMap.ts @@ -1,4 +1,5 @@ import IAttr from '../attr/IAttr.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; import HTMLAnchorElement from './HTMLAnchorElement.js'; import HTMLAnchorElementUtility from './HTMLAnchorElementUtility.js'; @@ -9,7 +10,7 @@ import HTMLAnchorElementUtility from './HTMLAnchorElementUtility.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLAnchorElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected __ownerElement__: HTMLAnchorElement; + protected [PropertySymbol.ownerElement]: HTMLAnchorElement; /** * @override @@ -17,11 +18,11 @@ export default class HTMLAnchorElementNamedNodeMap extends HTMLElementNamedNodeM public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if (item.name === 'rel' && this.__ownerElement__.__relList__) { - this.__ownerElement__.__relList__.__updateIndices__(); + if (item.name === 'rel' && this[PropertySymbol.ownerElement][PropertySymbol.relList]) { + this[PropertySymbol.ownerElement][PropertySymbol.relList][PropertySymbol.updateIndices](); } else if (item.name === 'href') { - this.__ownerElement__.__url__ = HTMLAnchorElementUtility.getUrl( - this.__ownerElement__.ownerDocument, + this[PropertySymbol.ownerElement][PropertySymbol.url] = HTMLAnchorElementUtility.getUrl( + this[PropertySymbol.ownerElement].ownerDocument, item.value ); } @@ -32,14 +33,14 @@ export default class HTMLAnchorElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override __removeNamedItem__(name: string): IAttr | null { - const removedItem = super.__removeNamedItem__(name); + public override [PropertySymbol.removeNamedItem](name: string): IAttr | null { + const removedItem = super[PropertySymbol.removeNamedItem](name); if (removedItem) { - if (removedItem.name === 'rel' && this.__ownerElement__.__relList__) { - this.__ownerElement__.__relList__.__updateIndices__(); + if (removedItem.name === 'rel' && this[PropertySymbol.ownerElement][PropertySymbol.relList]) { + this[PropertySymbol.ownerElement][PropertySymbol.relList][PropertySymbol.updateIndices](); } else if (removedItem.name === 'href') { - this.__ownerElement__.__url__ = null; + this[PropertySymbol.ownerElement][PropertySymbol.url] = null; } } diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts index 130938e4f..86767a50c 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElement.ts @@ -1,4 +1,5 @@ import Event from '../../event/Event.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import EventPhaseEnum from '../../event/EventPhaseEnum.js'; import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import ValidityState from '../../validity-state/ValidityState.js'; @@ -129,7 +130,7 @@ export default class HTMLButtonElement extends HTMLElement implements IHTMLButto * @returns Form. */ public get form(): IHTMLFormElement { - return this.__formNode__; + return this[PropertySymbol.formNode]; } /** @@ -187,10 +188,10 @@ export default class HTMLButtonElement extends HTMLElement implements IHTMLButto event.type === 'click' && (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && - this.__formNode__ && + this[PropertySymbol.formNode] && this.isConnected ) { - const form = this.__formNode__; + const form = this[PropertySymbol.formNode]; switch (this.type) { case 'submit': form.requestSubmit(); @@ -207,19 +208,19 @@ export default class HTMLButtonElement extends HTMLElement implements IHTMLButto /** * @override */ - public override __connectToNode__(parentNode: INode = null): void { - const oldFormNode = this.__formNode__; + public override [PropertySymbol.connectToNode](parentNode: INode = null): void { + const oldFormNode = this[PropertySymbol.formNode]; - super.__connectToNode__(parentNode); + super[PropertySymbol.connectToNode](parentNode); - if (oldFormNode !== this.__formNode__) { + if (oldFormNode !== this[PropertySymbol.formNode]) { if (oldFormNode) { - oldFormNode.__removeFormControlItem__(this, this.name); - oldFormNode.__removeFormControlItem__(this, this.id); + oldFormNode[PropertySymbol.removeFormControlItem](this, this.name); + oldFormNode[PropertySymbol.removeFormControlItem](this, this.id); } - if (this.__formNode__) { - (this.__formNode__).__appendFormControlItem__(this, this.name); - (this.__formNode__).__appendFormControlItem__(this, this.id); + if (this[PropertySymbol.formNode]) { + (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem](this, this.name); + (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem](this, this.id); } } } diff --git a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts index 1ee5e2dbe..9c1b6e113 100644 --- a/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-button-element/HTMLButtonElementNamedNodeMap.ts @@ -1,4 +1,5 @@ import IAttr from '../attr/IAttr.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLButtonElement from './HTMLButtonElement.js'; @@ -9,7 +10,7 @@ import HTMLButtonElement from './HTMLButtonElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLButtonElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected __ownerElement__: HTMLButtonElement; + protected [PropertySymbol.ownerElement]: HTMLButtonElement; /** * @override @@ -17,16 +18,16 @@ export default class HTMLButtonElementNamedNodeMap extends HTMLElementNamedNodeM public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if ((item.name === 'id' || item.name === 'name') && this.__ownerElement__.__formNode__) { + if ((item.name === 'id' || item.name === 'name') && this[PropertySymbol.ownerElement][PropertySymbol.formNode]) { if (replacedItem?.value) { - (this.__ownerElement__.__formNode__).__removeFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.removeFormControlItem]( + this[PropertySymbol.ownerElement], replacedItem.value ); } if (item.value) { - (this.__ownerElement__.__formNode__).__appendFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( + this[PropertySymbol.ownerElement], item.value ); } @@ -38,16 +39,16 @@ export default class HTMLButtonElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override __removeNamedItem__(name: string): IAttr | null { - const removedItem = super.__removeNamedItem__(name); + public override [PropertySymbol.removeNamedItem](name: string): IAttr | null { + const removedItem = super[PropertySymbol.removeNamedItem](name); if ( removedItem && (removedItem.name === 'id' || removedItem.name === 'name') && - this.__ownerElement__.__formNode__ + this[PropertySymbol.ownerElement][PropertySymbol.formNode] ) { - (this.__ownerElement__.__formNode__).__removeFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.removeFormControlItem]( + this[PropertySymbol.ownerElement], removedItem.value ); } diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts index 9e513c6ca..1ca646de0 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElement.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElement.ts @@ -1,4 +1,5 @@ import Element from '../element/Element.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IHTMLElement from './IHTMLElement.js'; import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration.js'; import PointerEvent from '../../event/events/PointerEvent.js'; @@ -31,7 +32,7 @@ export default class HTMLElement extends Element implements IHTMLElement { public readonly clientLeft = 0; public readonly clientTop = 0; - public __style__: CSSStyleDeclaration = null; + public [PropertySymbol.style]: CSSStyleDeclaration = null; #dataset: Dataset = null; // Events @@ -97,10 +98,10 @@ export default class HTMLElement extends Element implements IHTMLElement { let result = ''; - for (const childNode of this.__childNodes__) { + for (const childNode of this[PropertySymbol.childNodes]) { if (childNode.nodeType === NodeTypeEnum.elementNode) { const childElement = childNode; - const computedStyle = this.ownerDocument.__defaultView__.getComputedStyle(childElement); + const computedStyle = this.ownerDocument[PropertySymbol.defaultView].getComputedStyle(childElement); if (childElement.tagName !== 'SCRIPT' && childElement.tagName !== 'STYLE') { const display = computedStyle.display; @@ -143,7 +144,7 @@ export default class HTMLElement extends Element implements IHTMLElement { * @param innerText Inner text. */ public set innerText(text: string) { - for (const child of this.__childNodes__.slice()) { + for (const child of this[PropertySymbol.childNodes].slice()) { this.removeChild(child); } @@ -198,10 +199,10 @@ export default class HTMLElement extends Element implements IHTMLElement { * @returns Style. */ public get style(): CSSStyleDeclaration { - if (!this.__style__) { - this.__style__ = new CSSStyleDeclaration(this); + if (!this[PropertySymbol.style]) { + this[PropertySymbol.style] = new CSSStyleDeclaration(this); } - return this.__style__; + return this[PropertySymbol.style]; } /** @@ -307,8 +308,8 @@ export default class HTMLElement extends Element implements IHTMLElement { bubbles: true, composed: true }); - event.__target__ = this; - event.__currentTarget__ = this; + event[PropertySymbol.target] = this; + event[PropertySymbol.currentTarget] = this; this.dispatchEvent(event); } @@ -337,8 +338,8 @@ export default class HTMLElement extends Element implements IHTMLElement { (clone.contentEditable) = this.contentEditable; (clone.isContentEditable) = this.isContentEditable; - if (this.__style__) { - clone.style.cssText = this.__style__.cssText; + if (this[PropertySymbol.style]) { + clone.style.cssText = this[PropertySymbol.style].cssText; } return clone; diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts index 54252115b..bf2449233 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElementNamedNodeMap.ts @@ -1,4 +1,5 @@ import IAttr from '../attr/IAttr.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import ElementNamedNodeMap from '../element/ElementNamedNodeMap.js'; import HTMLElement from './HTMLElement.js'; @@ -8,7 +9,7 @@ import HTMLElement from './HTMLElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLElementNamedNodeMap extends ElementNamedNodeMap { - protected __ownerElement__: HTMLElement; + protected [PropertySymbol.ownerElement]: HTMLElement; /** * @override @@ -16,8 +17,8 @@ export default class HTMLElementNamedNodeMap extends ElementNamedNodeMap { public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if (item.name === 'style' && this.__ownerElement__.__style__) { - this.__ownerElement__.__style__.cssText = item.value; + if (item.name === 'style' && this[PropertySymbol.ownerElement][PropertySymbol.style]) { + this[PropertySymbol.ownerElement][PropertySymbol.style].cssText = item.value; } return replacedItem || null; @@ -26,11 +27,11 @@ export default class HTMLElementNamedNodeMap extends ElementNamedNodeMap { /** * @override */ - public override __removeNamedItem__(name: string): IAttr | null { - const removedItem = super.__removeNamedItem__(name); + public override [PropertySymbol.removeNamedItem](name: string): IAttr | null { + const removedItem = super[PropertySymbol.removeNamedItem](name); - if (removedItem && removedItem.name === 'style' && this.__ownerElement__.__style__) { - this.__ownerElement__.__style__.cssText = ''; + if (removedItem && removedItem.name === 'style' && this[PropertySymbol.ownerElement][PropertySymbol.style]) { + this[PropertySymbol.ownerElement][PropertySymbol.style].cssText = ''; } return removedItem; diff --git a/packages/happy-dom/src/nodes/html-element/HTMLElementUtility.ts b/packages/happy-dom/src/nodes/html-element/HTMLElementUtility.ts index b0e954bf8..c3b8cc7da 100644 --- a/packages/happy-dom/src/nodes/html-element/HTMLElementUtility.ts +++ b/packages/happy-dom/src/nodes/html-element/HTMLElementUtility.ts @@ -1,4 +1,5 @@ import FocusEvent from '../../event/events/FocusEvent.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IHTMLElement from '../html-element/IHTMLElement.js'; import ISVGElement from '../svg-element/ISVGElement.js'; @@ -12,13 +13,13 @@ export default class HTMLElementUtility { * @param element Element. */ public static blur(element: IHTMLElement | ISVGElement): void { - if (element.ownerDocument['__activeElement__'] !== element || !element.isConnected) { + if (element.ownerDocument[PropertySymbol.activeElement] !== element || !element.isConnected) { return; } - const relatedTarget = element.ownerDocument['__nextActiveElement__'] ?? null; + const relatedTarget = element.ownerDocument[PropertySymbol.nextActiveElement] ?? null; - element.ownerDocument['__activeElement__'] = null; + element.ownerDocument[PropertySymbol.activeElement] = null; element.dispatchEvent( new FocusEvent('blur', { @@ -42,23 +43,23 @@ export default class HTMLElementUtility { * @param element Element. */ public static focus(element: IHTMLElement | ISVGElement): void { - if (element.ownerDocument['__activeElement__'] === element || !element.isConnected) { + if (element.ownerDocument[PropertySymbol.activeElement] === element || !element.isConnected) { return; } // Set the next active element so `blur` can use it for `relatedTarget`. - element.ownerDocument['__nextActiveElement__'] = element; + element.ownerDocument[PropertySymbol.nextActiveElement] = element; - const relatedTarget = element.ownerDocument['__activeElement__']; + const relatedTarget = element.ownerDocument[PropertySymbol.activeElement]; - if (element.ownerDocument['__activeElement__'] !== null) { - element.ownerDocument['__activeElement__'].blur(); + if (element.ownerDocument[PropertySymbol.activeElement] !== null) { + element.ownerDocument[PropertySymbol.activeElement].blur(); } // Clean up after blur, so it does not affect next blur call. - element.ownerDocument['__nextActiveElement__'] = null; + element.ownerDocument[PropertySymbol.nextActiveElement] = null; - element.ownerDocument['__activeElement__'] = element; + element.ownerDocument[PropertySymbol.activeElement] = element; element.dispatchEvent( new FocusEvent('focus', { diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts index a2f5e78a6..1001a944d 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormControlsCollection.ts @@ -1,4 +1,5 @@ import IHTMLFormControlsCollection from './IHTMLFormControlsCollection.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IHTMLInputElement from '../html-input-element/IHTMLInputElement.js'; import IHTMLTextAreaElement from '../html-text-area-element/IHTMLTextAreaElement.js'; import IHTMLSelectElement from '../html-select-element/IHTMLSelectElement.js'; @@ -14,7 +15,7 @@ export default class HTMLFormControlsCollection extends Array implements IHTMLFormControlsCollection { - public __namedItems__: { [k: string]: RadioNodeList } = {}; + public [PropertySymbol.namedItems]: { [k: string]: RadioNodeList } = {}; /** * Returns item by index. @@ -42,11 +43,11 @@ export default class HTMLFormControlsCollection | IHTMLButtonElement | RadioNodeList | null { - if (this.__namedItems__[name] && this.__namedItems__[name].length) { - if (this.__namedItems__[name].length === 1) { - return this.__namedItems__[name][0]; + if (this[PropertySymbol.namedItems][name] && this[PropertySymbol.namedItems][name].length) { + if (this[PropertySymbol.namedItems][name].length === 1) { + return this[PropertySymbol.namedItems][name][0]; } - return this.__namedItems__[name]; + return this[PropertySymbol.namedItems][name]; } return null; } @@ -57,22 +58,22 @@ export default class HTMLFormControlsCollection * @param node Node. * @param name Name. */ - public __appendNamedItem__( + public [PropertySymbol.appendNamedItem]( node: IHTMLInputElement | IHTMLTextAreaElement | IHTMLSelectElement | IHTMLButtonElement, name: string ): void { if (name) { - this.__namedItems__[name] = this.__namedItems__[name] || new RadioNodeList(); + this[PropertySymbol.namedItems][name] = this[PropertySymbol.namedItems][name] || new RadioNodeList(); - if (!this.__namedItems__[name].includes(node)) { - this.__namedItems__[name].push(node); + if (!this[PropertySymbol.namedItems][name].includes(node)) { + this[PropertySymbol.namedItems][name].push(node); } - if (this.__isValidPropertyName__(name)) { + if (this[PropertySymbol.isValidPropertyName](name)) { this[name] = - this.__namedItems__[name].length > 1 - ? this.__namedItems__[name] - : this.__namedItems__[name][0]; + this[PropertySymbol.namedItems][name].length > 1 + ? this[PropertySymbol.namedItems][name] + : this[PropertySymbol.namedItems][name][0]; } } } @@ -83,26 +84,26 @@ export default class HTMLFormControlsCollection * @param node Node. * @param name Name. */ - public __removeNamedItem__( + public [PropertySymbol.removeNamedItem]( node: IHTMLInputElement | IHTMLTextAreaElement | IHTMLSelectElement | IHTMLButtonElement, name: string ): void { - if (name && this.__namedItems__[name]) { - const index = this.__namedItems__[name].indexOf(node); + if (name && this[PropertySymbol.namedItems][name]) { + const index = this[PropertySymbol.namedItems][name].indexOf(node); if (index > -1) { - this.__namedItems__[name].splice(index, 1); + this[PropertySymbol.namedItems][name].splice(index, 1); - if (this.__namedItems__[name].length === 0) { - delete this.__namedItems__[name]; - if (this.hasOwnProperty(name) && this.__isValidPropertyName__(name)) { + if (this[PropertySymbol.namedItems][name].length === 0) { + delete this[PropertySymbol.namedItems][name]; + if (this.hasOwnProperty(name) && this[PropertySymbol.isValidPropertyName](name)) { delete this[name]; } - } else if (this.__isValidPropertyName__(name)) { + } else if (this[PropertySymbol.isValidPropertyName](name)) { this[name] = - this.__namedItems__[name].length > 1 - ? this.__namedItems__[name] - : this.__namedItems__[name][0]; + this[PropertySymbol.namedItems][name].length > 1 + ? this[PropertySymbol.namedItems][name] + : this[PropertySymbol.namedItems][name][0]; } } } @@ -114,7 +115,7 @@ export default class HTMLFormControlsCollection * @param name Name. * @returns True if the property name is valid. */ - protected __isValidPropertyName__(name: string): boolean { + protected [PropertySymbol.isValidPropertyName](name: string): boolean { return ( !this.constructor.prototype.hasOwnProperty(name) && !Array.prototype.hasOwnProperty(name) && diff --git a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts index c2cc17942..a91cb1b27 100644 --- a/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts +++ b/packages/happy-dom/src/nodes/html-form-element/HTMLFormElement.ts @@ -1,4 +1,5 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IHTMLFormElement from './IHTMLFormElement.js'; import Event from '../../event/Event.js'; import SubmitEvent from '../../event/events/SubmitEvent.js'; @@ -27,7 +28,7 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle public onsubmit: (event: Event) => void | null = null; // Private properties - public __formNode__: INode = this; + public [PropertySymbol.formNode]: INode = this; /** * Returns name. @@ -222,10 +223,10 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle public reset(): void { for (const element of this.elements) { if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { - element['__value__'] = null; - element['__checked__'] = null; + element[PropertySymbol.value] = null; + element[PropertySymbol.checked] = null; } else if (element.tagName === 'TEXTAREA') { - element['__value__'] = null; + element[PropertySymbol.value] = null; } else if (element.tagName === 'SELECT') { let hasSelectedAttribute = false; for (const option of (element).options) { @@ -295,7 +296,7 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle * @param node Node. * @param name Name */ - public __appendFormControlItem__( + public [PropertySymbol.appendFormControlItem]( node: IHTMLInputElement | IHTMLTextAreaElement | IHTMLSelectElement | IHTMLButtonElement, name: string ): void { @@ -305,7 +306,7 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle (this.length) = this.elements.length; } - (this.elements).__appendNamedItem__(node, name); + (this.elements)[PropertySymbol.appendNamedItem](node, name); this[name] = this.elements[name]; } @@ -315,7 +316,7 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle * @param node Node. * @param name Name. */ - public __removeFormControlItem__( + public [PropertySymbol.removeFormControlItem]( node: IHTMLInputElement | IHTMLTextAreaElement | IHTMLSelectElement | IHTMLButtonElement, name: string ): void { @@ -330,7 +331,7 @@ export default class HTMLFormElement extends HTMLElement implements IHTMLFormEle (this.length)--; } - (this.elements).__removeNamedItem__(node, name); + (this.elements)[PropertySymbol.removeNamedItem](node, name); if (this.elements[name]) { this[name] = this.elements[name]; diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index 7a98283ef..930a480b9 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -1,4 +1,5 @@ import Event from '../../event/Event.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IBrowserWindow from '../../window/IBrowserWindow.js'; import IDocument from '../document/IDocument.js'; import HTMLElement from '../html-element/HTMLElement.js'; @@ -191,11 +192,11 @@ export default class HTMLIFrameElement extends HTMLElement implements IHTMLIFram /** * @override */ - public override __connectToNode__(parentNode: INode = null): void { + public override [PropertySymbol.connectToNode](parentNode: INode = null): void { const isConnected = this.isConnected; const isParentConnected = parentNode ? parentNode.isConnected : false; - super.__connectToNode__(parentNode); + super[PropertySymbol.connectToNode](parentNode); if (isConnected !== isParentConnected) { if (isParentConnected) { diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts index 7192bdbe3..3982d975e 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementPageLoader.ts @@ -1,4 +1,5 @@ import Event from '../../event/Event.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IBrowserWindow from '../../window/IBrowserWindow.js'; import CrossOriginBrowserWindow from '../../window/CrossOriginBrowserWindow.js'; import WindowErrorUtility from '../../window/WindowErrorUtility.js'; @@ -51,7 +52,7 @@ export default class HTMLIFrameElementPageLoader { return; } - const window = this.#element.ownerDocument.__defaultView__; + const window = this.#element.ownerDocument[PropertySymbol.defaultView]; const originURL = this.#browserParentFrame.window.location; const targetURL = BrowserFrameURL.getRelativeURL(this.#browserParentFrame, this.#element.src); diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index 1ed499093..c19a9a1fe 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -1,4 +1,5 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import ValidityState from '../../validity-state/ValidityState.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; @@ -40,13 +41,13 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE public formMethod = ''; // Any type of input - public __value__ = null; - public __height__ = 0; - public __width__ = 0; + public [PropertySymbol.value] = null; + public [PropertySymbol.height] = 0; + public [PropertySymbol.width] = 0; // Type specific: checkbox/radio public defaultChecked = false; - public __checked__: boolean | null = null; + public [PropertySymbol.checked]: boolean | null = null; // Type specific: file public files: IFileList = new FileList(); @@ -72,7 +73,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns Height. */ public get height(): number { - return this.__height__; + return this[PropertySymbol.height]; } /** @@ -81,7 +82,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @param height Height. */ public set height(height: number) { - this.__height__ = height; + this[PropertySymbol.height] = height; this.setAttribute('height', String(height)); } @@ -91,7 +92,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns Width. */ public get width(): number { - return this.__width__; + return this[PropertySymbol.width]; } /** @@ -100,7 +101,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @param width Width. */ public set width(width: number) { - this.__width__ = width; + this[PropertySymbol.width] = width; this.setAttribute('width', String(width)); } @@ -560,8 +561,8 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns Checked. */ public get checked(): boolean { - if (this.__checked__ !== null) { - return this.__checked__; + if (this[PropertySymbol.checked] !== null) { + return this[PropertySymbol.checked]; } return this.getAttribute('checked') !== null; } @@ -596,11 +597,11 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE return this.files.length > 0 ? '/fake/path/' + this.files[0].name : ''; } - if (this.__value__ === null) { + if (this[PropertySymbol.value] === null) { return this.getAttribute('value') || ''; } - return this.__value__; + return this[PropertySymbol.value]; } /** @@ -630,12 +631,12 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE } break; default: - const oldValue = this.__value__; - this.__value__ = HTMLInputElementValueSanitizer.sanitize(this, value); + const oldValue = this[PropertySymbol.value]; + this[PropertySymbol.value] = HTMLInputElementValueSanitizer.sanitize(this, value); - if (oldValue !== this.__value__) { - this.#selectionStart = this.__value__.length; - this.#selectionEnd = this.__value__.length; + if (oldValue !== this[PropertySymbol.value]) { + this.#selectionStart = this[PropertySymbol.value].length; + this.#selectionEnd = this[PropertySymbol.value].length; this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none; } @@ -766,7 +767,7 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns Form. */ public get form(): IHTMLFormElement { - return this.__formNode__; + return this[PropertySymbol.formNode]; } /** @@ -1152,9 +1153,9 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE const clone = super.cloneNode(deep); clone.formAction = this.formAction; clone.formMethod = this.formMethod; - clone.__value__ = this.__value__; - clone.__height__ = this.__height__; - clone.__width__ = this.__width__; + clone[PropertySymbol.value] = this[PropertySymbol.value]; + clone[PropertySymbol.height] = this[PropertySymbol.height]; + clone[PropertySymbol.width] = this[PropertySymbol.width]; clone.defaultChecked = this.defaultChecked; clone.files = this.files.slice(); clone.#selectionStart = this.#selectionStart; @@ -1203,12 +1204,12 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE this.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); this.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); } else if (inputType === 'submit') { - const form = this.__formNode__; + const form = this[PropertySymbol.formNode]; if (form) { form.requestSubmit(); } } else if (inputType === 'reset' && this.isConnected) { - const form = this.__formNode__; + const form = this[PropertySymbol.formNode]; if (form) { form.reset(); } @@ -1236,19 +1237,19 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE /** * @override */ - public override __connectToNode__(parentNode: INode = null): void { - const oldFormNode = this.__formNode__; + public override [PropertySymbol.connectToNode](parentNode: INode = null): void { + const oldFormNode = this[PropertySymbol.formNode]; - super.__connectToNode__(parentNode); + super[PropertySymbol.connectToNode](parentNode); - if (oldFormNode !== this.__formNode__) { + if (oldFormNode !== this[PropertySymbol.formNode]) { if (oldFormNode) { - oldFormNode.__removeFormControlItem__(this, this.name); - oldFormNode.__removeFormControlItem__(this, this.id); + oldFormNode[PropertySymbol.removeFormControlItem](this, this.name); + oldFormNode[PropertySymbol.removeFormControlItem](this, this.id); } - if (this.__formNode__) { - (this.__formNode__).__appendFormControlItem__(this, this.name); - (this.__formNode__).__appendFormControlItem__(this, this.id); + if (this[PropertySymbol.formNode]) { + (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem](this, this.name); + (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem](this, this.id); } } } @@ -1275,15 +1276,15 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @param checked Checked. */ #setChecked(checked: boolean): void { - this.__checked__ = checked; + this[PropertySymbol.checked] = checked; if (checked && this.type === 'radio' && this.name) { - const root = (this.__formNode__ || this.getRootNode()); + const root = (this[PropertySymbol.formNode] || this.getRootNode()); const radioButtons = root.querySelectorAll(`input[type="radio"][name="${this.name}"]`); for (const radioButton of radioButtons) { if (radioButton !== this) { - radioButton['__checked__'] = false; + radioButton[PropertySymbol.checked] = false; } } } diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts index 3aa44d717..3d9c77019 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementNamedNodeMap.ts @@ -1,4 +1,5 @@ import IAttr from '../attr/IAttr.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLInputElement from './HTMLInputElement.js'; @@ -9,7 +10,7 @@ import HTMLInputElement from './HTMLInputElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLInputElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected __ownerElement__: HTMLInputElement; + protected [PropertySymbol.ownerElement]: HTMLInputElement; /** * @override @@ -17,16 +18,16 @@ export default class HTMLInputElementNamedNodeMap extends HTMLElementNamedNodeMa public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if ((item.name === 'id' || item.name === 'name') && this.__ownerElement__.__formNode__) { + if ((item.name === 'id' || item.name === 'name') && this[PropertySymbol.ownerElement][PropertySymbol.formNode]) { if (replacedItem && replacedItem.value) { - (this.__ownerElement__.__formNode__).__removeFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.removeFormControlItem]( + this[PropertySymbol.ownerElement], replacedItem.value ); } if (item.value) { - (this.__ownerElement__.__formNode__).__appendFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( + this[PropertySymbol.ownerElement], item.value ); } @@ -38,16 +39,16 @@ export default class HTMLInputElementNamedNodeMap extends HTMLElementNamedNodeMa /** * @override */ - public override __removeNamedItem__(name: string): IAttr | null { - const removedItem = super.__removeNamedItem__(name); + public override [PropertySymbol.removeNamedItem](name: string): IAttr | null { + const removedItem = super[PropertySymbol.removeNamedItem](name); if ( removedItem && (removedItem.name === 'id' || removedItem.name === 'name') && - this.__ownerElement__.__formNode__ + this[PropertySymbol.ownerElement][PropertySymbol.formNode] ) { - (this.__ownerElement__.__formNode__).__removeFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.removeFormControlItem]( + this[PropertySymbol.ownerElement], removedItem.value ); } diff --git a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts index 0ccf1476a..56060e68f 100644 --- a/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts +++ b/packages/happy-dom/src/nodes/html-label-element/HTMLLabelElement.ts @@ -1,4 +1,5 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IHTMLElement from '../html-element/IHTMLElement.js'; import IHTMLFormElement from '../html-form-element/IHTMLFormElement.js'; import IHTMLLabelElement from './IHTMLLabelElement.js'; @@ -57,7 +58,7 @@ export default class HTMLLabelElement extends HTMLElement implements IHTMLLabelE * @returns Form. */ public get form(): IHTMLFormElement { - return this.__formNode__; + return this[PropertySymbol.formNode]; } /** diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts index 3be0d0481..774b89fb8 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts @@ -1,4 +1,5 @@ import CSSStyleSheet from '../../css/CSSStyleSheet.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElement from '../html-element/HTMLElement.js'; import IHTMLLinkElement from './IHTMLLinkElement.js'; import Event from '../../event/Event.js'; @@ -22,8 +23,8 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle public onerror: (event: ErrorEvent) => void = null; public onload: (event: Event) => void = null; public readonly sheet: CSSStyleSheet = null; - public __evaluateCSS__ = true; - public __relList__: DOMTokenList = null; + public [PropertySymbol.evaluateCSS] = true; + public [PropertySymbol.relList]: DOMTokenList = null; #styleSheetLoader: HTMLLinkElementStyleSheetLoader; /** @@ -48,10 +49,10 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle * @returns Rel list. */ public get relList(): IDOMTokenList { - if (!this.__relList__) { - this.__relList__ = new DOMTokenList(this, 'rel'); + if (!this[PropertySymbol.relList]) { + this[PropertySymbol.relList] = new DOMTokenList(this, 'rel'); } - return this.__relList__; + return this[PropertySymbol.relList]; } /** @@ -201,13 +202,13 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle /** * @override */ - public override __connectToNode__(parentNode: INode = null): void { + public override [PropertySymbol.connectToNode](parentNode: INode = null): void { const isConnected = this.isConnected; const isParentConnected = parentNode ? parentNode.isConnected : false; - super.__connectToNode__(parentNode); + super[PropertySymbol.connectToNode](parentNode); - if (isParentConnected && isConnected !== isParentConnected && this.__evaluateCSS__) { + if (isParentConnected && isConnected !== isParentConnected && this[PropertySymbol.evaluateCSS]) { this.#styleSheetLoader.loadStyleSheet(this.getAttribute('href'), this.getAttribute('rel')); } } diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts index bc063a7f1..565de52e6 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementNamedNodeMap.ts @@ -1,4 +1,5 @@ import IAttr from '../attr/IAttr.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; import HTMLLinkElement from './HTMLLinkElement.js'; import HTMLLinkElementStyleSheetLoader from './HTMLLinkElementStyleSheetLoader.js'; @@ -9,7 +10,7 @@ import HTMLLinkElementStyleSheetLoader from './HTMLLinkElementStyleSheetLoader.j * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLLinkElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected __ownerElement__: HTMLLinkElement; + protected [PropertySymbol.ownerElement]: HTMLLinkElement; #styleSheetLoader: HTMLLinkElementStyleSheetLoader; /** @@ -29,14 +30,14 @@ export default class HTMLLinkElementNamedNodeMap extends HTMLElementNamedNodeMap public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if (item.name === 'rel' && this.__ownerElement__.__relList__) { - this.__ownerElement__.__relList__.__updateIndices__(); + if (item.name === 'rel' && this[PropertySymbol.ownerElement][PropertySymbol.relList]) { + this[PropertySymbol.ownerElement][PropertySymbol.relList][PropertySymbol.updateIndices](); } if (item.name === 'rel') { - this.#styleSheetLoader.loadStyleSheet(this.__ownerElement__.getAttribute('href'), item.value); + this.#styleSheetLoader.loadStyleSheet(this[PropertySymbol.ownerElement].getAttribute('href'), item.value); } else if (item.name === 'href') { - this.#styleSheetLoader.loadStyleSheet(item.value, this.__ownerElement__.getAttribute('rel')); + this.#styleSheetLoader.loadStyleSheet(item.value, this[PropertySymbol.ownerElement].getAttribute('rel')); } return replacedItem || null; @@ -45,11 +46,11 @@ export default class HTMLLinkElementNamedNodeMap extends HTMLElementNamedNodeMap /** * @override */ - public override __removeNamedItem__(name: string): IAttr | null { - const removedItem = super.__removeNamedItem__(name); + public override [PropertySymbol.removeNamedItem](name: string): IAttr | null { + const removedItem = super[PropertySymbol.removeNamedItem](name); - if (removedItem && removedItem.name === 'rel' && this.__ownerElement__.__relList__) { - this.__ownerElement__.__relList__.__updateIndices__(); + if (removedItem && removedItem.name === 'rel' && this[PropertySymbol.ownerElement][PropertySymbol.relList]) { + this[PropertySymbol.ownerElement][PropertySymbol.relList][PropertySymbol.updateIndices](); } return removedItem; diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts index 03ded902b..198bbe6cb 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementStyleSheetLoader.ts @@ -1,4 +1,5 @@ import Event from '../../event/Event.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import ResourceFetch from '../../fetch/ResourceFetch.js'; import CSSStyleSheet from '../../css/CSSStyleSheet.js'; import DOMException from '../../exception/DOMException.js'; @@ -44,7 +45,7 @@ export default class HTMLLinkElementStyleSheetLoader { let absoluteURL: string; try { - absoluteURL = new URL(url, element.ownerDocument.__defaultView__.location).href; + absoluteURL = new URL(url, element.ownerDocument[PropertySymbol.defaultView].location).href; } catch (error) { this.#loadedStyleSheetURL = null; element.dispatchEvent(new Event('error')); @@ -68,11 +69,11 @@ export default class HTMLLinkElementStyleSheetLoader { const resourceFetch = new ResourceFetch({ browserFrame: this.#browserFrame, - window: element.ownerDocument.__defaultView__ + window: element.ownerDocument[PropertySymbol.defaultView] }); - const readyStateManager = (<{ __readyStateManager__: DocumentReadyStateManager }>( - (element.ownerDocument.__defaultView__) - )).__readyStateManager__; + const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>( + (element.ownerDocument[PropertySymbol.defaultView]) + ))[PropertySymbol.readyStateManager]; this.#loadedStyleSheetURL = absoluteURL; diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts index 37b6fd906..722923219 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElement.ts @@ -1,4 +1,5 @@ import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElement from '../html-element/HTMLElement.js'; import IHTMLFormElement from '../html-form-element/IHTMLFormElement.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; @@ -15,8 +16,8 @@ import IHTMLOptionElement from './IHTMLOptionElement.js'; */ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptionElement { public override readonly attributes: INamedNodeMap = new HTMLOptionElementNamedNodeMap(this); - public __selectedness__ = false; - public __dirtyness__ = false; + public [PropertySymbol.selectedness] = false; + public [PropertySymbol.dirtyness] = false; /** * Returns inner text, which is the rendered appearance of text. @@ -42,8 +43,8 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @returns Index. */ public get index(): number { - return this.__selectNode__ - ? (this.__selectNode__).options.indexOf(this) + return this[PropertySymbol.selectNode] + ? (this[PropertySymbol.selectNode]).options.indexOf(this) : 0; } @@ -53,7 +54,7 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @returns Form. */ public get form(): IHTMLFormElement { - return this.__formNode__; + return this[PropertySymbol.formNode]; } /** @@ -62,7 +63,7 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @returns Selected. */ public get selected(): boolean { - return this.__selectedness__; + return this[PropertySymbol.selectedness]; } /** @@ -71,13 +72,13 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio * @param selected Selected. */ public set selected(selected: boolean) { - const selectNode = this.__selectNode__; + const selectNode = this[PropertySymbol.selectNode]; - this.__dirtyness__ = true; - this.__selectedness__ = Boolean(selected); + this[PropertySymbol.dirtyness] = true; + this[PropertySymbol.selectedness] = Boolean(selected); if (selectNode) { - selectNode.__updateOptionItems__(this.__selectedness__ ? this : null); + selectNode[PropertySymbol.updateOptionItems](this[PropertySymbol.selectedness] ? this : null); } } @@ -124,17 +125,17 @@ export default class HTMLOptionElement extends HTMLElement implements IHTMLOptio /** * @override */ - public override __connectToNode__(parentNode: INode = null): void { - const oldSelectNode = this.__selectNode__; + public override [PropertySymbol.connectToNode](parentNode: INode = null): void { + const oldSelectNode = this[PropertySymbol.selectNode]; - super.__connectToNode__(parentNode); + super[PropertySymbol.connectToNode](parentNode); - if (oldSelectNode !== this.__selectNode__) { + if (oldSelectNode !== this[PropertySymbol.selectNode]) { if (oldSelectNode) { - oldSelectNode.__updateOptionItems__(); + oldSelectNode[PropertySymbol.updateOptionItems](); } - if (this.__selectNode__) { - (this.__selectNode__).__updateOptionItems__(); + if (this[PropertySymbol.selectNode]) { + (this[PropertySymbol.selectNode])[PropertySymbol.updateOptionItems](); } } } diff --git a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts index 0ac134765..d7803f51b 100644 --- a/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-option-element/HTMLOptionElementNamedNodeMap.ts @@ -1,4 +1,5 @@ import IAttr from '../attr/IAttr.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; import HTMLSelectElement from '../html-select-element/HTMLSelectElement.js'; import HTMLOptionElement from './HTMLOptionElement.js'; @@ -9,7 +10,7 @@ import HTMLOptionElement from './HTMLOptionElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLOptionElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected __ownerElement__: HTMLOptionElement; + protected [PropertySymbol.ownerElement]: HTMLOptionElement; /** * @override @@ -18,16 +19,16 @@ export default class HTMLOptionElementNamedNodeMap extends HTMLElementNamedNodeM const replacedItem = super.setNamedItem(item); if ( - !this.__ownerElement__.__dirtyness__ && + !this[PropertySymbol.ownerElement][PropertySymbol.dirtyness] && item.name === 'selected' && replacedItem?.value !== item.value ) { - const selectNode = this.__ownerElement__.__selectNode__; + const selectNode = this[PropertySymbol.ownerElement][PropertySymbol.selectNode]; - this.__ownerElement__.__selectedness__ = true; + this[PropertySymbol.ownerElement][PropertySymbol.selectedness] = true; if (selectNode) { - selectNode.__updateOptionItems__(this.__ownerElement__); + selectNode[PropertySymbol.updateOptionItems](this[PropertySymbol.ownerElement]); } } @@ -37,16 +38,16 @@ export default class HTMLOptionElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override __removeNamedItem__(name: string): IAttr | null { - const removedItem = super.__removeNamedItem__(name); + public override [PropertySymbol.removeNamedItem](name: string): IAttr | null { + const removedItem = super[PropertySymbol.removeNamedItem](name); - if (removedItem && !this.__ownerElement__.__dirtyness__ && removedItem.name === 'selected') { - const selectNode = this.__ownerElement__.__selectNode__; + if (removedItem && !this[PropertySymbol.ownerElement][PropertySymbol.dirtyness] && removedItem.name === 'selected') { + const selectNode = this[PropertySymbol.ownerElement][PropertySymbol.selectNode]; - this.__ownerElement__.__selectedness__ = false; + this[PropertySymbol.ownerElement][PropertySymbol.selectedness] = false; if (selectNode) { - selectNode.__updateOptionItems__(); + selectNode[PropertySymbol.updateOptionItems](); } } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index 3d062b4c4..6b032f008 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -1,4 +1,5 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IHTMLScriptElement from './IHTMLScriptElement.js'; import Event from '../../event/Event.js'; import ErrorEvent from '../../event/events/ErrorEvent.js'; @@ -21,7 +22,7 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip public override readonly attributes: INamedNodeMap; public onerror: (event: ErrorEvent) => void = null; public onload: (event: Event) => void = null; - public __evaluateScript__ = true; + public [PropertySymbol.evaluateScript] = true; #scriptLoader: HTMLScriptElementScriptLoader; /** @@ -188,16 +189,16 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip /** * @override */ - public override __connectToNode__(parentNode: INode = null): void { + public override [PropertySymbol.connectToNode](parentNode: INode = null): void { const isConnected = this.isConnected; const isParentConnected = parentNode ? parentNode.isConnected : false; const browserSettings = WindowBrowserSettingsReader.getSettings( - this.ownerDocument.__defaultView__ + this.ownerDocument[PropertySymbol.defaultView] ); - super.__connectToNode__(parentNode); + super[PropertySymbol.connectToNode](parentNode); - if (isParentConnected && isConnected !== isParentConnected && this.__evaluateScript__) { + if (isParentConnected && isConnected !== isParentConnected && this[PropertySymbol.evaluateScript]) { const src = this.getAttribute('src'); if (src !== null) { @@ -212,23 +213,23 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip type === 'application/x-javascript' || type.startsWith('text/javascript')) ) { - this.ownerDocument['__currentScript__'] = this; + this.ownerDocument[PropertySymbol.currentScript] = this; const code = - `//# sourceURL=${this.ownerDocument.__defaultView__.location.href}\n` + textContent; + `//# sourceURL=${this.ownerDocument[PropertySymbol.defaultView].location.href}\n` + textContent; if ( browserSettings.disableErrorCapturing || browserSettings.errorCapturing !== BrowserErrorCapturingEnum.tryAndCatch ) { - this.ownerDocument.__defaultView__.eval(code); + this.ownerDocument[PropertySymbol.defaultView].eval(code); } else { - WindowErrorUtility.captureError(this.ownerDocument.__defaultView__, () => - this.ownerDocument.__defaultView__.eval(code) + WindowErrorUtility.captureError(this.ownerDocument[PropertySymbol.defaultView], () => + this.ownerDocument[PropertySymbol.defaultView].eval(code) ); } - this.ownerDocument['__currentScript__'] = null; + this.ownerDocument[PropertySymbol.currentScript] = null; } } } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts index 63d6bbb10..5532aa878 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementNamedNodeMap.ts @@ -1,4 +1,5 @@ import IAttr from '../attr/IAttr.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; import HTMLScriptElement from './HTMLScriptElement.js'; import HTMLScriptElementScriptLoader from './HTMLScriptElementScriptLoader.js'; @@ -9,7 +10,7 @@ import HTMLScriptElementScriptLoader from './HTMLScriptElementScriptLoader.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLScriptElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected __ownerElement__: HTMLScriptElement; + protected [PropertySymbol.ownerElement]: HTMLScriptElement; #scriptLoader: HTMLScriptElementScriptLoader; /** @@ -29,7 +30,7 @@ export default class HTMLScriptElementNamedNodeMap extends HTMLElementNamedNodeM public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if (item.name === 'src' && item.value !== null && this.__ownerElement__.isConnected) { + if (item.name === 'src' && item.value !== null && this[PropertySymbol.ownerElement].isConnected) { this.#scriptLoader.loadScript(item.value); } diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts index ff64eea1f..7ca0dc135 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementScriptLoader.ts @@ -1,4 +1,5 @@ import Event from '../../event/Event.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import ResourceFetch from '../../fetch/ResourceFetch.js'; @@ -44,7 +45,7 @@ export default class HTMLScriptElementScriptLoader { let absoluteURL: string; try { - absoluteURL = new URL(url, element.ownerDocument.__defaultView__.location).href; + absoluteURL = new URL(url, element.ownerDocument[PropertySymbol.defaultView].location).href; } catch (error) { this.#loadedScriptURL = null; element.dispatchEvent(new Event('error')); @@ -71,7 +72,7 @@ export default class HTMLScriptElementScriptLoader { const resourceFetch = new ResourceFetch({ browserFrame: this.#browserFrame, - window: element.ownerDocument.__defaultView__ + window: element.ownerDocument[PropertySymbol.defaultView] }); let code: string | null = null; let error: Error | null = null; @@ -79,9 +80,9 @@ export default class HTMLScriptElementScriptLoader { this.#loadedScriptURL = absoluteURL; if (async) { - const readyStateManager = (<{ __readyStateManager__: DocumentReadyStateManager }>( - (element.ownerDocument.__defaultView__) - )).__readyStateManager__; + const readyStateManager = (<{ [PropertySymbol.readyStateManager]: DocumentReadyStateManager }>( + (element.ownerDocument[PropertySymbol.defaultView]) + ))[PropertySymbol.readyStateManager]; readyStateManager.startTask(); @@ -103,20 +104,20 @@ export default class HTMLScriptElementScriptLoader { if (error) { WindowErrorUtility.dispatchError(element, error); } else { - element.ownerDocument['__currentScript__'] = element; + element.ownerDocument[PropertySymbol.currentScript] = element; code = '//# sourceURL=' + absoluteURL + '\n' + code; if ( browserSettings.disableErrorCapturing || browserSettings.errorCapturing !== BrowserErrorCapturingEnum.tryAndCatch ) { - element.ownerDocument.__defaultView__.eval(code); + element.ownerDocument[PropertySymbol.defaultView].eval(code); } else { - WindowErrorUtility.captureError(element.ownerDocument.__defaultView__, () => - element.ownerDocument.__defaultView__.eval(code) + WindowErrorUtility.captureError(element.ownerDocument[PropertySymbol.defaultView], () => + element.ownerDocument[PropertySymbol.defaultView].eval(code) ); } - element.ownerDocument['__currentScript__'] = null; + element.ownerDocument[PropertySymbol.currentScript] = null; element.dispatchEvent(new Event('load')); } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts index ab32158d1..00d536196 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts @@ -1,4 +1,5 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IHTMLElement from '../html-element/IHTMLElement.js'; import IHTMLFormElement from '../html-form-element/IHTMLFormElement.js'; import ValidityState from '../../validity-state/ValidityState.js'; @@ -34,7 +35,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec public readonly validity = new ValidityState(this); // Private properties - public __selectNode__: INode = this; + public [PropertySymbol.selectNode]: INode = this; // Events public onchange: (event: Event) => void | null = null; @@ -163,7 +164,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec public get value(): string { for (let i = 0, max = this.options.length; i < max; i++) { const option = this.options[i]; - if (option.__selectedness__) { + if (option[PropertySymbol.selectedness]) { return option.value; } } @@ -180,10 +181,10 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec for (let i = 0, max = this.options.length; i < max; i++) { const option = this.options[i]; if (option.value === value) { - option.__selectedness__ = true; - option.__dirtyness__ = true; + option[PropertySymbol.selectedness] = true; + option[PropertySymbol.dirtyness] = true; } else { - option.__selectedness__ = false; + option[PropertySymbol.selectedness] = false; } } } @@ -195,7 +196,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec */ public get selectedIndex(): number { for (let i = 0, max = this.options.length; i < max; i++) { - if ((this.options[i]).__selectedness__) { + if ((this.options[i])[PropertySymbol.selectedness]) { return i; } } @@ -210,13 +211,13 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec public set selectedIndex(selectedIndex: number) { if (typeof selectedIndex === 'number' && !isNaN(selectedIndex)) { for (let i = 0, max = this.options.length; i < max; i++) { - (this.options[i]).__selectedness__ = false; + (this.options[i])[PropertySymbol.selectedness] = false; } const selectedOption = this.options[selectedIndex]; if (selectedOption) { - selectedOption.__selectedness__ = true; - selectedOption.__dirtyness__ = true; + selectedOption[PropertySymbol.selectedness] = true; + selectedOption[PropertySymbol.dirtyness] = true; } } } @@ -236,7 +237,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec * @returns Form. */ public get form(): IHTMLFormElement { - return this.__formNode__; + return this[PropertySymbol.formNode]; } /** @@ -326,7 +327,7 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec * @see https://html.spec.whatwg.org/multipage/form-elements.html#selectedness-setting-algorithm * @param [selectedOption] Selected option. */ - public __updateOptionItems__(selectedOption?: IHTMLOptionElement): void { + public [PropertySymbol.updateOptionItems](selectedOption?: IHTMLOptionElement): void { const optionElements = >this.getElementsByTagName('option'); if (optionElements.length < this.options.length) { @@ -346,11 +347,11 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec if (!isMultiple) { if (selectedOption) { - (optionElements[i]).__selectedness__ = + (optionElements[i])[PropertySymbol.selectedness] = optionElements[i] === selectedOption; } - if ((optionElements[i]).__selectedness__) { + if ((optionElements[i])[PropertySymbol.selectedness]) { selected.push(optionElements[i]); } } @@ -376,13 +377,13 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec } if (!disabled) { - option.__selectedness__ = true; + option[PropertySymbol.selectedness] = true; break; } } } else if (selected.length >= 2) { for (let i = 0, max = optionElements.length; i < max; i++) { - (optionElements[i]).__selectedness__ = i === selected.length - 1; + (optionElements[i])[PropertySymbol.selectedness] = i === selected.length - 1; } } } @@ -390,19 +391,19 @@ export default class HTMLSelectElement extends HTMLElement implements IHTMLSelec /** * @override */ - public override __connectToNode__(parentNode: INode = null): void { - const oldFormNode = this.__formNode__; + public override [PropertySymbol.connectToNode](parentNode: INode = null): void { + const oldFormNode = this[PropertySymbol.formNode]; - super.__connectToNode__(parentNode); + super[PropertySymbol.connectToNode](parentNode); - if (oldFormNode !== this.__formNode__) { + if (oldFormNode !== this[PropertySymbol.formNode]) { if (oldFormNode) { - oldFormNode.__removeFormControlItem__(this, this.name); - oldFormNode.__removeFormControlItem__(this, this.id); + oldFormNode[PropertySymbol.removeFormControlItem](this, this.name); + oldFormNode[PropertySymbol.removeFormControlItem](this, this.id); } - if (this.__formNode__) { - (this.__formNode__).__appendFormControlItem__(this, this.name); - (this.__formNode__).__appendFormControlItem__(this, this.id); + if (this[PropertySymbol.formNode]) { + (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem](this, this.name); + (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem](this, this.id); } } } diff --git a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts index ad3c912a7..7415c347f 100644 --- a/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-select-element/HTMLSelectElementNamedNodeMap.ts @@ -1,4 +1,5 @@ import IAttr from '../attr/IAttr.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLSelectElement from './HTMLSelectElement.js'; @@ -9,7 +10,7 @@ import HTMLSelectElement from './HTMLSelectElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLSelectElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected __ownerElement__: HTMLSelectElement; + protected [PropertySymbol.ownerElement]: HTMLSelectElement; /** * @override @@ -17,16 +18,16 @@ export default class HTMLSelectElementNamedNodeMap extends HTMLElementNamedNodeM public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if ((item.name === 'id' || item.name === 'name') && this.__ownerElement__.__formNode__) { + if ((item.name === 'id' || item.name === 'name') && this[PropertySymbol.ownerElement][PropertySymbol.formNode]) { if (replacedItem && replacedItem.value) { - (this.__ownerElement__.__formNode__).__removeFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.removeFormControlItem]( + this[PropertySymbol.ownerElement], replacedItem.value ); } if (item.value) { - (this.__ownerElement__.__formNode__).__appendFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( + this[PropertySymbol.ownerElement], item.value ); } @@ -38,16 +39,16 @@ export default class HTMLSelectElementNamedNodeMap extends HTMLElementNamedNodeM /** * @override */ - public override __removeNamedItem__(name: string): IAttr | null { - const removedItem = super.__removeNamedItem__(name); + public override [PropertySymbol.removeNamedItem](name: string): IAttr | null { + const removedItem = super[PropertySymbol.removeNamedItem](name); if ( removedItem && (removedItem.name === 'id' || removedItem.name === 'name') && - this.__ownerElement__.__formNode__ + this[PropertySymbol.ownerElement][PropertySymbol.formNode] ) { - (this.__ownerElement__.__formNode__).__removeFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.removeFormControlItem]( + this[PropertySymbol.ownerElement], removedItem.value ); } diff --git a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts index 853ae9c94..2baac06dc 100644 --- a/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts +++ b/packages/happy-dom/src/nodes/html-slot-element/HTMLSlotElement.ts @@ -1,4 +1,5 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IShadowRoot from '../shadow-root/IShadowRoot.js'; import IHTMLSlotElement from './IHTMLSlotElement.js'; import IText from '../text/IText.js'; @@ -62,7 +63,7 @@ export default class HTMLSlotElement extends HTMLElement implements IHTMLSlotEle return this.assignedElements(options); } - return (host).__childNodes__.slice(); + return (host)[PropertySymbol.childNodes].slice(); } return []; @@ -86,7 +87,7 @@ export default class HTMLSlotElement extends HTMLElement implements IHTMLSlotEle if (name) { const assignedElements = []; - for (const child of (host).__children__) { + for (const child of (host)[PropertySymbol.children]) { if (child.slot === name) { assignedElements.push(child); } @@ -95,7 +96,7 @@ export default class HTMLSlotElement extends HTMLElement implements IHTMLSlotEle return assignedElements; } - return (host).__children__.slice(); + return (host)[PropertySymbol.children].slice(); } return []; 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 7354c4135..3f4b9787b 100644 --- a/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts +++ b/packages/happy-dom/src/nodes/html-style-element/HTMLStyleElement.ts @@ -1,4 +1,5 @@ import CSSStyleSheet from '../../css/CSSStyleSheet.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElement from '../html-element/HTMLElement.js'; import IHTMLStyleElement from './IHTMLStyleElement.js'; @@ -9,7 +10,7 @@ import IHTMLStyleElement from './IHTMLStyleElement.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLStyleElement. */ export default class HTMLStyleElement extends HTMLElement implements IHTMLStyleElement { - private __styleSheet__: CSSStyleSheet | null = null; + private [PropertySymbol.styleSheet]: CSSStyleSheet | null = null; /** * Returns CSS style sheet. @@ -20,11 +21,11 @@ export default class HTMLStyleElement extends HTMLElement implements IHTMLStyleE if (!this.isConnected) { return null; } - if (!this.__styleSheet__) { - this.__styleSheet__ = new CSSStyleSheet(); + if (!this[PropertySymbol.styleSheet]) { + this[PropertySymbol.styleSheet] = new CSSStyleSheet(); } - this.__styleSheet__.replaceSync(this.textContent); - return this.__styleSheet__; + this[PropertySymbol.styleSheet].replaceSync(this.textContent); + return this[PropertySymbol.styleSheet]; } /** diff --git a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts index 98897a022..839138e84 100644 --- a/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts +++ b/packages/happy-dom/src/nodes/html-template-element/HTMLTemplateElement.ts @@ -1,4 +1,5 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IDocumentFragment from '../document-fragment/IDocumentFragment.js'; import INode from '../node/INode.js'; import IHTMLTemplateElement from './IHTMLTemplateElement.js'; @@ -26,7 +27,7 @@ export default class HTMLTemplateElement extends HTMLElement implements IHTMLTem * @override */ public set innerHTML(html: string) { - for (const child of (this.content).__childNodes__.slice()) { + for (const child of (this.content)[PropertySymbol.childNodes].slice()) { this.content.removeChild(child); } @@ -56,7 +57,7 @@ export default class HTMLTemplateElement extends HTMLElement implements IHTMLTem escapeEntities: false }); let xml = ''; - for (const node of (this.content).__childNodes__) { + for (const node of (this.content)[PropertySymbol.childNodes]) { xml += xmlSerializer.serializeToString(node); } return xml; diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index 34e05912e..c614c70c0 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -1,4 +1,5 @@ import Event from '../../event/Event.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import DOMException from '../../exception/DOMException.js'; import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum.js'; import HTMLElement from '../html-element/HTMLElement.js'; @@ -31,11 +32,11 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex public oninput: (event: Event) => void | null = null; public onselectionchange: (event: Event) => void | null = null; - public __value__ = null; + public [PropertySymbol.value] = null; #selectionStart = null; #selectionEnd = null; #selectionDirection = HTMLInputElementSelectionDirectionEnum.none; - public __textAreaNode__: HTMLTextAreaElement = this; + public [PropertySymbol.textAreaNode]: HTMLTextAreaElement = this; /** * Returns the default value. @@ -301,11 +302,11 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @returns Value. */ public get value(): string { - if (this.__value__ === null) { + if (this[PropertySymbol.value] === null) { return this.textContent; } - return this.__value__; + return this[PropertySymbol.value]; } /** @@ -314,12 +315,12 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @param value Value. */ public set value(value: string) { - const oldValue = this.__value__; - this.__value__ = value; + const oldValue = this[PropertySymbol.value]; + this[PropertySymbol.value] = value; - if (oldValue !== this.__value__) { - this.#selectionStart = this.__value__.length; - this.#selectionEnd = this.__value__.length; + if (oldValue !== this[PropertySymbol.value]) { + this.#selectionStart = this[PropertySymbol.value].length; + this.#selectionEnd = this[PropertySymbol.value].length; this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none; } } @@ -392,7 +393,7 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex * @returns Form. */ public get form(): IHTMLFormElement { - return this.__formNode__; + return this[PropertySymbol.formNode]; } /** @@ -553,7 +554,7 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex public cloneNode(deep = false): IHTMLTextAreaElement { const clone = super.cloneNode(deep); - clone.__value__ = this.__value__; + clone[PropertySymbol.value] = this[PropertySymbol.value]; clone.#selectionStart = this.#selectionStart; clone.#selectionEnd = this.#selectionEnd; clone.#selectionDirection = this.#selectionDirection; @@ -564,8 +565,8 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex /** * Resets selection. */ - public __resetSelection__(): void { - if (this.__value__ === null) { + public [PropertySymbol.resetSelection](): void { + if (this[PropertySymbol.value] === null) { this.#selectionStart = null; this.#selectionEnd = null; this.#selectionDirection = HTMLInputElementSelectionDirectionEnum.none; @@ -575,19 +576,19 @@ export default class HTMLTextAreaElement extends HTMLElement implements IHTMLTex /** * @override */ - public override __connectToNode__(parentNode: INode = null): void { - const oldFormNode = this.__formNode__; + public override [PropertySymbol.connectToNode](parentNode: INode = null): void { + const oldFormNode = this[PropertySymbol.formNode]; - super.__connectToNode__(parentNode); + super[PropertySymbol.connectToNode](parentNode); - if (oldFormNode !== this.__formNode__) { + if (oldFormNode !== this[PropertySymbol.formNode]) { if (oldFormNode) { - oldFormNode.__removeFormControlItem__(this, this.name); - oldFormNode.__removeFormControlItem__(this, this.id); + oldFormNode[PropertySymbol.removeFormControlItem](this, this.name); + oldFormNode[PropertySymbol.removeFormControlItem](this, this.id); } - if (this.__formNode__) { - (this.__formNode__).__appendFormControlItem__(this, this.name); - (this.__formNode__).__appendFormControlItem__(this, this.id); + if (this[PropertySymbol.formNode]) { + (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem](this, this.name); + (this[PropertySymbol.formNode])[PropertySymbol.appendFormControlItem](this, this.id); } } } diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts index 4ed117fb8..7c0616c6d 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElementNamedNodeMap.ts @@ -1,4 +1,5 @@ import IAttr from '../attr/IAttr.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; import HTMLFormElement from '../html-form-element/HTMLFormElement.js'; import HTMLTextAreaElement from './HTMLTextAreaElement.js'; @@ -9,7 +10,7 @@ import HTMLTextAreaElement from './HTMLTextAreaElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class HTMLTextAreaElementNamedNodeMap extends HTMLElementNamedNodeMap { - protected __ownerElement__: HTMLTextAreaElement; + protected [PropertySymbol.ownerElement]: HTMLTextAreaElement; /** * @override @@ -17,16 +18,16 @@ export default class HTMLTextAreaElementNamedNodeMap extends HTMLElementNamedNod public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if ((item.name === 'id' || item.name === 'name') && this.__ownerElement__.__formNode__) { + if ((item.name === 'id' || item.name === 'name') && this[PropertySymbol.ownerElement][PropertySymbol.formNode]) { if (replacedItem && replacedItem.value) { - (this.__ownerElement__.__formNode__).__removeFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.removeFormControlItem]( + this[PropertySymbol.ownerElement], replacedItem.value ); } if (item.value) { - (this.__ownerElement__.__formNode__).__appendFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.appendFormControlItem]( + this[PropertySymbol.ownerElement], item.value ); } @@ -38,16 +39,16 @@ export default class HTMLTextAreaElementNamedNodeMap extends HTMLElementNamedNod /** * @override */ - public override __removeNamedItem__(name: string): IAttr | null { - const removedItem = super.__removeNamedItem__(name); + public override [PropertySymbol.removeNamedItem](name: string): IAttr | null { + const removedItem = super[PropertySymbol.removeNamedItem](name); if ( removedItem && (removedItem.name === 'id' || removedItem.name === 'name') && - this.__ownerElement__.__formNode__ + this[PropertySymbol.ownerElement][PropertySymbol.formNode] ) { - (this.__ownerElement__.__formNode__).__removeFormControlItem__( - this.__ownerElement__, + (this[PropertySymbol.ownerElement][PropertySymbol.formNode])[PropertySymbol.removeFormControlItem]( + this[PropertySymbol.ownerElement], removedItem.value ); } diff --git a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts index dcce059b3..2730b2014 100644 --- a/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts +++ b/packages/happy-dom/src/nodes/html-unknown-element/HTMLUnknownElement.ts @@ -1,4 +1,5 @@ import HTMLElement from '../html-element/HTMLElement.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import INode from '../node/INode.js'; import IHTMLElement from '../html-element/IHTMLElement.js'; import INodeList from '../node/INodeList.js'; @@ -22,62 +23,62 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem * * @param parentNode Parent node. */ - public __connectToNode__(parentNode: INode = null): void { + public [PropertySymbol.connectToNode](parentNode: INode = null): void { const tagName = this.tagName; // This element can potentially be a custom element that has not been defined yet // Therefore we need to register a callback for when it is defined in CustomElementRegistry and replace it with the registered element (see #404) - if (tagName.includes('-') && this.ownerDocument.__defaultView__.customElements.__callbacks__) { - const callbacks = this.ownerDocument.__defaultView__.customElements.__callbacks__; + if (tagName.includes('-') && this.ownerDocument[PropertySymbol.defaultView].customElements[PropertySymbol.callbacks]) { + const callbacks = this.ownerDocument[PropertySymbol.defaultView].customElements[PropertySymbol.callbacks]; if (parentNode && !this.#customElementDefineCallback) { const callback = (): void => { if (this.parentNode) { const newElement = this.ownerDocument.createElement(tagName); - (>newElement.__childNodes__) = this.__childNodes__; - (>newElement.__children__) = this.__children__; + (>newElement[PropertySymbol.childNodes]) = this[PropertySymbol.childNodes]; + (>newElement[PropertySymbol.children]) = this[PropertySymbol.children]; (newElement.isConnected) = this.isConnected; - newElement.__rootNode__ = this.__rootNode__; - newElement.__formNode__ = this.__formNode__; - newElement.__selectNode__ = this.__selectNode__; - newElement.__textAreaNode__ = this.__textAreaNode__; - newElement.__observers__ = this.__observers__; - newElement.__isValue__ = this.__isValue__; + newElement[PropertySymbol.rootNode] = this[PropertySymbol.rootNode]; + newElement[PropertySymbol.formNode] = this[PropertySymbol.formNode]; + newElement[PropertySymbol.selectNode] = this[PropertySymbol.selectNode]; + newElement[PropertySymbol.textAreaNode] = this[PropertySymbol.textAreaNode]; + newElement[PropertySymbol.observers] = this[PropertySymbol.observers]; + newElement[PropertySymbol.isValue] = this[PropertySymbol.isValue]; for (let i = 0, max = this.attributes.length; i < max; i++) { newElement.attributes.setNamedItem(this.attributes[i]); } - (>this.__childNodes__) = new NodeList(); - (>this.__children__) = new HTMLCollection(); - this.__rootNode__ = null; - this.__formNode__ = null; - this.__selectNode__ = null; - this.__textAreaNode__ = null; - this.__observers__ = []; - this.__isValue__ = null; + (>this[PropertySymbol.childNodes]) = new NodeList(); + (>this[PropertySymbol.children]) = new HTMLCollection(); + this[PropertySymbol.rootNode] = null; + this[PropertySymbol.formNode] = null; + this[PropertySymbol.selectNode] = null; + this[PropertySymbol.textAreaNode] = null; + this[PropertySymbol.observers] = []; + this[PropertySymbol.isValue] = null; (this.attributes) = new HTMLElementNamedNodeMap(this); for ( - let i = 0, max = (this.parentNode).__childNodes__.length; + let i = 0, max = (this.parentNode)[PropertySymbol.childNodes].length; i < max; i++ ) { - if ((this.parentNode).__childNodes__[i] === this) { - (this.parentNode).__childNodes__[i] = newElement; + if ((this.parentNode)[PropertySymbol.childNodes][i] === this) { + (this.parentNode)[PropertySymbol.childNodes][i] = newElement; break; } } - if ((this.parentNode).__children__) { + if ((this.parentNode)[PropertySymbol.children]) { for ( - let i = 0, max = (this.parentNode).__children__.length; + let i = 0, max = (this.parentNode)[PropertySymbol.children].length; i < max; i++ ) { - if ((this.parentNode).__children__[i] === this) { - (this.parentNode).__children__[i] = newElement; + if ((this.parentNode)[PropertySymbol.children][i] === this) { + (this.parentNode)[PropertySymbol.children][i] = newElement; break; } } @@ -87,7 +88,7 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem newElement.connectedCallback(); } - this.__connectToNode__(null); + this[PropertySymbol.connectToNode](null); } }; callbacks[tagName] = callbacks[tagName] || []; @@ -105,6 +106,6 @@ export default class HTMLUnknownElement extends HTMLElement implements IHTMLElem } } - super.__connectToNode__(parentNode); + super[PropertySymbol.connectToNode](parentNode); } } diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts index 6682dc99f..c7c3aff45 100644 --- a/packages/happy-dom/src/nodes/node/Node.ts +++ b/packages/happy-dom/src/nodes/node/Node.ts @@ -1,4 +1,5 @@ import EventTarget from '../../event/EventTarget.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import MutationListener from '../../mutation-observer/MutationListener.js'; import INode from './INode.js'; import IDocument from '../document/IDocument.js'; @@ -16,7 +17,7 @@ import INodeList from './INodeList.js'; */ export default class Node extends EventTarget implements INode { // Can be set before the Node is created. - public static __ownerDocument__: IDocument | null = null; + public static [PropertySymbol.ownerDocument]: IDocument | null = null; // Public properties public static readonly ELEMENT_NODE = NodeTypeEnum.elementNode; @@ -56,12 +57,12 @@ export default class Node extends EventTarget implements INode { public readonly isConnected: boolean = false; // Custom Properties (not part of HTML standard) - public __rootNode__: INode = null; - public __formNode__: INode = null; - public __selectNode__: INode = null; - public __textAreaNode__: INode = null; - public __observers__: MutationListener[] = []; - public readonly __childNodes__: INodeList = new NodeList(); + public [PropertySymbol.rootNode]: INode = null; + public [PropertySymbol.formNode]: INode = null; + public [PropertySymbol.selectNode]: INode = null; + public [PropertySymbol.textAreaNode]: INode = null; + public [PropertySymbol.observers]: MutationListener[] = []; + public readonly [PropertySymbol.childNodes]: INodeList = new NodeList(); public readonly ownerDocument: IDocument = null; /** @@ -69,8 +70,8 @@ export default class Node extends EventTarget implements INode { */ constructor() { super(); - if ((this.constructor).__ownerDocument__) { - this.ownerDocument = (this.constructor).__ownerDocument__; + if ((this.constructor)[PropertySymbol.ownerDocument]) { + this.ownerDocument = (this.constructor)[PropertySymbol.ownerDocument]; } } @@ -89,7 +90,7 @@ export default class Node extends EventTarget implements INode { * @returns Child nodes list. */ public get childNodes(): INodeList { - return this.__childNodes__; + return this[PropertySymbol.childNodes]; } /** @@ -144,9 +145,9 @@ export default class Node extends EventTarget implements INode { */ public get previousSibling(): INode { if (this.parentNode) { - const index = (this.parentNode).__childNodes__.indexOf(this); + const index = (this.parentNode)[PropertySymbol.childNodes].indexOf(this); if (index > 0) { - return (this.parentNode).__childNodes__[index - 1]; + return (this.parentNode)[PropertySymbol.childNodes][index - 1]; } } return null; @@ -159,9 +160,9 @@ export default class Node extends EventTarget implements INode { */ public get nextSibling(): INode { if (this.parentNode) { - const index = (this.parentNode).__childNodes__.indexOf(this); - if (index > -1 && index + 1 < (this.parentNode).__childNodes__.length) { - return (this.parentNode).__childNodes__[index + 1]; + const index = (this.parentNode)[PropertySymbol.childNodes].indexOf(this); + if (index > -1 && index + 1 < (this.parentNode)[PropertySymbol.childNodes].length) { + return (this.parentNode)[PropertySymbol.childNodes][index + 1]; } } return null; @@ -173,8 +174,8 @@ export default class Node extends EventTarget implements INode { * @returns Node. */ public get firstChild(): INode { - if (this.__childNodes__.length > 0) { - return this.__childNodes__[0]; + if (this[PropertySymbol.childNodes].length > 0) { + return this[PropertySymbol.childNodes][0]; } return null; } @@ -185,8 +186,8 @@ export default class Node extends EventTarget implements INode { * @returns Node. */ public get lastChild(): INode { - if (this.__childNodes__.length > 0) { - return this.__childNodes__[this.__childNodes__.length - 1]; + if (this[PropertySymbol.childNodes].length > 0) { + return this[PropertySymbol.childNodes][this[PropertySymbol.childNodes].length - 1]; } return null; } @@ -214,7 +215,7 @@ export default class Node extends EventTarget implements INode { if (base) { return base.href; } - return this.ownerDocument.__defaultView__.location.href; + return this.ownerDocument[PropertySymbol.defaultView].location.href; } /** @@ -233,7 +234,7 @@ export default class Node extends EventTarget implements INode { * @returns "true" if the node has child nodes. */ public hasChildNodes(): boolean { - return this.__childNodes__.length > 0; + return this[PropertySymbol.childNodes].length > 0; } /** @@ -258,8 +259,8 @@ export default class Node extends EventTarget implements INode { return this; } - if (this.__rootNode__ && !options?.composed) { - return this.__rootNode__; + if (this[PropertySymbol.rootNode] && !options?.composed) { + return this[PropertySymbol.rootNode]; } return this.ownerDocument; @@ -272,22 +273,22 @@ export default class Node extends EventTarget implements INode { * @returns Cloned node. */ public cloneNode(deep = false): INode { - (this.constructor).__ownerDocument__ = this.ownerDocument; + (this.constructor)[PropertySymbol.ownerDocument] = this.ownerDocument; const clone = new (this.constructor)(); - (this.constructor).__ownerDocument__ = null; + (this.constructor)[PropertySymbol.ownerDocument] = null; // Document has childNodes directly when it is created - if (clone.__childNodes__.length) { - for (const node of clone.__childNodes__.slice()) { + if (clone[PropertySymbol.childNodes].length) { + for (const node of clone[PropertySymbol.childNodes].slice()) { node.parentNode.removeChild(node); } } if (deep) { - for (const childNode of this.__childNodes__) { + for (const childNode of this[PropertySymbol.childNodes]) { const childClone = childNode.cloneNode(true); (childClone.parentNode) = clone; - clone.__childNodes__.push(childClone); + clone[PropertySymbol.childNodes].push(childClone); } } @@ -359,11 +360,11 @@ export default class Node extends EventTarget implements INode { * * @param listener Listener. */ - public __observe__(listener: MutationListener): void { - this.__observers__.push(listener); + public [PropertySymbol.observe](listener: MutationListener): void { + this[PropertySymbol.observers].push(listener); if (listener.options.subtree) { - for (const node of this.__childNodes__) { - (node).__observe__(listener); + for (const node of this[PropertySymbol.childNodes]) { + (node)[PropertySymbol.observe](listener); } } } @@ -374,14 +375,14 @@ export default class Node extends EventTarget implements INode { * * @param listener Listener. */ - public __unobserve__(listener: MutationListener): void { - const index = this.__observers__.indexOf(listener); + public [PropertySymbol.unobserve](listener: MutationListener): void { + const index = this[PropertySymbol.observers].indexOf(listener); if (index !== -1) { - this.__observers__.splice(index, 1); + this[PropertySymbol.observers].splice(index, 1); } if (listener.options.subtree) { - for (const node of this.__childNodes__) { - (node).__unobserve__(listener); + for (const node of this[PropertySymbol.childNodes]) { + (node)[PropertySymbol.unobserve](listener); } } } @@ -391,27 +392,27 @@ export default class Node extends EventTarget implements INode { * * @param parentNode Parent node. */ - public __connectToNode__(parentNode: INode = null): void { + public [PropertySymbol.connectToNode](parentNode: INode = null): void { const isConnected = !!parentNode && parentNode.isConnected; - const formNode = (this).__formNode__; - const selectNode = (this).__selectNode__; - const textAreaNode = (this).__textAreaNode__; + const formNode = (this)[PropertySymbol.formNode]; + const selectNode = (this)[PropertySymbol.selectNode]; + const textAreaNode = (this)[PropertySymbol.textAreaNode]; if (this.nodeType !== NodeTypeEnum.documentFragmentNode) { (this.parentNode) = parentNode; - (this).__rootNode__ = - isConnected && parentNode ? (parentNode).__rootNode__ : null; + (this)[PropertySymbol.rootNode] = + isConnected && parentNode ? (parentNode)[PropertySymbol.rootNode] : null; if (this['tagName'] !== 'FORM') { - (this).__formNode__ = parentNode ? (parentNode).__formNode__ : null; + (this)[PropertySymbol.formNode] = parentNode ? (parentNode)[PropertySymbol.formNode] : null; } if (this['tagName'] !== 'SELECT') { - (this).__selectNode__ = parentNode ? (parentNode).__selectNode__ : null; + (this)[PropertySymbol.selectNode] = parentNode ? (parentNode)[PropertySymbol.selectNode] : null; } if (this['tagName'] !== 'TEXTAREA') { - (this).__textAreaNode__ = parentNode ? (parentNode).__textAreaNode__ : null; + (this)[PropertySymbol.textAreaNode] = parentNode ? (parentNode)[PropertySymbol.textAreaNode] : null; } } @@ -422,8 +423,8 @@ export default class Node extends EventTarget implements INode { if (!this.ownerDocument) { debugger; } - if (this.ownerDocument['__activeElement__'] === this) { - this.ownerDocument['__activeElement__'] = null; + if (this.ownerDocument[PropertySymbol.activeElement] === this) { + this.ownerDocument[PropertySymbol.activeElement] = null; } } @@ -433,22 +434,22 @@ export default class Node extends EventTarget implements INode { this.disconnectedCallback(); } - for (const child of this.__childNodes__) { - (child).__connectToNode__(this); + for (const child of this[PropertySymbol.childNodes]) { + (child)[PropertySymbol.connectToNode](this); } // eslint-disable-next-line - if ((this).__shadowRoot__) { + if ((this)[PropertySymbol.shadowRoot]) { // eslint-disable-next-line - (this).__shadowRoot__.__connectToNode__(this); + (this)[PropertySymbol.shadowRoot][PropertySymbol.connectToNode](this); } } else if ( - formNode !== this.__formNode__ || - selectNode !== this.__selectNode__ || - textAreaNode !== this.__textAreaNode__ + formNode !== this[PropertySymbol.formNode] || + selectNode !== this[PropertySymbol.selectNode] || + textAreaNode !== this[PropertySymbol.textAreaNode] ) { - for (const child of this.__childNodes__) { - (child).__connectToNode__(this); + for (const child of this[PropertySymbol.childNodes]) { + (child)[PropertySymbol.connectToNode](this); } } } @@ -599,7 +600,7 @@ export default class Node extends EventTarget implements INode { const computeNodeIndexes = (nodes: INode[]): void => { for (const childNode of nodes) { - computeNodeIndexes((childNode).__childNodes__); + computeNodeIndexes((childNode)[PropertySymbol.childNodes]); if (childNode === node2Node) { node2Index = indexes; @@ -615,7 +616,7 @@ export default class Node extends EventTarget implements INode { } }; - computeNodeIndexes((commonAncestor).__childNodes__); + computeNodeIndexes((commonAncestor)[PropertySymbol.childNodes]); /** * 9. If node1 is preceding node2, then return DOCUMENT_POSITION_PRECEDING. diff --git a/packages/happy-dom/src/nodes/node/NodeUtility.ts b/packages/happy-dom/src/nodes/node/NodeUtility.ts index cd3ed067c..1fdc7734e 100644 --- a/packages/happy-dom/src/nodes/node/NodeUtility.ts +++ b/packages/happy-dom/src/nodes/node/NodeUtility.ts @@ -1,4 +1,5 @@ import IText from '../text/IText.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IComment from '../comment/IComment.js'; import INode from './INode.js'; import NodeTypeEnum from './NodeTypeEnum.js'; @@ -47,7 +48,7 @@ export default class NodeUtility { // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment if (node.nodeType === NodeTypeEnum.documentFragmentNode) { - for (const child of (node).__childNodes__.slice()) { + for (const child of (node)[PropertySymbol.childNodes].slice()) { ancestorNode.appendChild(child); } return node; @@ -55,30 +56,30 @@ export default class NodeUtility { // Remove the node from its previous parent if it has any. if (node.parentNode) { - const index = (node.parentNode).__childNodes__.indexOf(node); + const index = (node.parentNode)[PropertySymbol.childNodes].indexOf(node); if (index !== -1) { - (node.parentNode).__childNodes__.splice(index, 1); + (node.parentNode)[PropertySymbol.childNodes].splice(index, 1); } } if (ancestorNode.isConnected) { - (ancestorNode.ownerDocument || this)['__cacheID__']++; + (ancestorNode.ownerDocument || this)[PropertySymbol.cacheID]++; } - (ancestorNode).__childNodes__.push(node); + (ancestorNode)[PropertySymbol.childNodes].push(node); - (node).__connectToNode__(ancestorNode); + (node)[PropertySymbol.connectToNode](ancestorNode); // MutationObserver - if ((ancestorNode).__observers__.length > 0) { + if ((ancestorNode)[PropertySymbol.observers].length > 0) { const record = new MutationRecord(); record.target = ancestorNode; record.type = MutationTypeEnum.childList; record.addedNodes = [node]; - for (const observer of (ancestorNode).__observers__) { + for (const observer of (ancestorNode)[PropertySymbol.observers]) { if (observer.options.subtree) { - (node).__observe__(observer); + (node)[PropertySymbol.observe](observer); } if (observer.options.childList) { observer.callback([record], observer.observer); @@ -97,29 +98,29 @@ export default class NodeUtility { * @returns Removed node. */ public static removeChild(ancestorNode: INode, node: INode): INode { - const index = (ancestorNode).__childNodes__.indexOf(node); + const index = (ancestorNode)[PropertySymbol.childNodes].indexOf(node); if (index === -1) { throw new DOMException('Failed to remove node. Node is not child of parent.'); } if (ancestorNode.isConnected) { - (ancestorNode.ownerDocument || this)['__cacheID__']++; + (ancestorNode.ownerDocument || this)[PropertySymbol.cacheID]++; } - (ancestorNode).__childNodes__.splice(index, 1); + (ancestorNode)[PropertySymbol.childNodes].splice(index, 1); - (node).__connectToNode__(null); + (node)[PropertySymbol.connectToNode](null); // MutationObserver - if ((ancestorNode).__observers__.length > 0) { + if ((ancestorNode)[PropertySymbol.observers].length > 0) { const record = new MutationRecord(); record.target = ancestorNode; record.type = MutationTypeEnum.childList; record.removedNodes = [node]; - for (const observer of (ancestorNode).__observers__) { - (node).__unobserve__(observer); + for (const observer of (ancestorNode)[PropertySymbol.observers]) { + (node)[PropertySymbol.unobserve](observer); if (observer.options.childList) { observer.callback([record], observer.observer); } @@ -158,7 +159,7 @@ export default class NodeUtility { // If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node. // See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment if (newNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - for (const child of (newNode).__childNodes__.slice()) { + for (const child of (newNode)[PropertySymbol.childNodes].slice()) { ancestorNode.insertBefore(child, referenceNode); } return newNode; @@ -171,41 +172,41 @@ export default class NodeUtility { return newNode; } - if ((ancestorNode).__childNodes__.indexOf(referenceNode) === -1) { + if ((ancestorNode)[PropertySymbol.childNodes].indexOf(referenceNode) === -1) { throw new DOMException( "Failed to execute 'insertBefore' on 'Node': The node before which the new node is to be inserted is not a child of this node." ); } if (ancestorNode.isConnected) { - (ancestorNode.ownerDocument || this)['__cacheID__']++; + (ancestorNode.ownerDocument || this)[PropertySymbol.cacheID]++; } if (newNode.parentNode) { - const index = (newNode.parentNode).__childNodes__.indexOf(newNode); + const index = (newNode.parentNode)[PropertySymbol.childNodes].indexOf(newNode); if (index !== -1) { - (newNode.parentNode).__childNodes__.splice(index, 1); + (newNode.parentNode)[PropertySymbol.childNodes].splice(index, 1); } } - (ancestorNode).__childNodes__.splice( - (ancestorNode).__childNodes__.indexOf(referenceNode), + (ancestorNode)[PropertySymbol.childNodes].splice( + (ancestorNode)[PropertySymbol.childNodes].indexOf(referenceNode), 0, newNode ); - (newNode).__connectToNode__(ancestorNode); + (newNode)[PropertySymbol.connectToNode](ancestorNode); // MutationObserver - if ((ancestorNode).__observers__.length > 0) { + if ((ancestorNode)[PropertySymbol.observers].length > 0) { const record = new MutationRecord(); record.target = ancestorNode; record.type = MutationTypeEnum.childList; record.addedNodes = [newNode]; - for (const observer of (ancestorNode).__observers__) { + for (const observer of (ancestorNode)[PropertySymbol.observers]) { if (observer.options.subtree) { - (newNode).__observe__(observer); + (newNode)[PropertySymbol.observe](observer); } if (observer.options.childList) { observer.callback([record], observer.observer); @@ -251,7 +252,7 @@ export default class NodeUtility { return true; } - if (!(ancestorNode).__childNodes__.length) { + if (!(ancestorNode)[PropertySymbol.childNodes].length) { return false; } @@ -334,7 +335,7 @@ export default class NodeUtility { return (node).data.length; default: - return (node).__childNodes__.length; + return (node)[PropertySymbol.childNodes].length; } } @@ -492,13 +493,13 @@ export default class NodeUtility { return false; } - if ((nodeA).__childNodes__.length !== (nodeB).__childNodes__.length) { + if ((nodeA)[PropertySymbol.childNodes].length !== (nodeB)[PropertySymbol.childNodes].length) { return false; } - for (let i = 0; i < (nodeA).__childNodes__.length; i++) { - const childNodeA = (nodeA).__childNodes__[i]; - const childNodeB = (nodeB).__childNodes__[i]; + for (let i = 0; i < (nodeA)[PropertySymbol.childNodes].length; i++) { + const childNodeA = (nodeA)[PropertySymbol.childNodes][i]; + const childNodeB = (nodeB)[PropertySymbol.childNodes][i]; if (!NodeUtility.isEqualNode(childNodeA, childNodeB)) { return false; diff --git a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts index e49042464..6aa389fc5 100644 --- a/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts +++ b/packages/happy-dom/src/nodes/parent-node/ParentNodeUtility.ts @@ -1,4 +1,5 @@ import XMLParser from '../../xml-parser/XMLParser.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import IDocumentFragment from '../document-fragment/IDocumentFragment.js'; import IDocument from '../document/IDocument.js'; import IElement from '../element/IElement.js'; @@ -45,7 +46,7 @@ export default class ParentNodeUtility { if (typeof node === 'string') { const newChildNodes = (( XMLParser.parse(parentNode.ownerDocument, node) - )).__childNodes__.slice(); + ))[PropertySymbol.childNodes].slice(); for (const newChildNode of newChildNodes) { parentNode.insertBefore(newChildNode, firstChild); } @@ -65,7 +66,7 @@ export default class ParentNodeUtility { parentNode: IElement | IDocument | IDocumentFragment, ...nodes: (string | INode)[] ): void { - for (const node of (parentNode).__childNodes__.slice()) { + for (const node of (parentNode)[PropertySymbol.childNodes].slice()) { parentNode.removeChild(node); } @@ -84,7 +85,7 @@ export default class ParentNodeUtility { ): IHTMLCollection { let matches = new HTMLCollection(); - for (const child of (parentNode).__children__) { + for (const child of (parentNode)[PropertySymbol.children]) { if (child.className.split(' ').includes(className)) { matches.push(child); } @@ -111,7 +112,7 @@ export default class ParentNodeUtility { const includeAll = tagName === '*'; let matches = new HTMLCollection(); - for (const child of (parentNode).__children__) { + for (const child of (parentNode)[PropertySymbol.children]) { if (includeAll || child.tagName === upperTagName) { matches.push(child); } @@ -140,7 +141,7 @@ export default class ParentNodeUtility { const includeAll = tagName === '*'; let matches = new HTMLCollection(); - for (const child of (parentNode).__children__) { + for (const child of (parentNode)[PropertySymbol.children]) { if ((includeAll || child.tagName === upperTagName) && child.namespaceURI === namespaceURI) { matches.push(child); } @@ -166,7 +167,7 @@ export default class ParentNodeUtility { ): IElement { const upperTagName = tagName.toUpperCase(); - for (const child of (parentNode).__children__) { + for (const child of (parentNode)[PropertySymbol.children]) { if (child.tagName === upperTagName) { return child; } @@ -191,7 +192,7 @@ export default class ParentNodeUtility { id: string ): IElement { id = String(id); - for (const child of (parentNode).__children__) { + for (const child of (parentNode)[PropertySymbol.children]) { if (child.id === id) { return child; } diff --git a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts index a7693f93b..405d6bf86 100644 --- a/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts +++ b/packages/happy-dom/src/nodes/shadow-root/ShadowRoot.ts @@ -1,4 +1,5 @@ import DocumentFragment from '../document-fragment/DocumentFragment.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import XMLParser from '../../xml-parser/XMLParser.js'; import XMLSerializer from '../../xml-serializer/XMLSerializer.js'; import IElement from '../element/IElement.js'; @@ -28,7 +29,7 @@ export default class ShadowRoot extends DocumentFragment implements IShadowRoot escapeEntities: false }); let xml = ''; - for (const node of this.__childNodes__) { + for (const node of this[PropertySymbol.childNodes]) { xml += xmlSerializer.serializeToString(node); } return xml; @@ -40,7 +41,7 @@ export default class ShadowRoot extends DocumentFragment implements IShadowRoot * @param html HTML. */ public set innerHTML(html: string) { - for (const child of this.__childNodes__.slice()) { + for (const child of this[PropertySymbol.childNodes].slice()) { this.removeChild(child); } @@ -53,7 +54,7 @@ export default class ShadowRoot extends DocumentFragment implements IShadowRoot * @returns Active element. */ public get activeElement(): IHTMLElement | null { - const activeElement: IHTMLElement = this.ownerDocument['__activeElement__']; + const activeElement: IHTMLElement = this.ownerDocument[PropertySymbol.activeElement]; if (activeElement && activeElement.isConnected && activeElement.getRootNode() === this) { return activeElement; } diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts index 772798ee3..fd79b82a2 100644 --- a/packages/happy-dom/src/nodes/svg-element/SVGElement.ts +++ b/packages/happy-dom/src/nodes/svg-element/SVGElement.ts @@ -1,4 +1,5 @@ import CSSStyleDeclaration from '../../css/declaration/CSSStyleDeclaration.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import Element from '../element/Element.js'; import ISVGElement from './ISVGElement.js'; import ISVGSVGElement from './ISVGSVGElement.js'; @@ -26,7 +27,7 @@ export default class SVGElement extends Element implements ISVGElement { public onunload: (event: Event) => void | null = null; // Private properties - public __style__: CSSStyleDeclaration = null; + public [PropertySymbol.style]: CSSStyleDeclaration = null; #dataset: Dataset = null; /** @@ -70,10 +71,10 @@ export default class SVGElement extends Element implements ISVGElement { * @returns Style. */ public get style(): CSSStyleDeclaration { - if (!this.__style__) { - this.__style__ = new CSSStyleDeclaration(this); + if (!this[PropertySymbol.style]) { + this[PropertySymbol.style] = new CSSStyleDeclaration(this); } - return this.__style__; + return this[PropertySymbol.style]; } /** diff --git a/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts index 3e75195bd..143787bd9 100644 --- a/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/svg-element/SVGElementNamedNodeMap.ts @@ -1,4 +1,5 @@ import IAttr from '../attr/IAttr.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import ElementNamedNodeMap from '../element/ElementNamedNodeMap.js'; import SVGElement from './SVGElement.js'; @@ -8,7 +9,7 @@ import SVGElement from './SVGElement.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/NamedNodeMap */ export default class SVGElementNamedNodeMap extends ElementNamedNodeMap { - protected __ownerElement__: SVGElement; + protected [PropertySymbol.ownerElement]: SVGElement; /** * @override @@ -16,8 +17,8 @@ export default class SVGElementNamedNodeMap extends ElementNamedNodeMap { public override setNamedItem(item: IAttr): IAttr | null { const replacedItem = super.setNamedItem(item); - if (item.name === 'style' && this.__ownerElement__.__style__) { - this.__ownerElement__.__style__.cssText = item.value; + if (item.name === 'style' && this[PropertySymbol.ownerElement][PropertySymbol.style]) { + this[PropertySymbol.ownerElement][PropertySymbol.style].cssText = item.value; } return replacedItem || null; @@ -26,11 +27,11 @@ export default class SVGElementNamedNodeMap extends ElementNamedNodeMap { /** * @override */ - public override __removeNamedItem__(name: string): IAttr | null { - const removedItem = super.__removeNamedItem__(name); + public override [PropertySymbol.removeNamedItem](name: string): IAttr | null { + const removedItem = super[PropertySymbol.removeNamedItem](name); - if (removedItem && removedItem.name === 'style' && this.__ownerElement__.__style__) { - this.__ownerElement__.__style__.cssText = ''; + if (removedItem && removedItem.name === 'style' && this[PropertySymbol.ownerElement][PropertySymbol.style]) { + this[PropertySymbol.ownerElement][PropertySymbol.style].cssText = ''; } return removedItem; diff --git a/packages/happy-dom/src/nodes/text/Text.ts b/packages/happy-dom/src/nodes/text/Text.ts index 1a482a3fb..975a63bd5 100644 --- a/packages/happy-dom/src/nodes/text/Text.ts +++ b/packages/happy-dom/src/nodes/text/Text.ts @@ -1,4 +1,5 @@ import Node from '../node/Node.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; import CharacterData from '../character-data/CharacterData.js'; import IText from './IText.js'; import DOMException from '../../exception/DOMException.js'; @@ -25,7 +26,7 @@ export default class Text extends CharacterData implements IText { * @override */ public override get data(): string { - return this.__data__; + return this[PropertySymbol.data]; } /** @@ -34,8 +35,8 @@ export default class Text extends CharacterData implements IText { public override set data(data: string) { super.data = data; - if (this.__textAreaNode__) { - (this.__textAreaNode__).__resetSelection__(); + if (this[PropertySymbol.textAreaNode]) { + (this[PropertySymbol.textAreaNode])[PropertySymbol.resetSelection](); } } @@ -47,7 +48,7 @@ export default class Text extends CharacterData implements IText { * @returns New text node. */ public splitText(offset: number): IText { - const length = this.__data__.length; + const length = this[PropertySymbol.data].length; if (offset < 0 || offset > length) { throw new DOMException( @@ -92,17 +93,17 @@ export default class Text extends CharacterData implements IText { /** * @override */ - public override __connectToNode__(parentNode: INode = null): void { - const oldTextAreaNode = this.__textAreaNode__; + public override [PropertySymbol.connectToNode](parentNode: INode = null): void { + const oldTextAreaNode = this[PropertySymbol.textAreaNode]; - super.__connectToNode__(parentNode); + super[PropertySymbol.connectToNode](parentNode); - if (oldTextAreaNode !== this.__textAreaNode__) { + if (oldTextAreaNode !== this[PropertySymbol.textAreaNode]) { if (oldTextAreaNode) { - oldTextAreaNode.__resetSelection__(); + oldTextAreaNode[PropertySymbol.resetSelection](); } - if (this.__textAreaNode__) { - (this.__textAreaNode__).__resetSelection__(); + if (this[PropertySymbol.textAreaNode]) { + (this[PropertySymbol.textAreaNode])[PropertySymbol.resetSelection](); } } } diff --git a/packages/happy-dom/src/query-selector/QuerySelector.ts b/packages/happy-dom/src/query-selector/QuerySelector.ts index b48a6c29d..8d3291721 100644 --- a/packages/happy-dom/src/query-selector/QuerySelector.ts +++ b/packages/happy-dom/src/query-selector/QuerySelector.ts @@ -1,4 +1,5 @@ import IElement from '../nodes/element/IElement.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import INodeList from '../nodes/node/INodeList.js'; import SelectorItem from './SelectorItem.js'; import NodeList from '../nodes/node/NodeList.js'; @@ -49,7 +50,7 @@ export default class QuerySelector { matches = matches.concat( node.nodeType === NodeTypeEnum.elementNode ? this.findAll(node, [node], items) - : this.findAll(null, (node).__children__, items) + : this.findAll(null, (node)[PropertySymbol.children], items) ); } @@ -93,7 +94,7 @@ export default class QuerySelector { const match = node.nodeType === NodeTypeEnum.elementNode ? this.findFirst(node, [node], items) - : this.findFirst(null, (node).__children__, items); + : this.findFirst(null, (node)[PropertySymbol.children], items); if (match) { return match; @@ -251,7 +252,7 @@ export default class QuerySelector { matched = matched.concat( this.findAll( rootElement, - (child).__children__, + (child)[PropertySymbol.children], selectorItems.slice(1), position ) @@ -263,10 +264,10 @@ export default class QuerySelector { if ( selectorItem.combinator === SelectorCombinatorEnum.descendant && - (child).__children__.length + (child)[PropertySymbol.children].length ) { matched = matched.concat( - this.findAll(rootElement, (child).__children__, selectorItems, position) + this.findAll(rootElement, (child)[PropertySymbol.children], selectorItems, position) ); } } @@ -314,7 +315,7 @@ export default class QuerySelector { case SelectorCombinatorEnum.child: const match = this.findFirst( rootElement, - (child).__children__, + (child)[PropertySymbol.children], selectorItems.slice(1) ); if (match) { @@ -327,9 +328,9 @@ export default class QuerySelector { if ( selectorItem.combinator === SelectorCombinatorEnum.descendant && - (child).__children__.length + (child)[PropertySymbol.children].length ) { - const match = this.findFirst(rootElement, (child).__children__, selectorItems); + const match = this.findFirst(rootElement, (child)[PropertySymbol.children], selectorItems); if (match) { return match; diff --git a/packages/happy-dom/src/query-selector/SelectorItem.ts b/packages/happy-dom/src/query-selector/SelectorItem.ts index 77f5a550b..704d16320 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.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import IElement from '../nodes/element/IElement.js'; import Element from '../nodes/element/Element.js'; import IHTMLInputElement from '../nodes/html-input-element/IHTMLInputElement.js'; @@ -112,7 +113,7 @@ export default class SelectorItem { */ private matchPsuedo(element: IElement): boolean { const parent = element.parentNode; - const parentChildren = element.parentNode ? (element.parentNode).__children__ : []; + const parentChildren = element.parentNode ? (element.parentNode)[PropertySymbol.children] : []; if (!this.pseudos) { return true; @@ -185,7 +186,7 @@ export default class SelectorItem { case 'checked': return element.tagName === 'INPUT' && (element).checked; case 'empty': - return !(element).__children__.length; + return !(element)[PropertySymbol.children].length; case 'root': return element.tagName === 'HTML'; case 'not': diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts index 075ba707d..60717aef8 100644 --- a/packages/happy-dom/src/range/Range.ts +++ b/packages/happy-dom/src/range/Range.ts @@ -1,4 +1,5 @@ import INode from '../nodes/node/INode.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import Node from '../nodes/node/Node.js'; import IDocument from '../nodes/document/IDocument.js'; import IDocumentFragment from '../nodes/document-fragment/IDocumentFragment.js'; @@ -35,10 +36,10 @@ export default class Range { public readonly END_TO_START: number = RangeHowEnum.endToStart; public readonly START_TO_END: number = RangeHowEnum.startToEnd; public readonly START_TO_START: number = RangeHowEnum.startToStart; - public __start__: IRangeBoundaryPoint | null = null; - public __end__: IRangeBoundaryPoint | null = null; + public [PropertySymbol.start]: IRangeBoundaryPoint | null = null; + public [PropertySymbol.end]: IRangeBoundaryPoint | null = null; #window: IBrowserWindow; - public readonly __ownerDocument__: IDocument; + public readonly [PropertySymbol.ownerDocument]: IDocument; /** * Constructor. @@ -47,9 +48,9 @@ export default class Range { */ constructor(window: IBrowserWindow) { this.#window = window; - this.__ownerDocument__ = window.document; - this.__start__ = { node: window.document, offset: 0 }; - this.__end__ = { node: window.document, offset: 0 }; + this[PropertySymbol.ownerDocument] = window.document; + this[PropertySymbol.start] = { node: window.document, offset: 0 }; + this[PropertySymbol.end] = { node: window.document, offset: 0 }; } /** @@ -59,7 +60,7 @@ export default class Range { * @returns Start container. */ public get startContainer(): INode { - return this.__start__.node; + return this[PropertySymbol.start].node; } /** @@ -69,7 +70,7 @@ export default class Range { * @returns End container. */ public get endContainer(): INode { - return this.__end__.node; + return this[PropertySymbol.end].node; } /** @@ -79,14 +80,14 @@ export default class Range { * @returns Start offset. */ public get startOffset(): number { - if (this.__start__.offset > 0) { - const length = NodeUtility.getNodeLength(this.__start__.node); - if (this.__start__.offset > length) { - this.__start__.offset = length; + if (this[PropertySymbol.start].offset > 0) { + const length = NodeUtility.getNodeLength(this[PropertySymbol.start].node); + if (this[PropertySymbol.start].offset > length) { + this[PropertySymbol.start].offset = length; } } - return this.__start__.offset; + return this[PropertySymbol.start].offset; } /** @@ -96,14 +97,14 @@ export default class Range { * @returns End offset. */ public get endOffset(): number { - if (this.__end__.offset > 0) { - const length = NodeUtility.getNodeLength(this.__end__.node); - if (this.__end__.offset > length) { - this.__end__.offset = length; + if (this[PropertySymbol.end].offset > 0) { + const length = NodeUtility.getNodeLength(this[PropertySymbol.end].node); + if (this[PropertySymbol.end].offset > length) { + this[PropertySymbol.end].offset = length; } } - return this.__end__.offset; + return this[PropertySymbol.end].offset; } /** @@ -113,7 +114,7 @@ export default class Range { * @returns Collapsed. */ public get collapsed(): boolean { - return this.__start__.node === this.__end__.node && this.startOffset === this.endOffset; + return this[PropertySymbol.start].node === this[PropertySymbol.end].node && this.startOffset === this.endOffset; } /** @@ -123,10 +124,10 @@ export default class Range { * @returns Node. */ public get commonAncestorContainer(): INode { - let container = this.__start__.node; + let container = this[PropertySymbol.start].node; while (container) { - if (NodeUtility.isInclusiveAncestor(container, this.__end__.node)) { + if (NodeUtility.isInclusiveAncestor(container, this[PropertySymbol.end].node)) { return container; } container = container.parentNode; @@ -143,9 +144,9 @@ export default class Range { */ public collapse(toStart = false): void { if (toStart) { - this.__end__ = Object.assign({}, this.__start__); + this[PropertySymbol.end] = Object.assign({}, this[PropertySymbol.start]); } else { - this.__start__ = Object.assign({}, this.__end__); + this[PropertySymbol.start] = Object.assign({}, this[PropertySymbol.end]); } } @@ -170,7 +171,7 @@ export default class Range { ); } - if (this.__ownerDocument__ !== sourceRange.__ownerDocument__) { + if (this[PropertySymbol.ownerDocument] !== sourceRange[PropertySymbol.ownerDocument]) { throw new DOMException( `The two Ranges are not in the same tree.`, DOMExceptionNameEnum.wrongDocumentError @@ -188,27 +189,27 @@ export default class Range { switch (how) { case RangeHowEnum.startToStart: - thisPoint.node = this.__start__.node; + thisPoint.node = this[PropertySymbol.start].node; thisPoint.offset = this.startOffset; - sourcePoint.node = sourceRange.__start__.node; + sourcePoint.node = sourceRange[PropertySymbol.start].node; sourcePoint.offset = sourceRange.startOffset; break; case RangeHowEnum.startToEnd: - thisPoint.node = this.__end__.node; + thisPoint.node = this[PropertySymbol.end].node; thisPoint.offset = this.endOffset; - sourcePoint.node = sourceRange.__start__.node; + sourcePoint.node = sourceRange[PropertySymbol.start].node; sourcePoint.offset = sourceRange.startOffset; break; case RangeHowEnum.endToEnd: - thisPoint.node = this.__end__.node; + thisPoint.node = this[PropertySymbol.end].node; thisPoint.offset = this.endOffset; - sourcePoint.node = sourceRange.__end__.node; + sourcePoint.node = sourceRange[PropertySymbol.end].node; sourcePoint.offset = sourceRange.endOffset; break; case RangeHowEnum.endToStart: - thisPoint.node = this.__start__.node; + thisPoint.node = this[PropertySymbol.start].node; thisPoint.offset = this.startOffset; - sourcePoint.node = sourceRange.__end__.node; + sourcePoint.node = sourceRange[PropertySymbol.end].node; sourcePoint.offset = sourceRange.endOffset; break; } @@ -225,7 +226,7 @@ export default class Range { * @returns -1,0, or 1. */ public comparePoint(node: INode, offset): number { - if (node.ownerDocument !== this.__ownerDocument__) { + if (node.ownerDocument !== this[PropertySymbol.ownerDocument]) { throw new DOMException( `The two Ranges are not in the same tree.`, DOMExceptionNameEnum.wrongDocumentError @@ -238,14 +239,14 @@ export default class Range { if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.__start__.node, + node: this[PropertySymbol.start].node, offset: this.startOffset }) === -1 ) { return -1; } else if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.__end__.node, + node: this[PropertySymbol.end].node, offset: this.endOffset }) === 1 ) { @@ -262,7 +263,7 @@ export default class Range { * @returns Document fragment. */ public cloneContents(): IDocumentFragment { - const fragment = this.__ownerDocument__.createDocumentFragment(); + const fragment = this[PropertySymbol.ownerDocument].createDocumentFragment(); const startOffset = this.startOffset; const endOffset = this.endOffset; @@ -271,24 +272,24 @@ export default class Range { } if ( - this.__start__.node === this.__end__.node && - (this.__start__.node.nodeType === NodeTypeEnum.textNode || - this.__start__.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.__start__.node.nodeType === NodeTypeEnum.commentNode) + this[PropertySymbol.start].node === this[PropertySymbol.end].node && + (this[PropertySymbol.start].node.nodeType === NodeTypeEnum.textNode || + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.processingInstructionNode || + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this.__start__.node).cloneNode(false); - clone['__data__'] = clone.substringData(startOffset, endOffset - startOffset); + const clone = (this[PropertySymbol.start].node).cloneNode(false); + clone[PropertySymbol.data] = clone.substringData(startOffset, endOffset - startOffset); fragment.appendChild(clone); return fragment; } - let commonAncestor = this.__start__.node; - while (!NodeUtility.isInclusiveAncestor(commonAncestor, this.__end__.node)) { + let commonAncestor = this[PropertySymbol.start].node; + while (!NodeUtility.isInclusiveAncestor(commonAncestor, this[PropertySymbol.end].node)) { commonAncestor = commonAncestor.parentNode; } let firstPartialContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this.__start__.node, this.__end__.node)) { + if (!NodeUtility.isInclusiveAncestor(this[PropertySymbol.start].node, this[PropertySymbol.end].node)) { let candidate = commonAncestor.firstChild; while (!firstPartialContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -300,7 +301,7 @@ export default class Range { } let lastPartiallyContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this.__end__.node, this.__start__.node)) { + if (!NodeUtility.isInclusiveAncestor(this[PropertySymbol.end].node, this[PropertySymbol.start].node)) { let candidate = commonAncestor.lastChild; while (!lastPartiallyContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -313,7 +314,7 @@ export default class Range { const containedChildren = []; - for (const node of (commonAncestor).__childNodes__) { + for (const node of (commonAncestor)[PropertySymbol.childNodes]) { if (RangeUtility.isContained(node, this)) { if (node.nodeType === NodeTypeEnum.documentTypeNode) { throw new DOMException( @@ -331,10 +332,10 @@ export default class Range { firstPartialContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || firstPartialContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this.__start__.node).cloneNode(false); - clone['__data__'] = clone.substringData( + const clone = (this[PropertySymbol.start].node).cloneNode(false); + clone[PropertySymbol.data] = clone.substringData( startOffset, - NodeUtility.getNodeLength(this.__start__.node) - startOffset + NodeUtility.getNodeLength(this[PropertySymbol.start].node) - startOffset ); fragment.appendChild(clone); @@ -343,10 +344,10 @@ export default class Range { fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange.__start__.node = this.__start__.node; - subRange.__start__.offset = startOffset; - subRange.__end__.node = firstPartialContainedChild; - subRange.__end__.offset = NodeUtility.getNodeLength(firstPartialContainedChild); + subRange[PropertySymbol.start].node = this[PropertySymbol.start].node; + subRange[PropertySymbol.start].offset = startOffset; + subRange[PropertySymbol.end].node = firstPartialContainedChild; + subRange[PropertySymbol.end].offset = NodeUtility.getNodeLength(firstPartialContainedChild); const subDocumentFragment = subRange.cloneContents(); clone.appendChild(subDocumentFragment); @@ -363,8 +364,8 @@ export default class Range { lastPartiallyContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || lastPartiallyContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = (this.__end__.node).cloneNode(false); - clone['__data__'] = clone.substringData(0, endOffset); + const clone = (this[PropertySymbol.end].node).cloneNode(false); + clone[PropertySymbol.data] = clone.substringData(0, endOffset); fragment.appendChild(clone); } else if (lastPartiallyContainedChild !== null) { @@ -372,10 +373,10 @@ export default class Range { fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange.__start__.node = lastPartiallyContainedChild; - subRange.__start__.offset = 0; - subRange.__end__.node = this.__end__.node; - subRange.__end__.offset = endOffset; + subRange[PropertySymbol.start].node = lastPartiallyContainedChild; + subRange[PropertySymbol.start].offset = 0; + subRange[PropertySymbol.end].node = this[PropertySymbol.end].node; + subRange[PropertySymbol.end].offset = endOffset; const subFragment = subRange.cloneContents(); clone.appendChild(subFragment); @@ -393,10 +394,10 @@ export default class Range { public cloneRange(): Range { const clone = new this.#window.Range(); - clone.__start__.node = this.__start__.node; - clone.__start__.offset = this.__start__.offset; - clone.__end__.node = this.__end__.node; - clone.__end__.offset = this.__end__.offset; + clone[PropertySymbol.start].node = this[PropertySymbol.start].node; + clone[PropertySymbol.start].offset = this[PropertySymbol.start].offset; + clone[PropertySymbol.end].node = this[PropertySymbol.end].node; + clone[PropertySymbol.end].offset = this[PropertySymbol.end].offset; return clone; } @@ -410,7 +411,7 @@ export default class Range { */ public createContextualFragment(tagString: string): IDocumentFragment { // TODO: We only have support for HTML in the parser currently, so it is not necessary to check which context it is - return XMLParser.parse(this.__ownerDocument__, tagString); + return XMLParser.parse(this[PropertySymbol.ownerDocument], tagString); } /** @@ -427,18 +428,18 @@ export default class Range { } if ( - this.__start__.node === this.__end__.node && - (this.__start__.node.nodeType === NodeTypeEnum.textNode || - this.__start__.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.__start__.node.nodeType === NodeTypeEnum.commentNode) + this[PropertySymbol.start].node === this[PropertySymbol.end].node && + (this[PropertySymbol.start].node.nodeType === NodeTypeEnum.textNode || + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.processingInstructionNode || + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.commentNode) ) { - (this.__start__.node).replaceData(startOffset, endOffset - startOffset, ''); + (this[PropertySymbol.start].node).replaceData(startOffset, endOffset - startOffset, ''); return; } const nodesToRemove = []; - let currentNode = this.__start__.node; - const endNode = NodeUtility.nextDescendantNode(this.__end__.node); + let currentNode = this[PropertySymbol.start].node; + const endNode = NodeUtility.nextDescendantNode(this[PropertySymbol.end].node); while (currentNode && currentNode !== endNode) { if ( RangeUtility.isContained(currentNode, this) && @@ -452,31 +453,31 @@ export default class Range { let newNode; let newOffset; - if (NodeUtility.isInclusiveAncestor(this.__start__.node, this.__end__.node)) { - newNode = this.__start__.node; + if (NodeUtility.isInclusiveAncestor(this[PropertySymbol.start].node, this[PropertySymbol.end].node)) { + newNode = this[PropertySymbol.start].node; newOffset = startOffset; } else { - let referenceNode = this.__start__.node; + let referenceNode = this[PropertySymbol.start].node; while ( referenceNode && - !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this.__end__.node) + !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this[PropertySymbol.end].node) ) { referenceNode = referenceNode.parentNode; } newNode = referenceNode.parentNode; - newOffset = (referenceNode.parentNode).__childNodes__.indexOf(referenceNode) + 1; + newOffset = (referenceNode.parentNode)[PropertySymbol.childNodes].indexOf(referenceNode) + 1; } if ( - this.__start__.node.nodeType === NodeTypeEnum.textNode || - this.__start__.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.__start__.node.nodeType === NodeTypeEnum.commentNode + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.textNode || + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.processingInstructionNode || + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.commentNode ) { - (this.__start__.node).replaceData( + (this[PropertySymbol.start].node).replaceData( this.startOffset, - NodeUtility.getNodeLength(this.__start__.node) - this.startOffset, + NodeUtility.getNodeLength(this[PropertySymbol.start].node) - this.startOffset, '' ); } @@ -487,17 +488,17 @@ export default class Range { } if ( - this.__end__.node.nodeType === NodeTypeEnum.textNode || - this.__end__.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.__end__.node.nodeType === NodeTypeEnum.commentNode + this[PropertySymbol.end].node.nodeType === NodeTypeEnum.textNode || + this[PropertySymbol.end].node.nodeType === NodeTypeEnum.processingInstructionNode || + this[PropertySymbol.end].node.nodeType === NodeTypeEnum.commentNode ) { - (this.__end__.node).replaceData(0, endOffset, ''); + (this[PropertySymbol.end].node).replaceData(0, endOffset, ''); } - this.__start__.node = newNode; - this.__start__.offset = newOffset; - this.__end__.node = newNode; - this.__end__.offset = newOffset; + this[PropertySymbol.start].node = newNode; + this[PropertySymbol.start].offset = newOffset; + this[PropertySymbol.end].node = newNode; + this[PropertySymbol.end].offset = newOffset; } /** @@ -516,7 +517,7 @@ export default class Range { * @returns Document fragment. */ public extractContents(): IDocumentFragment { - const fragment = this.__ownerDocument__.createDocumentFragment(); + const fragment = this[PropertySymbol.ownerDocument].createDocumentFragment(); const startOffset = this.startOffset; const endOffset = this.endOffset; @@ -525,28 +526,28 @@ export default class Range { } if ( - this.__start__.node === this.__end__.node && - (this.__start__.node.nodeType === NodeTypeEnum.textNode || - this.__start__.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.__start__.node.nodeType === NodeTypeEnum.commentNode) + this[PropertySymbol.start].node === this[PropertySymbol.end].node && + (this[PropertySymbol.start].node.nodeType === NodeTypeEnum.textNode || + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.processingInstructionNode || + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this.__start__.node.cloneNode(false); - clone['__data__'] = clone.substringData(startOffset, endOffset - startOffset); + const clone = this[PropertySymbol.start].node.cloneNode(false); + clone[PropertySymbol.data] = clone.substringData(startOffset, endOffset - startOffset); fragment.appendChild(clone); - (this.__start__.node).replaceData(startOffset, endOffset - startOffset, ''); + (this[PropertySymbol.start].node).replaceData(startOffset, endOffset - startOffset, ''); return fragment; } - let commonAncestor = this.__start__.node; - while (!NodeUtility.isInclusiveAncestor(commonAncestor, this.__end__.node)) { + let commonAncestor = this[PropertySymbol.start].node; + while (!NodeUtility.isInclusiveAncestor(commonAncestor, this[PropertySymbol.end].node)) { commonAncestor = commonAncestor.parentNode; } let firstPartialContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this.__start__.node, this.__end__.node)) { + if (!NodeUtility.isInclusiveAncestor(this[PropertySymbol.start].node, this[PropertySymbol.end].node)) { let candidate = commonAncestor.firstChild; while (!firstPartialContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -558,7 +559,7 @@ export default class Range { } let lastPartiallyContainedChild = null; - if (!NodeUtility.isInclusiveAncestor(this.__end__.node, this.__start__.node)) { + if (!NodeUtility.isInclusiveAncestor(this[PropertySymbol.end].node, this[PropertySymbol.start].node)) { let candidate = commonAncestor.lastChild; while (!lastPartiallyContainedChild) { if (RangeUtility.isPartiallyContained(candidate, this)) { @@ -571,7 +572,7 @@ export default class Range { const containedChildren = []; - for (const node of (commonAncestor).__childNodes__) { + for (const node of (commonAncestor)[PropertySymbol.childNodes]) { if (RangeUtility.isContained(node, this)) { if (node.nodeType === NodeTypeEnum.documentTypeNode) { throw new DOMException( @@ -585,21 +586,21 @@ export default class Range { let newNode; let newOffset; - if (NodeUtility.isInclusiveAncestor(this.__start__.node, this.__end__.node)) { - newNode = this.__start__.node; + if (NodeUtility.isInclusiveAncestor(this[PropertySymbol.start].node, this[PropertySymbol.end].node)) { + newNode = this[PropertySymbol.start].node; newOffset = startOffset; } else { - let referenceNode = this.__start__.node; + let referenceNode = this[PropertySymbol.start].node; while ( referenceNode && - !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this.__end__.node) + !NodeUtility.isInclusiveAncestor(referenceNode.parentNode, this[PropertySymbol.end].node) ) { referenceNode = referenceNode.parentNode; } newNode = referenceNode.parentNode; - newOffset = (referenceNode.parentNode).__childNodes__.indexOf(referenceNode) + 1; + newOffset = (referenceNode.parentNode)[PropertySymbol.childNodes].indexOf(referenceNode) + 1; } if ( @@ -608,17 +609,17 @@ export default class Range { firstPartialContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || firstPartialContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this.__start__.node.cloneNode(false); - clone['__data__'] = clone.substringData( + const clone = this[PropertySymbol.start].node.cloneNode(false); + clone[PropertySymbol.data] = clone.substringData( startOffset, - NodeUtility.getNodeLength(this.__start__.node) - startOffset + NodeUtility.getNodeLength(this[PropertySymbol.start].node) - startOffset ); fragment.appendChild(clone); - (this.__start__.node).replaceData( + (this[PropertySymbol.start].node).replaceData( startOffset, - NodeUtility.getNodeLength(this.__start__.node) - startOffset, + NodeUtility.getNodeLength(this[PropertySymbol.start].node) - startOffset, '' ); } else if (firstPartialContainedChild !== null) { @@ -626,10 +627,10 @@ export default class Range { fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange.__start__.node = this.__start__.node; - subRange.__start__.offset = startOffset; - subRange.__end__.node = firstPartialContainedChild; - subRange.__end__.offset = NodeUtility.getNodeLength(firstPartialContainedChild); + subRange[PropertySymbol.start].node = this[PropertySymbol.start].node; + subRange[PropertySymbol.start].offset = startOffset; + subRange[PropertySymbol.end].node = firstPartialContainedChild; + subRange[PropertySymbol.end].offset = NodeUtility.getNodeLength(firstPartialContainedChild); const subFragment = subRange.extractContents(); clone.appendChild(subFragment); @@ -645,30 +646,30 @@ export default class Range { lastPartiallyContainedChild.nodeType === NodeTypeEnum.processingInstructionNode || lastPartiallyContainedChild.nodeType === NodeTypeEnum.commentNode) ) { - const clone = this.__end__.node.cloneNode(false); - clone['__data__'] = clone.substringData(0, endOffset); + const clone = this[PropertySymbol.end].node.cloneNode(false); + clone[PropertySymbol.data] = clone.substringData(0, endOffset); fragment.appendChild(clone); - (this.__end__.node).replaceData(0, endOffset, ''); + (this[PropertySymbol.end].node).replaceData(0, endOffset, ''); } else if (lastPartiallyContainedChild !== null) { const clone = lastPartiallyContainedChild.cloneNode(false); fragment.appendChild(clone); const subRange = new this.#window.Range(); - subRange.__start__.node = lastPartiallyContainedChild; - subRange.__start__.offset = 0; - subRange.__end__.node = this.__end__.node; - subRange.__end__.offset = endOffset; + subRange[PropertySymbol.start].node = lastPartiallyContainedChild; + subRange[PropertySymbol.start].offset = 0; + subRange[PropertySymbol.end].node = this[PropertySymbol.end].node; + subRange[PropertySymbol.end].offset = endOffset; const subFragment = subRange.extractContents(); clone.appendChild(subFragment); } - this.__start__.node = newNode; - this.__start__.offset = newOffset; - this.__end__.node = newNode; - this.__end__.offset = newOffset; + this[PropertySymbol.start].node = newNode; + this[PropertySymbol.start].offset = newOffset; + this[PropertySymbol.end].node = newNode; + this[PropertySymbol.end].offset = newOffset; return fragment; } @@ -702,7 +703,7 @@ export default class Range { * @returns "true" if in range. */ public isPointInRange(node: INode, offset = 0): boolean { - if (node.ownerDocument !== this.__ownerDocument__) { + if (node.ownerDocument !== this[PropertySymbol.ownerDocument]) { return false; } @@ -712,11 +713,11 @@ export default class Range { if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.__start__.node, + node: this[PropertySymbol.start].node, offset: this.startOffset }) === -1 || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.__end__.node, + node: this[PropertySymbol.end].node, offset: this.endOffset }) === 1 ) { @@ -734,22 +735,22 @@ export default class Range { */ public insertNode(newNode: INode): void { if ( - this.__start__.node.nodeType === NodeTypeEnum.processingInstructionNode || - this.__start__.node.nodeType === NodeTypeEnum.commentNode || - (this.__start__.node.nodeType === NodeTypeEnum.textNode && !this.__start__.node.parentNode) || - newNode === this.__start__.node + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.processingInstructionNode || + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.commentNode || + (this[PropertySymbol.start].node.nodeType === NodeTypeEnum.textNode && !this[PropertySymbol.start].node.parentNode) || + newNode === this[PropertySymbol.start].node ) { throw new DOMException('Invalid start node.', DOMExceptionNameEnum.hierarchyRequestError); } let referenceNode = - this.__start__.node.nodeType === NodeTypeEnum.textNode - ? this.__start__.node - : (this.__start__.node).__childNodes__[this.startOffset] || null; - const parent = !referenceNode ? this.__start__.node : referenceNode.parentNode; + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.textNode + ? this[PropertySymbol.start].node + : (this[PropertySymbol.start].node)[PropertySymbol.childNodes][this.startOffset] || null; + const parent = !referenceNode ? this[PropertySymbol.start].node : referenceNode.parentNode; - if (this.__start__.node.nodeType === NodeTypeEnum.textNode) { - referenceNode = (this.__start__.node).splitText(this.startOffset); + if (this[PropertySymbol.start].node.nodeType === NodeTypeEnum.textNode) { + referenceNode = (this[PropertySymbol.start].node).splitText(this.startOffset); } if (newNode === referenceNode) { @@ -763,7 +764,7 @@ export default class Range { let newOffset = !referenceNode ? NodeUtility.getNodeLength(parent) - : (referenceNode.parentNode).__childNodes__.indexOf(referenceNode); + : (referenceNode.parentNode)[PropertySymbol.childNodes].indexOf(referenceNode); newOffset += newNode.nodeType === NodeTypeEnum.documentFragmentNode ? NodeUtility.getNodeLength(newNode) @@ -772,8 +773,8 @@ export default class Range { parent.insertBefore(newNode, referenceNode); if (this.collapsed) { - this.__end__.node = parent; - this.__end__.offset = newOffset; + this[PropertySymbol.end].node = parent; + this[PropertySymbol.end].offset = newOffset; } } @@ -785,7 +786,7 @@ export default class Range { * @returns "true" if it intersects. */ public intersectsNode(node: INode): boolean { - if (node.ownerDocument !== this.__ownerDocument__) { + if (node.ownerDocument !== this[PropertySymbol.ownerDocument]) { return false; } @@ -795,16 +796,16 @@ export default class Range { return true; } - const offset = (parent).__childNodes__.indexOf(node); + const offset = (parent)[PropertySymbol.childNodes].indexOf(node); return ( RangeUtility.compareBoundaryPointsPosition( { node: parent, offset }, - { node: this.__end__.node, offset: this.endOffset } + { node: this[PropertySymbol.end].node, offset: this.endOffset } ) === -1 && RangeUtility.compareBoundaryPointsPosition( { node: parent, offset: offset + 1 }, - { node: this.__start__.node, offset: this.startOffset } + { node: this[PropertySymbol.start].node, offset: this.startOffset } ) === 1 ); } @@ -823,12 +824,12 @@ export default class Range { ); } - const index = (node.parentNode).__childNodes__.indexOf(node); + const index = (node.parentNode)[PropertySymbol.childNodes].indexOf(node); - this.__start__.node = node.parentNode; - this.__start__.offset = index; - this.__end__.node = node.parentNode; - this.__end__.offset = index + 1; + this[PropertySymbol.start].node = node.parentNode; + this[PropertySymbol.start].offset = index; + this[PropertySymbol.end].node = node.parentNode; + this[PropertySymbol.end].offset = index + 1; } /** @@ -845,10 +846,10 @@ export default class Range { ); } - this.__start__.node = node; - this.__start__.offset = 0; - this.__end__.node = node; - this.__end__.offset = NodeUtility.getNodeLength(node); + this[PropertySymbol.start].node = node; + this[PropertySymbol.start].offset = 0; + this[PropertySymbol.end].node = node; + this[PropertySymbol.end].offset = NodeUtility.getNodeLength(node); } /** @@ -864,18 +865,18 @@ export default class Range { const boundaryPoint = { node, offset }; if ( - node.ownerDocument !== this.__ownerDocument__ || + node.ownerDocument !== this[PropertySymbol.ownerDocument] || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.__start__.node, + node: this[PropertySymbol.start].node, offset: this.startOffset }) === -1 ) { - this.__start__.node = node; - this.__start__.offset = offset; + this[PropertySymbol.start].node = node; + this[PropertySymbol.start].offset = offset; } - this.__end__.node = node; - this.__end__.offset = offset; + this[PropertySymbol.end].node = node; + this[PropertySymbol.end].offset = offset; } /** @@ -891,18 +892,18 @@ export default class Range { const boundaryPoint = { node, offset }; if ( - node.ownerDocument !== this.__ownerDocument__ || + node.ownerDocument !== this[PropertySymbol.ownerDocument] || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { - node: this.__end__.node, + node: this[PropertySymbol.end].node, offset: this.endOffset }) === 1 ) { - this.__end__.node = node; - this.__end__.offset = offset; + this[PropertySymbol.end].node = node; + this[PropertySymbol.end].offset = offset; } - this.__start__.node = node; - this.__start__.offset = offset; + this[PropertySymbol.start].node = node; + this[PropertySymbol.start].offset = offset; } /** @@ -918,7 +919,7 @@ export default class Range { DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setEnd(node.parentNode, (node.parentNode).__childNodes__.indexOf(node) + 1); + this.setEnd(node.parentNode, (node.parentNode)[PropertySymbol.childNodes].indexOf(node) + 1); } /** @@ -934,7 +935,7 @@ export default class Range { DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setEnd(node.parentNode, (node.parentNode).__childNodes__.indexOf(node)); + this.setEnd(node.parentNode, (node.parentNode)[PropertySymbol.childNodes].indexOf(node)); } /** @@ -950,7 +951,7 @@ export default class Range { DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setStart(node.parentNode, (node.parentNode).__childNodes__.indexOf(node) + 1); + this.setStart(node.parentNode, (node.parentNode)[PropertySymbol.childNodes].indexOf(node) + 1); } /** @@ -966,7 +967,7 @@ export default class Range { DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setStart(node.parentNode, (node.parentNode).__childNodes__.indexOf(node)); + this.setStart(node.parentNode, (node.parentNode)[PropertySymbol.childNodes].indexOf(node)); } /** @@ -1024,18 +1025,18 @@ export default class Range { let string = ''; if ( - this.__start__.node === this.__end__.node && - this.__start__.node.nodeType === NodeTypeEnum.textNode + this[PropertySymbol.start].node === this[PropertySymbol.end].node && + this[PropertySymbol.start].node.nodeType === NodeTypeEnum.textNode ) { - return (this.__start__.node).data.slice(startOffset, endOffset); + return (this[PropertySymbol.start].node).data.slice(startOffset, endOffset); } - if (this.__start__.node.nodeType === NodeTypeEnum.textNode) { - string += (this.__start__.node).data.slice(startOffset); + if (this[PropertySymbol.start].node.nodeType === NodeTypeEnum.textNode) { + string += (this[PropertySymbol.start].node).data.slice(startOffset); } - const endNode = NodeUtility.nextDescendantNode(this.__end__.node); - let currentNode = this.__start__.node; + const endNode = NodeUtility.nextDescendantNode(this[PropertySymbol.end].node); + let currentNode = this[PropertySymbol.start].node; while (currentNode && currentNode !== endNode) { if ( @@ -1048,8 +1049,8 @@ export default class Range { currentNode = NodeUtility.following(currentNode); } - if (this.__end__.node.nodeType === NodeTypeEnum.textNode) { - string += (this.__end__.node).data.slice(0, endOffset); + if (this[PropertySymbol.end].node.nodeType === NodeTypeEnum.textNode) { + string += (this[PropertySymbol.end].node).data.slice(0, endOffset); } return string; diff --git a/packages/happy-dom/src/range/RangeUtility.ts b/packages/happy-dom/src/range/RangeUtility.ts index 1dbabcb9a..56b72bc70 100644 --- a/packages/happy-dom/src/range/RangeUtility.ts +++ b/packages/happy-dom/src/range/RangeUtility.ts @@ -1,4 +1,5 @@ import DOMException from '../exception/DOMException.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import NodeTypeEnum from '../nodes/node/NodeTypeEnum.js'; import INode from '../nodes/node/INode.js'; @@ -50,7 +51,7 @@ export default class RangeUtility { child = child.parentNode; } - if ((child.parentNode).__childNodes__.indexOf(child) < pointA.offset) { + if ((child.parentNode)[PropertySymbol.childNodes].indexOf(child) < pointA.offset) { return 1; } } diff --git a/packages/happy-dom/src/selection/Selection.ts b/packages/happy-dom/src/selection/Selection.ts index 86adf6d0a..e6279c38e 100644 --- a/packages/happy-dom/src/selection/Selection.ts +++ b/packages/happy-dom/src/selection/Selection.ts @@ -1,4 +1,5 @@ import Event from '../event/Event.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import DOMException from '../exception/DOMException.js'; import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js'; import IDocument from '../nodes/document/IDocument.js'; @@ -172,7 +173,7 @@ export default class Selection { if (!newRange) { throw new Error('Failed to execute addRange on Selection. Parameter 1 is not of type Range.'); } - if (!this.#range && newRange.__ownerDocument__ === this.#ownerDocument) { + if (!this.#range && newRange[PropertySymbol.ownerDocument] === this.#ownerDocument) { this.#associateRange(newRange); } } @@ -249,12 +250,12 @@ export default class Selection { return; } - const newRange = new this.#ownerDocument.__defaultView__.Range(); + const newRange = new this.#ownerDocument[PropertySymbol.defaultView].Range(); - newRange.__start__.node = node; - newRange.__start__.offset = offset; - newRange.__end__.node = node; - newRange.__end__.offset = offset; + newRange[PropertySymbol.start].node = node; + newRange[PropertySymbol.start].offset = offset; + newRange[PropertySymbol.end].node = node; + newRange[PropertySymbol.end].offset = offset; this.#associateRange(newRange); } @@ -284,13 +285,13 @@ export default class Selection { ); } - const { node, offset } = this.#range.__end__; - const newRange = new this.#ownerDocument.__defaultView__.Range(); + const { node, offset } = this.#range[PropertySymbol.end]; + const newRange = new this.#ownerDocument[PropertySymbol.defaultView].Range(); - newRange.__start__.node = node; - newRange.__start__.offset = offset; - newRange.__end__.node = node; - newRange.__end__.offset = offset; + newRange[PropertySymbol.start].node = node; + newRange[PropertySymbol.start].offset = offset; + newRange[PropertySymbol.end].node = node; + newRange[PropertySymbol.end].offset = offset; this.#associateRange(newRange); } @@ -308,13 +309,13 @@ export default class Selection { ); } - const { node, offset } = this.#range.__start__; - const newRange = new this.#ownerDocument.__defaultView__.Range(); + const { node, offset } = this.#range[PropertySymbol.start]; + const newRange = new this.#ownerDocument[PropertySymbol.defaultView].Range(); - newRange.__start__.node = node; - newRange.__start__.offset = offset; - newRange.__end__.node = node; - newRange.__end__.offset = offset; + newRange[PropertySymbol.start].node = node; + newRange[PropertySymbol.start].offset = offset; + newRange[PropertySymbol.end].node = node; + newRange[PropertySymbol.end].offset = offset; this.#associateRange(newRange); } @@ -333,9 +334,9 @@ export default class Selection { } const startIsBeforeNode = - RangeUtility.compareBoundaryPointsPosition(this.#range.__start__, { node, offset: 0 }) === -1; + RangeUtility.compareBoundaryPointsPosition(this.#range[PropertySymbol.start], { node, offset: 0 }) === -1; const endIsAfterNode = - RangeUtility.compareBoundaryPointsPosition(this.#range.__end__, { + RangeUtility.compareBoundaryPointsPosition(this.#range[PropertySymbol.end], { node, offset: NodeUtility.getNodeLength(node) }) === 1; @@ -377,30 +378,30 @@ export default class Selection { const anchorNode = this.anchorNode; const anchorOffset = this.anchorOffset; - const newRange = new this.#ownerDocument.__defaultView__.Range(); - newRange.__start__.node = node; - newRange.__start__.offset = 0; - newRange.__end__.node = node; - newRange.__end__.offset = 0; - - if (node.ownerDocument !== this.#range.__ownerDocument__) { - newRange.__start__.offset = offset; - newRange.__end__.offset = offset; + const newRange = new this.#ownerDocument[PropertySymbol.defaultView].Range(); + newRange[PropertySymbol.start].node = node; + newRange[PropertySymbol.start].offset = 0; + newRange[PropertySymbol.end].node = node; + newRange[PropertySymbol.end].offset = 0; + + if (node.ownerDocument !== this.#range[PropertySymbol.ownerDocument]) { + newRange[PropertySymbol.start].offset = offset; + newRange[PropertySymbol.end].offset = offset; } else if ( RangeUtility.compareBoundaryPointsPosition( { node: anchorNode, offset: anchorOffset }, { node, offset } ) <= 0 ) { - newRange.__start__.node = anchorNode; - newRange.__start__.offset = anchorOffset; - newRange.__end__.node = node; - newRange.__end__.offset = offset; + newRange[PropertySymbol.start].node = anchorNode; + newRange[PropertySymbol.start].offset = anchorOffset; + newRange[PropertySymbol.end].node = node; + newRange[PropertySymbol.end].offset = offset; } else { - newRange.__start__.node = node; - newRange.__start__.offset = offset; - newRange.__end__.node = anchorNode; - newRange.__end__.offset = anchorOffset; + newRange[PropertySymbol.start].node = node; + newRange[PropertySymbol.start].offset = offset; + newRange[PropertySymbol.end].node = anchorNode; + newRange[PropertySymbol.end].offset = anchorOffset; } this.#associateRange(newRange); @@ -432,12 +433,12 @@ export default class Selection { } const length = node.childNodes.length; - const newRange = new this.#ownerDocument.__defaultView__.Range(); + const newRange = new this.#ownerDocument[PropertySymbol.defaultView].Range(); - newRange.__start__.node = node; - newRange.__start__.offset = 0; - newRange.__end__.node = node; - newRange.__end__.offset = length; + newRange[PropertySymbol.start].node = node; + newRange[PropertySymbol.start].offset = 0; + newRange[PropertySymbol.end].node = node; + newRange[PropertySymbol.end].offset = length; this.#associateRange(newRange); } @@ -476,14 +477,14 @@ export default class Selection { const anchor = { node: anchorNode, offset: anchorOffset }; const focus = { node: focusNode, offset: focusOffset }; - const newRange = new this.#ownerDocument.__defaultView__.Range(); + const newRange = new this.#ownerDocument[PropertySymbol.defaultView].Range(); if (RangeUtility.compareBoundaryPointsPosition(anchor, focus) === -1) { - newRange.__start__ = anchor; - newRange.__end__ = focus; + newRange[PropertySymbol.start] = anchor; + newRange[PropertySymbol.end] = focus; } else { - newRange.__start__ = focus; - newRange.__end__ = anchor; + newRange[PropertySymbol.start] = focus; + newRange[PropertySymbol.end] = anchor; } this.#associateRange(newRange); diff --git a/packages/happy-dom/src/tree-walker/TreeWalker.ts b/packages/happy-dom/src/tree-walker/TreeWalker.ts index c0e5f8b09..0c391db86 100644 --- a/packages/happy-dom/src/tree-walker/TreeWalker.ts +++ b/packages/happy-dom/src/tree-walker/TreeWalker.ts @@ -1,4 +1,5 @@ import Node from '../nodes/node/Node.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import NodeFilter from './NodeFilter.js'; import INodeFilter from './INodeFilter.js'; import NodeFilterMask from './NodeFilterMask.js'; @@ -83,7 +84,7 @@ export default class TreeWalker { * @returns Current node. */ public firstChild(): INode { - const childNodes = this.currentNode ? (this.currentNode).__childNodes__ : []; + const childNodes = this.currentNode ? (this.currentNode)[PropertySymbol.childNodes] : []; if (childNodes.length > 0) { this.currentNode = childNodes[0]; @@ -104,7 +105,7 @@ export default class TreeWalker { * @returns Current node. */ public lastChild(): INode { - const childNodes = this.currentNode ? (this.currentNode).__childNodes__ : []; + const childNodes = this.currentNode ? (this.currentNode)[PropertySymbol.childNodes] : []; if (childNodes.length > 0) { this.currentNode = childNodes[childNodes.length - 1]; @@ -126,7 +127,7 @@ export default class TreeWalker { */ public previousSibling(): INode { if (this.currentNode !== this.root && this.currentNode && this.currentNode.parentNode) { - const siblings = (this.currentNode.parentNode).__childNodes__; + const siblings = (this.currentNode.parentNode)[PropertySymbol.childNodes]; const index = siblings.indexOf(this.currentNode); if (index > 0) { @@ -150,7 +151,7 @@ export default class TreeWalker { */ public nextSibling(): INode { if (this.currentNode !== this.root && this.currentNode && this.currentNode.parentNode) { - const siblings = (this.currentNode.parentNode).__childNodes__; + const siblings = (this.currentNode.parentNode)[PropertySymbol.childNodes]; const index = siblings.indexOf(this.currentNode); if (index + 1 < siblings.length) { diff --git a/packages/happy-dom/src/url/URL.ts b/packages/happy-dom/src/url/URL.ts index 61536d44d..4d5dc93d8 100644 --- a/packages/happy-dom/src/url/URL.ts +++ b/packages/happy-dom/src/url/URL.ts @@ -1,4 +1,5 @@ import { URL as NodeJSURL } from 'url'; +import * as PropertySymbol from '../PropertySymbol.js'; import { Blob as NodeJSBlob } from 'buffer'; import Blob from '../file/Blob.js'; @@ -14,7 +15,7 @@ export default class URL extends NodeJSURL { */ public static override createObjectURL(object: NodeJSBlob | Blob): string { if (object instanceof Blob) { - const blob = new NodeJSBlob([object.__buffer__], { type: object.type }); + const blob = new NodeJSBlob([object[PropertySymbol.buffer]], { type: object.type }); return super.createObjectURL(blob); } return super.createObjectURL(object); diff --git a/packages/happy-dom/src/validity-state/ValidityState.ts b/packages/happy-dom/src/validity-state/ValidityState.ts index d6b12a144..53d9af6f3 100644 --- a/packages/happy-dom/src/validity-state/ValidityState.ts +++ b/packages/happy-dom/src/validity-state/ValidityState.ts @@ -1,4 +1,5 @@ import IHTMLButtonElement from '../nodes/html-button-element/IHTMLButtonElement.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import IHTMLFormElement from '../nodes/html-form-element/IHTMLFormElement.js'; import HTMLInputElement from '../nodes/html-input-element/HTMLInputElement.js'; import IHTMLInputElement from '../nodes/html-input-element/IHTMLInputElement.js'; @@ -182,7 +183,7 @@ export default class ValidityState { return true; } const root = - this.element.__formNode__ || this.element.getRootNode(); + this.element[PropertySymbol.formNode] || this.element.getRootNode(); return !root || !root.querySelector(`input[name="${this.element.name}"]:checked`); } } diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index aedbed864..07a216e7d 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -1,4 +1,5 @@ import CustomElementRegistry from '../custom-element/CustomElementRegistry.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import DocumentImplementation from '../nodes/document/Document.js'; import HTMLDocumentImplementation from '../nodes/html-document/HTMLDocument.js'; import XMLDocumentImplementation from '../nodes/xml-document/XMLDocument.js'; @@ -491,8 +492,8 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow // Used for tracking capture event listeners to improve performance when they are not used. // See EventTarget class. - public __captureEventListenerCount__: { [eventType: string]: number } = {}; - public readonly __readyStateManager__ = new DocumentReadyStateManager(this); + public [PropertySymbol.captureEventListenerCount]: { [eventType: string]: number } = {}; + public readonly [PropertySymbol.readyStateManager] = new DocumentReadyStateManager(this); // Private properties #setTimeout: (callback: Function, delay?: number, ...args: unknown[]) => NodeJS.Timeout; @@ -564,9 +565,9 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow } const window = this; - const asyncTaskManager = this.#browserFrame.__asyncTaskManager__; + const asyncTaskManager = this.#browserFrame[PropertySymbol.asyncTaskManager]; - this.__setupVMContext__(); + this[PropertySymbol.setupVMContext](); // Class overrides // For classes that need to be bound to the correct context. @@ -579,7 +580,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow } } class Response extends ResponseImplementation { - protected static __window__ = window; + protected static [PropertySymbol.window] = window; constructor(body?: IResponseBody, init?: IResponseInit) { super({ window, browserFrame }, body, init); } @@ -668,12 +669,12 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow (this.document.defaultView) = this; // Override owner document - this.Audio.__ownerDocument__ = this.document; - this.Image.__ownerDocument__ = this.document; - this.DocumentFragment.__ownerDocument__ = this.document; + this.Audio[PropertySymbol.ownerDocument] = this.document; + this.Image[PropertySymbol.ownerDocument] = this.document; + this.DocumentFragment[PropertySymbol.ownerDocument] = this.document; // Ready state manager - this.__readyStateManager__.whenComplete().then(() => { + this[PropertySymbol.readyStateManager].whenComplete().then(() => { (this.document.readyState) = DocumentReadyStateEnum.complete; this.document.dispatchEvent(new Event('readystatechange')); this.document.dispatchEvent(new Event('load', { bubbles: true })); @@ -732,9 +733,9 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow * @returns CSS style declaration. */ public getComputedStyle(element: IElement): CSSStyleDeclaration { - element['__computedStyle__'] = - element['__computedStyle__'] || new CSSStyleDeclaration(element, true); - return element['__computedStyle__']; + element[PropertySymbol.computedStyle] = + element[PropertySymbol.computedStyle] || new CSSStyleDeclaration(element, true); + return element[PropertySymbol.computedStyle]; } /** @@ -828,9 +829,9 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow * Closes the window. */ public close(): void { - this.Audio.__ownerDocument__ = null; - this.Image.__ownerDocument__ = null; - this.DocumentFragment.__ownerDocument__ = null; + this.Audio[PropertySymbol.ownerDocument] = null; + this.Image[PropertySymbol.ownerDocument] = null; + this.DocumentFragment[PropertySymbol.ownerDocument] = null; if (this.#browserFrame.page?.mainFrame === this.#browserFrame) { this.#browserFrame.page.close(); } @@ -866,9 +867,9 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow } else { callback(...args); } - this.#browserFrame.__asyncTaskManager__.endTimer(id); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTimer(id); }, delay); - this.#browserFrame.__asyncTaskManager__.startTimer(id); + this.#browserFrame[PropertySymbol.asyncTaskManager].startTimer(id); return id; } @@ -879,7 +880,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow */ public clearTimeout(id: NodeJS.Timeout): void { this.#clearTimeout(id); - this.#browserFrame.__asyncTaskManager__.endTimer(id); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTimer(id); } /** @@ -907,7 +908,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow callback(...args); } }, delay); - this.#browserFrame.__asyncTaskManager__.startTimer(id); + this.#browserFrame[PropertySymbol.asyncTaskManager].startTimer(id); return id; } @@ -918,7 +919,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow */ public clearInterval(id: NodeJS.Timeout): void { this.#clearInterval(id); - this.#browserFrame.__asyncTaskManager__.endTimer(id); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTimer(id); } /** @@ -939,9 +940,9 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow } else { callback(this.performance.now()); } - this.#browserFrame.__asyncTaskManager__.endImmediate(id); + this.#browserFrame[PropertySymbol.asyncTaskManager].endImmediate(id); }); - this.#browserFrame.__asyncTaskManager__.startImmediate(id); + this.#browserFrame[PropertySymbol.asyncTaskManager].startImmediate(id); return id; } @@ -952,7 +953,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow */ public cancelAnimationFrame(id: NodeJS.Immediate): void { global.clearImmediate(id); - this.#browserFrame.__asyncTaskManager__.endImmediate(id); + this.#browserFrame[PropertySymbol.asyncTaskManager].endImmediate(id); } /** @@ -962,7 +963,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow */ public queueMicrotask(callback: Function): void { let isAborted = false; - const taskId = this.#browserFrame.__asyncTaskManager__.startTask(() => (isAborted = true)); + const taskId = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(() => (isAborted = true)); const settings = this.#browserFrame.page?.context?.browser?.settings; const useTryCatch = !settings || @@ -975,7 +976,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow } else { callback(); } - this.#browserFrame.__asyncTaskManager__.endTask(taskId); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskId); } }); } @@ -1065,7 +1066,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow /** * Setup of VM context. */ - protected __setupVMContext__(): void { + protected [PropertySymbol.setupVMContext](): void { if (!VM.isContext(this)) { VM.createContext(this); diff --git a/packages/happy-dom/src/window/GlobalWindow.ts b/packages/happy-dom/src/window/GlobalWindow.ts index 1c0e6f8c0..0769e3eb3 100644 --- a/packages/happy-dom/src/window/GlobalWindow.ts +++ b/packages/happy-dom/src/window/GlobalWindow.ts @@ -1,4 +1,5 @@ import IWindow from './IWindow.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import Window from './Window.js'; import { Buffer } from 'buffer'; @@ -73,7 +74,7 @@ export default class GlobalWindow extends Window implements IWindow { /** * Setup of VM context. */ - protected override __setupVMContext__(): void { + protected override [PropertySymbol.setupVMContext](): void { // Do nothing } } diff --git a/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts b/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts index cd406985f..a8b51b8a7 100644 --- a/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts +++ b/packages/happy-dom/src/window/WindowBrowserSettingsReader.ts @@ -1,4 +1,5 @@ import IBrowserSettings from '../browser/types/IBrowserSettings.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import IBrowserWindow from './IBrowserWindow.js'; /** @@ -14,7 +15,7 @@ export default class WindowBrowserSettingsReader { * @returns Settings. */ public static getSettings(window: IBrowserWindow): IBrowserSettings | null { - const id = window['__happyDOMSettingsID__']; + const id = window[PropertySymbol.happyDOMSettingsID]; if (id === undefined || !this.#settings[id]) { return null; @@ -30,10 +31,10 @@ export default class WindowBrowserSettingsReader { * @param settings Settings. */ public static setSettings(window: IBrowserWindow, settings: IBrowserSettings): void { - if (window['__happyDOMSettingsID__'] !== undefined) { + if (window[PropertySymbol.happyDOMSettingsID] !== undefined) { return; } - window['__happyDOMSettingsID__'] = this.#settings.length; + window[PropertySymbol.happyDOMSettingsID] = this.#settings.length; this.#settings.push(settings); } @@ -43,12 +44,12 @@ export default class WindowBrowserSettingsReader { * @param window Window. */ public static removeSettings(window: IBrowserWindow): void { - const id = window['__happyDOMSettingsID__']; + const id = window[PropertySymbol.happyDOMSettingsID]; if (id !== undefined && this.#settings[id]) { delete this.#settings[id]; } - delete window['__happyDOMSettingsID__']; + delete window[PropertySymbol.happyDOMSettingsID]; } } diff --git a/packages/happy-dom/src/window/WindowErrorUtility.ts b/packages/happy-dom/src/window/WindowErrorUtility.ts index d154cc385..564eaba19 100644 --- a/packages/happy-dom/src/window/WindowErrorUtility.ts +++ b/packages/happy-dom/src/window/WindowErrorUtility.ts @@ -1,4 +1,5 @@ import IBrowserWindow from './IBrowserWindow.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import ErrorEvent from '../event/events/ErrorEvent.js'; import IElement from '../nodes/element/IElement.js'; @@ -56,7 +57,7 @@ export default class WindowErrorUtility { (elementOrWindow).console.error(error); elementOrWindow.dispatchEvent(new ErrorEvent('error', { message: error.message, error })); } else { - (elementOrWindow).ownerDocument.__defaultView__.console.error(error); + (elementOrWindow).ownerDocument[PropertySymbol.defaultView].console.error(error); (elementOrWindow).dispatchEvent( new ErrorEvent('error', { message: error.message, error }) ); diff --git a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts index 017c10633..e5051906b 100644 --- a/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts +++ b/packages/happy-dom/src/xml-http-request/XMLHttpRequest.ts @@ -1,4 +1,5 @@ import XMLHttpRequestEventTarget from './XMLHttpRequestEventTarget.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import XMLHttpRequestReadyStateEnum from './XMLHttpRequestReadyStateEnum.js'; import Event from '../event/Event.js'; import IDocument from '../nodes/document/IDocument.js'; @@ -327,7 +328,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { * @param body Optional data to send as request body. */ async #sendAsync(body?: IRequestBody): Promise { - const taskID = this.#browserFrame.__asyncTaskManager__.startTask(() => this.abort()); + const taskID = this.#browserFrame[PropertySymbol.asyncTaskManager].startTask(() => this.abort()); this.#readyState = XMLHttpRequestReadyStateEnum.loading; @@ -350,7 +351,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.dispatchEvent(new Event('abort')); this.dispatchEvent(new Event('loadend')); this.dispatchEvent(new Event('readystatechange')); - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); }); const onError = (error: Error) => { @@ -366,7 +367,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { } this.dispatchEvent(new Event('loadend')); this.dispatchEvent(new Event('readystatechange')); - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); }; const fetch = new Fetch({ @@ -435,7 +436,7 @@ export default class XMLHttpRequest extends XMLHttpRequestEventTarget { this.dispatchEvent(new Event('load')); this.dispatchEvent(new Event('loadend')); - this.#browserFrame.__asyncTaskManager__.endTask(taskID); + this.#browserFrame[PropertySymbol.asyncTaskManager].endTask(taskID); } /** diff --git a/packages/happy-dom/src/xml-parser/XMLParser.ts b/packages/happy-dom/src/xml-parser/XMLParser.ts index b91d9f8b5..508b0c6f5 100755 --- a/packages/happy-dom/src/xml-parser/XMLParser.ts +++ b/packages/happy-dom/src/xml-parser/XMLParser.ts @@ -1,4 +1,5 @@ import IDocument from '../nodes/document/IDocument.js'; +import * as PropertySymbol from '../PropertySymbol.js'; import VoidElements from '../config/VoidElements.js'; import UnnestableElements from '../config/UnnestableElements.js'; import NamespaceURI from '../config/NamespaceURI.js'; @@ -286,10 +287,10 @@ export default class XMLParser { // However, they are allowed to be executed when document.write() is used. // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement if (plainTextTagName === 'SCRIPT') { - (currentNode).__evaluateScript__ = evaluateScripts; + (currentNode)[PropertySymbol.evaluateScript] = evaluateScripts; } else if (plainTextTagName === 'LINK') { // An assumption that the same rule should be applied for the HTMLLinkElement is made here. - (currentNode).__evaluateCSS__ = evaluateScripts; + (currentNode)[PropertySymbol.evaluateCSS] = evaluateScripts; } // Plain text elements such as - `); - - await new Promise((resolve) => setTimeout(resolve, 2)); - - observer.disconnect(); - - if (!(errorEvent instanceof window.ErrorEvent)) { - throw new Error('Error event not dispatched.'); - } - - if (errorEvent.error.message !== 'Test error') { - throw new Error('Error message not correct.'); - } - - if (errorEvent.message !== 'Test error') { - throw new Error('Error message not correct.'); - } -} - -async function itObservesUnhandledJavaScriptFetchRejections(): Promise { - const window = new Window({ - settings: { - disableErrorCapturing: true - } - }); - const document = window.document; - const observer = new UncaughtExceptionObserver(); - let errorEvent: ErrorEvent | null = null; - - observer.observe(window); - - window.addEventListener('error', (event) => (errorEvent = event)); - - document.write(` - - `); - - await new Promise((resolve) => setTimeout(resolve, 10)); - - observer.disconnect(); - - if (!(errorEvent instanceof window.ErrorEvent)) { - throw new Error('Error event not dispatched.'); - } - - if ( - errorEvent.error.message !== - 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED 127.0.0.1:3000' - ) { - throw new Error('Error message not correct.'); - } - - if ( - errorEvent.message !== - 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED 127.0.0.1:3000' - ) { - throw new Error('Error message not correct.'); - } -} - -async function itObservesUncaughtExceptions(): Promise { - const window = new Window(); - const document = window.document; - const observer = new UncaughtExceptionObserver(); - let errorEvent: ErrorEvent | null = null; - - observer.observe(window); - - window.addEventListener('error', (event) => (errorEvent = event)); - - window['customSetTimeout'] = setTimeout.bind(globalThis); - - document.write(` - - `); - - await new Promise((resolve) => setTimeout(resolve, 2)); - - observer.disconnect(); - - const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString(); - - if (consoleOutput.startsWith('Error: Test error\nat Timeout.eval')) { - throw new Error(`Console output not correct.`); - } - - if (!(errorEvent instanceof window.ErrorEvent)) { - throw new Error('Error event not dispatched.'); - } - - if (errorEvent.error.message !== 'Test error') { - throw new Error('Error message not correct.'); - } - - if (errorEvent.message !== 'Test error') { - throw new Error('Error message not correct.'); - } -} - -async function main(): Promise { - try { - await itObservesUnhandledFetchRejections(); - await itObservesUnhandledJavaScriptFetchRejections(); - await itObservesUncaughtExceptions(); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - process.exit(1); - } -} - -main(); diff --git a/packages/uncaught-exception-observer/test/tsconfig.json b/packages/uncaught-exception-observer/test/tsconfig.json deleted file mode 100644 index a42397ae1..000000000 --- a/packages/uncaught-exception-observer/test/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../tmp", - "rootDir": "../test" - }, - "include": [ - "@types/node", - ".", - "../lib" - ] -} \ No newline at end of file diff --git a/packages/uncaught-exception-observer/tsconfig.json b/packages/uncaught-exception-observer/tsconfig.json deleted file mode 100644 index 25f063331..000000000 --- a/packages/uncaught-exception-observer/tsconfig.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "compilerOptions": { - "outDir": "lib", - "rootDir": "src", - "target": "ES2020", - "declaration": true, - "declarationMap": true, - "module": "Node16", - "moduleResolution": "Node16", - "esModuleInterop": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "removeComments": false, - "preserveConstEnums": true, - "sourceMap": true, - "skipLibCheck": true, - "baseUrl": ".", - "composite": false, - "incremental": false, - "lib": [ - "es2020" - ], - "types": [ - "node" - ] - }, - "include": [ - "@types/node", - "src" - ], - "exclude": [ - "@types/dom" - ] -} diff --git a/packages/uncaught-exception-observer/vitest.config.ts b/packages/uncaught-exception-observer/vitest.config.ts deleted file mode 100644 index 9ace40c13..000000000 --- a/packages/uncaught-exception-observer/vitest.config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - environment: 'node', - include: ['./test/**/*.test.ts'] - } -}); From 2211596ad9484f65f85ec82442da851d1fc2ceb3 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 10 Jan 2024 02:00:35 +0100 Subject: [PATCH 60/63] #466@trivial: Fixes MutationObserver and adds support for BrowserFrame.waitForNavigation(). --- packages/happy-dom/README.md | 3 --- packages/integration-test/test/tests/Browser.test.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/happy-dom/README.md b/packages/happy-dom/README.md index c0aef71ca..741ed234b 100644 --- a/packages/happy-dom/README.md +++ b/packages/happy-dom/README.md @@ -81,9 +81,6 @@ const page = browser.newPage(); // Navigates page await page.goto('https://github.com/capricorn86'); -// Waits for all operations on the page to complete (fetch, timers etc.) -await page.waitUntilComplete(); - // Clicks on link page.mainFrame.document.querySelector('a[href*="capricorn86/happy-dom"]').click(); diff --git a/packages/integration-test/test/tests/Browser.test.js b/packages/integration-test/test/tests/Browser.test.js index ba6aae4e7..2539c57bb 100644 --- a/packages/integration-test/test/tests/Browser.test.js +++ b/packages/integration-test/test/tests/Browser.test.js @@ -11,7 +11,7 @@ describe('Browser', () => { await page.goto('https://github.com/capricorn86'); page.mainFrame.document.querySelector('a[href="/capricorn86/happy-dom"]').click(); - await page.waitForNavigation(); + await page.waitUntilComplete(); expect(page.mainFrame.url).toBe('https://github.com/capricorn86/happy-dom'); expect( From f245b8b1d58f0e0ffd8415ea75573222f1a2796f Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 10 Jan 2024 12:56:33 +0100 Subject: [PATCH 61/63] #466@major: Adds support for Browser API. --- .../test/browser/BrowserFrame.test.ts | 1 + .../DetachedBrowserFrame.test.ts | 1 + packages/happy-dom/test/fetch/Fetch.test.ts | 34 +++++++++---------- .../test/fetch/ResourceFetch.test.ts | 22 +++++++++--- packages/happy-dom/test/window/Window.test.ts | 4 +-- 5 files changed, 39 insertions(+), 23 deletions(-) diff --git a/packages/happy-dom/test/browser/BrowserFrame.test.ts b/packages/happy-dom/test/browser/BrowserFrame.test.ts index 97f6574f7..47de4b452 100644 --- a/packages/happy-dom/test/browser/BrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/BrowserFrame.test.ts @@ -11,6 +11,7 @@ import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum'; import BrowserNavigationCrossOriginPolicyEnum from '../../src/browser/enums/BrowserNavigationCrossOriginPolicyEnum'; import BrowserFrameFactory from '../../src/browser/utilities/BrowserFrameFactory'; import BrowserErrorCaptureEnum from '../../src/browser/enums/BrowserErrorCaptureEnum'; +import Headers from '../../src/fetch/Headers'; describe('BrowserFrame', () => { afterEach(() => { diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts index 43b304a4a..147d2f59d 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts @@ -12,6 +12,7 @@ import DOMExceptionNameEnum from '../../../src/exception/DOMExceptionNameEnum'; import BrowserNavigationCrossOriginPolicyEnum from '../../../src/browser/enums/BrowserNavigationCrossOriginPolicyEnum'; import BrowserFrameFactory from '../../../src/browser/utilities/BrowserFrameFactory'; import BrowserErrorCaptureEnum from '../../../src/browser/enums/BrowserErrorCaptureEnum'; +import Headers from '../../../src/fetch/Headers'; describe('DetachedBrowserFrame', () => { afterEach(() => { diff --git a/packages/happy-dom/test/fetch/Fetch.test.ts b/packages/happy-dom/test/fetch/Fetch.test.ts index dd241f50f..877c90b0f 100644 --- a/packages/happy-dom/test/fetch/Fetch.test.ts +++ b/packages/happy-dom/test/fetch/Fetch.test.ts @@ -3585,7 +3585,7 @@ describe('Fetch', () => { expect(requestCount).toBe(1); }); - it('Revalidates cache with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.002".', async () => { + it('Revalidates cache with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.003".', async () => { const window = new Window({ url: 'https://localhost:8080/' }); const url = 'https://localhost:8080/some/path'; const responseText = 'some text'; @@ -3612,7 +3612,7 @@ describe('Fetch', () => { 'last-modified', 'Mon, 11 Dec 2023 02:00:00 GMT', 'cache-control', - 'max-age=0.002' + 'max-age=0.003' ]; callback(response); @@ -3632,7 +3632,7 @@ describe('Fetch', () => { 'content-length', String(responseText.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.003', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT' ]; @@ -3677,7 +3677,7 @@ describe('Fetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.003`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT' }); @@ -3690,7 +3690,7 @@ describe('Fetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'Cache-Control': 'max-age=0.002', + 'Cache-Control': 'max-age=0.003', 'Last-Modified': 'Mon, 11 Dec 2023 02:00:00 GMT' }); @@ -3737,7 +3737,7 @@ describe('Fetch', () => { ]); }); - it('Updates cache after a failed revalidation with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.002".', async () => { + it('Updates cache after a failed revalidation with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.003".', async () => { const window = new Window({ url: 'https://localhost:8080/' }); const url = '/some/path'; const responseText1 = 'some text'; @@ -3771,7 +3771,7 @@ describe('Fetch', () => { 'content-length', String(responseText2.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.003', 'last-modified', 'Mon, 11 Dec 2023 02:00:00 GMT' ]; @@ -3793,7 +3793,7 @@ describe('Fetch', () => { 'content-length', String(responseText1.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.004', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT' ]; @@ -3846,7 +3846,7 @@ describe('Fetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText1.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.004`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT' }); @@ -3859,7 +3859,7 @@ describe('Fetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText2.length), - 'cache-control': 'max-age=0.002', + 'cache-control': 'max-age=0.003', 'last-modified': 'Mon, 11 Dec 2023 02:00:00 GMT' }); @@ -3963,7 +3963,7 @@ describe('Fetch', () => { 'content-length', String(responseText.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.003', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT', 'etag', @@ -4013,7 +4013,7 @@ describe('Fetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.003`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT', etag: etag1 }); @@ -4027,7 +4027,7 @@ describe('Fetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.003`, 'Last-Modified': 'Mon, 11 Dec 2023 02:00:00 GMT', ETag: etag2 }); @@ -4111,7 +4111,7 @@ describe('Fetch', () => { 'content-length', String(responseText2.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.003', 'last-modified', 'Mon, 11 Dec 2023 02:00:00 GMT', 'etag', @@ -4135,7 +4135,7 @@ describe('Fetch', () => { 'content-length', String(responseText1.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.003', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT', 'etag', @@ -4182,7 +4182,7 @@ describe('Fetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText1.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.003`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT', etag: etag1 }); @@ -4196,7 +4196,7 @@ describe('Fetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText2.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.003`, 'last-modified': 'Mon, 11 Dec 2023 02:00:00 GMT', etag: etag2 }); diff --git a/packages/happy-dom/test/fetch/ResourceFetch.test.ts b/packages/happy-dom/test/fetch/ResourceFetch.test.ts index 746db9c4b..46035c3ca 100644 --- a/packages/happy-dom/test/fetch/ResourceFetch.test.ts +++ b/packages/happy-dom/test/fetch/ResourceFetch.test.ts @@ -6,6 +6,7 @@ import Browser from '../../src/browser/Browser.js'; import Fetch from '../../src/fetch/Fetch.js'; import SyncFetch from '../../src/fetch/SyncFetch.js'; import ISyncResponse from '../../src/fetch/types/ISyncResponse.js'; +import DOMException from '../../src/exception/DOMException.js'; const URL = 'https://localhost:8080/base/'; @@ -52,7 +53,7 @@ describe('ResourceFetch', () => { expect(test).toBe('test'); }); - it('Handles error when resource is fetched asynchrounously.', () => { + it('Handles error when resource is fetched asynchrounously.', async () => { vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function () { return { ok: false, @@ -61,7 +62,14 @@ describe('ResourceFetch', () => { }; }); - expect(resourceFetch.fetch('path/to/script/')).rejects.toEqual( + let error: Error | null = null; + try { + await resourceFetch.fetch('path/to/script/'); + } catch (e) { + error = e; + } + + expect(error).toEqual( new DOMException( `Failed to perform request to "${URL}path/to/script/". Status 404 Not Found.` ) @@ -103,9 +111,15 @@ describe('ResourceFetch', () => { }; }); - expect(() => { + let error: Error | null = null; + + try { resourceFetch.fetchSync('path/to/script/'); - }).toThrowError( + } catch (e) { + error = e; + } + + expect(error).toEqual( new DOMException( `Failed to perform request to "${URL}path/to/script/". Status 404 Not Found.` ) diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index fcac18924..3478b1f5d 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -315,7 +315,7 @@ describe('Window', () => { expect(newWindow2.name).toBe(''); expect(newWindow2.location.href).toBe('https://localhost:8080/test/2/'); - await new Promise((resolve) => setTimeout(resolve, 1)); + await new Promise((resolve) => setTimeout(resolve, 2)); expect(newWindow2.document.body.innerHTML).toBe('Test'); }); @@ -343,7 +343,7 @@ describe('Window', () => { expect(newWindow2.name).toBe(''); expect(newWindow2.location.href).toBe('https://localhost:8080/test/2/'); - await new Promise((resolve) => setTimeout(resolve, 1)); + await new Promise((resolve) => setTimeout(resolve, 2)); expect(newWindow2.document.body.innerHTML).toBe('Test'); }); From c6ca530c6f8ec02a5fedf3ff857519fa18413d5c Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 10 Jan 2024 13:16:19 +0100 Subject: [PATCH 62/63] #466@trivial: Adds unit tests for waitForNavigation(). --- .../test/browser/BrowserFrame.test.ts | 36 +++++++++++++++++++ .../test/browser/BrowserPage.test.ts | 17 +++++++++ .../DetachedBrowserFrame.test.ts | 36 +++++++++++++++++++ .../DetachedBrowserPage.test.ts | 17 +++++++++ 4 files changed, 106 insertions(+) diff --git a/packages/happy-dom/test/browser/BrowserFrame.test.ts b/packages/happy-dom/test/browser/BrowserFrame.test.ts index 47de4b452..58984f7f5 100644 --- a/packages/happy-dom/test/browser/BrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/BrowserFrame.test.ts @@ -12,6 +12,7 @@ import BrowserNavigationCrossOriginPolicyEnum from '../../src/browser/enums/Brow import BrowserFrameFactory from '../../src/browser/utilities/BrowserFrameFactory'; import BrowserErrorCaptureEnum from '../../src/browser/enums/BrowserErrorCaptureEnum'; import Headers from '../../src/fetch/Headers'; +import IHTMLAnchorElement from '../../src/nodes/html-anchor-element/IHTMLAnchorElement'; describe('BrowserFrame', () => { afterEach(() => { @@ -135,6 +136,41 @@ describe('BrowserFrame', () => { }); }); + describe('waitForNavigation()', () => { + it('Waits page to have been navigated.', async () => { + let count = 0; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + count++; + return Promise.resolve({ + text: () => + new Promise((resolve) => + setTimeout( + () => + resolve( + count === 1 ? '' : 'Navigated' + ), + 1 + ) + ) + }); + }); + + const browser = new Browser(); + const page = browser.newPage(); + + await page.mainFrame.goto('http://localhost:3000', { + referrer: 'http://localhost:3000/referrer', + referrerPolicy: 'no-referrer-when-downgrade' + }); + + (page.mainFrame.document.querySelector('a')).click(); + + await page.mainFrame.waitForNavigation(); + + expect(page.mainFrame.document.querySelector('b')?.textContent).toBe('Navigated'); + }); + }); + describe('abort()', () => { it('Aborts all ongoing operations.', async () => { const browser = new Browser(); diff --git a/packages/happy-dom/test/browser/BrowserPage.test.ts b/packages/happy-dom/test/browser/BrowserPage.test.ts index d99036981..1310aba28 100644 --- a/packages/happy-dom/test/browser/BrowserPage.test.ts +++ b/packages/happy-dom/test/browser/BrowserPage.test.ts @@ -137,6 +137,23 @@ describe('BrowserPage', () => { }); }); + describe('waitForNavigation()', () => { + it('Waits page to have been navigated.', async () => { + const browser = new Browser(); + const page = browser.newPage(); + let isCalled = false; + + vi.spyOn(page.mainFrame, 'waitForNavigation').mockImplementation((): Promise => { + isCalled = true; + return Promise.resolve(); + }); + + await page.waitForNavigation(); + + expect(isCalled).toBe(true); + }); + }); + describe('abort()', () => { it('Aborts all ongoing operations.', async () => { const browser = new Browser(); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts index 147d2f59d..c433b9b0c 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts @@ -13,6 +13,7 @@ import BrowserNavigationCrossOriginPolicyEnum from '../../../src/browser/enums/B import BrowserFrameFactory from '../../../src/browser/utilities/BrowserFrameFactory'; import BrowserErrorCaptureEnum from '../../../src/browser/enums/BrowserErrorCaptureEnum'; import Headers from '../../../src/fetch/Headers'; +import IHTMLAnchorElement from '../../../src/nodes/html-anchor-element/IHTMLAnchorElement'; describe('DetachedBrowserFrame', () => { afterEach(() => { @@ -145,6 +146,41 @@ describe('DetachedBrowserFrame', () => { }); }); + describe('waitForNavigation()', () => { + it('Waits page to have been navigated.', async () => { + let count = 0; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + count++; + return Promise.resolve({ + text: () => + new Promise((resolve) => + setTimeout( + () => + resolve( + count === 1 ? '' : 'Navigated' + ), + 1 + ) + ) + }); + }); + + const browser = new DetachedBrowser(BrowserWindow); + const page = browser.newPage(); + + await page.mainFrame.goto('http://localhost:3000', { + referrer: 'http://localhost:3000/referrer', + referrerPolicy: 'no-referrer-when-downgrade' + }); + + (page.mainFrame.document.querySelector('a')).click(); + + await page.mainFrame.waitForNavigation(); + + expect(page.mainFrame.document.querySelector('b')?.textContent).toBe('Navigated'); + }); + }); + describe('abort()', () => { it('Aborts all ongoing operations.', async () => { const browser = new DetachedBrowser(BrowserWindow); diff --git a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts index 43077d8b3..934681d4b 100644 --- a/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts +++ b/packages/happy-dom/test/browser/detached-browser/DetachedBrowserPage.test.ts @@ -147,6 +147,23 @@ describe('DetachedBrowserPage', () => { }); }); + describe('waitForNavigation()', () => { + it('Waits page to have been navigated.', async () => { + const browser = new DetachedBrowser(BrowserWindow); + const page = browser.newPage(); + let isCalled = false; + + vi.spyOn(page.mainFrame, 'waitForNavigation').mockImplementation((): Promise => { + isCalled = true; + return Promise.resolve(); + }); + + await page.waitForNavigation(); + + expect(isCalled).toBe(true); + }); + }); + describe('abort()', () => { it('Aborts all ongoing operations.', async () => { const browser = new DetachedBrowser(BrowserWindow); From 32e144e14fc31d50b3a6b4e0aff9c2f86480d761 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 10 Jan 2024 17:06:32 +0100 Subject: [PATCH 63/63] #466@trivial: Adds unit test for importNode(). --- .../happy-dom/test/fetch/SyncFetch.test.ts | 34 +++++++++---------- .../test/nodes/document/Document.test.ts | 29 ++++++++++++++-- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/packages/happy-dom/test/fetch/SyncFetch.test.ts b/packages/happy-dom/test/fetch/SyncFetch.test.ts index 7388434f8..c9938447c 100644 --- a/packages/happy-dom/test/fetch/SyncFetch.test.ts +++ b/packages/happy-dom/test/fetch/SyncFetch.test.ts @@ -2195,7 +2195,7 @@ describe('SyncFetch', () => { expect(requestCount).toBe(1); }); - it('Revalidates cache with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.002".', async () => { + it('Revalidates cache with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.005".', async () => { browserFrame.url = 'https://localhost:8080/'; const url = 'https://localhost:8080/some/path'; @@ -2216,7 +2216,7 @@ describe('SyncFetch', () => { 'last-modified', 'Mon, 11 Dec 2023 02:00:00 GMT', 'cache-control', - 'max-age=0.002' + 'max-age=0.005' ], data: '' } @@ -2233,7 +2233,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.005', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT' ], @@ -2279,7 +2279,7 @@ describe('SyncFetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.005`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT' }); @@ -2292,7 +2292,7 @@ describe('SyncFetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'Cache-Control': 'max-age=0.002', + 'Cache-Control': 'max-age=0.005', 'Last-Modified': 'Mon, 11 Dec 2023 02:00:00 GMT' }); @@ -2329,7 +2329,7 @@ describe('SyncFetch', () => { ]); }); - it('Updates cache after a failed revalidation with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.002".', async () => { + it('Updates cache after a failed revalidation with a "If-Modified-Since" request for a GET response with "Cache-Control" set to "max-age=0.005".', async () => { browserFrame.url = 'https://localhost:8080/'; const url = 'https://localhost:8080/some/path'; @@ -2353,7 +2353,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText2.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.005', 'last-modified', 'Mon, 11 Dec 2023 02:00:00 GMT' ], @@ -2372,7 +2372,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText1.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.005', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT' ], @@ -2426,7 +2426,7 @@ describe('SyncFetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText1.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.005`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT' }); @@ -2439,7 +2439,7 @@ describe('SyncFetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText2.length), - 'cache-control': 'max-age=0.002', + 'cache-control': 'max-age=0.005', 'last-modified': 'Mon, 11 Dec 2023 02:00:00 GMT' }); @@ -2519,7 +2519,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.005', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT', 'etag', @@ -2575,7 +2575,7 @@ describe('SyncFetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.005`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT', etag: etag1 }); @@ -2589,7 +2589,7 @@ describe('SyncFetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.005`, 'Last-Modified': 'Mon, 11 Dec 2023 02:00:00 GMT', ETag: etag2 }); @@ -2653,7 +2653,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText2.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.005', 'last-modified', 'Mon, 11 Dec 2023 02:00:00 GMT', 'etag', @@ -2674,7 +2674,7 @@ describe('SyncFetch', () => { 'content-length', String(responseText1.length), 'cache-control', - 'max-age=0.002', + 'max-age=0.005', 'last-modified', 'Mon, 11 Dec 2023 01:00:00 GMT', 'etag', @@ -2722,7 +2722,7 @@ describe('SyncFetch', () => { expect(headers1).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText1.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.005`, 'last-modified': 'Mon, 11 Dec 2023 01:00:00 GMT', etag: etag1 }); @@ -2736,7 +2736,7 @@ describe('SyncFetch', () => { expect(headers2).toEqual({ 'content-type': 'text/html', 'content-length': String(responseText2.length), - 'cache-control': `max-age=0.002`, + 'cache-control': `max-age=0.005`, 'last-modified': 'Mon, 11 Dec 2023 02:00:00 GMT', etag: etag2 }); diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts index 52d7b1d6f..fb44b19f3 100644 --- a/packages/happy-dom/test/nodes/document/Document.test.ts +++ b/packages/happy-dom/test/nodes/document/Document.test.ts @@ -1088,12 +1088,35 @@ describe('Document', () => { describe('importNode()', () => { it('Creates a clone of a Node and sets the ownerDocument to be the current document.', () => { - const node = new Window().document.createElement('div'); - const clone = document.importNode(node); + const window1 = new Window(); + const window2 = new Window(); + const node = window1.document.createElement('div'); + const clone = window2.document.importNode(node); expect(clone.tagName).toBe('DIV'); - expect(clone.ownerDocument === document).toBe(true); + expect(clone.ownerDocument === window2.document).toBe(true); expect(clone instanceof HTMLElement).toBe(true); }); + + it('Creates a clone of a Node and sets the ownerDocument to be the current document on child nodes when setting the "deep" parameter to "true".', () => { + const window1 = new Window(); + const window2 = new Window(); + const node = window1.document.createElement('div'); + const childNode1 = window1.document.createElement('span'); + const childNode2 = window1.document.createElement('span'); + + node.appendChild(childNode1); + node.appendChild(childNode2); + + const clone = window2.document.importNode(node, true); + expect(clone.tagName).toBe('DIV'); + expect(clone.ownerDocument === window2.document).toBe(true); + + expect(clone.children.length).toBe(2); + expect(clone.children[0].tagName).toBe('SPAN'); + expect(clone.children[0].ownerDocument === window2.document).toBe(true); + expect(clone.children[1].tagName).toBe('SPAN'); + expect(clone.children[1].ownerDocument === window2.document).toBe(true); + }); }); describe('cloneNode()', () => {