Repository: capricorn86/happy-dom
Pull request #379 (2022-02-22 17:57:24)

#370@minor: Adds basic support for Document.getSelection() and the Se…
…lection interface.


Patch:
diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts
index aee454fe..c82fa83a 100644
--- a/packages/happy-dom/src/index.ts
+++ b/packages/happy-dom/src/index.ts
@@ -106,6 +106,7 @@ import ChildLessElements from './config/ChildLessElements';
 import CSSStyleSheet from './css/CSSStyleSheet';
 import Storage from './storage/Storage';
 import URLSearchParams from './url-search-params/URLSearchParams';
+import Selection from './selection/Selection';
 
 export {
 	AsyncWindow,
@@ -215,5 +216,6 @@ export {
 	ChildLessElements,
 	CSSStyleSheet,
 	Storage,
-	URLSearchParams
+	URLSearchParams,
+	Selection
 };
diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts
index bc58d803..0bfa4e3e 100644
--- a/packages/happy-dom/src/nodes/document/Document.ts
+++ b/packages/happy-dom/src/nodes/document/Document.ts
@@ -35,6 +35,7 @@ import IHTMLStyleElement from '../html-style-element/IHTMLStyleElement';
 import DocumentReadyStateEnum from './DocumentReadyStateEnum';
 import DocumentReadyStateManager from './DocumentReadyStateManager';
 import Location from '../../location/Location';
+import Selection from '../../selection/Selection';
 
 /**
  * Document.
@@ -699,6 +700,15 @@ export default class Document extends Node implements IDocument {
 		return adopted;
 	}
 
+	/**
+	 * Returns selection.
+	 *
+	 * @returns Selection.
+	 */
+	public getSelection(): Selection {
+		return new Selection();
+	}
+
 	/**
 	 * @override
 	 */
diff --git a/packages/happy-dom/src/nodes/document/IDocument.ts b/packages/happy-dom/src/nodes/document/IDocument.ts
index da4dbbbe..8c8b3098 100644
--- a/packages/happy-dom/src/nodes/document/IDocument.ts
+++ b/packages/happy-dom/src/nodes/document/IDocument.ts
@@ -11,6 +11,7 @@ import IParentNode from '../parent-node/IParentNode';
 import INode from '../node/INode';
 import ICharacterData from '../character-data/ICharacterData';
 import IDocumentFragment from '../document-fragment/IDocumentFragment';
+import Selection from '../../selection/Selection';
 
 /**
  * Document.
@@ -152,4 +153,11 @@ export default interface IDocument extends IParentNode {
 	 * @returns Adopted node.
 	 */
 	adoptNode(node: INode): INode;
+
+	/**
+	 * Returns selection.
+	 *
+	 * @returns Selection.
+	 */
+	getSelection(): Selection;
 }
