Skip to content

Commit

Permalink
#1105@minor: Adds support for sending in Window options to GlobalRegi…
Browse files Browse the repository at this point in the history
…strator.register(). Fixes problem with getters and setters not being added to the global object.
  • Loading branch information
capricorn86 committed Jan 24, 2024
1 parent 1a85ba1 commit 0b95343
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 15 deletions.
81 changes: 67 additions & 14 deletions packages/global-registrator/src/GlobalRegistrator.ts
Original file line number Diff line number Diff line change
@@ -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)
});
}
}
}
}

Expand All @@ -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];
}
Expand Down
29 changes: 29 additions & 0 deletions packages/global-registrator/test/react/React.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,35 @@ async function main(): Promise<void> {
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();
6 changes: 5 additions & 1 deletion packages/happy-dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -248,7 +250,9 @@ export type {
IText,
IUIEventInit,
IWheelEventInit,
IWindow
IWindow,
IBrowserSettings,
IOptionalBrowserSettings
};

export {
Expand Down

0 comments on commit 0b95343

Please sign in to comment.