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 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'; 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 ImageImplementation from '../nodes/html-image-element/Image.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 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 { URL, URLSearchParams } from 'url'; import Location from '../location/Location.js'; import NonImplementedEventTypes from '../event/NonImplementedEventTypes.js'; import MutationObserver from '../mutation-observer/MutationObserver.js'; import NonImplemenetedElementClasses from '../config/NonImplemenetedElementClasses.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'; 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 AsyncTaskManager from '../async-task-manager/AsyncTaskManager.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'; 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 RangeImplementation from '../range/Range.js'; import VMGlobalPropertyScript from './VMGlobalPropertyScript.js'; import * as PerfHooks from 'perf_hooks'; import VM from 'vm'; import { Buffer } from 'buffer'; 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 Attr from '../nodes/attr/Attr.js'; import NamedNodeMap from '../named-node-map/NamedNodeMap.js'; import ProcessingInstruction from '../nodes/processing-instruction/ProcessingInstruction.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'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; const ORIGINAL_SET_INTERVAL = setInterval; const ORIGINAL_CLEAR_INTERVAL = clearInterval; /** * Browser window. * * Reference: * https://developer.mozilla.org/en-US/docs/Web/API/Window. */ export default class Window extends EventTarget { /** * Constructor. * * @param [options] Options. * @param [options.width] Window width. Defaults to "1024". * @param [options.height] Window height. Defaults to "768". * @param [options.innerWidth] Inner width. Deprecated. Defaults to "1024". * @param [options.innerHeight] Inner height. Deprecated. Defaults to "768". * @param [options.url] URL. * @param [options.settings] Settings. */ constructor(options) { super(); // The Happy DOM property this.happyDOM = { whenAsyncComplete: async () => { return await this.happyDOM.asyncTaskManager.whenComplete(); }, cancelAsync: () => { this.happyDOM.asyncTaskManager.cancelAll(); }, asyncTaskManager: new AsyncTaskManager(), setURL: (url) => { this.location.href = url; }, setWindowSize: (options) => { 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')); } }, settings: { disableJavaScriptEvaluation: false, disableJavaScriptFileLoading: false, disableCSSFileLoading: false, disableIframePageLoading: false, disableComputedStyleRendering: false, enableFileSystemHttpRequests: false, device: { prefersColorScheme: 'light', mediaType: 'screen' } }, /** * @param width The width of the viewport. * @deprecated */ setInnerWidth: (width) => this.happyDOM.setWindowSize({ width }), /** * @param height The height of the viewport. * @deprecated */ setInnerHeight: (height) => this.happyDOM.setWindowSize({ height }) }; // Global classes this.Node = Node; this.HTMLElement = HTMLElement; this.HTMLUnknownElement = HTMLUnknownElement; this.HTMLTemplateElement = HTMLTemplateElement; this.HTMLFormElement = HTMLFormElement; this.HTMLInputElement = HTMLInputElement; this.HTMLSelectElement = HTMLSelectElement; this.HTMLTextAreaElement = HTMLTextAreaElement; this.HTMLImageElement = HTMLImageElement; this.HTMLScriptElement = HTMLScriptElement; this.HTMLLinkElement = HTMLLinkElement; this.HTMLStyleElement = HTMLStyleElement; this.HTMLLabelElement = HTMLLabelElement; this.HTMLSlotElement = HTMLSlotElement; this.HTMLMetaElement = HTMLMetaElement; this.HTMLMediaElement = HTMLMediaElement; this.HTMLAudioElement = HTMLAudioElement; this.HTMLVideoElement = HTMLVideoElement; this.HTMLBaseElement = HTMLBaseElement; this.HTMLIFrameElement = HTMLIFrameElement; this.HTMLDialogElement = HTMLDialogElement; this.Attr = Attr; this.NamedNodeMap = NamedNodeMap; this.SVGSVGElement = SVGSVGElement; this.SVGElement = SVGElement; this.SVGGraphicsElement = SVGGraphicsElement; this.Text = Text; this.Comment = Comment; this.ShadowRoot = ShadowRoot; this.ProcessingInstruction = ProcessingInstruction; this.Element = Element; this.DocumentFragment = DocumentFragment; this.CharacterData = CharacterData; this.NodeFilter = NodeFilter; this.NodeIterator = NodeIterator; this.TreeWalker = TreeWalker; this.MutationObserver = MutationObserver; this.Document = Document; this.HTMLDocument = HTMLDocument; this.XMLDocument = XMLDocument; this.SVGDocument = SVGDocument; this.Event = Event; this.UIEvent = UIEvent; this.CustomEvent = CustomEvent; this.AnimationEvent = AnimationEvent; this.KeyboardEvent = KeyboardEvent; this.MessageEvent = MessageEvent; this.MouseEvent = MouseEvent; this.PointerEvent = PointerEvent; this.FocusEvent = FocusEvent; this.WheelEvent = WheelEvent; this.InputEvent = InputEvent; this.ErrorEvent = ErrorEvent; this.StorageEvent = StorageEvent; this.SubmitEvent = SubmitEvent; this.ProgressEvent = ProgressEvent; this.MediaQueryListEvent = MediaQueryListEvent; this.EventTarget = EventTarget; this.MessagePort = MessagePort; this.DataTransfer = DataTransfer; this.DataTransferItem = DataTransferItem; this.DataTransferItemList = DataTransferItemList; this.URL = URL; this.Location = Location; this.CustomElementRegistry = CustomElementRegistry; this.Window = this.constructor; this.XMLSerializer = XMLSerializer; this.ResizeObserver = ResizeObserver; this.CSSStyleSheet = CSSStyleSheet; this.Blob = Blob; this.File = File; this.DOMException = DOMException; this.History = History; this.Screen = Screen; this.Storage = Storage; this.URLSearchParams = URLSearchParams; this.HTMLCollection = HTMLCollection; this.HTMLFormControlsCollection = HTMLFormControlsCollection; this.NodeList = NodeList; this.CSSUnitValue = CSSUnitValue; this.CSSRule = CSSRule; this.CSSContainerRule = CSSContainerRule; this.CSSFontFaceRule = CSSFontFaceRule; this.CSSKeyframeRule = CSSKeyframeRule; this.CSSKeyframesRule = CSSKeyframesRule; this.CSSMediaRule = CSSMediaRule; this.CSSStyleRule = CSSStyleRule; this.CSSSupportsRule = CSSSupportsRule; this.Selection = Selection; this.Navigator = Navigator; this.MimeType = MimeType; this.MimeTypeArray = MimeTypeArray; this.Plugin = Plugin; this.PluginArray = PluginArray; this.FileList = FileList; this.Headers = Headers; this.XMLHttpRequestUpload = XMLHttpRequestUpload; this.XMLHttpRequestEventTarget = XMLHttpRequestEventTarget; this.ReadableStream = Stream.Readable; this.WritableStream = Stream.Writable; this.TransformStream = Stream.Transform; this.AbortController = AbortController; this.AbortSignal = AbortSignal; this.FormData = FormData; // Events this.onload = null; this.onerror = null; this.console = console; this.self = this; this.top = this; this.parent = this; this.window = this; this.globalThis = this; this.devicePixelRatio = 1; this.performance = PerfHooks.performance; this.Buffer = Buffer; // Public internal properties // Used for tracking capture event listeners to improve performance when they are not used. // See EventTarget class. this._captureEventListenerCount = {}; this.customElements = new CustomElementRegistry(); this.location = new Location(); this.navigator = new Navigator(); this.history = new History(); this.screen = new Screen(); this.sessionStorage = new Storage(); this.localStorage = new Storage(); if (options && options.width !== undefined) { this.innerWidth = options.width; this.outerWidth = options.width; } else if (options && options.innerWidth !== undefined) { this.innerWidth = options.innerWidth; this.outerWidth = options.innerWidth; } else { this.innerWidth = 1024; this.outerWidth = 1024; } if (options && options.height !== undefined) { this.innerHeight = options.height; this.outerHeight = options.height; } else if (options && options.innerHeight !== undefined) { this.innerHeight = options.innerHeight; this.outerHeight = options.innerHeight; } else { this.innerHeight = 768; this.outerHeight = 768; } if (options && options.url !== undefined) { this.location.href = options.url; } if (options && options.settings) { this.happyDOM.settings = { ...this.happyDOM.settings, ...options.settings, device: { ...this.happyDOM.settings.device, ...options.settings.device } }; } this._setTimeout = ORIGINAL_SET_TIMEOUT; this._clearTimeout = ORIGINAL_CLEAR_TIMEOUT; this._setInterval = ORIGINAL_SET_INTERVAL; this._clearInterval = ORIGINAL_CLEAR_INTERVAL; // Non-implemented event types for (const eventType of NonImplementedEventTypes) { if (!this[eventType]) { this[eventType] = Event; } } // Non implemented element classes for (const className of NonImplemenetedElementClasses) { if (!this[className]) { this[className] = HTMLElement; } } // 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') { this[key] = this[key].bind(this); } } HTMLDocument._defaultView = this; const document = new HTMLDocument(); this.document = document; // 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; FileReaderImplementation._ownerDocument = document; DOMParserImplementation._ownerDocument = document; RangeImplementation._ownerDocument = document; XMLHttpRequestImplementation._ownerDocument = document; /* eslint-disable jsdoc/require-jsdoc */ class Response extends ResponseImplementation { } Response._ownerDocument = document; class Request extends RequestImplementation { } Request._ownerDocument = document; class Image extends ImageImplementation { } Image._ownerDocument = document; class FileReader extends FileReaderImplementation { } FileReader._ownerDocument = document; class DOMParser extends DOMParserImplementation { } DOMParser._ownerDocument = document; class XMLHttpRequest extends XMLHttpRequestImplementation { } XMLHttpRequest._ownerDocument = document; class Range extends RangeImplementation { } Range._ownerDocument = document; class Audio extends AudioImplementation { } Audio._ownerDocument = document; /* eslint-enable jsdoc/require-jsdoc */ this.Response = Response; this.Request = Request; this.Image = Image; this.FileReader = FileReader; this.DOMParser = DOMParser; this.XMLHttpRequest = XMLHttpRequest; this.Range = Range; this.Audio = Audio; this._setupVMContext(); this.document._onWindowReady(); } /** * The number of pixels that the document is currently scrolled horizontally * * @returns number */ get scrollX() { return this.document?.documentElement?.scrollLeft ?? 0; } /** * The read-only Window property pageXOffset is an alias for scrollX. * * @returns number */ get pageXOffset() { return this.scrollX; } /** * The number of pixels that the document is currently scrolled vertically * * @returns number */ get scrollY() { return this.document?.documentElement?.scrollTop ?? 0; } /** * The read-only Window property pageYOffset is an alias for scrollY. * * @returns number */ get pageYOffset() { return this.scrollY; } /** * The CSS interface holds useful CSS-related methods. * * @returns CSS interface. */ get CSS() { return new CSS(); } /** * Evaluates code. * * @override * @param code Code. * @returns Result. */ eval(code) { if (VM.isContext(this)) { return VM.runInContext(code, this); } return eval(code); } /** * Returns an object containing the values of all CSS properties of an element. * * @param element Element. * @returns CSS style declaration. */ getComputedStyle(element) { element['_computedStyle'] = element['_computedStyle'] || new CSSStyleDeclaration(element, true); return element['_computedStyle']; } /** * Returns selection. * * @returns Selection. */ getSelection() { return this.document.getSelection(); } /** * Scrolls to a particular set of coordinates. * * @param x X position or options object. * @param y Y position. */ scroll(x, y) { 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. */ scrollTo(x, y) { this.scroll(x, y); } /** * 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) { 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. */ setTimeout(callback, delay = 0, ...args) { const id = this._setTimeout(() => { this.happyDOM.asyncTaskManager.endTimer(id); callback(...args); }, delay); this.happyDOM.asyncTaskManager.startTimer(id); return id; } /** * Cancels a timeout previously established by calling setTimeout(). * * @param id ID of the timeout. */ clearTimeout(id) { this._clearTimeout(id); this.happyDOM.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. */ setInterval(callback, delay = 0, ...args) { const id = this._setInterval(callback, delay, ...args); this.happyDOM.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. */ clearInterval(id) { this._clearInterval(id); this.happyDOM.asyncTaskManager.endTimer(id); } /** * Mock animation frames with timeouts. * * @param callback Callback. * @returns Timeout ID. */ requestAnimationFrame(callback) { return this.setTimeout(() => { callback(this.performance.now()); }); } /** * Mock animation frames with timeouts. * * @param id Timeout ID. */ cancelAnimationFrame(id) { this.clearTimeout(id); } /** * This method provides an easy, logical way to fetch resources asynchronously across the network. * * @param url URL. * @param [init] Init. * @returns Promise. */ async fetch(url, init) { return await new Fetch({ ownerDocument: this.document, 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. */ btoa(data) { 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. */ atob(data) { 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. */ postMessage(message, targetOrigin = '*', _transfer) { // 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.dispatchEvent(new MessageEvent('message', { data: message, origin: this.parent.location.origin, source: this.parent, lastEventId: '' })); } /** * Setup of VM context. */ _setupVMContext() { 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. VM.runInContext(VMGlobalPropertyScript, this); } } } //# sourceMappingURL=Window.js.map