Repository: capricorn86/happy-dom
Pull request #295 (2021-10-05 14:58:32)

#291@minor: Adds support for URLSearchParams.


Patch:
diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts
index 25df65b9..99379c3e 100644
--- a/packages/happy-dom/src/index.ts
+++ b/packages/happy-dom/src/index.ts
@@ -97,6 +97,7 @@ import SelfClosingElements from './config/SelfClosingElements';
 import UnclosedElements from './config/UnclosedElements';
 import CSSStyleSheet from './css/CSSStyleSheet';
 import Storage from './storage/Storage';
+import URLSearchParams from './url-search-params/URLSearchParams';
 
 export {
 	AsyncWindow,
@@ -197,5 +198,6 @@ export {
 	SelfClosingElements,
 	UnclosedElements,
 	CSSStyleSheet,
-	Storage
+	Storage,
+	URLSearchParams
 };
diff --git a/packages/happy-dom/src/url-search-params/URLSearchParams.ts b/packages/happy-dom/src/url-search-params/URLSearchParams.ts
new file mode 100644
index 00000000..08df8735
--- /dev/null
+++ b/packages/happy-dom/src/url-search-params/URLSearchParams.ts
@@ -0,0 +1,198 @@
+/**
+ * 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/Window.ts b/packages/happy-dom/src/window/Window.ts
index 31fc9eb1..48d8ad8d 100644
--- a/packages/happy-dom/src/window/Window.ts
+++ b/packages/happy-dom/src/window/Window.ts
@@ -60,6 +60,7 @@ import HTMLLinkElement from '../nodes/html-link-element/HTMLLinkElement';
 import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement';
 import IFetchOptions from './IFetchOptions';
 import IWindow from './IWindow';
+import URLSearchParams from '../url-search-params/URLSearchParams';
 
 const FETCH_RESPONSE_TYPE_METHODS = ['blob', 'json', 'text'];
 
@@ -134,6 +135,7 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 	public readonly History = History;
 	public readonly Screen = Screen;
 	public readonly Storage = Storage;
+	public readonly URLSearchParams = URLSearchParams;
 
 	// Events
 	public onload: (event: Event) => void = null;


Test patch:
diff --git a/packages/happy-dom/test/url-search-params/URLSearchParams.test.ts b/packages/happy-dom/test/url-search-params/URLSearchParams.test.ts
new file mode 100644
index 00000000..46415cd2
--- /dev/null
+++ b/packages/happy-dom/test/url-search-params/URLSearchParams.test.ts
@@ -0,0 +1,125 @@
+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']);
+		});
+	});
+});