From 16a7c2fb6f943d59dcbc901e5ea989006119449a Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 1 Jul 2022 00:05:46 +0200 Subject: [PATCH] #450@major: Adds support for Document.createRange(). Adds integration for Document.getSelection(). Fixes issue preventing usage of multiple Window instances. --- packages/happy-dom/src/index.ts | 4 +- packages/happy-dom/src/selection/Selection.ts | 33 +- .../test/nodes/document/Document.test.ts | 15 +- .../test/selection/Selection.test.ts | 711 ++++++++++++++++-- packages/happy-dom/test/window/Window.test.ts | 49 ++ 5 files changed, 735 insertions(+), 77 deletions(-) diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index a1c97ae2d..d3200b912 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -104,6 +104,7 @@ import Storage from './storage/Storage'; import DOMRect from './nodes/element/DOMRect'; import { URLSearchParams } from 'url'; import Selection from './selection/Selection'; +import Range from './range/Range'; export { GlobalWindow, @@ -211,5 +212,6 @@ export { Storage, DOMRect, URLSearchParams, - Selection + Selection, + Range }; diff --git a/packages/happy-dom/src/selection/Selection.ts b/packages/happy-dom/src/selection/Selection.ts index d28ede2a9..b40332120 100644 --- a/packages/happy-dom/src/selection/Selection.ts +++ b/packages/happy-dom/src/selection/Selection.ts @@ -102,7 +102,7 @@ export default class Selection { * Returns anchor node. * * @deprecated - * @alias this.anchorNode + * @alias anchorNode * @returns Node. */ public get baseNode(): INode { @@ -113,7 +113,7 @@ export default class Selection { * Returns anchor offset. * * @deprecated - * @alias this.anchorOffset + * @alias anchorOffset * @returns Node. */ public get baseOffset(): number { @@ -127,12 +127,7 @@ export default class Selection { * @returns Node. */ public get focusNode(): INode { - if (!this._range) { - return null; - } - return this._direction === SelectionDirectionEnum.forwards - ? this._range.startContainer - : this._range.endContainer; + return this.anchorNode; } /** @@ -142,19 +137,14 @@ export default class Selection { * @returns Node. */ public get focusOffset(): number { - if (!this._range) { - return null; - } - return this._direction === SelectionDirectionEnum.forwards - ? this._range.startOffset - : this._range.endOffset; + return this.anchorOffset; } /** * Returns focus node. * * @deprecated - * @alias this.focusNode + * @alias focusNode * @returns Node. */ public get extentNode(): INode { @@ -165,7 +155,7 @@ export default class Selection { * Returns focus offset. * * @deprecated - * @alias this.focusOffset + * @alias focusOffset * @returns Node. */ public get extentOffset(): number { @@ -225,7 +215,7 @@ export default class Selection { /** * Removes all ranges. * - * @alias this.removeAllRanges() + * @alias removeAllRanges() */ public empty(): void { this.removeAllRanges(); @@ -273,7 +263,7 @@ export default class Selection { * Collapses the current selection to a single point. * * @see https://w3c.github.io/selection-api/#dom-selection-setposition - * @alias this.collapse() + * @alias collapse() * @param node Node. * @param offset Offset. */ @@ -528,12 +518,7 @@ export default class Selection { if (oldRange !== this._range) { // https://w3c.github.io/selection-api/#selectionchange-event - this._ownerDocument.dispatchEvent( - new Event('selectionchange', { - bubbles: false, - cancelable: false - }) - ); + this._ownerDocument.dispatchEvent(new Event('selectionchange')); } } } diff --git a/packages/happy-dom/test/nodes/document/Document.test.ts b/packages/happy-dom/test/nodes/document/Document.test.ts index a105b295b..02c151f2a 100644 --- a/packages/happy-dom/test/nodes/document/Document.test.ts +++ b/packages/happy-dom/test/nodes/document/Document.test.ts @@ -29,6 +29,7 @@ import DocumentReadyStateEnum from '../../../src/nodes/document/DocumentReadySta import ISVGElement from '../../../src/nodes/svg-element/ISVGElement'; import CustomEvent from '../../../src/event/events/CustomEvent'; import Selection from '../../../src/selection/Selection'; +import Range from '../../../src/range/Range'; /* eslint-disable jsdoc/require-jsdoc */ @@ -1071,9 +1072,21 @@ describe('Document', () => { }); describe('getSelection()', () => { - it('Returns selection.', () => { + it('Returns an instance of Selection.', () => { expect(document.getSelection() instanceof Selection).toBe(true); }); + + it('Returns the same instance when called multiple times.', () => { + const selection1 = document.getSelection(); + const selection2 = document.getSelection(); + expect(selection1 === selection2).toBe(true); + }); + }); + + describe('createRange()', () => { + it('Returns an instance of Range.', () => { + expect(document.createRange() instanceof Range).toBe(true); + }); }); describe('hasFocus()', () => { diff --git a/packages/happy-dom/test/selection/Selection.test.ts b/packages/happy-dom/test/selection/Selection.test.ts index 87867843a..c5795c0e1 100644 --- a/packages/happy-dom/test/selection/Selection.test.ts +++ b/packages/happy-dom/test/selection/Selection.test.ts @@ -2,6 +2,10 @@ import Window from '../../src/window/Window'; import IWindow from '../../src/window/IWindow'; import IDocument from '../../src/nodes/document/IDocument'; import Selection from '../../src/selection/Selection'; +import SelectionDirectionEnum from '../../src/selection/SelectionDirectionEnum'; +import DOMException from '../../src/exception/DOMException'; +import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum'; +import NodeTypeEnum from '../../src/nodes/node/NodeTypeEnum'; describe('Selection', () => { let window: IWindow; @@ -15,93 +19,698 @@ describe('Selection', () => { }); describe('get rangeCount()', () => { - it('Returns number of Ranges.', () => { + it('Returns 1 if there is a Range added.', () => { const range = document.createRange(); + expect(selection.rangeCount).toBe(0); + + selection.addRange(range); + expect(selection.rangeCount).toBe(1); + selection.addRange(range); expect(selection.rangeCount).toBe(1); }); }); - describe('get anchorNode()', () => { - xit('Returns "null".', () => { - expect(selection.anchorNode).toBe(null); + describe('get isCollapsed()', () => { + it('Returns "true" if the Range is collapsed.', () => { + const range = document.createRange(); + selection.addRange(range); + expect(selection.isCollapsed).toBe(true); }); }); - describe('get anchorOffset()', () => { - xit('Returns "0".', () => { - expect(selection.anchorOffset).toBe(0); + describe('get type()', () => { + it('Returns "None" if no Range has been added.', () => { + expect(selection.type).toBe('None'); + }); + + it('Returns "Caret" if an added Range is collapsed.', () => { + const range = document.createRange(); + selection.addRange(range); + expect(selection.type).toBe('Caret'); + }); + + it('Returns "Range" if an added Range is not collapsed.', () => { + const range = document.createRange(); + const start = document.createElement('div'); + const end = document.createElement('div'); + + document.body.appendChild(start); + document.body.appendChild(end); + + range.setStart(start, 0); + range.setEnd(end, 0); + + selection.addRange(range); + + expect(selection.type).toBe('Range'); }); }); - describe('get baseNode()', () => { - xit('Returns "null".', () => { - expect(selection.baseNode).toBe(null); + for (const property of ['anchorNode', 'baseNode', 'focusNode', 'extentNode']) { + describe(`get ${property}()`, () => { + it('Returns null if no Range has been added.', () => { + expect(selection[property]).toBe(null); + }); + + it(`Returns start container of Range if direction is "${SelectionDirectionEnum.forwards}".`, () => { + const range = document.createRange(); + const start = document.createElement('div'); + const end = document.createElement('div'); + + document.body.appendChild(start); + document.body.appendChild(end); + + range.setStart(start, 0); + range.setEnd(end, 0); + + selection.addRange(range); + + expect(selection[property] === start).toBe(true); + expect(range.endContainer === end).toBe(true); + }); + + it(`Returns end container of Range if direction is not "${SelectionDirectionEnum.forwards}".`, () => { + const range = document.createRange(); + const extend = document.createElement('div'); + const start = document.createElement('div'); + const end = document.createElement('div'); + + document.body.appendChild(extend); + document.body.appendChild(start); + document.body.appendChild(end); + + range.setStart(start, 0); + range.setEnd(end, 0); + + selection.addRange(range); + + selection.extend(extend, 0); + + expect(selection[property] === selection.getRangeAt(0).endContainer).toBe(true); + }); + }); + } + + for (const property of ['anchorOffset', 'baseOffset', 'focusOffset', 'extentOffset']) { + describe(`get ${property}()`, () => { + it('Returns null if no Range has been added.', () => { + expect(selection[property]).toBe(null); + }); + + it(`Returns start offset of Range if direction is "${SelectionDirectionEnum.forwards}".`, () => { + const range = document.createRange(); + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + + document.body.appendChild(start); + document.body.appendChild(end); + + range.setStart(start, 1); + range.setEnd(end, 2); + + selection.addRange(range); + + expect(selection[property] === 1).toBe(true); + expect(range.endOffset === 2).toBe(true); + }); + + it(`Returns end offset of Range if direction is not "${SelectionDirectionEnum.forwards}".`, () => { + const range = document.createRange(); + const extend = document.createTextNode('extend'); + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + + document.body.appendChild(extend); + document.body.appendChild(start); + document.body.appendChild(end); + + range.setStart(start, 1); + range.setEnd(end, 2); + + selection.addRange(range); + + selection.extend(extend, 3); + + expect(selection[property] === selection.getRangeAt(0).endOffset).toBe(true); + }); + }); + } + + describe('addRange()', () => { + it('Adds a Range.', () => { + const range = document.createRange(); + selection.addRange(range); + expect(selection.getRangeAt(0)).toBe(range); + }); + + it('Does not add a new Range if there already is one added.', () => { + const range1 = document.createRange(); + const range2 = document.createRange(); + selection.addRange(range1); + selection.addRange(range2); + expect(selection.rangeCount).toBe(1); + expect(selection.getRangeAt(0)).toBe(range1); + }); + + it('Does not add a Range from another document.', () => { + const range = new Window().document.createRange(); + selection.addRange(range); + expect(selection.rangeCount).toBe(0); + }); + + it('Triggers a "selectionchange" event.', () => { + let triggeredEvent = null; + document.addEventListener('selectionchange', (event) => (triggeredEvent = event)); + const range = document.createRange(); + selection.addRange(range); + expect(triggeredEvent.bubbles).toBe(false); + expect(triggeredEvent.cancelable).toBe(false); }); }); - describe('get baseOffset()', () => { - xit('Returns "0".', () => { - expect(selection.baseOffset).toBe(0); + describe('getRangeAt()', () => { + it('Returns an added Range.', () => { + const range1 = document.createRange(); + selection.addRange(range1); + expect(selection.getRangeAt(0)).toBe(range1); + }); + + it('Throws error if there is no Range added.', () => { + expect(() => selection.getRangeAt(0)).toThrowError( + new DOMException('Invalid range index.', DOMExceptionNameEnum.indexSizeError) + ); + }); + + it('Throws error for any other index than 0.', () => { + const range1 = document.createRange(); + const range2 = document.createRange(); + selection.addRange(range1); + selection.addRange(range2); + expect(() => selection.getRangeAt(1)).toThrowError( + new DOMException('Invalid range index.', DOMExceptionNameEnum.indexSizeError) + ); }); }); - describe('get extentNode()', () => { - xit('Returns "null".', () => { - expect(selection.extentNode).toBe(null); + describe('removeRange()', () => { + it('Removes a range.', () => { + const range = document.createRange(); + selection.addRange(range); + selection.removeRange(range); + expect(selection.rangeCount).toBe(0); + }); + + it('Throws error if there is no Range added.', () => { + const range = document.createRange(); + expect(() => selection.removeRange(range)).toThrowError( + new DOMException('Invalid range.', DOMExceptionNameEnum.notFoundError) + ); + }); + + it("Throws error if there ranges doesn't match.", () => { + const range1 = document.createRange(); + const range2 = document.createRange(); + selection.addRange(range1); + expect(() => selection.removeRange(range2)).toThrowError( + new DOMException('Invalid range.', DOMExceptionNameEnum.notFoundError) + ); + }); + + it('Triggers a "selectionchange" event.', () => { + let triggeredEvent = null; + const range = document.createRange(); + selection.addRange(range); + document.addEventListener('selectionchange', (event) => (triggeredEvent = event)); + selection.removeRange(range); + expect(triggeredEvent.bubbles).toBe(false); + expect(triggeredEvent.cancelable).toBe(false); }); }); - describe('get extentOffset()', () => { - xit('Returns "0".', () => { - expect(selection.extentOffset).toBe(0); + for (const method of ['removeAllRanges', 'empty']) { + describe(`${method}()`, () => { + it('Removes all ranges.', () => { + const range = document.createRange(); + selection.addRange(range); + selection[method](); + expect(selection.rangeCount).toBe(0); + }); + + it('Triggers a "selectionchange" event.', () => { + let triggeredEvent = null; + const range = document.createRange(); + selection.addRange(range); + document.addEventListener('selectionchange', (event) => (triggeredEvent = event)); + selection[method](); + expect(triggeredEvent.bubbles).toBe(false); + expect(triggeredEvent.cancelable).toBe(false); + }); + }); + } + + for (const method of ['collapse', 'setPosition']) { + describe(`${method}()`, () => { + it('Removes all ranges if node is null.', () => { + const range = document.createRange(); + selection.addRange(range); + selection[method](null, 0); + expect(selection.rangeCount).toBe(0); + }); + + it(`Throws error if node type is ${NodeTypeEnum.documentTypeNode}.`, () => { + const documentType = document.implementation.createDocumentType('', '', ''); + + expect(() => selection[method](documentType, 0)).toThrowError( + new DOMException( + "DocumentType Node can't be used as boundary point.", + DOMExceptionNameEnum.invalidNodeTypeError + ) + ); + }); + + it('Throws error if offset is greater than the node length.', () => { + const range = document.createRange(); + const text = document.createTextNode('Text'); + + selection.addRange(range); + expect(() => selection[method](text, 5)).toThrowError( + new DOMException('Invalid range index.', DOMExceptionNameEnum.indexSizeError) + ); + }); + + it('Applies new range collapsed to the given node and offset.', () => { + const range = document.createRange(); + const text = document.createTextNode('Text'); + + selection.addRange(range); + selection[method](text, 2); + + const newRange = selection.getRangeAt(0); + expect(range !== newRange).toBe(true); + expect(newRange.startContainer).toBe(text); + expect(newRange.startOffset).toBe(2); + expect(newRange.startContainer).toBe(text); + expect(newRange.endOffset).toBe(2); + }); + + it('Triggers a "selectionchange" event.', () => { + const range = document.createRange(); + const text = document.createTextNode('Text'); + let triggeredEvent = null; + + selection.addRange(range); + + document.addEventListener('selectionchange', (event) => (triggeredEvent = event)); + + selection[method](text, 2); + + expect(triggeredEvent.bubbles).toBe(false); + expect(triggeredEvent.cancelable).toBe(false); + }); + }); + } + + describe('collapseToEnd()', () => { + it('Collapses to end.', () => { + const range = document.createRange(); + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + + document.body.appendChild(start); + document.body.appendChild(end); + + range.setStart(start, 1); + range.setEnd(end, 2); + + selection.addRange(range); + selection.collapseToEnd(); + + const newRange = selection.getRangeAt(0); + + expect(newRange !== range).toBe(true); + expect(newRange.startContainer).toBe(end); + expect(newRange.startOffset).toBe(2); + expect(newRange.startContainer).toBe(end); + expect(newRange.endOffset).toBe(2); + }); + + it('Throws error if there is no Range added.', () => { + expect(() => selection.collapseToEnd()).toThrowError( + new DOMException( + 'There is no selection to collapse.', + DOMExceptionNameEnum.invalidStateError + ) + ); + }); + + it('Triggers a "selectionchange" event.', () => { + const range = document.createRange(); + let triggeredEvent = null; + + selection.addRange(range); + + document.addEventListener('selectionchange', (event) => (triggeredEvent = event)); + + selection.collapseToEnd(); + + expect(triggeredEvent.bubbles).toBe(false); + expect(triggeredEvent.cancelable).toBe(false); }); }); - describe('get focusNode()', () => { - xit('Returns "null".', () => { - expect(selection.focusNode).toBe(null); + describe('collapseToStart()', () => { + it('Collapses to start.', () => { + const range = document.createRange(); + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + + document.body.appendChild(start); + document.body.appendChild(end); + + range.setStart(start, 1); + range.setEnd(end, 2); + + selection.addRange(range); + selection.collapseToStart(); + + const newRange = selection.getRangeAt(0); + + expect(newRange !== range).toBe(true); + expect(newRange.startContainer).toBe(start); + expect(newRange.startOffset).toBe(1); + expect(newRange.endContainer).toBe(start); + expect(newRange.endOffset).toBe(1); + }); + + it('Throws error if there is no Range added.', () => { + expect(() => selection.collapseToStart()).toThrowError( + new DOMException( + 'There is no selection to collapse.', + DOMExceptionNameEnum.invalidStateError + ) + ); + }); + + it('Triggers a "selectionchange" event.', () => { + const range = document.createRange(); + let triggeredEvent = null; + + selection.addRange(range); + + document.addEventListener('selectionchange', (event) => (triggeredEvent = event)); + + selection.collapseToStart(); + + expect(triggeredEvent.bubbles).toBe(false); + expect(triggeredEvent.cancelable).toBe(false); }); }); - describe('get focusOffset()', () => { - xit('Returns "0".', () => { - expect(selection.focusOffset).toBe(0); + describe('containsNode()', () => { + it('Returns "true" if the selection range contains a node.', () => { + const range = document.createRange(); + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + const node = document.createTextNode('node'); + + document.body.appendChild(start); + document.body.appendChild(node); + document.body.appendChild(end); + + range.setStart(start, 1); + range.setEnd(end, 2); + + selection.addRange(range); + + expect(selection.containsNode(node)).toBe(true); + }); + + it('Returns "false" if the selection range does not contain a node.', () => { + const range = document.createRange(); + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + const node = document.createTextNode('node'); + + document.body.appendChild(start); + document.body.appendChild(end); + document.body.appendChild(node); + + range.setStart(start, 1); + range.setEnd(end, 2); + + selection.addRange(range); + + expect(selection.containsNode(node)).toBe(false); + }); + + it('Returns "true" if the selection range partially contains a node.', () => { + const range = document.createRange(); + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + const node = document.createTextNode('node'); + + document.body.appendChild(start); + document.body.appendChild(end); + document.body.appendChild(node); + + range.setStart(start, 1); + range.setEnd(end, 2); + + selection.addRange(range); + + expect(selection.containsNode(node, true)).toBe(true); }); }); - describe('get isCollapsed()', () => { - xit('Returns "true".', () => { - expect(selection.isCollapsed).toBe(true); + describe('deleteFromDocument()', () => { + it('Does nothing if there is no Range added.', () => { + selection.deleteFromDocument(); + }); + + it('Removes the selection Range from the document.', () => { + const range = document.createRange(); + const before = document.createTextNode('before'); + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + const after = document.createTextNode('after'); + + document.body.appendChild(before); + document.body.appendChild(start); + document.body.appendChild(end); + document.body.appendChild(after); + + range.setStart(start, 1); + range.setEnd(end, 2); + + selection.addRange(range); + + selection.deleteFromDocument(); + + expect(document.body.innerHTML).toBe('beforesdafter'); }); }); - describe('get type()', () => { - xit('Returns "None".', () => { - expect(selection.type).toBe('None'); + describe('extend()', () => { + it('Extends the selection with a node and offset.', () => { + const range = document.createRange(); + const before = document.createTextNode('before'); + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + const after = document.createTextNode('after'); + + document.body.appendChild(before); + document.body.appendChild(start); + document.body.appendChild(end); + document.body.appendChild(after); + + range.setStart(start, 1); + range.setEnd(end, 2); + + selection.addRange(range); + selection.extend(after, 3); + + selection.deleteFromDocument(); + + expect(document.body.innerHTML).toBe('beforeser'); + }); + + it('Throws error if there is no Range added.', () => { + const node = document.createTextNode('after'); + expect(() => selection.extend(node, 3)).toThrowError( + new DOMException('There is no selection to extend.', DOMExceptionNameEnum.invalidStateError) + ); + }); + + it('Triggers a "selectionchange" event.', () => { + const range = document.createRange(); + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + const after = document.createTextNode('after'); + let triggeredEvent = null; + + document.body.appendChild(start); + document.body.appendChild(end); + document.body.appendChild(after); + + range.setStart(start, 1); + range.setEnd(end, 2); + + selection.addRange(range); + + document.addEventListener('selectionchange', (event) => (triggeredEvent = event)); + + selection.extend(after, 3); + + expect(triggeredEvent.bubbles).toBe(false); + expect(triggeredEvent.cancelable).toBe(false); }); }); - for (const methodName of [ - 'addRange', - 'collapse', - 'collapseToEnd', - 'collapseToStart', - 'containsNode', - 'deleteFromDocument', - 'extend', - 'getRangeAt', - 'removeRange', - 'removeAllRanges', - 'selectAllChildren', - 'setBaseAndExtent', - 'toString' - ]) { - describe(`${methodName}()`, () => { - xit('Method exists.', () => { - expect(typeof selection[methodName]).toBe('function'); - }); + describe('selectAllChildren()', () => { + it('Selects all children of a given Node.', () => { + const container = document.createElement('div'); + const text1 = document.createTextNode('text1'); + const text2 = document.createTextNode('text2'); + const text3 = document.createTextNode('text3'); + + container.appendChild(text1); + container.appendChild(text2); + container.appendChild(text3); + + selection.selectAllChildren(container); + + const newRange = selection.getRangeAt(0); + + expect(newRange.startContainer).toBe(container); + expect(newRange.startOffset).toBe(0); + expect(newRange.endContainer).toBe(container); + expect(newRange.endOffset).toBe(3); }); - } + + it(`Throws error if node type is ${NodeTypeEnum.documentTypeNode}.`, () => { + const documentType = document.implementation.createDocumentType('', '', ''); + + expect(() => selection.selectAllChildren(documentType)).toThrowError( + new DOMException( + "DocumentType Node can't be used as boundary point.", + DOMExceptionNameEnum.invalidNodeTypeError + ) + ); + }); + + it('Triggers a "selectionchange" event.', () => { + const container = document.createElement('div'); + const child = document.createTextNode('child'); + let triggeredEvent = null; + + container.appendChild(child); + + document.addEventListener('selectionchange', (event) => (triggeredEvent = event)); + + selection.selectAllChildren(container); + + expect(triggeredEvent.bubbles).toBe(false); + expect(triggeredEvent.cancelable).toBe(false); + }); + }); + + describe('setBaseAndExtent()', () => { + it('Sets the selection to be a Range forward.', () => { + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + + document.body.appendChild(start); + document.body.appendChild(end); + + selection.setBaseAndExtent(start, 1, end, 2); + + const newRange = selection.getRangeAt(0); + + expect(newRange.startContainer).toBe(start); + expect(newRange.startOffset).toBe(1); + expect(newRange.endContainer).toBe(end); + expect(newRange.endOffset).toBe(2); + + expect(selection['_direction']).toBe(SelectionDirectionEnum.forwards); + }); + + it('Sets the selection to be a Range backward.', () => { + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + + document.body.appendChild(start); + document.body.appendChild(end); + + selection.setBaseAndExtent(end, 2, start, 1); + + const newRange = selection.getRangeAt(0); + + expect(newRange.startContainer).toBe(start); + expect(newRange.startOffset).toBe(1); + expect(newRange.endContainer).toBe(end); + expect(newRange.endOffset).toBe(2); + + expect(selection['_direction']).toBe(SelectionDirectionEnum.backwards); + }); + + it('Throws error if wrong offset.', () => { + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + + document.body.appendChild(start); + document.body.appendChild(end); + + expect(() => selection.setBaseAndExtent(start, 6, end, 2)).toThrowError( + new DOMException('Invalid anchor or focus offset.', DOMExceptionNameEnum.indexSizeError) + ); + + expect(() => selection.setBaseAndExtent(start, 1, end, 4)).toThrowError( + new DOMException('Invalid anchor or focus offset.', DOMExceptionNameEnum.indexSizeError) + ); + }); + + it('Triggers a "selectionchange" event.', () => { + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + let triggeredEvent = null; + + document.body.appendChild(start); + document.body.appendChild(end); + + document.addEventListener('selectionchange', (event) => (triggeredEvent = event)); + + selection.setBaseAndExtent(start, 1, end, 2); + + expect(triggeredEvent.bubbles).toBe(false); + expect(triggeredEvent.cancelable).toBe(false); + }); + }); + + describe('toString()', () => { + it('Returns empty string if there is no Range added.', () => { + expect(selection.toString()).toBe(''); + }); + + it('Returns the text in the selected Range as a string.', () => { + const range = document.createRange(); + const before = document.createTextNode('before'); + const start = document.createTextNode('start'); + const end = document.createTextNode('end'); + const after = document.createTextNode('after'); + + document.body.appendChild(before); + document.body.appendChild(start); + document.body.appendChild(end); + document.body.appendChild(after); + + range.setStart(start, 1); + range.setEnd(end, 2); + + selection.addRange(range); + + expect(selection.toString()).toBe('tarten'); + }); + }); }); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 4713efa4d..5c2239e25 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -30,6 +30,55 @@ describe('Window', () => { jest.restoreAllMocks(); }); + describe('constructor()', () => { + it('Is able to handle multiple instances of Window', () => { + const secondWindow = new Window(); + const thirdWindow = new Window(); + + for (const className of [ + 'Response', + 'Request', + 'Image', + 'FileReader', + 'DOMParser', + 'Range' + ]) { + const thirdInstance = new thirdWindow[className](); + const firstInstance = new window[className](); + const secondInstance = new secondWindow[className](); + const property = className === 'Image' ? 'ownerDocument' : '_ownerDocument'; + + expect(firstInstance[property] === window.document).toBe(true); + expect(secondInstance[property] === secondWindow.document).toBe(true); + expect(thirdInstance[property] === thirdWindow.document).toBe(true); + } + + const thirdElement = thirdWindow.document.createElement('div'); + const firstElement = window.document.createElement('div'); + const secondElement = secondWindow.document.createElement('div'); + + expect(firstElement.ownerDocument === window.document).toBe(true); + expect(secondElement.ownerDocument === secondWindow.document).toBe(true); + expect(thirdElement.ownerDocument === thirdWindow.document).toBe(true); + + const thirdText = thirdWindow.document.createTextNode('Test'); + const firstText = window.document.createTextNode('Test'); + const secondText = secondWindow.document.createTextNode('Test'); + + expect(firstText.ownerDocument === window.document).toBe(true); + expect(secondText.ownerDocument === secondWindow.document).toBe(true); + expect(thirdText.ownerDocument === thirdWindow.document).toBe(true); + + const thirdComment = thirdWindow.document.createComment('Test'); + const firstComment = window.document.createComment('Test'); + const secondComment = secondWindow.document.createComment('Test'); + + expect(firstComment.ownerDocument === window.document).toBe(true); + expect(secondComment.ownerDocument === secondWindow.document).toBe(true); + expect(thirdComment.ownerDocument === thirdWindow.document).toBe(true); + }); + }); + describe('get Object()', () => { it('Is not the same as {}.constructor when inside the VM.', () => { expect(typeof window.Object).toBe('function');