Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [#1339] Fixes problem with properties defined as getters not bei… #1340

Merged
merged 1 commit into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/global-registrator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,10 @@
"access": "public"
},
"scripts": {
"compile": "rm -rf lib cjs && 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",
"compile": "npm run compile:esm && npm run compile:cjs",
"compile:esm": "tsc",
"compile:cjs": "rm -rf cjs && tsc --moduleResolution Node --module CommonJS --outDir cjs && npm run compile:change-cjs-file-extension",
"compile: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",
"test": "rm -rf tmp && 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"
Expand Down
29 changes: 3 additions & 26 deletions packages/global-registrator/src/GlobalRegistrator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GlobalWindow, Window, EventTarget } from 'happy-dom';
import { GlobalWindow } from 'happy-dom';
import type { IOptionalBrowserSettings } from 'happy-dom';

const IGNORE_LIST = ['constructor', 'undefined', 'NaN', 'global', 'globalThis'];
Expand Down Expand Up @@ -41,8 +41,8 @@ export default class GlobalRegistrator {
const globalPropertyDescriptor = Object.getOwnPropertyDescriptor(global, key);

if (
windowPropertyDescriptor.value !== undefined &&
(!globalPropertyDescriptor ||
!globalPropertyDescriptor ||
(windowPropertyDescriptor.value !== undefined &&
windowPropertyDescriptor.value !== globalPropertyDescriptor.value)
) {
this.registered[key] = globalPropertyDescriptor || null;
Expand All @@ -62,29 +62,6 @@ export default class GlobalRegistrator {
}
}

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)
});
}
}
}
}

