From 0b953438acf85f25a0da5cb9b4bc6f88559668ee Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 24 Jan 2024 02:54:11 +0100 Subject: [PATCH] #1105@minor: Adds support for sending in Window options to GlobalRegistrator.register(). Fixes problem with getters and setters not being added to the global object. --- .../src/GlobalRegistrator.ts | 81 +++++++++++++++---- .../test/react/React.test.tsx | 29 +++++++ packages/happy-dom/src/index.ts | 6 +- 3 files changed, 101 insertions(+), 15 deletions(-) diff --git a/packages/global-registrator/src/GlobalRegistrator.ts b/packages/global-registrator/src/GlobalRegistrator.ts index 774165e63..e19dd0931 100644 --- a/packages/global-registrator/src/GlobalRegistrator.ts +++ b/packages/global-registrator/src/GlobalRegistrator.ts @@ -1,34 +1,87 @@ -import { GlobalWindow } from 'happy-dom'; +import { GlobalWindow, Window, EventTarget } from 'happy-dom'; +import type { IOptionalBrowserSettings } from 'happy-dom'; -const IGNORE_LIST = ['undefined', 'NaN', 'global', 'globalThis']; +const IGNORE_LIST = ['constructor', 'undefined', 'NaN', 'global', 'globalThis']; const SELF_REFERRING = ['self', 'top', 'parent', 'window']; /** * */ export default class GlobalRegistrator { - private static registered: { [key: string]: string } | null = null; + private static registered: { [key: string]: PropertyDescriptor } | null = null; /** * Registers Happy DOM globally. + * + * @param [options] Options. + * @param [options.width] Window width. Defaults to "1024". + * @param [options.height] Window height. Defaults to "768". + * @param [options.url] URL. + * @param [options.settings] Settings. */ - public static register(): void { + public static register(options?: { + width?: number; + height?: number; + url?: string; + settings?: IOptionalBrowserSettings; + }): void { if (this.registered !== null) { throw new Error('Failed to register. Happy DOM has already been globally registered.'); } - const window = new GlobalWindow(); + const window = new GlobalWindow({ ...options, console: global.console }); this.registered = {}; - for (const key of Object.keys(window)) { - if (global[key] !== window[key] && !IGNORE_LIST.includes(key)) { - this.registered[key] = - global[key] !== window[key] && global[key] !== undefined ? global[key] : null; - global[key] = - typeof window[key] === 'function' && !window[key].toString().startsWith('class ') - ? window[key].bind(global) - : window[key]; + const propertyDescriptors = Object.getOwnPropertyDescriptors(window); + + for (const key of Object.keys(propertyDescriptors)) { + if (!IGNORE_LIST.includes(key)) { + const windowPropertyDescriptor = propertyDescriptors[key]; + const globalPropertyDescriptor = Object.getOwnPropertyDescriptor(global, key); + + if ( + windowPropertyDescriptor.value !== undefined && + (!globalPropertyDescriptor || + windowPropertyDescriptor.value !== globalPropertyDescriptor.value) + ) { + this.registered[key] = globalPropertyDescriptor || null; + + if ( + typeof windowPropertyDescriptor.value === 'function' && + !windowPropertyDescriptor.value.toString().startsWith('class ') + ) { + Object.defineProperty(global, key, { + ...windowPropertyDescriptor, + value: windowPropertyDescriptor.value.bind(global) + }); + } else { + Object.defineProperty(global, key, windowPropertyDescriptor); + } + } + } + } + + for (const windowClass of [GlobalWindow, Window, EventTarget]) { + const propertyDescriptors = Object.getOwnPropertyDescriptors( + Reflect.getPrototypeOf(windowClass.prototype) + ); + for (const key of Object.keys(propertyDescriptors)) { + if (!IGNORE_LIST.includes(key) && !this.registered[key]) { + const windowPropertyDescriptor = propertyDescriptors[key]; + if (windowPropertyDescriptor.get || windowPropertyDescriptor.set) { + const globalPropertyDescriptor = Object.getOwnPropertyDescriptor(global, key); + + this.registered[key] = globalPropertyDescriptor || null; + + Object.defineProperty(global, key, { + configurable: true, + enumerable: windowPropertyDescriptor.enumerable, + get: windowPropertyDescriptor.get?.bind(window), + set: windowPropertyDescriptor.set?.bind(window) + }); + } + } } } @@ -50,7 +103,7 @@ export default class GlobalRegistrator { for (const key of Object.keys(this.registered)) { if (this.registered[key] !== null) { - global[key] = this.registered[key]; + Object.defineProperty(global, key, this.registered[key]); } else { delete global[key]; } diff --git a/packages/global-registrator/test/react/React.test.tsx b/packages/global-registrator/test/react/React.test.tsx index e7b8cac87..1f60bdbc5 100644 --- a/packages/global-registrator/test/react/React.test.tsx +++ b/packages/global-registrator/test/react/React.test.tsx @@ -63,6 +63,35 @@ async function main(): Promise { if (global.setTimeout !== originalSetTimeout) { throw Error('Global property was not restored.'); } + + GlobalRegistrator.register({ + url: 'https://example.com/', + width: 1920, + height: 1080, + settings: { + navigator: { + userAgent: 'Custom User Agent' + } + } + }); + + if (globalThis.location.href !== 'https://example.com/') { + throw Error('The option "url" has no affect.'); + } + + if (globalThis.innerWidth !== 1920) { + throw Error('The option "width" has no affect.'); + } + + if (globalThis.innerHeight !== 1080) { + throw Error('The option "height" has no affect.'); + } + + if (globalThis.navigator.userAgent !== 'Custom User Agent') { + throw Error('The option "settings.userAgent" has no affect.'); + } + + GlobalRegistrator.unregister(); } main(); diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index d2b8cb3ec..081119ae2 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -184,6 +184,8 @@ import type IBrowserContext from './browser/types/IBrowserContext.js'; import type IBrowserFrame from './browser/types/IBrowserFrame.js'; import type IBrowserPage from './browser/types/IBrowserPage.js'; import type ICrossOriginBrowserWindow from './window/ICrossOriginBrowserWindow.js'; +import type IOptionalBrowserSettings from './browser/types/IOptionalBrowserSettings.js'; +import type IBrowserSettings from './browser/types/IBrowserSettings.js'; export type { IAnimationEventInit, @@ -248,7 +250,9 @@ export type { IText, IUIEventInit, IWheelEventInit, - IWindow + IWindow, + IBrowserSettings, + IOptionalBrowserSettings }; export {