diff --git a/packages/happy-dom/src/selection/Selection.ts b/packages/happy-dom/src/selection/Selection.ts
new file mode 100644
index 00000000..dbc5e89b
--- /dev/null
+++ b/packages/happy-dom/src/selection/Selection.ts
@@ -0,0 +1,140 @@
+import INode from '../nodes/node/INode';
+
+/**
+ * Selection.
+ *
+ * Reference:
+ * https://developer.mozilla.org/en-US/docs/Web/API/Selection.
+ */
+export default class Selection {
+	public readonly anchorNode: INode = null;
+	public readonly anchorOffset: number = 0;
+	public readonly baseNode: INode = null;
+	public readonly baseOffset: number = 0;
+	public readonly extentNode: INode = null;
+	public readonly extentOffset: number = 0;
+	public readonly focusNode: INode = null;
+	public readonly focusOffset: number = 0;
+	public readonly isCollapsed: boolean = true;
+	public readonly rangeCount: number = 0;
+	public readonly type: string = 'None';
+
+	/**
+	 * Adds a range.
+	 *
+	 * @param _range Range.
+	 */
+	public addRange(_range: object): void {
+		// Do nothing.
+	}
+
+	/**
+	 * Collapses the current selection to a single point.
+	 *
+	 * @param _node Node.
+	 * @param _offset Offset.
+	 */
+	public collapse(_node: INode, _offset?: number): void {
+		// Do nothing.
+	}
+
+	/**
+	 * Collapses the selection to the end.
+	 */
+	public collapseToEnd(): void {
+		// Do nothing.
+	}
+
+	/**
+	 * Collapses the selection to the start.
+	 */
+	public collapseToStart(): void {
+		// Do nothing.
+	}
+
+	/**
+	 * Indicates whether a specified node is part of the selection.
+	 *
+	 * @param _node Node.
+	 * @param _partialContainer Partial container.
+	 * @returns Always returns "true" for now.
+	 */
+	public containsNode(_node: INode, _partialContainer?: INode): boolean {
+		return true;
+	}
+
+	/**
+	 * Deletes the selected text from the document's DOM.
+	 */
+	public deleteFromDocument(): void {
+		// Do nothing.
+	}
+
+	/**
+	 * Moves the focus of the selection to a specified point.
+	 *
+	 * @param _node Node.
+	 * @param _offset Offset.
+	 */
+	public extend(_node: INode, _offset?: number): void {
+		// Do nothing.
+	}
+
+	/**
+	 * Moves the focus of the selection to a specified point.
+	 *
+	 * @param _index Index.
+	 */
+	public getRangeAt(_index: number): object {
+		throw new Error('Not a valid index.');
+	}
+
+	/**
+	 * Removes a range from a selection.
+	 *
+	 * @param _range Range.
+	 */
+	public removeRange(_range: object): void {
+		// Do nothing.
+	}
+
+	/**
+	 * Removes all ranges.
+	 */
+	public removeAllRanges(): void {
+		// Do nothing.
+	}
+
+	/**
+	 * Selects all children.
+	 *
+	 * @param _parentNode Parent node.
+	 */
+	public selectAllChildren(_parentNode: INode): void {
+		// Do nothing.
+	}
+
+	/**
+	 * Sets the selection to be a range including all or parts of two specified DOM nodes, and any content located between them.
+	 *
+	 * @param _anchorNode Anchor node.
+	 * @param _anchorOffset Anchor offset.
+	 * @param _focusNode Focus node.
+	 * @param _focusOffset Focus offset.
+	 */
+	public setBaseAndExtent(
+		_anchorNode: INode,
+		_anchorOffset: number,
+		_focusNode: INode,
+		_focusOffset: number
+	): void {
+		// Do nothing.
+	}
+
+	/**
+	 * Returns string currently being represented by the selection object.
+	 */
+	public toString(): string {
+		return '';
+	}
+}
diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts
index 472289a4..ed6eeedc 100644
--- a/packages/happy-dom/src/window/IWindow.ts
+++ b/packages/happy-dom/src/window/IWindow.ts
@@ -68,6 +68,7 @@ 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';
 
 /**
  * Window.
@@ -148,6 +149,7 @@ export default interface IWindow {
 	readonly NodeList: typeof NodeList;
 	readonly CSSUnitValue: typeof CSSUnitValue;
 	readonly CSS: CSS;
+	readonly Selection: typeof Selection;
 
 	// Events
 	onload: (event: Event) => void;
diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts
index a2266de3..6a330935 100644
--- a/packages/happy-dom/src/window/Window.ts
+++ b/packages/happy-dom/src/window/Window.ts
@@ -73,6 +73,7 @@ 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';
+import Selection from '../selection/Selection';
 import * as PerfHooks from 'perf_hooks';
 
 const FETCH_RESPONSE_TYPE_METHODS = ['blob', 'json', 'text'];
@@ -160,6 +161,7 @@ export default class Window extends EventTarget implements IWindow, NodeJS.Globa
 	public readonly NodeList = NodeList;
 	public readonly MediaQueryList = MediaQueryList;
 	public readonly CSSUnitValue = CSSUnitValue;
+	public readonly Selection = Selection;
 
 	// Events
 	public onload: (event: Event) => void = null;


Test patch:
diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts
index 33dcc098..7893cbb4 100644
--- a/packages/happy-dom/test/nodes/document/Document.test.ts
+++ b/packages/happy-dom/test/nodes/document/Document.test.ts
@@ -27,6 +27,7 @@ import IHTMLScriptElement from '../../../src/nodes/html-script-element/IHTMLScri
 import DocumentReadyStateEnum from '../../../src/nodes/document/DocumentReadyStateEnum';
 import ISVGElement from '../../../src/nodes/svg-element/ISVGElement';
 import CustomEvent from '../../../src/event/events/CustomEvent';
+import Selection from '../../../src/selection/Selection';
 
 describe('Document', () => {
 	let window: Window;
@@ -939,6 +940,12 @@ describe('Document', () => {
 		});
 	});
 
+	describe('getSelection()', () => {
+		it('Returns selection.', () => {
+			expect(document.getSelection() instanceof Selection).toBe(true);
+		});
+	});
+
 	describe('dispatchEvent()', () => {
 		it('Bubbles events to Window.', () => {
 			const event = new Event('click', { bubbles: true });
diff --git a/packages/happy-dom/test/selection/Selection.test.ts b/packages/happy-dom/test/selection/Selection.test.ts
new file mode 100644
index 00000000..8b408da4
--- /dev/null
+++ b/packages/happy-dom/test/selection/Selection.test.ts
@@ -0,0 +1,97 @@
+import Selection from '../../src/selection/Selection';
+
+describe('History', () => {
+	let selection: Selection;
+
+	beforeEach(() => {
+		selection = new Selection();
+	});
+
+	describe('get anchorNode()', () => {
+		it('Returns "null".', () => {
+			expect(selection.anchorNode).toBe(null);
+		});
+	});
+
+	describe('get anchorOffset()', () => {
+		it('Returns "0".', () => {
+			expect(selection.anchorOffset).toBe(0);
+		});
+	});
+
+	describe('get baseNode()', () => {
+		it('Returns "null".', () => {
+			expect(selection.baseNode).toBe(null);
+		});
+	});
+
+	describe('get baseOffset()', () => {
+		it('Returns "0".', () => {
+			expect(selection.baseOffset).toBe(0);
+		});
+	});
+
+	describe('get extentNode()', () => {
+		it('Returns "null".', () => {
+			expect(selection.extentNode).toBe(null);
+		});
+	});
+
+	describe('get extentOffset()', () => {
+		it('Returns "0".', () => {
+			expect(selection.extentOffset).toBe(0);
+		});
+	});
+
+	describe('get focusNode()', () => {
+		it('Returns "null".', () => {
+			expect(selection.focusNode).toBe(null);
+		});
+	});
+
+	describe('get focusOffset()', () => {
+		it('Returns "0".', () => {
+			expect(selection.focusOffset).toBe(0);
+		});
+	});
+
+	describe('get isCollapsed()', () => {
+		it('Returns "true".', () => {
+			expect(selection.isCollapsed).toBe(true);
+		});
+	});
+
+	describe('get rangeCount()', () => {
+		it('Returns "0".', () => {
+			expect(selection.rangeCount).toBe(0);
+		});
+	});
+
+	describe('get type()', () => {
+		it('Returns "None".', () => {
+			expect(selection.type).toBe('None');
+		});
+	});
+
+	for (const methodName of [
+		'addRange',
+		'collapse',
+		'collapseToEnd',
+		'collapseToStart',
+		'containsNode',
+		'deleteFromDocument',
+		'extend',
+		'getRangeAt',
+		'removeRange',
+		'removeAllRanges',
+		'selectAllChildren',
+		'setBaseAndExtent',
+		'toString'
+	]) {
+		describe(`${methodName}()`, () => {
+			it('Method exists.', () => {
+				expect(typeof selection[methodName]).toBe('function');
+			});
+		});
+	}
+});