for (const key of SELF_REFERRING) {
this.registered[key] = null;
global[key] = global;
Expand Down
209 changes: 156 additions & 53 deletions packages/global-registrator/test/react/React.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,94 +4,197 @@ import ReactDOM from 'react-dom/client';
import { act } from 'react-dom/test-utils';
import ReactComponent from './ReactComponent.js';

const GETTERS = [
'location',
'history',
'navigator',
'screen',
'sessionStorage',
'localStorage',
'opener',
'scrollX',
'pageXOffset',
'scrollY',
'pageYOffset',
'CSS',
'innerWidth',
'innerHeight',
'outerWidth',
'outerHeight',
'devicePixelRatio'
];

async function main(): Promise<void> {
const selfReferringProperties = ['self', 'top', 'parent', 'window'];

// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const originalSetTimeout = global.setTimeout;

/**
* Registers Happy DOM globally.
*/
GlobalRegistrator.register();

const appElement = document.createElement('app');
let root;
document.body.appendChild(appElement);
/**
* Test if all properties defined as getter are included in the global object.
*/
function testGetters(): void {
const included: string[] = [];
const propertyNames = Object.getOwnPropertyNames(global);
for (const name of GETTERS) {
if (propertyNames.includes(name)) {
included.push(name);
}
}

async function mountReactComponent(): Promise<void> {
act(() => {
root = ReactDOM.createRoot(appElement);
root.render(<ReactComponent />);
});
if (included.length !== GETTERS.length) {
throw Error(
'Object.getOwnPropertyNames() did not return all properties defined as getter. Expected: ' +
GETTERS.join(', ') +
'. Got: ' +
included.join(', ') +
'.'
);
}
}

testGetters();

/**
* Test if it is possible to create a React component and mount it.
*/
async function testReactComponent(): Promise<void> {
const appElement = document.createElement('app');
let root;
document.body.appendChild(appElement);

async function mountReactComponent(): Promise<void> {
act(() => {
root = ReactDOM.createRoot(appElement);
root.render(<ReactComponent />);
});

await new Promise((resolve) => setTimeout(resolve, 2));
await new Promise((resolve) => setTimeout(resolve, 2));

if (appElement.innerHTML !== '<div>Test</div>') {
throw Error('React not rendered correctly.');
if (appElement.innerHTML !== '<div>Test</div>') {
throw Error('React not rendered correctly.');
}
}
}

function unmountReactComponent(): void {
act(() => {
root.unmount();
});
function unmountReactComponent(): void {
act(() => {
root.unmount();
});

if (appElement.innerHTML !== '') {
throw Error('React not unmounted correctly.');
if (appElement.innerHTML !== '') {
throw Error('React not unmounted correctly.');
}
}
}

if (global.setTimeout === originalSetTimeout) {
throw Error('Happy DOM function not registered.');
}
if (global.setTimeout === originalSetTimeout) {
throw Error('Happy DOM function not registered.');
}

for (const property of selfReferringProperties) {
if (global[property] !== global) {
throw Error('Self referring property property was not registered.');
for (const property of selfReferringProperties) {
if (global[property] !== global) {
throw Error('Self referring property property was not registered.');
}
}

await mountReactComponent();
unmountReactComponent();
}

/** @see https://github.com/capricorn86/happy-dom/issues/1230 */
globalThis.location.href = 'https://example.com/';
if (globalThis.location.href !== 'https://example.com/') {
throw Error('The property "location.href" could not be set.');
await testReactComponent();

/**
* Test if it is possible to set the location.href property and that location isn't replaced to a new object.
*
* @see https://github.com/capricorn86/happy-dom/issues/1230
* */
function testLocationHref(): void {
globalThis.location.href = 'https://example.com/';
if (globalThis.location.href !== 'https://example.com/') {
throw Error('The property "location.href" could not be set.');
}
}

await mountReactComponent();
unmountReactComponent();
testLocationHref();

/**
* Unregisters Happy DOM globally.
*/
GlobalRegistrator.unregister();

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'
/**
* Test if all properties defined as getter are removed from the global object.
*/
function testGettersAfterUnregister(): void {
const included: string[] = [];
const propertyNames = Object.getOwnPropertyNames(global);
for (const name of GETTERS) {
if (propertyNames.includes(name)) {
included.push(name);
}
}
});

if (globalThis.location.href !== 'https://example.com/') {
throw Error('The option "url" has no affect.');
if (included.length !== 0) {
throw Error(
'Object.getOwnPropertyNames() did not remove all properties defined as getter. Expected: []. Got: ' +
included.join(', ') +
'.'
);
}
}

if (globalThis.innerWidth !== 1920) {
throw Error('The option "width" has no affect.');
}
testGettersAfterUnregister();

if (globalThis.innerHeight !== 1080) {
throw Error('The option "height" has no affect.');
/**
* Test if setTimeout is restored.
*/
function testSetTimeout(): void {
if (global.setTimeout !== originalSetTimeout) {
throw Error('Global property was not restored.');
}
}

if (globalThis.navigator.userAgent !== 'Custom User Agent') {
throw Error('The option "settings.userAgent" has no affect.');
testSetTimeout();

/**
* Test registering with options.
*/
function testWindowOptions(): void {
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();
}

GlobalRegistrator.unregister();
testWindowOptions();
}

main();
4 changes: 2 additions & 2 deletions packages/happy-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@
"scripts": {
"compile": "npm run compile:esm && npm run compile:cjs npm run build-version-file",
"compile:esm": "tsc",
"compile:cjs": "rm -rf ./cjs && tsc --moduleResolution Node --module CommonJS --outDir cjs && npm run change-cjs-file-extension",
"change-cjs-file-extension": "node ./bin/change-file-extension.cjs --dir=./cjs --fromExt=.js --toExt=.cjs",
"compile:cjs": "rm -rf ./cjs && tsc --moduleResolution Node --module CommonJS --outDir cjs && npm run compile:change-cjs-file-extension",
"compile: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",
Expand Down
57 changes: 57 additions & 0 deletions packages/happy-dom/src/window/GlobalWindow.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import * as PropertySymbol from '../PropertySymbol.js';
import { IOptionalBrowserSettings } from '../index.js';
import BrowserWindow from './BrowserWindow.js';
import Window from './Window.js';
import { Buffer } from 'buffer';

Expand Down Expand Up @@ -70,6 +72,61 @@ export default class GlobalWindow extends Window {
public gc: () => void = globalThis.gc;
public v8debug?: unknown = globalThis.v8debug;

/**
* 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.console] Console.
* @param [options.settings] Settings.
*/
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;
}) {
super(options);

/**
* Binds getts and setters, so that they will appear as an "own" property when using Object.getOwnPropertyNames().
*
* This is needed for Vitest to work as it relies on Object.getOwnPropertyNames() to get the list of properties.
*
* @see https://github.com/capricorn86/happy-dom/issues/1339
*/
for (const windowClass of [GlobalWindow, Window, BrowserWindow]) {
const propertyDescriptors = Object.getOwnPropertyDescriptors(
Reflect.getPrototypeOf(windowClass.prototype)
);

for (const key of Object.keys(propertyDescriptors)) {
const windowPropertyDescriptor = propertyDescriptors[key];
if (windowPropertyDescriptor.get || windowPropertyDescriptor.set) {
const ownPropertyDescriptor = Object.getOwnPropertyDescriptor(this, key);

if (!ownPropertyDescriptor) {
Object.defineProperty(this, key, {
configurable: true,
enumerable: windowPropertyDescriptor.enumerable,
get: windowPropertyDescriptor.get?.bind(this),
set: windowPropertyDescriptor.set?.bind(this)
});
}
}
}
}
}

/**
* Setup of VM context.
*/
Expand Down
Loading
Loading