Repository: capricorn86/happy-dom
Pull request #431 (2022-03-29 13:12:48)

Task/317 expose fetch apis constructors


Patch:
diff --git a/package-lock.json b/package-lock.json
index 39ba2278..5d43cd8b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
 				"@jest/fake-timers": "^27.5.1",
 				"@jest/types": "^27.5.1",
 				"@lit/reactive-element": "^1.3.1",
+				"@types/node-fetch": "^2.6.1",
 				"cpy": "^8.1.2",
 				"css.escape": "^1.5.1",
 				"glob": "^7.2.0",
@@ -2632,6 +2633,15 @@
 			"resolved": "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz",
 			"integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A=="
 		},
+		"node_modules/@types/node-fetch": {
+			"version": "2.6.1",
+			"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz",
+			"integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==",
+			"dependencies": {
+				"@types/node": "*",
+				"form-data": "^3.0.0"
+			}
+		},
 		"node_modules/@types/normalize-package-data": {
 			"version": "2.4.1",
 			"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz",
@@ -6121,7 +6131,6 @@
 			"version": "3.0.1",
 			"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
 			"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
-			"dev": true,
 			"dependencies": {
 				"asynckit": "^0.4.0",
 				"combined-stream": "^1.0.8",
@@ -9759,9 +9768,9 @@
 			"dev": true
 		},
 		"node_modules/nanoid": {
-			"version": "3.3.1",
-			"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
-			"integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==",
+			"version": "3.3.2",
+			"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz",
+			"integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==",
 			"bin": {
 				"nanoid": "bin/nanoid.cjs"
 			},
@@ -9812,9 +9821,9 @@
 			"dev": true
 		},
 		"node_modules/nested-error-stacks": {
-			"version": "2.1.0",
-			"resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz",
-			"integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug=="
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz",
+			"integrity": "sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw=="
 		},
 		"node_modules/nice-try": {
 			"version": "1.0.5",
@@ -16094,6 +16103,15 @@
 			"resolved": "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz",
 			"integrity": "sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A=="
 		},
+		"@types/node-fetch": {
+			"version": "2.6.1",
+			"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz",
+			"integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==",
+			"requires": {
+				"@types/node": "*",
+				"form-data": "^3.0.0"
+			}
+		},
 		"@types/normalize-package-data": {
 			"version": "2.4.1",
 			"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz",
@@ -18795,7 +18813,6 @@
 			"version": "3.0.1",
 			"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
 			"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
-			"dev": true,
 			"requires": {
 				"asynckit": "^0.4.0",
 				"combined-stream": "^1.0.8",
@@ -21640,9 +21657,9 @@
 			"dev": true
 		},
 		"nanoid": {
-			"version": "3.3.1",
-			"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz",
-			"integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw=="
+			"version": "3.3.2",
+			"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz",
+			"integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA=="
 		},
 		"nanomatch": {
 			"version": "1.2.13",
@@ -21681,9 +21698,9 @@
 			"dev": true
 		},
 		"nested-error-stacks": {
-			"version": "2.1.0",
-			"resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz",
-			"integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug=="
+			"version": "2.1.1",
+			"resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz",
+			"integrity": "sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw=="
 		},
 		"nice-try": {
 			"version": "1.0.5",
diff --git a/packages/happy-dom/package.json b/packages/happy-dom/package.json
index 6e7ad8ac..a206fcd1 100644
--- a/packages/happy-dom/package.json
+++ b/packages/happy-dom/package.json
@@ -60,6 +60,7 @@
 		"@types/he": "^1.1.2",
 		"@types/jest": "^27.4.1",
 		"@types/node": "^15.6.0",
+		"@types/node-fetch": "^2.6.1",
 		"@typescript-eslint/eslint-plugin": "^5.16.0",
 		"@typescript-eslint/parser": "^5.16.0",
 		"eslint": "^8.11.0",
diff --git a/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts
new file mode 100644
index 00000000..2c2917a7
--- /dev/null
+++ b/packages/happy-dom/src/async-task-manager/AsyncTaskManager.ts
@@ -0,0 +1,128 @@
+/**
+ * Handles async tasks.
+ */
+export default class AsyncTaskManager {
+	private static taskID = 0;
+	private runningTasks: number[] = [];
+	private runningTimers: NodeJS.Timeout[] = [];
+	private queue: { resolve: () => void; reject: (error: Error) => void }[] = [];
+
+	/**
+	 * Returns a promise that is fulfilled when async tasks are complete.
+	 * This method is not part of the HTML standard.
+	 *
+	 * @returns Promise.
+	 */
+	public async whenComplete(): Promise<void> {
+		return new Promise((resolve, reject) => {
+			const timerID = global.setTimeout(() => {
+				this.endTimer(timerID);
+			}, 0);
+			this.startTimer(timerID);
+			this.queue.push({ resolve, reject });
+		});
+	}
+
+	/**
+	 * Cancels all tasks.
+	 *
+	 * @param [error] Error.
+	 */
+	public cancelAll(error?: Error): void {
+		for (const timerID of this.runningTimers) {
+			global.clearTimeout(timerID);
+		}
+
+		const promises = this.queue;
+
+		this.runningTasks = [];
+		this.runningTimers = [];
+		this.queue = [];
+
+		for (const promise of promises) {
+			if (error) {
+				promise.reject(error);
+			} else {
+				promise.resolve();
+			}
+		}
+	}
+
+	/**
+	 * Starts a timer.
+	 *
+	 * @param timerID Timer ID.
+	 */
+	public startTimer(timerID: NodeJS.Timeout): void {
+		this.runningTimers.push(timerID);
+	}
+
+	/**
+	 * Ends a timer.
+	 *
+	 * @param timerID Timer ID.
+	 */
+	public endTimer(timerID: NodeJS.Timeout): void {
+		const index = this.runningTimers.indexOf(timerID);
+		if (index !== -1) {
+			this.runningTimers.splice(index, 1);
+		}
+		if (!this.runningTasks.length && !this.runningTimers.length) {
+			this.cancelAll();
+		}
+	}
+
+	/**
+	 * Starts an async task.
+	 *
+	 * @returns Task ID.
+	 */
+	public startTask(): number {
+		const taskID = this.newTaskID();
+		this.runningTasks.push(taskID);
+		return taskID;
+	}
+
+	/**
+	 * Ends an async task.
+	 *
+	 * @param taskID Task ID.
+	 */
+	public endTask(taskID: number): void {
+		const index = this.runningTasks.indexOf(taskID);
+		if (index !== -1) {
+			this.runningTasks.splice(index, 1);
+		}
+		if (!this.runningTasks.length && !this.runningTimers.length) {
+			this.cancelAll();
+		}
+	}
+
+	/**
+	 * Returns the amount of running tasks.
+	 *
+	 * @returns Count.
+	 */
+	public getTaskCount(): number {
+		return this.runningTasks.length;
+	}
+
+	/**
+	 * Returns the amount of running timers.
+	 *
+	 * @returns Count.
+	 */
+	public getTimerCount(): number {
+		return this.runningTimers.length;
+	}
+
+	/**
+	 * Returns a new task ID.
+	 *
+	 * @returns Task ID.
+	 */
+	private newTaskID(): number {
+		(<typeof AsyncTaskManager>this.constructor).taskID++;
+		return (<typeof AsyncTaskManager>this.constructor).taskID;
+	}
+}
diff --git a/packages/happy-dom/src/fetch/FetchHandler.ts b/packages/happy-dom/src/fetch/FetchHandler.ts
new file mode 100644
index 00000000..07295850
--- /dev/null
+++ b/packages/happy-dom/src/fetch/FetchHandler.ts
@@ -0,0 +1,54 @@
+import RelativeURL from '../location/RelativeURL';
+import IRequestInit from './IRequestInit';
+import IDocument from '../nodes/document/IDocument';
+import IResponse from './IResponse';
+
+/**
+ * Helper class for performing fetch.
+ */
+export default class FetchHandler {
+	/**
+	 * Returns resource data asynchonously.
+	 *
+	 * @param document Document.
+	 * @param url URL to resource.
+	 * @param [init] Init.
+	 * @returns Response.
+	 */
+	public static fetch(document: IDocument, url: string, init?: IRequestInit): Promise<IResponse> {
+		// We want to only load NodeFetch when it is needed to improve performance and not have direct dependencies to server side packages.
+		const nodeFetch = require('node-fetch');
+		const Response = require('./Response').default;
+		const taskManager = document.defaultView.happyDOM.asyncTaskManager;
+
+		return new Promise((resolve, reject) => {
+			const taskID = taskManager.startTask();
+
+			nodeFetch(RelativeURL.getAbsoluteURL(document.defaultView.location, url), init)
+				.then((response) => {
+					if (taskManager.getTaskCount() === 0) {
+						reject(new Error('Failed to complete fetch request. Task was canceled.'));
+					} else {
+						response.constructor['_ownerDocument'] = document;
+
+						for (const key of Object.keys(Response.prototype)) {
+							if (Response.prototype.hasOwnProperty(key) && key !== 'constructor') {
+								if (typeof Response.prototype[key] === 'function') {
+									response[key] = Response.prototype[key].bind(response);
+								} else {
+									response[key] = Response.prototype[key];
+								}
+							}
+						}
+
+						taskManager.endTask(taskID);
+						resolve(response);
+					}
+				})
+				.catch((error) => {
+					reject(error);
+					taskManager.cancelAll(error);
+				});
+		});
+	}
+}
diff --git a/packages/happy-dom/src/fetch/Headers.ts b/packages/happy-dom/src/fetch/Headers.ts
new file mode 100644
index 00000000..3ba5a2a5
--- /dev/null
+++ b/packages/happy-dom/src/fetch/Headers.ts
@@ -0,0 +1,7 @@
+import * as NodeFetch from 'node-fetch';
+import IHeaders from './IHeaders';
+
+/**
+ * Fetch headers.
+ */
+export default class Headers extends NodeFetch.Headers implements IHeaders {}
diff --git a/packages/happy-dom/src/fetch/IAbortSignal.ts b/packages/happy-dom/src/fetch/IAbortSignal.ts
new file mode 100644
index 00000000..f8ae1ea3
--- /dev/null
+++ b/packages/happy-dom/src/fetch/IAbortSignal.ts
@@ -0,0 +1,34 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+/**
+ * Abort signal.
+ */
+export default interface IAbortSignal {
+	aborted: boolean;
+
+	addEventListener: (
+		type: 'abort',
+		listener: (this: IAbortSignal, event: any) => any,
+		options?:
+			| boolean
+			| {
+					capture?: boolean | undefined;
+					once?: boolean | undefined;
+					passive?: boolean | undefined;
+			  }
+	) => void;
+
+	removeEventListener: (
+		type: 'abort',
+		listener: (this: IAbortSignal, event: any) => any,
+		options?:
+			| boolean
+			| {
+					capture?: boolean | undefined;
+			  }
+	) => void;
+
+	dispatchEvent: (event: any) => boolean;
+
+	onabort: null | ((this: IAbortSignal, event: any) => void);
+}
diff --git a/packages/happy-dom/src/fetch/IBody.ts b/packages/happy-dom/src/fetch/IBody.ts
new file mode 100644
index 00000000..c3e432bc
--- /dev/null
+++ b/packages/happy-dom/src/fetch/IBody.ts
@@ -0,0 +1,18 @@
+import IBlob from '../file/IBlob';
+
+/**
+ * Fetch response.
+ */
+export default interface IBody {
+	readonly body: NodeJS.ReadableStream;
+	readonly bodyUsed: boolean;
+	readonly size: number;
+	readonly timeout: number;
+
+	arrayBuffer(): Promise<ArrayBuffer>;
+	blob(): Promise<IBlob>;
+	buffer(): Promise<Buffer>;
+	json(): Promise<unknown>;
+	text(): Promise<string>;
+	textConverted(): Promise<string>;
+}
diff --git a/packages/happy-dom/src/fetch/IHeaders.ts b/packages/happy-dom/src/fetch/IHeaders.ts
new file mode 100644
index 00000000..86a4b4e3
--- /dev/null
+++ b/packages/happy-dom/src/fetch/IHeaders.ts
@@ -0,0 +1,18 @@
+/**
+ * Fetch headers.
+ */
+export default interface IHeaders extends Iterable<[string, string]> {
+	forEach(callback: (value: string, name: string) => void): void;
+	append(name: string, value: string): void;
+	delete(name: string): void;
+	get(name: string): string | null;
+	has(name: string): boolean;
+	raw(): { [k: string]: string[] };
+	set(name: string, value: string): void;
+
+	// Iterable methods
+	entries(): IterableIterator<[string, string]>;
+	keys(): IterableIterator<string>;
+	values(): IterableIterator<string>;
+	[Symbol.iterator](): Iterator<[string, string]>;
+}
diff --git a/packages/happy-dom/src/fetch/IHeadersInit.ts b/packages/happy-dom/src/fetch/IHeadersInit.ts
new file mode 100644
index 00000000..9d6fc300
--- /dev/null
+++ b/packages/happy-dom/src/fetch/IHeadersInit.ts
@@ -0,0 +1,5 @@
+import IHeaders from './IHeaders';
+
+type IHeadersInit = string[][] | { [key: string]: string } | IHeaders;
+
+export default IHeadersInit;
diff --git a/packages/happy-dom/src/fetch/IRequest.ts b/packages/happy-dom/src/fetch/IRequest.ts
new file mode 100644
index 00000000..7c35aeb9
--- /dev/null
+++ b/packages/happy-dom/src/fetch/IRequest.ts
@@ -0,0 +1,59 @@
+import IHeaders from './IHeaders';
+import IBody from './IBody';
+
+/**
+ * Fetch request.
+ */
+export default interface IRequest extends IBody {
+	readonly headers: IHeaders;
+	readonly method: string;
+	readonly redirect: 'error' | 'follow' | 'manual';
+	readonly referrer: string;
+	readonly url: string;
+
+	/**
+	 * Returns a clone.
+	 *
+	 * @returns Clone.
+	 */
+	clone(): IRequest;
+
+	// Not implemented:
+	// Readonly cache: 'default' | 'force-cache' | 'no-cache' | 'no-store' | 'only-if-cached' | 'reload';
+	// Readonly credentials: 'include' | 'omit' | 'same-origin';
+	// Readonly destination:
+	// 	| ''
+	// 	| 'object'
+	// 	| 'audio'
+	// 	| 'audioworklet'
+	// 	| 'document'
+	// 	| 'embed'
+	// 	| 'font'
+	// 	| 'frame'
+	// 	| 'iframe'
+	// 	| 'image'
+	// 	| 'manifest'
+	// 	| 'paintworklet'
+	// 	| 'report'
+	// 	| 'script'
+	// 	| 'sharedworker'
+	// 	| 'style'
+	// 	| 'track'
+	// 	| 'video'
+	// 	| 'worker'
+	// 	| 'xslt';
+	// Readonly referrerPolicy:
+	// 	| ''
+	// 	| 'same-origin'
+	// 	| 'no-referrer'
+	// 	| 'no-referrer-when-downgrade'
+	// 	| 'origin'
+	// 	| 'origin-when-cross-origin'
+	// 	| 'strict-origin'
+	// 	| 'strict-origin-when-cross-origin'
+	// 	| 'unsafe-url';
+	// Readonly signal: AbortSignal;
+	// Readonly integrity: string;
+	// Readonly keepalive: boolean;
+	// Readonly mode: 'same-origin' | 'cors' | 'navigate' | 'no-cors';
+}
diff --git a/packages/happy-dom/src/fetch/IRequestInit.ts b/packages/happy-dom/src/fetch/IRequestInit.ts
new file mode 100644
index 00000000..875c70c4
--- /dev/null
+++ b/packages/happy-dom/src/fetch/IRequestInit.ts
@@ -0,0 +1,41 @@
+import IHeadersInit from './IHeadersInit';
+import IAbortSignal from './IAbortSignal';
+import { URLSearchParams } from 'url';
+import IFormData from '../form-data/IFormData';
+
+/**
+ * Fetch request init.
+ */
+export default interface IRequestInit {
+	body?:
+		| ArrayBuffer
+		| ArrayBufferView
+		| NodeJS.ReadableStream
+		| string
+		| URLSearchParams
+		| IFormData
+		| null;
+	headers?: IHeadersInit;
+	method?: string;
+	redirect?: 'error' | 'manual' | 'follow';
+	signal?: IAbortSignal | null;
+
+	// Not implemented:
+	// Cache?: 'default' | 'force-cache' | 'no-cache' | 'no-store' | 'only-if-cached' | 'reload';
+	// Credentials?: 'include' | 'omit' | 'same-origin';
+	// Integrity?: string;
+	// Keepalive?: boolean;
+	// Mode?: 'same-origin' | 'cors' | 'navigate' | 'no-cors';
+	// Referrer?: string;
+	// ReferrerPolicy?:
+	// 	| ''
+	// 	| 'same-origin'
+	// 	| 'no-referrer'
+	// 	| 'no-referrer-when-downgrade'
+	// 	| 'origin'
+	// 	| 'origin-when-cross-origin'
+	// 	| 'strict-origin'
+	// 	| 'strict-origin-when-cross-origin'
+	// 	| 'unsafe-url';
+	// Window?: unknown;
+}
diff --git a/packages/happy-dom/src/fetch/IResponse.ts b/packages/happy-dom/src/fetch/IResponse.ts
new file mode 100644
index 00000000..61cd4227
--- /dev/null
+++ b/packages/happy-dom/src/fetch/IResponse.ts
@@ -0,0 +1,22 @@
+import IHeaders from './IHeaders';
+import IBody from './IBody';
+
+/**
+ * Fetch response.
+ */
+export default interface IResponse extends IBody {
+	readonly headers: IHeaders;
+	readonly ok: boolean;
+	readonly redirected: boolean;
+	readonly status: number;
+	readonly statusText: string;
+	readonly type: 'basic' | 'cors' | 'default' | 'error' | 'opaque' | 'opaqueredirect';
+	readonly url: string;
+
+	/**
+	 * Returns a clone.
+	 *
+	 * @returns Clone.
+	 */
+	clone(): IResponse;
+}
diff --git a/packages/happy-dom/src/fetch/IResponseInit.ts b/packages/happy-dom/src/fetch/IResponseInit.ts
new file mode 100644
index 00000000..8486680e
--- /dev/null
+++ b/packages/happy-dom/src/fetch/IResponseInit.ts
@@ -0,0 +1,10 @@
+import IHeaders from './IHeaders';
+
+/**
+ * Response init.
+ */
+export default interface IResponseInit {
+	headers?: IHeaders | string[][] | Record<string, string>;
+	status?: number;
+	statusText?: string;
+}
diff --git a/packages/happy-dom/src/fetch/Request.ts b/packages/happy-dom/src/fetch/Request.ts
new file mode 100644
index 00000000..422f3468
--- /dev/null
+++ b/packages/happy-dom/src/fetch/Request.ts
@@ -0,0 +1,149 @@
+import * as NodeFetch from 'node-fetch';
+import IRequest from './IRequest';
+import IBlob from '../file/IBlob';
+import IDocument from '../nodes/document/IDocument';
+
+/**
+ * Fetch request.
+ */
+export default class Request extends NodeFetch.Request implements IRequest {
+	public static _ownerDocument: IDocument = null;
+
+	/**
+	 * Returns array buffer.
+	 *
+	 * @returns Array buffer.
+	 */
+	public arrayBuffer(): Promise<ArrayBuffer> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.arrayBuffer()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Returns blob.
+	 *
+	 * @returns Blob.
+	 */
+	public blob(): Promise<IBlob> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.blob()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Returns buffer.
+	 *
+	 * @returns Buffer.
+	 */
+	public buffer(): Promise<Buffer> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.buffer()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Returns json.
+	 *
+	 * @returns JSON.
+	 */
+	public json(): Promise<unknown> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.json()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Returns json.
+	 *
+	 * @returns JSON.
+	 */
+	public text(): Promise<string> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.text()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Returns json.
+	 *
+	 * @returns JSON.
+	 */
+	public textConverted(): Promise<string> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.textConverted()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Handles promise start.
+	 *
+	 * @returns Task ID.
+	 */
+	private _handlePromiseStart(): number {
+		const taskManager = (<typeof Request>this.constructor)._ownerDocument.defaultView.happyDOM
+			.asyncTaskManager;
+		return taskManager.startTask();
+	}
+
+	/**
+	 * Handles promise end.
+	 *
+	 * @param resolve Resolve.
+	 * @param reject Reject.
+	 * @param taskID Task ID.
+	 * @param response Response.
+	 */
+	private _handlePromiseEnd(
+		resolve: (response: unknown) => void,
+		reject: (error: Error) => void,
+		taskID: number,
+		response: unknown
+	): void {
+		const taskManager = (<typeof Request>this.constructor)._ownerDocument.defaultView.happyDOM
+			.asyncTaskManager;
+		if (taskManager.getTaskCount() === 0) {
+			reject(new Error('Failed to complete fetch request. Task was canceled.'));
+		} else {
+			resolve(response);
+			taskManager.endTask(taskID);
+		}
+	}
+
+	/**
+	 * Handles promise error.
+	 *
+	 * @param error
+	 * @param reject
+	 */
+	private _handlePromiseError(reject: (error: Error) => void, error: Error): void {
+		const taskManager = (<typeof Request>this.constructor)._ownerDocument.defaultView.happyDOM
+			.asyncTaskManager;
+		reject(error);
+		taskManager.cancelAll(error);
+	}
+}
diff --git a/packages/happy-dom/src/fetch/ResourceFetchHandler.ts b/packages/happy-dom/src/fetch/ResourceFetchHandler.ts
new file mode 100644
index 00000000..eb26492e
--- /dev/null
+++ b/packages/happy-dom/src/fetch/ResourceFetchHandler.ts
@@ -0,0 +1,47 @@
+import RelativeURL from '../location/RelativeURL';
+import DOMException from '../exception/DOMException';
+import IDocument from '../nodes/document/IDocument';
+
+/**
+ * Helper class for performing fetch of resources.
+ */
+export default class ResourceFetchHandler {
+	/**
+	 * Returns resource data asynchonously.
+	 *
+	 * @param document Document.
+	 * @param url URL.
+	 * @returns Response.
+	 */
+	public static async fetch(document: IDocument, url: string): Promise<string> {
+		const response = await document.defaultView.fetch(url);
+		if (!response.ok) {
+			throw new DOMException(
+				`Failed to perform request to "${url}". Status code: ${response.status}`
+			);
+		}
+		return await response.text();
+	}
+
+	/**
+	 * Returns resource data synchonously.
+	 *
+	 * @param document Document.
+	 * @param url URL.
+	 * @returns Response.
+	 */
+	public static fetchSync(document: IDocument, url: string): string {
+		// We want to only load SyncRequest when it is needed to improve performance and not have direct dependencies to server side packages.
+		const syncRequest = require('sync-request');
+		const absoluteURL = RelativeURL.getAbsoluteURL(document.defaultView.location, url);
+		const response = syncRequest('GET', absoluteURL);
+
+		if (response.isError()) {
+			throw new DOMException(
+				`Failed to perform request to "${absoluteURL}". Status code: ${response.statusCode}`
+			);
+		}
+
+		return response.getBody();
+	}
+}
diff --git a/packages/happy-dom/src/fetch/ResourceFetcher.ts b/packages/happy-dom/src/fetch/ResourceFetcher.ts
deleted file mode 100644
index 26884b0f..00000000
--- a/packages/happy-dom/src/fetch/ResourceFetcher.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import RelativeURL from '../location/RelativeURL';
-import Window from '../window/Window';
-import DOMException from '../exception/DOMException';
-
-/**
- * Helper class for performing an asynchonous or synchrounous request to a resource.
- */
-export default class ResourceFetcher {
-	/**
-	 * Returns resource data asynchonously.
-	 *
-	 * @param options Options.
-	 * @param options.window Location.
-	 * @param options.url URL.
-	 * @returns Response.
-	 */
-	public static async fetch(options: { window: Window; url: string }): Promise<string> {
-		const response = await options.window.fetch(options.url);
-		if (!response.ok) {
-			throw new DOMException(
-				`Failed to perform request to "${options.url}". Status code: ${response.status}`
-			);
-		}
-		return await response.text();
-	}
-
-	/**
-	 * Returns resource data synchonously.
-	 *
-	 * @param options Options.
-	 * @param options.window Location.
-	 * @param options.url URL.
-	 * @returns Response.
-	 */
-	public static fetchSync(options: { window: Window; url: string }): string {
-		const url = RelativeURL.getAbsoluteURL(options.window.location, options.url);
-		let request = null;
-
-		try {
-			request = require('sync-request');
-		} catch (error) {
-			throw new DOMException('Failed to load script. "sync-request" could not be loaded.');
-		}
-
-		const response = request('GET', url);
-
-		if (response.isError()) {
-			throw new DOMException(
-				`Failed to perform request to "${url}". Status code: ${response.statusCode}`
-			);
-		}
-
-		return response.getBody();
-	}
-}
diff --git a/packages/happy-dom/src/fetch/Response.ts b/packages/happy-dom/src/fetch/Response.ts
new file mode 100644
index 00000000..82a2bd7e
--- /dev/null
+++ b/packages/happy-dom/src/fetch/Response.ts
@@ -0,0 +1,149 @@
+import IResponse from './IResponse';
+import IBlob from '../file/IBlob';
+import IDocument from '../nodes/document/IDocument';
+import * as NodeFetch from 'node-fetch';
+
+/**
+ * Fetch response.
+ */
+export default class Response extends NodeFetch.Response implements IResponse {
+	public static _ownerDocument: IDocument = null;
+
+	/**
+	 * Returns array buffer.
+	 *
+	 * @returns Array buffer.
+	 */
+	public arrayBuffer(): Promise<ArrayBuffer> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.arrayBuffer()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Returns blob.
+	 *
+	 * @returns Blob.
+	 */
+	public blob(): Promise<IBlob> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.blob()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Returns buffer.
+	 *
+	 * @returns Buffer.
+	 */
+	public buffer(): Promise<Buffer> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.buffer()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Returns json.
+	 *
+	 * @returns JSON.
+	 */
+	public json(): Promise<unknown> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.json()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Returns json.
+	 *
+	 * @returns JSON.
+	 */
+	public text(): Promise<string> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.text()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Returns json.
+	 *
+	 * @returns JSON.
+	 */
+	public textConverted(): Promise<string> {
+		return new Promise((resolve, reject) => {
+			const taskID = this._handlePromiseStart();
+			super
+				.textConverted()
+				.then(this._handlePromiseEnd.bind(this, resolve, reject, taskID))
+				.catch(this._handlePromiseError.bind(this, reject));
+		});
+	}
+
+	/**
+	 * Handles promise start.
+	 *
+	 * @returns Task ID.
+	 */
+	private _handlePromiseStart(): number {
+		const taskManager = (<typeof Response>this.constructor)._ownerDocument.defaultView.happyDOM
+			.asyncTaskManager;
+		return taskManager.startTask();
+	}
+
+	/**
+	 * Handles promise end.
+	 *
+	 * @param resolve Resolve.
+	 * @param reject Reject.
+	 * @param taskID Task ID.
+	 * @param response Response.
+	 */
+	private _handlePromiseEnd(
+		resolve: (response: unknown) => void,
+		reject: (error: Error) => void,
+		taskID: number,
+		response: unknown
+	): void {
+		const taskManager = (<typeof Response>this.constructor)._ownerDocument.defaultView.happyDOM
+			.asyncTaskManager;
+		if (taskManager.getTaskCount() === 0) {
+			reject(new Error('Failed to complete fetch request. Task was canceled.'));
+		} else {
+			resolve(response);
+			taskManager.endTask(taskID);
+		}
+	}
+
+	/**
+	 * Handles promise error.
+	 *
+	 * @param error
+	 * @param reject
+	 */
+	private _handlePromiseError(reject: (error: Error) => void, error: Error): void {
+		const taskManager = (<typeof Response>this.constructor)._ownerDocument.defaultView.happyDOM
+			.asyncTaskManager;
+		reject(error);
+		taskManager.cancelAll(error);
+	}
+}
diff --git a/packages/happy-dom/src/file/Blob.ts b/packages/happy-dom/src/file/Blob.ts
index 3189aeaa..7d48b800 100644
--- a/packages/happy-dom/src/file/Blob.ts
+++ b/packages/happy-dom/src/file/Blob.ts
@@ -1,3 +1,5 @@
+import IBlob from './IBlob';
+
 /**
  * Reference:
  * https://developer.mozilla.org/en-US/docs/Web/API/Blob.
@@ -5,7 +7,7 @@
  * Based on:
  * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/file-api/Blob-impl.js (MIT licensed).
  */
-export default class Blob {
+export default class Blob implements IBlob {
 	public readonly _buffer: Buffer = null;
 	public readonly type: string = '';
 
@@ -108,6 +110,15 @@ export default class Blob {
 		return blob;
 	}
 
+	/**
+	 * Returns a Promise that resolves to a text.
+	 *
+	 * @returns Text.
+	 */
+	public async text(): Promise<string> {
+		return this._buffer.toString();
+	}
+
 	/**
 	 * Closes the blob.
 	 *
diff --git a/packages/happy-dom/src/file/IBlob.ts b/packages/happy-dom/src/file/IBlob.ts
new file mode 100644
index 00000000..574cbfb7
--- /dev/null
+++ b/packages/happy-dom/src/file/IBlob.ts
@@ -0,0 +1,10 @@
+/**
+ * Reference:
+ * https://developer.mozilla.org/en-US/docs/Web/API/Blob.
+ */
+export default interface IBlob {
+	readonly type: string;
+	readonly size: number;
+	slice(start?: number, end?: number): IBlob;
+	text(): Promise<string>;
+}
diff --git a/packages/happy-dom/src/form-data/IFormData.ts b/packages/happy-dom/src/form-data/IFormData.ts
new file mode 100644
index 00000000..c63149fd
--- /dev/null
+++ b/packages/happy-dom/src/form-data/IFormData.ts
@@ -0,0 +1,33 @@
+import Stream from 'stream';
+import HTTP from 'http';
+
+interface IHeaders {
+	[key: string]: unknown;
+}
+
+interface IAppendOptions {
+	header?: string | IHeaders;
+	knownLength?: number;
+	filename?: string;
+	filepath?: string;
+	contentType?: string;
+}
+
+interface ISubmitOptions extends HTTP.RequestOptions {
+	protocol?: 'https:' | 'http:';
+}
+
+export default interface IFormData extends Stream.Readable {
+	append(key: string, value: unknown, options?: IAppendOptions | string): void;
+	getHeaders(userHeaders?: IHeaders): { [key: string]: IHeaders };
+	submit(
+		params: string | ISubmitOptions,
+		callback?: (error: Error | null, response: HTTP.IncomingMessage) => void
+	): HTTP.ClientRequest;
+	getBuffer(): Buffer;
+	setBoundary(boundary: string): void;
+	getBoundary(): string;
+	getLength(callback: (err: Error | null, length: number) => void): void;
+	getLengthSync(): number;
+	hasKnownLength(): boolean;
+}
diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts
index b16d5217..b7b9cc02 100644
--- a/packages/happy-dom/src/index.ts
+++ b/packages/happy-dom/src/index.ts
@@ -14,9 +14,7 @@ import DOMException from './exception/DOMException';
 import History from './history/History';
 import CSSStyleDeclaration from './css/CSSStyleDeclaration';
 import Screen from './screen/Screen';
-import AsyncTaskManager from './window/AsyncTaskManager';
-import IResponse from './window/IResponse';
-import IFetchOptions from './window/IFetchOptions';
+import AsyncTaskManager from './async-task-manager/AsyncTaskManager';
 import NodeFilter from './tree-walker/NodeFilter';
 import Event from './event/Event';
 import EventTarget from './event/EventTarget';
@@ -101,7 +99,7 @@ import XMLParser from './xml-parser/XMLParser';
 import XMLSerializer from './xml-serializer/XMLSerializer';
 import CSSStyleSheet from './css/CSSStyleSheet';
 import Storage from './storage/Storage';
-import URLSearchParams from './url-search-params/URLSearchParams';
+import { URLSearchParams } from 'url';
 import Selection from './selection/Selection';
 
 export {
@@ -123,8 +121,6 @@ export {
 	CSSStyleDeclaration,
 	Screen,
 	AsyncTaskManager,
-	IResponse,
-	IFetchOptions,
 	NodeFilter,
 	Event,
 	EventTarget,
diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts
index d18741b8..f649b2be 100644
--- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts
+++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts
@@ -1,6 +1,6 @@
 import Attr from '../../attribute/Attr';
 import CSSStyleSheet from '../../css/CSSStyleSheet';
-import ResourceFetcher from '../../fetch/ResourceFetcher';
+import ResourceFetchHandler from '../../fetch/ResourceFetchHandler';
 import HTMLElement from '../html-element/HTMLElement';
 import Document from '../document/Document';
 import IHTMLLinkElement from './IHTMLLinkElement';
@@ -199,7 +199,7 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle
 			this.isConnected
 		) {
 			(<Document>this.ownerDocument)._readyStateManager.startTask();
-			ResourceFetcher.fetch({ window: this.ownerDocument.defaultView, url: href })
+			ResourceFetchHandler.fetch(this.ownerDocument, href)
 				.then((code) => {
 					const styleSheet = new CSSStyleSheet();
 					styleSheet.replaceSync(code);
@@ -245,7 +245,7 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle
 
 			if (href !== null && rel && rel.toLowerCase() === 'stylesheet') {
 				(<Document>this.ownerDocument)._readyStateManager.startTask();
-				ResourceFetcher.fetch({ window: this.ownerDocument.defaultView, url: href })
+				ResourceFetchHandler.fetch(this.ownerDocument, href)
 					.then((code) => {
 						const styleSheet = new CSSStyleSheet();
 						styleSheet.replaceSync(code);
diff --git a/packages/happy-dom/src/nodes/html-script-element/ScriptUtility.ts b/packages/happy-dom/src/nodes/html-script-element/ScriptUtility.ts
index 0dd56848..c6b5f94b 100644
--- a/packages/happy-dom/src/nodes/html-script-element/ScriptUtility.ts
+++ b/packages/happy-dom/src/nodes/html-script-element/ScriptUtility.ts
@@ -1,7 +1,7 @@
 import { Document } from '../..';
 import Event from '../../event/Event';
 import ErrorEvent from '../../event/events/ErrorEvent';
-import ResourceFetcher from '../../fetch/ResourceFetcher';
+import ResourceFetchHandler from '../../fetch/ResourceFetchHandler';
 import HTMLScriptElement from './HTMLScriptElement';
 
 /**
@@ -22,10 +22,7 @@ export default class ScriptUtility {
 			let code = null;
 			(<Document>element.ownerDocument)._readyStateManager.startTask();
 			try {
-				code = await ResourceFetcher.fetch({
-					window: element.ownerDocument.defaultView,
-					url: src
-				});
+				code = await ResourceFetchHandler.fetch(element.ownerDocument, src);
 			} catch (error) {
 				element.dispatchEvent(
 					new ErrorEvent('error', {
@@ -54,10 +51,7 @@ export default class ScriptUtility {
 		} else {
 			let code = null;
 			try {
-				code = ResourceFetcher.fetchSync({
-					window: element.ownerDocument.defaultView,
-					url: src
-				});
+				code = ResourceFetchHandler.fetchSync(element.ownerDocument, src);
 			} catch (error) {
 				element.dispatchEvent(
 					new ErrorEvent('error', {
diff --git a/packages/happy-dom/src/url-search-params/URLSearchParams.ts b/packages/happy-dom/src/url-search-params/URLSearchParams.ts
deleted file mode 100644
index ec26d8bb..00000000
--- a/packages/happy-dom/src/url-search-params/URLSearchParams.ts
+++ /dev/null
@@ -1,198 +0,0 @@
-/**
- * URLSearchParams.
- *
- * Reference:
- * https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams.
- */
-export default class URLSearchParams {
-	private _params: Array<[string, string]> = [];
-
-	/**
-	 * Constructor.
-	 *
-	 * @param [params] Params string.
-	 */
-	constructor(params?: string) {
-		if (params) {
-			for (const entry of params.split('&')) {
-				const [name, value] = entry.split('=');
-				if (name) {
-					this._params.push([name, value]);
-				}
-			}
-		}
-	}
-
-	/**
-	 * Appends a specified key/value pair as a new search parameter.
-	 *
-	 * @param name Name.
-	 * @param value Value.
-	 */
-	public append(name: string, value: string): void {
-		this._params.push([name, value]);
-	}
-
-	/**
-	 * Deletes the given search parameter, and its associated value, from the list of all search parameters.
-	 *
-	 * @param name Name.
-	 */
-	public delete(name: string): void {
-		for (let i = 0, max = this._params.length; i < max; i++) {
-			if (this._params[i][0] === name) {
-				this._params.splice(i, 1);
-				i--;
-				max--;
-			}
-		}
-	}
-
-	/**
-	 * Returns an iterator.
-	 *
-	 * @returns Entries.
-	 */
-	public entries(): IterableIterator<[string, string]> {
-		const params = this._params;
-		const Iterator = function Iterator(): void {
-			let index = 0;
-			this.next = () => {
-				const value = params[index];
-				const done = index >= params.length;
-				index++;
-				return { value, done: done };
-			};
-			this[Symbol.iterator] = () => new Iterator();
-		};
-		return <IterableIterator<[string, string]>>new Iterator();
-	}
-
-	/**
-	 * For each.
-	 *
-	 * @param callback Callback.
-	 */
-	public forEach(callback: (value, key) => void): void {
-		for (const param of this._params) {
-			callback(param[0], param[1]);
-		}
-	}
-
-	/**
-	 * Returns value.
-	 *
-	 * @param name Name.
-	 */
-	public get(name: string): string {
-		for (const param of this._params) {
-			if (param[0] === name) {
-				return param[1];
-			}
-		}
-		return undefined;
-	}
-
-	/**
-	 * Returns all values associated with a name.
-	 *
-	 * @param name Name.
-	 * @returns Values.
-	 */
-	public getAll(name: string): string[] {
-		const values = [];
-		for (const param of this._params) {
-			if (param[0] === name) {
-				values.push(param[1]);
-			}
-		}
-		return values;
-	}
-
-	/**
-	 * Returns "true" if param exists.
-	 *
-	 * @param name Name.
-	 * @returns "true" if param exists.
-	 */
-	public has(name: string): boolean {
-		for (const param of this._params) {
-			if (param[0] === name) {
-				return true;
-			}
-		}
-		return false;
-	}
-
-	/**
-	 * Returns an iterator.
-	 *
-	 * @returns Keys iterator.
-	 */
-	public keys(): IterableIterator<string> {
-		const params = this._params;
-		const Iterator = function Iterator(): void {
-			let index = 0;
-			this.next = () => {
-				const value = params[index] !== undefined ? params[index][0] : undefined;
-				const done = index >= params.length;
-				index++;
-				return { value, done: done };
-			};
-			this[Symbol.iterator] = () => new Iterator();
-		};
-		return <IterableIterator<string>>new Iterator();
-	}
-
-	/**
-	 * Sets the value associated with a given search parameter to the given value. If there are several values, the others are deleted.
-	 *
-	 * @param name Name.
-	 * @param value Value.
-	 */
-	public set(name: string, value: string): void {
-		this.delete(name);
-		this.append(name, value);
-	}
-
-	/**
-	 * Sorts all key/value pairs, if any, by their keys.
-	 */
-	public sort(): void {
-		this._params.sort((a, b) => {
-			if (a[0] < b[0]) {
-				return -1;
-			} else if (a[0] > b[0]) {
-				return 1;
-			}
-			return 0;
-		});
-	}
-
-	/**
-	 * Returns a string containing a query string suitable for use in a URL.
-	 */
-	public toString(): string {
-		return this._params.map((param) => param.join('=')).join('&');
-	}
-
-	/**
-	 * Returns an iterator.
-	 *
-	 * @returns Values iterator.
-	 */
-	public values(): IterableIterator<string> {
-		const params = this._params;
-		const Iterator = function Iterator(): void {
-			let index = 0;
-			this.next = () => {
-				const value = params[index] !== undefined ? params[index][1] : undefined;
-				const done = index >= params.length;
-				index++;
-				return { value, done: done };
-			};
-			this[Symbol.iterator] = () => new Iterator();
-		};
-		return <IterableIterator<string>>new Iterator();
-	}
-}
diff --git a/packages/happy-dom/src/window/AsyncTaskManager.ts b/packages/happy-dom/src/window/AsyncTaskManager.ts
deleted file mode 100644
index f22b323b..00000000
--- a/packages/happy-dom/src/window/AsyncTaskManager.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import AsyncTaskTypeEnum from './AsyncTaskTypeEnum';
-
-/**
- * Handles async tasks.
- */
-export default class AsyncTaskManager {
-	private tasks: { [k: string]: (NodeJS.Timeout | string)[] } = {};
-	private queue: { resolve: () => void; reject: (error: Error) => void }[] = [];
-
-	/**
-	 * Returns a promise that is fulfilled when async tasks are complete.
-	 * This method is not part of the HTML standard.
-	 *
-	 * @returns Promise.
-	 */
-	public async whenComplete(): Promise<void> {
-		return new Promise((resolve, reject) => {
-			const timerID = global.setTimeout(() => {
-				this.endTimer(timerID);
-			}, 0);
-			this.startTimer(timerID);
-			this.queue.push({ resolve, reject });
-		});
-	}
-
-	/**
-	 * Cancels all tasks.
-	 *
-	 * @param [error] Error.
-	 */
-	public cancelAllTasks(error?: Error): void {
-		if (this.tasks[AsyncTaskTypeEnum.timer]) {
-			for (const id of this.tasks[AsyncTaskTypeEnum.timer]) {
-				global.clearTimeout(<NodeJS.Timeout>id);
-			}
-		}
-
-		const promises = this.queue;
-
-		this.tasks = {};
-		this.queue = [];
-
-		for (const promise of promises) {
-			if (error) {
-				promise.reject(error);
-			} else {
-				promise.resolve();
-			}
-		}
-	}
-
-	/**
-	 * Starts a timer.
-	 *
-	 * @param id Timer ID.
-	 */
-	public startTimer(id: NodeJS.Timeout = null): void {
-		this.tasks[AsyncTaskTypeEnum.timer] = this.tasks[AsyncTaskTypeEnum.timer] || [];
-		this.tasks[AsyncTaskTypeEnum.timer].push(id);
-	}
-
-	/**
-	 * Ends a timer.
-	 *
-	 * @param id Timer ID.
-	 */
-	public endTimer(id: NodeJS.Timeout = null): void {
-		if (this.tasks[AsyncTaskTypeEnum.timer]) {
-			const index = this.tasks[AsyncTaskTypeEnum.timer].indexOf(id);
-			if (index !== -1) {
-				this.tasks[AsyncTaskTypeEnum.timer].splice(index, 1);
-				if (this.tasks[AsyncTaskTypeEnum.timer].length === 0) {
-					delete this.tasks[AsyncTaskTypeEnum.timer];
-
-					if (Object.keys(this.tasks).length === 0) {
-						this.cancelAllTasks();
-					}
-				}
-			}
-		}
-	}
-
-	/**
-	 * Starts an async task.
-	 *
-	 * @param type Task type.
-	 */
-	public startTask(type: AsyncTaskTypeEnum): void {
-		this.tasks[type] = this.tasks[type] || [];
-		this.tasks[type].push(type);
-	}
-
-	/**
-	 * Ends an async task.
-	 *
-	 * @param type Task type.
-	 * @param [error] Error.
-	 */
-	public endTask(type: AsyncTaskTypeEnum, error?: Error): void {
-		if (error) {
-			this.cancelAllTasks(error);
-		} else if (this.tasks[type]) {
-			this.tasks[type].pop();
-
-			if (this.tasks[type].length === 0) {
-				delete this.tasks[type];
-
-				if (Object.keys(this.tasks).length === 0) {
-					this.cancelAllTasks();
-				}
-			}
-		}
-	}
-
-	/**
-	 * Returns the amount of running tasks by type.
-	 *
-	 * @param type Task type.
-	 * @returns Count.
-	 */
-	public getRunningCount(type: AsyncTaskTypeEnum): number {
-		if (this.tasks[type]) {
-			return this.tasks[type].length;
-		}
-		return 0;
-	}
-}
diff --git a/packages/happy-dom/src/window/AsyncTaskTypeEnum.ts b/packages/happy-dom/src/window/AsyncTaskTypeEnum.ts
deleted file mode 100644
index cae401bc..00000000
--- a/packages/happy-dom/src/window/AsyncTaskTypeEnum.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-enum AsyncTaskTypeEnum {
-	timer = 'timer',
-	fetch = 'fetch'
-}
-export default AsyncTaskTypeEnum;
diff --git a/packages/happy-dom/src/window/IFetchOptions.ts b/packages/happy-dom/src/window/IFetchOptions.ts
deleted file mode 100644
index 6229a12a..00000000
--- a/packages/happy-dom/src/window/IFetchOptions.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { URLSearchParams } from 'url';
-
-/**
- * Fetch options.
- */
-export default interface IFetchOptions {
-	method?: string;
-	headers?: Map<string, string> | { [k: string]: string };
-	body?: URLSearchParams | string;
-	redirect?: string;
-}
diff --git a/packages/happy-dom/src/window/IResponse.ts b/packages/happy-dom/src/window/IResponse.ts
deleted file mode 100644
index bab3f195..00000000
--- a/packages/happy-dom/src/window/IResponse.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import Blob from '../file/Blob';
-
-/**
- * Fetch response.
- */
-export default interface IResponse {
-	readonly url: string;
-	readonly status: number;
-	readonly ok: boolean;
-	readonly redirected: boolean;
-	readonly statusText: string;
-	readonly headers: { [k: string]: string };
-
-	/**
-	 * Returns JSON.
-	 *
-	 * @returns JSON.
-	 */
-	json(): Promise<string>;
-
-	/**
-	 * Returns Text.
-	 *
-	 * @returns Text.
-	 */
-	text(): Promise<string>;
-
-	/**
-	 * Returns blob.
-	 *
-	 * @returns Blob.
-	 */
-	blob(): Promise<Blob>;
-}
diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts
index bfee56c1..cacfb9c3 100644
--- a/packages/happy-dom/src/window/IWindow.ts
+++ b/packages/happy-dom/src/window/IWindow.ts
@@ -60,13 +60,15 @@ import UIEvent from '../event/UIEvent';
 import ErrorEvent from '../event/events/ErrorEvent';
 import StorageEvent from '../event/events/StorageEvent';
 import Screen from '../screen/Screen';
-import AsyncTaskManager from './AsyncTaskManager';
-import IResponse from './IResponse';
+import AsyncTaskManager from '../async-task-manager/AsyncTaskManager';
+import IResponse from '../fetch/IResponse';
+import IResponseInit from '../fetch/IResponseInit';
+import IRequest from '../fetch/IRequest';
+import IRequestInit from '../fetch/IRequestInit';
+import IHeaders from '../fetch/IHeaders';
 import Storage from '../storage/Storage';
-import IFetchOptions from './IFetchOptions';
 import NodeFilter from '../tree-walker/NodeFilter';
 import Window from './Window';
-import URLSearchParams from '../url-search-params/URLSearchParams';
 import HTMLCollection from '../nodes/element/HTMLCollection';
 import NodeList from '../nodes/node/NodeList';
 import Selection from '../selection/Selection';
@@ -76,6 +78,7 @@ import MimeType from '../navigator/MimeType';
 import MimeTypeArray from '../navigator/MimeTypeArray';
 import Plugin from '../navigator/Plugin';
 import PluginArray from '../navigator/PluginArray';
+import { URLSearchParams } from 'url';
 
 /**
  * Window.
@@ -141,7 +144,9 @@ export default interface IWindow extends IEventTarget {
 	readonly Location: typeof Location;
 	readonly CustomElementRegistry: typeof CustomElementRegistry;
 	readonly Window: typeof Window;
-	readonly Headers: typeof Map;
+	readonly Headers: { new (init?: string[][] | Record<string, string> | IHeaders): IHeaders };
+	readonly Request: { new (input: string | IRequest, init?: IRequestInit): IRequest };
+	readonly Response: { new (body?: unknown | null, init?: IResponseInit): IResponse };
 	readonly XMLSerializer: typeof XMLSerializer;
 	readonly ResizeObserver: typeof ResizeObserver;
 	readonly CSSStyleSheet: typeof CSSStyleSheet;
@@ -221,7 +226,6 @@ export default interface IWindow extends IEventTarget {
 	/**
 	 * Sets a timer which executes a function once the timer expires.
 	 *
-	 * @override
 	 * @param callback Function to be executed.
 	 * @param [delay=0] Delay in ms.
 	 * @returns Timeout ID.
@@ -231,7 +235,6 @@ export default interface IWindow extends IEventTarget {
 	/**
 	 * Cancels a timeout previously established by calling setTimeout().
 	 *
-	 * @override
 	 * @param id ID of the timeout.
 	 */
 	clearTimeout(id: NodeJS.Timeout): void;
@@ -239,7 +242,6 @@ export default interface IWindow extends IEventTarget {
 	/**
 	 * Calls a function with a fixed time delay between each call.
 	 *
-	 * @override
 	 * @param callback Function to be executed.
 	 * @param [delay=0] Delay in ms.
 	 * @returns Interval ID.
@@ -249,7 +251,6 @@ export default interface IWindow extends IEventTarget {
 	/**
 	 * Cancels a timed repeating action which was previously established by a call to setInterval().
 	 *
-	 * @override
 	 * @param id ID of the interval.
 	 */
 	clearInterval(id: NodeJS.Timeout): void;
@@ -257,7 +258,6 @@ export default interface IWindow extends IEventTarget {
 	/**
 	 * Mock animation frames with timeouts.
 	 *
-	 * @override
 	 * @param {Function} callback Callback.
 	 * @returns {NodeJS.Timeout} Timeout ID.
 	 */
@@ -266,18 +266,16 @@ export default interface IWindow extends IEventTarget {
 	/**
 	 * Mock animation frames with timeouts.
 	 *
-	 * @override
 	 * @param {NodeJS.Timeout} id Timeout ID.
 	 */
 	cancelAnimationFrame(id: NodeJS.Timeout): void;
 
 	/**
-	 * Provides a global fetch() method that provides an easy, logical way to fetch resources asynchronously across the network.
+	 * This method provides an easy, logical way to fetch resources asynchronously across the network.
 	 *
-	 * @override
-	 * @param url URL to resource.
-	 * @param [options] Options.
+	 * @param url URL.
+	 * @param [init] Init.
 	 * @returns Promise.
 	 */
-	fetch(url: string, options?: IFetchOptions): Promise<IResponse>;
+	fetch(url: string, init?: IRequestInit): Promise<IResponse>;
 }
diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts
index 20c491f7..1de55cb5 100644
--- a/packages/happy-dom/src/window/Window.ts
+++ b/packages/happy-dom/src/window/Window.ts
@@ -63,14 +63,15 @@ import UIEvent from '../event/UIEvent';
 import ErrorEvent from '../event/events/ErrorEvent';
 import StorageEvent from '../event/events/StorageEvent';
 import Screen from '../screen/Screen';
-import AsyncTaskManager from './AsyncTaskManager';
-import IResponse from './IResponse';
-import AsyncTaskTypeEnum from './AsyncTaskTypeEnum';
-import RelativeURL from '../location/RelativeURL';
+import AsyncTaskManager from '../async-task-manager/AsyncTaskManager';
+import IResponse from '../fetch/IResponse';
+import IResponseInit from '../fetch/IResponseInit';
+import IRequest from '../fetch/IRequest';
+import IRequestInit from '../fetch/IRequestInit';
+import IHeaders from '../fetch/IHeaders';
+import IHeadersInit from '../fetch/IHeadersInit';
 import Storage from '../storage/Storage';
-import IFetchOptions from './IFetchOptions';
 import IWindow from './IWindow';
-import URLSearchParams from '../url-search-params/URLSearchParams';
 import HTMLCollection from '../nodes/element/HTMLCollection';
 import NodeList from '../nodes/node/NodeList';
 import MediaQueryList from '../match-media/MediaQueryList';
@@ -81,8 +82,8 @@ import MimeType from '../navigator/MimeType';
 import MimeTypeArray from '../navigator/MimeTypeArray';
 import Plugin from '../navigator/Plugin';
 import PluginArray from '../navigator/PluginArray';
-
-const FETCH_RESPONSE_TYPE_METHODS = ['blob', 'json', 'text'];
+import FetchHandler from '../fetch/FetchHandler';
+import { URLSearchParams } from 'url';
 
 /**
  * Handles the Window.
@@ -94,7 +95,7 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 			return await this.happyDOM.asyncTaskManager.whenComplete();
 		},
 		cancelAsync: (): void => {
-			this.happyDOM.asyncTaskManager.cancelAllTasks();
+			this.happyDOM.asyncTaskManager.cancelAll();
 		},
 		asyncTaskManager: new AsyncTaskManager()
 	};
@@ -152,7 +153,6 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 	public readonly Location = Location;
 	public readonly CustomElementRegistry = CustomElementRegistry;
 	public readonly Window = Window;
-	public readonly Headers = Map;
 	public readonly XMLSerializer = XMLSerializer;
 	public readonly ResizeObserver = ResizeObserver;
 	public readonly CSSStyleSheet = CSSStyleSheet;
@@ -200,7 +200,6 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 	public readonly performance = PerfHooks.performance;
 
 	// Node.js Globals
-	public Array = global ? global.Array : null;
 	public ArrayBuffer = global ? global.ArrayBuffer : null;
 	public Boolean = global ? global.Boolean : null;
 	public Buffer = null;
@@ -262,6 +261,7 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 
 	// Private properties
 	private _objectClass: typeof globalThis.Object = null;
+	private _arrayClass: typeof globalThis.Array = null;
 	private _functionClass: typeof globalThis.Function = null;
 
 	/**
@@ -315,6 +315,21 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 		return this._objectClass;
 	}
 
+	/**
+	 * Returns Array class.
+	 *
+	 * @returns Array class.
+	 */
+	public get Array(): typeof globalThis.Array {
+		if (this._arrayClass) {
+			return this._arrayClass;
+		}
+		// When inside a VM global.Object is not the same as [].constructor
+		// We will therefore run the code inside the VM to get the real constructor
+		this._arrayClass = <typeof globalThis.Array>this.eval('[].constructor');
+		return this._arrayClass;
+	}
+
 	/**
 	 * Returns Function class.
 	 *
@@ -339,6 +354,43 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 		return new CSS();
 	}
 
+	/**
+	 * Returns Headers class.
+	 *
+	 * @returns Headers.
+	 */
+	public get Headers(): {
+		new (init?: IHeadersInit): IHeaders;
+	} {
+		return require('../fetch/Headers').default;
+	}
+
+	/**
+	 * Returns Request class.
+	 *
+	 * @returns Request.
+	 */
+	public get Request(): {
+		new (input: string | { href: string } | IRequest, init?: IRequestInit): IRequest;
+	} {
+		const Request = require('../fetch/Request').default;
+		Request._ownerDocument = Request._ownerDocument || this.document;
+		return Request;
+	}
+
+	/**
+	 * Returns Response class.
+	 *
+	 * @returns Response.
+	 */
+	public get Response(): {
+		new (body?: NodeJS.ReadableStream | null, init?: IResponseInit): IResponse;
+	} {
+		const Response = require('../fetch/Response').default;
+		Response._ownerDocument = Response._ownerDocument || this.document;
+		return Response;
+	}
+
 	/**
 	 * Evaluates code.
 	 *
@@ -509,65 +561,14 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 	}
 
 	/**
-	 * Provides a global fetch() method that provides an easy, logical way to fetch resources asynchronously across the network.
+	 * This method provides an easy, logical way to fetch resources asynchronously across the network.
 	 *
 	 * @override
-	 * @param url URL to resource.
-	 * @param [options] Options.
+	 * @param url URL.
+	 * @param [init] Init.
 	 * @returns Promise.
 	 */
-	public async fetch(url: string, options?: IFetchOptions): Promise<IResponse> {
-		return new Promise((resolve, reject) => {
-			let fetch = null;
-
-			try {
-				fetch = require('node-fetch');
-			} catch (error) {
-				throw new Error('Failed to perform fetch. "node-fetch" could not be loaded.');
-			}
-
-			this.happyDOM.asyncTaskManager.startTask(AsyncTaskTypeEnum.fetch);
-
-			fetch(RelativeURL.getAbsoluteURL(this.location, url), options)
-				.then((response) => {
-					if (this.happyDOM.asyncTaskManager.getRunningCount(AsyncTaskTypeEnum.fetch) === 0) {
-						reject(new Error('Failed to complete fetch request. Task was canceled.'));
-					} else {
-						for (const methodName of FETCH_RESPONSE_TYPE_METHODS) {
-							const asyncMethod = response[methodName];
-							response[methodName] = () => {
-								return new Promise((resolve, reject) => {
-									this.happyDOM.asyncTaskManager.startTask(AsyncTaskTypeEnum.fetch);
-
-									asyncMethod
-										.call(response)
-										.then((response) => {
-											if (
-												this.happyDOM.asyncTaskManager.getRunningCount(AsyncTaskTypeEnum.fetch) ===
-												0
-											) {
-												reject(new Error('Failed to complete fetch request. Task was canceled.'));
-											} else {
-												resolve(response);
-												this.happyDOM.asyncTaskManager.endTask(AsyncTaskTypeEnum.fetch);
-											}
-										})
-										.catch((error) => {
-											reject(error);
-											this.happyDOM.asyncTaskManager.endTask(AsyncTaskTypeEnum.fetch, error);
-										});
-								});
-							};
-						}
-
-						resolve(response);
-						this.happyDOM.asyncTaskManager.endTask(AsyncTaskTypeEnum.fetch);
-					}
-				})
-				.catch((error) => {
-					reject(error);
-					this.happyDOM.asyncTaskManager.endTask(AsyncTaskTypeEnum.fetch, error);
-				});
-		});
+	public async fetch(url: string, init?: IRequestInit): Promise<IResponse> {
+		return await FetchHandler.fetch(this.document, url, init);
 	}
 }


Test patch:
diff --git a/packages/happy-dom/test/fetch/ResourceFetcher.test.ts b/packages/happy-dom/test/fetch/ResourceFetchHandler.test.ts
similarity index 71%
rename from packages/happy-dom/test/fetch/ResourceFetcher.test.ts
rename to packages/happy-dom/test/fetch/ResourceFetchHandler.test.ts
index 19cafde8..110a5c2a 100644
--- a/packages/happy-dom/test/fetch/ResourceFetcher.test.ts
+++ b/packages/happy-dom/test/fetch/ResourceFetchHandler.test.ts
@@ -1,9 +1,12 @@
 import Window from '../../src/window/Window';
-import ResourceFetcher from '../../src/fetch/ResourceFetcher';
-import IResponse from '../../src/window/IResponse';
+import IWindow from '../../src/window/IWindow';
+import IDocument from '../../src/nodes/document/IDocument';
+import ResourceFetchHandler from '../../src/fetch/ResourceFetchHandler';
+import IResponse from '../../src/fetch/IResponse';
 
-describe('ResourceFetcher', () => {
-	let window: Window;
+describe('ResourceFetchHandler', () => {
+	let window: IWindow;
+	let document: IDocument;
 	let syncRequestStatusCode;
 	let syncRequestBody;
 	let syncRequestOptions;
@@ -27,6 +30,7 @@ describe('ResourceFetcher', () => {
 		syncRequestStatusCode = 200;
 		syncRequestBody = 'test';
 		window = new Window();
+		document = window.document;
 	});
 
 	afterEach(() => {
@@ -37,7 +41,7 @@ describe('ResourceFetcher', () => {
 		it('Returns resource data asynchrounously.', async () => {
 			let fetchedURL = null;
 
-			jest.spyOn(window, 'fetch').mockImplementation((url) => {
+			jest.spyOn(window, 'fetch').mockImplementation((url: string) => {
 				fetchedURL = url;
 				return Promise.resolve(<IResponse>{
 					text: () => Promise.resolve('test'),
@@ -45,10 +49,7 @@ describe('ResourceFetcher', () => {
 				});
 			});
 
-			const test = await ResourceFetcher.fetch({
-				window,
-				url: 'path/to/script/'
-			});
+			const test = await ResourceFetchHandler.fetch(document, 'path/to/script/');
 
 			expect(fetchedURL).toBe('path/to/script/');
 			expect(test).toBe('test');
@@ -59,10 +60,7 @@ describe('ResourceFetcher', () => {
 		it('Returns resource data synchrounously.', () => {
 			window.location.href = 'https://localhost:8080/base/';
 
-			const test = ResourceFetcher.fetchSync({
-				window,
-				url: 'path/to/script/'
-			});
+			const test = ResourceFetchHandler.fetchSync(document, 'path/to/script/');
 
 			expect(syncRequestOptions).toEqual({
 				method: 'GET',
@@ -77,10 +75,7 @@ describe('ResourceFetcher', () => {
 			syncRequestStatusCode = 404;
 
 			expect(() => {
-				ResourceFetcher.fetchSync({
-					window,
-					url: 'path/to/script/'
-				});
+				ResourceFetchHandler.fetchSync(document, 'path/to/script/');
 			}).toThrowError(
 				'Failed to perform request to "https://localhost:8080/base/path/to/script/". Status code: 404'
 			);
diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts
index 662cd645..5cc68363 100644
--- a/packages/happy-dom/test/nodes/document/Document.test.ts
+++ b/packages/happy-dom/test/nodes/document/Document.test.ts
@@ -22,8 +22,8 @@ import IElement from '../../../src/nodes/element/IElement';
 import INodeList from '../../../src/nodes/node/INodeList';
 import IHTMLElement from '../../../src/nodes/html-element/IHTMLElement';
 import IHTMLLinkElement from '../../../src/nodes/html-link-element/IHTMLLinkElement';
-import IResponse from '../../../src/window/IResponse';
-import ResourceFetcher from '../../../src/fetch/ResourceFetcher';
+import IResponse from '../../../src/fetch/IResponse';
+import ResourceFetchHandler from '../../../src/fetch/ResourceFetchHandler';
 import IHTMLScriptElement from '../../../src/nodes/html-script-element/IHTMLScriptElement';
 import DocumentReadyStateEnum from '../../../src/nodes/document/DocumentReadyStateEnum';
 import ISVGElement from '../../../src/nodes/svg-element/ISVGElement';
@@ -250,14 +250,14 @@ describe('Document', () => {
 			const style = document.createElement('style');
 			const link = <IHTMLLinkElement>document.createElement('link');
 			let fetchedUrl = null;
-			let fetchedOptions = null;
+			let fetchedInit = null;
 
 			link.rel = 'stylesheet';
 			link.href = '/path/to/file.css';
 
-			jest.spyOn(window, 'fetch').mockImplementation((url, options) => {
+			jest.spyOn(window, 'fetch').mockImplementation((url, init) => {
 				fetchedUrl = url;
-				fetchedOptions = options;
+				fetchedInit = init;
 				return <Promise<IResponse>>Promise.resolve({
 					text: () => Promise.resolve('button { background-color: red }'),
 					ok: true
@@ -271,7 +271,7 @@ describe('Document', () => {
 
 			setTimeout(() => {
 				expect(fetchedUrl).toBe('/path/to/file.css');
-				expect(fetchedOptions).toBe(undefined);
+				expect(fetchedInit).toBe(undefined);
 
 				const styleSheets = document.styleSheets;
 
@@ -935,19 +935,25 @@ describe('Document', () => {
 			const jsURL = '/path/to/file.js';
 			const cssResponse = 'body { background-color: red; }';
 			const jsResponse = 'global.test = "test";';
-			let resourceFetchCSSOptions = null;
-			let resourceFetchJSOptions = null;
+			let resourceFetchCSSDocument = null;
+			let resourceFetchCSSURL = null;
+			let resourceFetchJSDocument = null;
+			let resourceFetchJSURL = null;
 			let readyChangeEvent = null;
 
-			jest.spyOn(ResourceFetcher, 'fetch').mockImplementation(async (options) => {
-				if (options.url.endsWith('.css')) {
-					resourceFetchCSSOptions = options;
-					return cssResponse;
-				}
+			jest
+				.spyOn(ResourceFetchHandler, 'fetch')
+				.mockImplementation(async (document: IDocument, url: string) => {
+					if (url.endsWith('.css')) {
+						resourceFetchCSSDocument = document;
+						resourceFetchCSSURL = url;
+						return cssResponse;
+					}
 
-				resourceFetchJSOptions = options;
-				return jsResponse;
-			});
+					resourceFetchJSDocument = document;
+					resourceFetchJSURL = url;
+					return jsResponse;
+				});
 
 			document.addEventListener('readystatechange', (event) => {
 				readyChangeEvent = event;
@@ -967,10 +973,10 @@ describe('Document', () => {
 			expect(document.readyState).toBe(DocumentReadyStateEnum.interactive);
 
 			setTimeout(() => {
-				expect(resourceFetchCSSOptions.window).toBe(window);
-				expect(resourceFetchCSSOptions.url).toBe(cssURL);
-				expect(resourceFetchJSOptions.window).toBe(window);
-				expect(resourceFetchJSOptions.url).toBe(jsURL);
+				expect(resourceFetchCSSDocument).toBe(document);
+				expect(resourceFetchCSSURL).toBe(cssURL);
+				expect(resourceFetchJSDocument).toBe(document);
+				expect(resourceFetchJSURL).toBe(jsURL);
 				expect(readyChangeEvent.target).toBe(document);
 				expect(document.readyState).toBe(DocumentReadyStateEnum.complete);
 				expect(document.styleSheets.length).toBe(1);
diff --git a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
index 89e3d985..5241d5be 100644
--- a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
+++ b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts
@@ -1,11 +1,12 @@
 import Window from '../../../src/window/Window';
-import Document from '../../../src/nodes/document/Document';
+import IWindow from '../../../src/window/IWindow';
+import IDocument from '../../../src/nodes/document/IDocument';
 import IHTMLLinkElement from '../../../src/nodes/html-link-element/IHTMLLinkElement';
-import ResourceFetcher from '../../../src/fetch/ResourceFetcher';
+import ResourceFetchHandler from '../../../src/fetch/ResourceFetchHandler';
 
 describe('HTMLLinkElement', () => {
-	let window: Window;
-	let document: Document;
+	let window: IWindow;
+	let document: IDocument;
 
 	beforeEach(() => {
 		window = new Window();
@@ -69,13 +70,17 @@ describe('HTMLLinkElement', () => {
 		it('Loads and evaluates an external CSS file when the attribute "href" and "rel" is set and the element is connected to DOM.', (done) => {
 			const element = <IHTMLLinkElement>document.createElement('link');
 			const css = 'div { background: red; }';
-			let loadedOptions = null;
+			let loadedDocument = null;
+			let loadedURL = null;
 			let loadEvent = null;
 
-			jest.spyOn(ResourceFetcher, 'fetch').mockImplementation(async (options) => {
-				loadedOptions = options;
-				return css;
-			});
+			jest
+				.spyOn(ResourceFetchHandler, 'fetch')
+				.mockImplementation(async (document: IDocument, url: string) => {
+					loadedDocument = document;
+					loadedURL = url;
+					return css;
+				});
 
 			document.body.appendChild(element);
 
@@ -87,9 +92,8 @@ describe('HTMLLinkElement', () => {
 			element.href = 'test';
 
 			setTimeout(() => {
-				expect(Object.keys(loadedOptions).length).toBe(2);
-				expect(loadedOptions.window).toBe(window);
-				expect(loadedOptions.url).toBe('test');
+				expect(loadedDocument).toBe(document);
+				expect(loadedURL).toBe('test');
 				expect(element.sheet.cssRules.length).toBe(1);
 				expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }');
 				expect(loadEvent.target).toBe(element);
@@ -102,7 +106,7 @@ describe('HTMLLinkElement', () => {
 			const thrownError = new Error('error');
 			let errorEvent = null;
 
-			jest.spyOn(ResourceFetcher, 'fetch').mockImplementation(async () => {
+			jest.spyOn(ResourceFetchHandler, 'fetch').mockImplementation(async () => {
 				throw thrownError;
 			});
 
@@ -125,17 +129,22 @@ describe('HTMLLinkElement', () => {
 		it('Does not load and evaluate external CSS files if the element is not connected to DOM.', () => {
 			const element = <IHTMLLinkElement>document.createElement('link');
 			const css = 'div { background: red; }';
-			let loadedOptions = null;
+			let loadedDocument = null;
+			let loadedURL = null;
 
-			jest.spyOn(ResourceFetcher, 'fetch').mockImplementation(async (options) => {
-				loadedOptions = options;
-				return css;
-			});
+			jest
+				.spyOn(ResourceFetchHandler, 'fetch')
+				.mockImplementation(async (document: IDocument, url: string) => {
+					loadedDocument = document;
+					loadedURL = url;
+					return css;
+				});
 
 			element.rel = 'stylesheet';
 			element.href = 'test';
 
-			expect(loadedOptions).toBe(null);
+			expect(loadedDocument).toBe(null);
+			expect(loadedURL).toBe(null);
 		});
 	});
 
@@ -144,12 +153,16 @@ describe('HTMLLinkElement', () => {
 			const element = <IHTMLLinkElement>document.createElement('link');
 			const css = 'div { background: red; }';
 			let loadEvent = null;
-			let loadedOptions = null;
+			let loadedDocument = null;
+			let loadedURL = null;
 
-			jest.spyOn(ResourceFetcher, 'fetch').mockImplementation(async (options) => {
-				loadedOptions = options;
-				return css;
-			});
+			jest
+				.spyOn(ResourceFetchHandler, 'fetch')
+				.mockImplementation(async (document: IDocument, url: string) => {
+					loadedDocument = document;
+					loadedURL = url;
+					return css;
+				});
 
 			element.rel = 'stylesheet';
 			element.href = 'test';
@@ -160,9 +173,8 @@ describe('HTMLLinkElement', () => {
 			document.body.appendChild(element);
 
 			setTimeout(() => {
-				expect(Object.keys(loadedOptions).length).toBe(2);
-				expect(loadedOptions.window).toBe(window);
-				expect(loadedOptions.url).toBe('test');
+				expect(loadedDocument).toBe(document);
+				expect(loadedURL).toBe('test');
 				expect(element.sheet.cssRules.length).toBe(1);
 				expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }');
 				expect(loadEvent.target).toBe(element);
@@ -175,7 +187,7 @@ describe('HTMLLinkElement', () => {
 			const thrownError = new Error('error');
 			let errorEvent = null;
 
-			jest.spyOn(ResourceFetcher, 'fetch').mockImplementation(async () => {
+			jest.spyOn(ResourceFetchHandler, 'fetch').mockImplementation(async () => {
 				throw thrownError;
 			});
 
@@ -197,17 +209,22 @@ describe('HTMLLinkElement', () => {
 		it('Does not load external scripts when "href" attribute has been set if the element is not connected to DOM.', () => {
 			const element = <IHTMLLinkElement>document.createElement('link');
 			const css = 'div { background: red; }';
-			let loadedOptions = null;
+			let loadedDocument = null;
+			let loadedURL = null;
 
-			jest.spyOn(ResourceFetcher, 'fetch').mockImplementation(async (options) => {
-				loadedOptions = options;
-				return css;
-			});
+			jest
+				.spyOn(ResourceFetchHandler, 'fetch')
+				.mockImplementation(async (document: IDocument, url: string) => {
+					loadedDocument = document;
+					loadedURL = url;
+					return css;
+				});
 
 			element.rel = 'stylesheet';
 			element.href = 'test';
 
-			expect(loadedOptions).toBe(null);
+			expect(loadedDocument).toBe(null);
+			expect(loadedURL).toBe(null);
 			expect(element.sheet).toBe(null);
 		});
 	});
diff --git a/packages/happy-dom/test/nodes/html-script-element/ScriptUtility.test.ts b/packages/happy-dom/test/nodes/html-script-element/ScriptUtility.test.ts
index 5ad56d75..c2ddf72d 100644
--- a/packages/happy-dom/test/nodes/html-script-element/ScriptUtility.test.ts
+++ b/packages/happy-dom/test/nodes/html-script-element/ScriptUtility.test.ts
@@ -1,14 +1,18 @@
 import Window from '../../../src/window/Window';
+import IWindow from '../../../src/window/IWindow';
+import IDocument from '../../../src/nodes/document/IDocument';
 import ScriptUtility from '../../../src/nodes/html-script-element/ScriptUtility';
-import IResponse from '../../../src/window/IResponse';
+import IResponse from '../../../src/fetch/IResponse';
 import HTMLScriptElement from '../../../src/nodes/html-script-element/HTMLScriptElement';
-import ResourceFetcher from '../../../src/fetch/ResourceFetcher';
+import ResourceFetchHandler from '../../../src/fetch/ResourceFetchHandler';
 
 describe('ScriptUtility', () => {
-	let window: Window;
+	let window: IWindow;
+	let document: IDocument;
 
 	beforeEach(() => {
 		window = new Window();
+		document = window.document;
 	});
 
 	afterEach(() => {
@@ -20,7 +24,7 @@ describe('ScriptUtility', () => {
 			let fetchedURL = null;
 			let loadEvent = null;
 
-			jest.spyOn(window, 'fetch').mockImplementation((url) => {
+			jest.spyOn(window, 'fetch').mockImplementation((url: string) => {
 				fetchedURL = url;
 				return Promise.resolve(<IResponse>{
 					text: () => Promise.resolve('global.test = "test";'),
@@ -70,15 +74,19 @@ describe('ScriptUtility', () => {
 		});
 
 		it('Loads external script synchronously with relative URL.', async () => {
-			let fetchedOptions = null;
+			let fetchedDocument = null;
+			let fetchedURL = null;
 			let loadEvent = null;
 
 			window.location.href = 'https://localhost:8080/base/';
 
-			jest.spyOn(ResourceFetcher, 'fetchSync').mockImplementation((options) => {
-				fetchedOptions = options;
-				return 'global.test = "test";';
-			});
+			jest
+				.spyOn(ResourceFetchHandler, 'fetchSync')
+				.mockImplementation((document: IDocument, url: string) => {
+					fetchedDocument = document;
+					fetchedURL = url;
+					return 'global.test = "test";';
+				});
 
 			const script = <HTMLScriptElement>window.document.createElement('script');
 			script.src = 'path/to/script/';
@@ -89,10 +97,8 @@ describe('ScriptUtility', () => {
 			await ScriptUtility.loadExternalScript(script);
 
 			expect(loadEvent.target).toBe(script);
-			expect(fetchedOptions).toEqual({
-				window,
-				url: 'path/to/script/'
-			});
+			expect(fetchedDocument).toBe(document);
+			expect(fetchedURL).toBe('path/to/script/');
 			expect(global['test']).toBe('test');
 
 			delete global['test'];
@@ -104,7 +110,7 @@ describe('ScriptUtility', () => {
 
 			window.location.href = 'https://localhost:8080/base/';
 
-			jest.spyOn(ResourceFetcher, 'fetchSync').mockImplementation(() => {
+			jest.spyOn(ResourceFetchHandler, 'fetchSync').mockImplementation(() => {
 				throw thrownError;
 			});
 
diff --git a/packages/happy-dom/test/url-search-params/URLSearchParams.test.ts b/packages/happy-dom/test/url-search-params/URLSearchParams.test.ts
deleted file mode 100644
index 46415cd2..00000000
--- a/packages/happy-dom/test/url-search-params/URLSearchParams.test.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import URLSearchParams from '../../src/url-search-params/URLSearchParams';
-
-describe('URLSearchParams', () => {
-	describe('constructor()', () => {
-		it('Applies params sent to the constructor.', () => {
-			const urlSearchParams = new URLSearchParams('key1=value1&key2=value2');
-			expect(Array.from(urlSearchParams.entries())).toEqual([
-				['key1', 'value1'],
-				['key2', 'value2']
-			]);
-		});
-
-		it('Params are optional.', () => {
-			const urlSearchParams = new URLSearchParams();
-			expect(Array.from(urlSearchParams.entries())).toEqual([]);
-		});
-	});
-
-	describe('append()', () => {
-		it('Appends params.', () => {
-			const urlSearchParams = new URLSearchParams('key1=value1&key2=value2');
-			urlSearchParams.append('key3', 'value3');
-			expect(Array.from(urlSearchParams.entries())).toEqual([
-				['key1', 'value1'],
-				['key2', 'value2'],
-				['key3', 'value3']
-			]);
-		});
-	});
-
-	describe('delete()', () => {
-		it('Removes all params matching a name.', () => {
-			const urlSearchParams = new URLSearchParams('key1=value1&key1=value1&key2=value2');
-			urlSearchParams.delete('key1');
-			expect(Array.from(urlSearchParams.entries())).toEqual([['key2', 'value2']]);
-		});
-	});
-
-	describe('entries()', () => {
-		it('Returns an iterator with key and value.', () => {
-			const urlSearchParams = new URLSearchParams('key1=value1&key2=value2');
-			expect(Array.from(urlSearchParams.entries())).toEqual([
-				['key1', 'value1'],
-				['key2', 'value2']
-			]);
-		});
-	});
-
-	describe('forEach()', () => {
-		it('Calls a callback function for each param.', () => {
-			const urlSearchParams = new URLSearchParams('key1=value1&key2=value2');
-			const entries = [];
-			urlSearchParams.forEach((name, value) => entries.push([name, value]));
-			expect(entries).toEqual([
-				['key1', 'value1'],
-				['key2', 'value2']
-			]);
-		});
-	});
-
-	describe('get()', () => {
-		it('Returns the value of the first matching param.', () => {
-			const urlSearchParams = new URLSearchParams('key1=value1&key2=value2');
-			expect(urlSearchParams.get('key2')).toBe('value2');
-		});
-	});
-
-	describe('getAll()', () => {
-		it('Returns all matching values of a param.', () => {
-			const urlSearchParams = new URLSearchParams('key1=value1&key2=value2&key2=value3');
-			expect(urlSearchParams.getAll('key2')).toEqual(['value2', 'value3']);
-		});
-	});
-
-	describe('has()', () => {
-		it('Returns "true" if a param exists.', () => {
-			const urlSearchParams = new URLSearchParams('key1=value1&key2=value2');
-			expect(urlSearchParams.has('key2')).toBe(true);
-		});
-	});
-
-	describe('keys()', () => {
-		it('Returns an iterator with keys.', () => {
-			const urlSearchParams = new URLSearchParams('key1=value1&key2=value2');
-			expect(Array.from(urlSearchParams.keys())).toEqual(['key1', 'key2']);
-		});
-	});
-
-	describe('set()', () => {
-		it('Sets a value and removes all previous params matching the name.', () => {
-			const urlSearchParams = new URLSearchParams('key1=value1&key2=value2&key2=value3');
-			urlSearchParams.set('key2', 'test');
-			expect(Array.from(urlSearchParams.entries())).toEqual([
-				['key1', 'value1'],
-				['key2', 'test']
-			]);
-		});
-	});
-
-	describe('sort()', () => {
-		it('Sorts the param list.', () => {
-			const urlSearchParams = new URLSearchParams('key2=value2&key3=value3&key1=value1');
-			urlSearchParams.sort();
-			expect(Array.from(urlSearchParams.entries())).toEqual([
-				['key1', 'value1'],
-				['key2', 'value2'],
-				['key3', 'value3']
-			]);
-		});
-	});
-
-	describe('toString()', () => {
-		it('Returns query string.', () => {
-			const urlSearchParams = new URLSearchParams('key2=value2&key3=value3&key1=value1');
-			expect(urlSearchParams.toString()).toBe('key2=value2&key3=value3&key1=value1');
-		});
-	});
-
-	describe('values()', () => {
-		it('Returns an iterator with values.', () => {
-			const urlSearchParams = new URLSearchParams('key1=value1&key2=value2');
-			expect(Array.from(urlSearchParams.values())).toEqual(['value1', 'value2']);
-		});
-	});
-});
diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts
index 5dd7c86b..3b5a61f5 100644
--- a/packages/happy-dom/test/window/Window.test.ts
+++ b/packages/happy-dom/test/window/Window.test.ts
@@ -1,40 +1,76 @@
-import { Document, HTMLElement, IHTMLLinkElement } from '../../src';
+import { Document, HTMLElement, IDocument, IHTMLLinkElement } from '../../src';
 import CSSStyleDeclaration from '../../src/css/CSSStyleDeclaration';
-import ResourceFetcher from '../../src/fetch/ResourceFetcher';
+import ResourceFetchHandler from '../../src/fetch/ResourceFetchHandler';
 import IHTMLScriptElement from '../../src/nodes/html-script-element/IHTMLScriptElement';
-import IFetchOptions from '../../src/window/IFetchOptions';
-import IResponse from '../../src/window/IResponse';
+import IRequestInit from '../../src/fetch/IRequestInit';
 import Window from '../../src/window/Window';
 import Navigator from '../../src/navigator/Navigator';
 import VM from 'vm';
 
+const MOCKED_RESPONSES = {
+	arrayBuffer: Symbol('arrayBuffer'),
+	blob: Symbol('blob'),
+	buffer: Symbol('buffer'),
+	json: Symbol('json'),
+	text: Symbol('text'),
+	textConverted: Symbol('textConverted')
+};
+
+/* eslint-disable jsdoc/require-jsdoc */
+class NodeFetchResponse {
+	public arrayBuffer(): Promise<symbol> {
+		return Promise.resolve(MOCKED_RESPONSES.arrayBuffer);
+	}
+	public blob(): Promise<symbol> {
+		return Promise.resolve(MOCKED_RESPONSES.blob);
+	}
+	public buffer(): Promise<symbol> {
+		return Promise.resolve(MOCKED_RESPONSES.buffer);
+	}
+	public json(): Promise<symbol> {
+		return Promise.resolve(MOCKED_RESPONSES.json);
+	}
+	public text(): Promise<symbol> {
+		return Promise.resolve(MOCKED_RESPONSES.text);
+	}
+	public textConverted(): Promise<symbol> {
+		return Promise.resolve(MOCKED_RESPONSES.textConverted);
+	}
+}
+
+class NodeFetchRequest extends NodeFetchResponse {}
+class NodeFetchHeaders {}
+
 describe('Window', () => {
 	let window: Window;
 	let document: Document;
 	let fetchedUrl: string;
-	let fetchedOptions: IFetchOptions;
-	let fetchResponseBody: string;
+	let fetchedInit: IRequestInit;
 	let fetchError: Error;
 
 	beforeAll(() => {
-		jest.mock('node-fetch', () => (url: string, options: IFetchOptions) => {
-			fetchedUrl = url;
-			fetchedOptions = options;
-			if (fetchError) {
-				return Promise.reject(fetchError);
-			}
-			return <Promise<IResponse>>Promise.resolve({
-				text: () => Promise.resolve(fetchResponseBody),
-				json: () => Promise.resolve(JSON.parse(fetchResponseBody)),
-				blob: () => Promise.resolve(null)
-			});
-		});
+		jest.mock('node-fetch', () =>
+			Object.assign(
+				(url: string, options: IRequestInit) => {
+					fetchedUrl = url;
+					fetchedInit = options;
+					if (fetchError) {
+						return Promise.reject(fetchError);
+					}
+					return Promise.resolve(new NodeFetchResponse());
+				},
+				{
+					Response: NodeFetchResponse,
+					Request: NodeFetchRequest,
+					Headers: NodeFetchHeaders
+				}
+			)
+		);
 	});
 
 	beforeEach(() => {
 		fetchedUrl = null;
-		fetchedOptions = null;
-		fetchResponseBody = null;
+		fetchedInit = null;
 		fetchError = null;
 		window = new Window();
 		document = window.document;
@@ -62,6 +98,51 @@ describe('Window', () => {
 		});
 	});
 
+	describe('get Array()', () => {
+		it('Is the same as [].constructor.', () => {
+			expect((() => {}).constructor).toBe(window.Function);
+
+			const context = VM.createContext(new Window());
+			expect(context.eval('[].constructor === window.Array')).toBe(true);
+		});
+	});
+
+	describe('get Headers()', () => {
+		it('Returns Headers class.', () => {
+			expect(window.Headers).toBe(require('../../src/fetch/Headers').default);
+		});
+	});
+
+	describe('get Response()', () => {
+		it('Returns Response class.', () => {
+			expect(window.Response['_ownerDocument']).toBe(document);
+			expect(window.Response).toBe(require('../../src/fetch/Response').default);
+		});
+
+		for (const method of ['arrayBuffer', 'blob', 'buffer', 'json', 'text', 'textConverted']) {
+			it(`Handles the "${method}" method with the async task manager.`, async () => {
+				const response = new window.Response();
+				const result = await response[method]();
+				expect(result).toBe(MOCKED_RESPONSES[method]);
+			});
+		}
+	});
+
+	describe('get Request()', () => {
+		it('Returns Request class.', () => {
+			expect(window.Request['_ownerDocument']).toBe(document);
+			expect(window.Request).toBe(require('../../src/fetch/Request').default);
+		});
+
+		for (const method of ['arrayBuffer', 'blob', 'buffer', 'json', 'text', 'textConverted']) {
+			it(`Handles the "${method}" method with the async task manager.`, async () => {
+				const request = new window.Request('test');
+				const result = await request[method]();
+				expect(result).toBe(MOCKED_RESPONSES[method]);
+			});
+		}
+	});
+
 	describe('get performance()', () => {
 		it('Exposes "performance" from the NodeJS perf_hooks package.', () => {
 			expect(typeof window.performance.now()).toBe('number');
@@ -194,33 +275,19 @@ describe('Window', () => {
 	});
 
 	describe('fetch()', () => {
-		it('Handles successful JSON request.', async () => {
-			const expectedUrl = 'https://localhost:8080/path/';
-			const expectedOptions = {};
+		for (const method of ['arrayBuffer', 'blob', 'buffer', 'json', 'text', 'textConverted']) {
+			it(`Handles successful "${method}" request.`, async () => {
+				const expectedUrl = 'https://localhost:8080/path/';
+				const expectedOptions = {};
 
-			fetchResponseBody = '{}';
-
-			const response = await window.fetch(expectedUrl, expectedOptions);
-			const jsonResponse = await response.json();
-
-			expect(fetchedUrl).toBe(expectedUrl);
-			expect(fetchedOptions).toBe(expectedOptions);
-			expect(jsonResponse).toEqual({});
-		});
+				const response = await window.fetch(expectedUrl, expectedOptions);
+				const result = await response[method]();
 
-		it('Handles successful Text request.', async () => {
-			const expectedUrl = 'https://localhost:8080/path/';
-			const expectedOptions = {};
-
-			fetchResponseBody = 'text';
-
-			const response = await window.fetch(expectedUrl, expectedOptions);
-			const textResponse = await response.text();
-
-			expect(fetchedUrl).toBe(expectedUrl);
-			expect(fetchedOptions).toBe(expectedOptions);
-			expect(textResponse).toEqual(fetchResponseBody);
-		});
+				expect(fetchedUrl).toBe(expectedUrl);
+				expect(fetchedInit).toBe(expectedOptions);
+				expect(result).toEqual(MOCKED_RESPONSES[method]);
+			});
+		}
 
 		it('Handles relative URL.', async () => {
 			const expectedPath = '/path/';
@@ -228,14 +295,12 @@ describe('Window', () => {
 
 			window.location.href = 'https://localhost:8080';
 
-			fetchResponseBody = 'text';
-
 			const response = await window.fetch(expectedPath, expectedOptions);
 			const textResponse = await response.text();
 
 			expect(fetchedUrl).toBe('https://localhost:8080' + expectedPath);
-			expect(fetchedOptions).toBe(expectedOptions);
-			expect(textResponse).toEqual(fetchResponseBody);
+			expect(fetchedInit).toBe(expectedOptions);
+			expect(textResponse).toEqual(MOCKED_RESPONSES.text);
 		});
 
 		it('Handles error JSON request.', async () => {
@@ -409,19 +474,25 @@ describe('Window', () => {
 			const jsURL = '/path/to/file.js';
 			const cssResponse = 'body { background-color: red; }';
 			const jsResponse = 'global.test = "test";';
-			let resourceFetchCSSOptions = null;
-			let resourceFetchJSOptions = null;
+			let resourceFetchCSSDocument = null;
+			let resourceFetchCSSURL = null;
+			let resourceFetchJSDocument = null;
+			let resourceFetchJSURL = null;
 			let loadEvent = null;
 
-			jest.spyOn(ResourceFetcher, 'fetch').mockImplementation(async (options) => {
-				if (options.url.endsWith('.css')) {
-					resourceFetchCSSOptions = options;
-					return cssResponse;
-				}
+			jest
+				.spyOn(ResourceFetchHandler, 'fetch')
+				.mockImplementation(async (document: IDocument, url: string) => {
+					if (url.endsWith('.css')) {
+						resourceFetchCSSDocument = document;
+						resourceFetchCSSURL = url;
+						return cssResponse;
+					}
 
-				resourceFetchJSOptions = options;
-				return jsResponse;
-			});
+					resourceFetchJSDocument = document;
+					resourceFetchJSURL = url;
+					return jsResponse;
+				});
 
 			window.addEventListener('load', (event) => {
 				loadEvent = event;
@@ -439,10 +510,10 @@ describe('Window', () => {
 			document.body.appendChild(link);
 
 			setTimeout(() => {
-				expect(resourceFetchCSSOptions.window).toBe(window);
-				expect(resourceFetchCSSOptions.url).toBe(cssURL);
-				expect(resourceFetchJSOptions.window).toBe(window);
-				expect(resourceFetchJSOptions.url).toBe(jsURL);
+				expect(resourceFetchCSSDocument).toBe(document);
+				expect(resourceFetchCSSURL).toBe(cssURL);
+				expect(resourceFetchJSDocument).toBe(document);
+				expect(resourceFetchJSURL).toBe(jsURL);
 				expect(loadEvent.target).toBe(window);
 				expect(document.styleSheets.length).toBe(1);
 				expect(document.styleSheets[0].cssRules[0].cssText).toBe(cssResponse);
@@ -460,9 +531,11 @@ describe('Window', () => {
 			const jsURL = '/path/to/file.js';
 			const errorEvents = [];
 
-			jest.spyOn(ResourceFetcher, 'fetch').mockImplementation(async (options) => {
-				throw new Error(options.url);
-			});
+			jest
+				.spyOn(ResourceFetchHandler, 'fetch')
+				.mockImplementation(async (_document: IDocument, url: string) => {
+					throw new Error(url);
+				});
 
 			window.addEventListener('error', (event) => {
 				errorEvents.push(event);