Skip to content

Commit

Permalink
Merge pull request #1369 from capricorn86/1367-problem-with-scope-in-…
Browse files Browse the repository at this point in the history
…globalregistrator

fix: [#1367] Fixes problem with Document.defaultView not referring to…
  • Loading branch information
capricorn86 committed Apr 4, 2024
2 parents 5e160ed + fd45d3c commit f456def
Show file tree
Hide file tree
Showing 11 changed files with 248 additions and 80 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/pull_request.yml
Expand Up @@ -21,6 +21,9 @@ jobs:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- name: Setup Bun
uses: oven-sh/setup-bun@v1

- name: Cache node modules
uses: actions/cache@v4
id: cache-node-modules
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Expand Up @@ -69,6 +69,9 @@ jobs:
with:
node-version: ${{ matrix.node-version }}

- name: Setup Bun
uses: oven-sh/setup-bun@v1

- name: Cache node modules
uses: actions/cache@v4
id: cache-node-modules
Expand Down
48 changes: 33 additions & 15 deletions docs/contributing.md
Expand Up @@ -6,6 +6,24 @@ We are very happy that you would like to contribute. In this guide you will find

### Install

**Install Bun**

To be able to run all tests in the "./packages/global-registrator" package, you need to install [Bun](https://bun.sh/).

Linux & MacOS

```bash
curl -fsSL https://bun.sh/install | bash
```

Windows

```bash
powershell -c "irm bun.sh/install.ps1 | iex"
```

**Install dependencies**

```bash
npm install
```
Expand All @@ -22,7 +40,21 @@ npm run compile
npm run watch
```

### Debugging
### Test

**Run tests**

```bash
npm test
```

**Watch tests**

```bash
npm run test:watch
```

### Debug

1. Go to the package you wish to test in the terminal (e.g. "cd ./packages/happy-dom")
2. Write "debugger;" at the place you want to place a breakpoint in the code.
Expand All @@ -38,20 +70,6 @@ npm run test:debug
7. Click on the green ball.
8. Click continue to jump to your breakpoint.

### Automated Tests

**Run tests**

```bash
npm test
```

**Watch tests**

```bash
npm run test:watch
```

# Commit Convention

We use the [Conventional Commits](https://www.conventionalcommits.org/en/) standard for our commit messages. The description should start with an uppercase character.
Expand Down
2 changes: 1 addition & 1 deletion packages/global-registrator/README.md
Expand Up @@ -32,7 +32,7 @@ import { GlobalRegistrator } from '@happy-dom/global-registrator';

GlobalRegistrator.register();

GlobalRegistrator.unregister();
await GlobalRegistrator.unregister();

// Outputs: "undefined"
console.log(global.document);
Expand Down
6 changes: 4 additions & 2 deletions packages/global-registrator/package.json
Expand Up @@ -70,8 +70,10 @@
"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"
"test": "npm run test:react && npm run test:bun",
"test:debug": "tsc --project ./test/react && node ../happy-dom/bin/change-file-extension.cjs --dir=./tmp --fromExt=.js --toExt=.cjs && node --inspect-brk ./tmp/react/React.test.cjs",
"test:react": "rm -rf tmp && tsc --project ./test/react && node ../happy-dom/bin/change-file-extension.cjs --dir=./tmp --fromExt=.js --toExt=.cjs && node ./tmp/react/React.test.cjs",
"test:bun": "bun test ./test/bun/Bun.test.js"
},
"dependencies": {
"happy-dom": "^0.0.0"
Expand Down
55 changes: 35 additions & 20 deletions packages/global-registrator/src/GlobalRegistrator.ts
@@ -1,14 +1,13 @@
import { GlobalWindow } from 'happy-dom';
import { GlobalWindow, PropertySymbol } from 'happy-dom';
import type { IOptionalBrowserSettings } from 'happy-dom';

const IGNORE_LIST = ['constructor', 'undefined', 'NaN', 'global', 'globalThis'];
const SELF_REFERRING = ['self', 'top', 'parent', 'window'];

/**
*
*/
export default class GlobalRegistrator {
private static registered: { [key: string]: PropertyDescriptor } | null = null;
private static registered: { [key: string | symbol]: PropertyDescriptor } | null = null;

/**
* Registers Happy DOM globally.
Expand All @@ -33,6 +32,7 @@ export default class GlobalRegistrator {

this.registered = {};

// Define properties on the global object
const propertyDescriptors = Object.getOwnPropertyDescriptors(window);

for (const key of Object.keys(propertyDescriptors)) {
Expand All @@ -41,43 +41,54 @@ export default class GlobalRegistrator {
const globalPropertyDescriptor = Object.getOwnPropertyDescriptor(global, key);

if (
!globalPropertyDescriptor ||
(windowPropertyDescriptor.value !== undefined &&
windowPropertyDescriptor.value !== globalPropertyDescriptor.value)
globalPropertyDescriptor?.value === undefined ||
globalPropertyDescriptor?.value !== windowPropertyDescriptor.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);
// If the property is the window object, replace it with the global object
if (windowPropertyDescriptor.value === window) {
window[key] = global;
windowPropertyDescriptor.value = global;
}

Object.defineProperty(global, key, {
...windowPropertyDescriptor,
configurable: true
});
}
}
}

for (const key of SELF_REFERRING) {
// Define symbol properties on the global object
const propertySymbols = Object.getOwnPropertySymbols(window);

for (const key of propertySymbols) {
const propertyDescriptor = Object.getOwnPropertyDescriptor(window, key);
this.registered[key] = null;
global[key] = global;
Object.defineProperty(global, key, {
...propertyDescriptor,
configurable: true
});
}

// Set owner window on document to global
global.document[PropertySymbol.ownerWindow] = global;
global.document[PropertySymbol.defaultView] = global;
}

/**
* Registers Happy DOM globally.
* Closes the window and unregisters Happy DOM from being global.
*/
public static unregister(): void {
public static async unregister(): Promise<void> {
if (this.registered === null) {
throw new Error(
'Failed to unregister. Happy DOM has not previously been globally registered.'
);
}

const happyDOM = global.happyDOM;

for (const key of Object.keys(this.registered)) {
if (this.registered[key] !== null) {
Object.defineProperty(global, key, this.registered[key]);
Expand All @@ -87,5 +98,9 @@ export default class GlobalRegistrator {
}

this.registered = null;

if (happyDOM) {
await happyDOM.close();
}
}
}
89 changes: 89 additions & 0 deletions packages/global-registrator/test/bun/Bun.test.js
@@ -0,0 +1,89 @@
import { GlobalRegistrator } from '../../lib/index.js';
import { test, expect } from 'bun:test';

GlobalRegistrator.register();

/* eslint-disable no-undef */

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

test('DOM', () => {
document.body.innerHTML = `<button>My button</button>`;
const button = document.querySelector('button');
expect(button?.innerText).toEqual('My button');
});

test('CSS', () => {
const style = document.createElement('style');

document.head.appendChild(style);
style.innerHTML = `
body {
background-color: red;
}
@media (min-width: 1000px) {
body {
background-color: green;
}
}
`;

expect(globalThis.getComputedStyle(document.body).backgroundColor).toBe('green');
});

test('Window getters', () => {
const included = [];
const propertyNames = Object.getOwnPropertyNames(global);

for (const name of GETTERS) {
if (propertyNames.includes(name)) {
included.push(name);
}
}

expect(included).toEqual(GETTERS);
});

test('Window location', () => {
globalThis.location.href = 'https://example.com/';
expect(globalThis.location.href).toBe('https://example.com/');
});

test('Window options', () => {
GlobalRegistrator.unregister();

GlobalRegistrator.register({
url: 'https://example.com/',
width: 1920,
height: 1080,
settings: {
navigator: {
userAgent: 'Custom User Agent'
}
}
});

expect(globalThis.location.href).toBe('https://example.com/');
expect(globalThis.innerWidth).toBe(1920);
expect(globalThis.innerHeight).toBe(1080);
expect(globalThis.navigator.userAgent).toBe('Custom User Agent');
});
44 changes: 35 additions & 9 deletions packages/global-registrator/test/react/React.test.tsx
Expand Up @@ -41,6 +41,7 @@ async function main(): Promise<void> {
function testGetters(): void {
const included: string[] = [];
const propertyNames = Object.getOwnPropertyNames(global);

for (const name of GETTERS) {
if (propertyNames.includes(name)) {
included.push(name);
Expand All @@ -49,11 +50,7 @@ async function main(): Promise<void> {

if (included.length !== GETTERS.length) {
throw Error(
'Object.getOwnPropertyNames() did not return all properties defined as getter. Expected: ' +
GETTERS.join(', ') +
'. Got: ' +
included.join(', ') +
'.'
`Object.getOwnPropertyNames() did not return all properties defined as getter. Expected: "${GETTERS.join(', ')}", Got: "${included.join(', ')}".`
);
}
}
Expand Down Expand Up @@ -121,6 +118,31 @@ async function main(): Promise<void> {

testLocationHref();

/**
* Test CSS.
*/
function testCSS(): void {
const style = document.createElement('style');
document.head.appendChild(style);
style.innerHTML = `
body {
background-color: red;
}
@media (min-width: 1000px) {
body {
background-color: green;
}
}
`;

if (globalThis.getComputedStyle(document.body).backgroundColor !== 'green') {
throw Error('The CSS was not applied correctly.');
}
}

testCSS();

/**
* Unregisters Happy DOM globally.
*/
Expand All @@ -132,17 +154,21 @@ async function main(): Promise<void> {
function testGettersAfterUnregister(): void {
const included: string[] = [];
const propertyNames = Object.getOwnPropertyNames(global);

for (const name of GETTERS) {
if (propertyNames.includes(name)) {
included.push(name);
}
}

if (included.length !== 0) {
// In Node.js v21 and later, the navigator property is available.
if (!included.includes('navigator')) {
included.push('navigator');
}

if (included.length !== 1 || included[0] !== 'navigator') {
throw Error(
'Object.getOwnPropertyNames() did not remove all properties defined as getter. Expected: []. Got: ' +
included.join(', ') +
'.'
`GlobalObserver.unregister() did not remove all properties defined as getter. Expected: "navigator", Got: "${included.join(', ')}".`
);
}
}
Expand Down

0 comments on commit f456def

Please sign in to comment.