Skip to content

Commit

Permalink
#466@trivial: Fixes MutationObserver and adds support for BrowserFram…
Browse files Browse the repository at this point in the history
…e.waitForNavigation().
  • Loading branch information
capricorn86 committed Jan 10, 2024
1 parent db3999b commit 4264264
Show file tree
Hide file tree
Showing 79 changed files with 726 additions and 748 deletions.
12 changes: 1 addition & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion packages/global-registrator/src/GlobalRegistrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export default class GlobalRegistrator {
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].bind(global) : window[key];
global[key] =
typeof window[key] === 'function' && !window[key].toString().startsWith('class ')
? window[key].bind(global)
: window[key];
}
}

Expand Down
35 changes: 30 additions & 5 deletions packages/happy-dom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,12 @@ npm install happy-dom

A simple example of how you can use Happy DOM.

## Window

```javascript
import { Window } from 'happy-dom';

const window = new Window({
url: 'https://localhost:8080',
width: 1024,
height: 768
});
const window = new Window({ url: 'https://localhost:8080' });
const document = window.document;

document.body.innerHTML = '<div class="container"></div>';
Expand All @@ -72,6 +70,33 @@ container.appendChild(button);
console.log(document.body.innerHTML);
```

## Browser

```javascript
import { Browser, BrowserErrorCaptureEnum } from 'happy-dom';

const browser = new Browser({ settings: { errorCapture: BrowserErrorCaptureEnum.processLevel } });
const page = browser.newPage();

// Navigates page
await page.goto('https://github.com/capricorn86');

// Waits for all operations on the page to complete (fetch, timers etc.)
await page.waitUntilComplete();

// Clicks on link
page.mainFrame.document.querySelector('a[href*="capricorn86/happy-dom"]').click();

// Waits for all operations on the page to complete (fetch, timers etc.)
await page.waitUntilComplete();

// Outputs "GitHub - capricorn86/happy-dom: Happy DOM..."
console.log(page.mainFrame.document.title);

// Closes the browser
await browser.close();
```

# Documentation

Read more about how to use Happy DOM in our [Wiki](https://github.com/capricorn86/happy-dom/wiki).
Expand Down
2 changes: 2 additions & 0 deletions packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const currentScript = Symbol('currentScript');
export const currentTarget = Symbol('currentTarget');
export const data = Symbol('data');
export const defaultView = Symbol('defaultView');
export const destroy = Symbol('destroy');
export const dirtyness = Symbol('dirtyness');
export const end = Symbol('end');
export const entries = Symbol('entries');
Expand Down Expand Up @@ -77,3 +78,4 @@ export const value = Symbol('value');
export const width = Symbol('width');
export const window = Symbol('window');
export const windowResizeListener = Symbol('windowResizeListener');
export const mutationObservers = Symbol('mutationObservers');
28 changes: 14 additions & 14 deletions packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ export default class AsyncTaskManager {
private runningTaskCount = 0;
private runningTimers: NodeJS.Timeout[] = [];
private runningImmediates: NodeJS.Immediate[] = [];
private whenCompleteImmediate: NodeJS.Immediate | null = null;
private whenCompleteResolvers: Array<() => void> = [];
private waitUntilCompleteTimer: NodeJS.Immediate | null = null;
private waitUntilCompleteResolvers: Array<() => void> = [];

/**
* Returns a promise that is resolved when async tasks are complete.
*
* @returns Promise.
*/
public whenComplete(): Promise<void> {
public waitUntilComplete(): Promise<void> {
return new Promise((resolve) => {
this.whenCompleteResolvers.push(resolve);
this.waitUntilCompleteResolvers.push(resolve);
this.endTask(this.startTask());
});
}
Expand Down Expand Up @@ -106,12 +106,12 @@ export default class AsyncTaskManager {
if (this.runningTasks[taskID]) {
delete this.runningTasks[taskID];
this.runningTaskCount--;
if (this.whenCompleteImmediate) {
global.clearImmediate(this.whenCompleteImmediate);
if (this.waitUntilCompleteTimer) {
global.clearImmediate(this.waitUntilCompleteTimer);
}
if (!this.runningTaskCount && !this.runningTimers.length && !this.runningImmediates.length) {
this.whenCompleteImmediate = global.setImmediate(() => {
this.whenCompleteImmediate = null;
this.waitUntilCompleteTimer = global.setImmediate(() => {
this.waitUntilCompleteTimer = null;
if (
!this.runningTaskCount &&
!this.runningTimers.length &&
Expand Down Expand Up @@ -147,8 +147,8 @@ export default class AsyncTaskManager {
* Resolves when complete.
*/
private resolveWhenComplete(): void {
const resolvers = this.whenCompleteResolvers;
this.whenCompleteResolvers = [];
const resolvers = this.waitUntilCompleteResolvers;
this.waitUntilCompleteResolvers = [];
for (const resolver of resolvers) {
resolver();
}
Expand All @@ -169,9 +169,9 @@ export default class AsyncTaskManager {
this.runningImmediates = [];
this.runningTimers = [];

if (this.whenCompleteImmediate) {
global.clearImmediate(this.whenCompleteImmediate);
this.whenCompleteImmediate = null;
if (this.waitUntilCompleteTimer) {
global.clearImmediate(this.waitUntilCompleteTimer);
this.waitUntilCompleteTimer = null;
}

for (const immediate of runningImmediates) {
Expand All @@ -187,6 +187,6 @@ export default class AsyncTaskManager {
}

// We need to wait for microtasks to complete before resolving.
return this.whenComplete();
return this.waitUntilComplete();
}
}
4 changes: 2 additions & 2 deletions packages/happy-dom/src/browser/Browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ export default class Browser implements IBrowser {
*
* @returns Promise.
*/
public async whenComplete(): Promise<void> {
public async waitUntilComplete(): Promise<void> {
if (this.contexts.length === 0) {
throw new Error('No default context. The browser has been closed.');
}
await Promise.all(this.contexts.map((page) => page.whenComplete()));
await Promise.all(this.contexts.map((page) => page.waitUntilComplete()));
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/happy-dom/src/browser/BrowserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ export default class BrowserContext implements IBrowserContext {
*
* @returns Promise.
*/
public async whenComplete(): Promise<void> {
await Promise.all(this.pages.map((page) => page.whenComplete()));
public async waitUntilComplete(): Promise<void> {
await Promise.all(this.pages.map((page) => page.waitUntilComplete()));
}

/**
Expand Down
22 changes: 14 additions & 8 deletions packages/happy-dom/src/browser/BrowserFrame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import BrowserFrameScriptEvaluator from './utilities/BrowserFrameScriptEvaluator
import BrowserFrameNavigator from './utilities/BrowserFrameNavigator.js';
import IReloadOptions from './types/IReloadOptions.js';
import BrowserFrameExceptionObserver from './utilities/BrowserFrameExceptionObserver.js';
import BrowserErrorCapturingEnum from './enums/BrowserErrorCapturingEnum.js';
import BrowserErrorCaptureEnum from './enums/BrowserErrorCaptureEnum.js';
import IDocument from '../nodes/document/IDocument.js';

/**
Expand All @@ -26,6 +26,7 @@ export default class BrowserFrame implements IBrowserFrame {
public readonly window: BrowserWindow;
public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager();
public [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null = null;
public [PropertySymbol.listeners]: { navigation: Array<() => void> } = { navigation: [] };

/**
* Constructor.
Expand All @@ -37,7 +38,7 @@ export default class BrowserFrame implements IBrowserFrame {
this.window = new BrowserWindow(this);

// Attach process level error capturing.
if (page.context.browser.settings.errorCapturing === BrowserErrorCapturingEnum.processLevel) {
if (page.context.browser.settings.errorCapture === BrowserErrorCaptureEnum.processLevel) {
this[PropertySymbol.exceptionObserver] = new BrowserFrameExceptionObserver();
this[PropertySymbol.exceptionObserver].observe(this);
}
Expand Down Expand Up @@ -95,17 +96,22 @@ export default class BrowserFrame implements IBrowserFrame {
}

/**
* Returns a promise that is resolved when all async tasks are complete.
*
* @returns Promise.
* Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
*/
public async whenComplete(): Promise<void> {
public async waitUntilComplete(): Promise<void> {
await Promise.all([
this[PropertySymbol.asyncTaskManager].whenComplete(),
...this.childFrames.map((frame) => frame.whenComplete())
this[PropertySymbol.asyncTaskManager].waitUntilComplete(),
...this.childFrames.map((frame) => frame.waitUntilComplete())
]);
}

/**
* Returns a promise that is resolved when the frame has navigated and the response HTML has been written to the document.
*/
public waitForNavigation(): Promise<void> {
return new Promise((resolve) => this[PropertySymbol.listeners].navigation.push(resolve));
}

/**
* Aborts all ongoing operations.
*/
Expand Down
15 changes: 10 additions & 5 deletions packages/happy-dom/src/browser/BrowserPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,17 @@ export default class BrowserPage implements IBrowserPage {
}

/**
* Returns a promise that is resolved when all async tasks are complete.
*
* @returns Promise.
* Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
*/
public waitUntilComplete(): Promise<void> {
return this.mainFrame.waitUntilComplete();
}

/**
* Returns a promise that is resolved when the page has navigated and the response HTML has been written to the document.
*/
public async whenComplete(): Promise<void> {
await this.mainFrame.whenComplete();
public waitForNavigation(): Promise<void> {
return this.mainFrame.waitForNavigation();
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/happy-dom/src/browser/DefaultBrowserSettings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PackageVersion from '../version.js';
import BrowserErrorCapturingEnum from './enums/BrowserErrorCapturingEnum.js';
import BrowserErrorCaptureEnum from './enums/BrowserErrorCaptureEnum.js';
import BrowserNavigationCrossOriginPolicyEnum from './enums/BrowserNavigationCrossOriginPolicyEnum.js';
import IBrowserSettings from './types/IBrowserSettings.js';

Expand All @@ -10,7 +10,7 @@ export default <IBrowserSettings>{
disableIframePageLoading: false,
disableComputedStyleRendering: false,
disableErrorCapturing: false,
errorCapturing: BrowserErrorCapturingEnum.tryAndCatch,
errorCapture: BrowserErrorCaptureEnum.tryAndCatch,
enableFileSystemHttpRequests: false,
navigation: {
disableMainFrameNavigation: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export default class DetachedBrowser implements IBrowser {
*
* @returns Promise.
*/
public async whenComplete(): Promise<void> {
await Promise.all(this.contexts.map((page) => page.whenComplete()));
public async waitUntilComplete(): Promise<void> {
await Promise.all(this.contexts.map((page) => page.waitUntilComplete()));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export default class DetachedBrowserContext implements IBrowserContext {
*
* @returns Promise.
*/
public async whenComplete(): Promise<void> {
await Promise.all(this.pages.map((page) => page.whenComplete()));
public async waitUntilComplete(): Promise<void> {
await Promise.all(this.pages.map((page) => page.waitUntilComplete()));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import BrowserFrameScriptEvaluator from '../utilities/BrowserFrameScriptEvaluato
import BrowserFrameNavigator from '../utilities/BrowserFrameNavigator.js';
import IBrowserWindow from '../../window/IBrowserWindow.js';
import IReloadOptions from '../types/IReloadOptions.js';
import BrowserErrorCapturingEnum from '../enums/BrowserErrorCapturingEnum.js';
import BrowserErrorCaptureEnum from '../enums/BrowserErrorCaptureEnum.js';
import BrowserFrameExceptionObserver from '../utilities/BrowserFrameExceptionObserver.js';
import IDocument from '../../nodes/document/IDocument.js';

Expand All @@ -27,6 +27,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame {
public window: IBrowserWindow;
public [PropertySymbol.asyncTaskManager] = new AsyncTaskManager();
public [PropertySymbol.exceptionObserver]: BrowserFrameExceptionObserver | null = null;
public [PropertySymbol.listeners]: { navigation: Array<() => void> } = { navigation: [] };

/**
* Constructor.
Expand All @@ -41,7 +42,7 @@ export default class DetachedBrowserFrame implements IBrowserFrame {
}

// Attach process level error capturing.
if (page.context.browser.settings.errorCapturing === BrowserErrorCapturingEnum.processLevel) {
if (page.context.browser.settings.errorCapture === BrowserErrorCaptureEnum.processLevel) {
this[PropertySymbol.exceptionObserver] = new BrowserFrameExceptionObserver();
this[PropertySymbol.exceptionObserver].observe(this);
}
Expand Down Expand Up @@ -111,17 +112,22 @@ export default class DetachedBrowserFrame implements IBrowserFrame {
}

/**
* Returns a promise that is resolved when all async tasks are complete.
*
* @returns Promise.
* Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
*/
public async whenComplete(): Promise<void> {
public async waitUntilComplete(): Promise<void> {
await Promise.all([
this[PropertySymbol.asyncTaskManager].whenComplete(),
...this.childFrames.map((frame) => frame.whenComplete())
this[PropertySymbol.asyncTaskManager].waitUntilComplete(),
...this.childFrames.map((frame) => frame.waitUntilComplete())
]);
}

/**
* Returns a promise that is resolved when the frame has navigated and the response HTML has been written to the document.
*/
public waitForNavigation(): Promise<void> {
return new Promise((resolve) => this[PropertySymbol.listeners].navigation.push(resolve));
}

/**
* Aborts all ongoing operations.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,17 @@ export default class DetachedBrowserPage implements IBrowserPage {
}

/**
* Returns a promise that is resolved when all async tasks are complete.
*
* @returns Promise.
* Returns a promise that is resolved when all resources has been loaded, fetch has completed, and all async tasks such as timers are complete.
*/
public waitUntilComplete(): Promise<void> {
return this.mainFrame.waitUntilComplete();
}

/**
* Returns a promise that is resolved when the page has navigated and the response HTML has been written to the document.
*/
public async whenComplete(): Promise<void> {
await this.mainFrame.whenComplete();
public waitForNavigation(): Promise<void> {
return this.mainFrame.waitForNavigation();
}

/**
Expand Down
10 changes: 10 additions & 0 deletions packages/happy-dom/src/browser/enums/BrowserErrorCaptureEnum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
enum BrowserErrorCaptureEnum {
/** Happy DOM use try and catch when evaluating code, but will not be able to catch all errors and Promise rejections. This will decrease performance as using try and catch makes the execution significally slower. This is the default setting. */
tryAndCatch = 'tryAndCatch',
/** Happy DOM will add an event listener to the Node.js process to catch all errors and Promise rejections. This will not work in Jest and Vitest as it conflicts with their error listeners. */
processLevel = 'processLevel',
/** Error capturing is disabled. Errors and Promise rejections will be thrown. */
disabled = 'disabled'
}

export default BrowserErrorCaptureEnum;
Loading

0 comments on commit 4264264

Please sign in to comment.