From a40354fb7ebe11aa6409aaadbeede89240a01476 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Tue, 14 Jun 2022 16:32:50 +0200 Subject: [PATCH] #450@trivial: Continue on Range implementation. --- packages/happy-dom/.eslintrc.js | 4 +- packages/happy-dom/src/base64/Base64.ts | 97 ++++++++++++++++ .../happy-dom/src/nodes/node/NodeUtility.ts | 61 ++++++++-- packages/happy-dom/src/range/RangeUtility.ts | 3 + packages/happy-dom/src/window/IWindow.ts | 20 ++++ packages/happy-dom/src/window/Window.ts | 30 ++++- packages/happy-dom/src/window/WindowBase64.ts | 95 ---------------- .../test/nodes/node/NodeUtility.test.ts | 106 ++++++++++++++++++ packages/happy-dom/test/range/Range.test.ts | 4 +- packages/happy-dom/test/window/Window.test.ts | 13 +-- 10 files changed, 313 insertions(+), 120 deletions(-) create mode 100644 packages/happy-dom/src/base64/Base64.ts delete mode 100644 packages/happy-dom/src/window/WindowBase64.ts create mode 100644 packages/happy-dom/test/nodes/node/NodeUtility.test.ts diff --git a/packages/happy-dom/.eslintrc.js b/packages/happy-dom/.eslintrc.js index 31fc0081e..bc2d5a1e8 100644 --- a/packages/happy-dom/.eslintrc.js +++ b/packages/happy-dom/.eslintrc.js @@ -34,12 +34,12 @@ const COMMON_CONFIG = { 'jsdoc/check-tag-names': WARN, 'jsdoc/check-types': WARN, 'jsdoc/implements-on-classes': WARN, - 'jsdoc/match-description': WARN, + 'jsdoc/match-description': OFF, 'jsdoc/newline-after-description': WARN, 'jsdoc/no-types': OFF, 'jsdoc/no-undefined-types': OFF, 'jsdoc/require-description': OFF, - 'jsdoc/require-description-complete-sentence': WARN, + 'jsdoc/require-description-complete-sentence': OFF, 'jsdoc/require-example': OFF, 'jsdoc/require-hyphen-before-param-description': [WARN, 'never'], 'jsdoc/require-param': WARN, diff --git a/packages/happy-dom/src/base64/Base64.ts b/packages/happy-dom/src/base64/Base64.ts new file mode 100644 index 000000000..9f259d646 --- /dev/null +++ b/packages/happy-dom/src/base64/Base64.ts @@ -0,0 +1,97 @@ +import DOMException from '../exception/DOMException'; +import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum'; + +const BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + +/** + * Base64 encoding and decoding. + */ +export default class Base64 { + /** + * Creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data). + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/btoa + * @param data Binay data. + * @returns Base64-encoded string. + */ + public static btoa(data: unknown): string { + const str = (data).toString(); + if (/[^\u0000-\u00ff]/.test(str)) { + throw new DOMException( + "Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.", + DOMExceptionNameEnum.invalidCharacterError + ); + } + + let t = ''; + let p = -6; + let a = 0; + let i = 0; + let v = 0; + let c; + while (i < str.length || p > -6) { + if (p < 0) { + if (i < str.length) { + c = str.charCodeAt(i++); + v += 8; + } else { + c = 0; + } + a = ((a & 255) << 8) | (c & 255); + p += 8; + } + t += BASE64_CHARS.charAt(v > 0 ? (a >> p) & 63 : 64); + p -= 6; + v -= 6; + } + return t; + } + + /** + * Decodes a string of data which has been encoded using Base64 encoding. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/atob + * @see https://infra.spec.whatwg.org/#forgiving-base64-encode. + * @see Https://html.spec.whatwg.org/multipage/webappapis.html#btoa. + * @param data Binay string. + * @returns An ASCII string containing decoded data from encodedData. + */ + public static atob(data: unknown): string { + const str = (data).toString(); + + if (/[^\u0000-\u00ff]/.test(str)) { + throw new DOMException( + "Failed to execute 'atob' on 'Window': The string to be decoded contains characters outside of the Latin1 range.", + DOMExceptionNameEnum.invalidCharacterError + ); + } + + if (/[^A-Za-z\d+/=]/.test(str) || str.length % 4 == 1) { + throw new DOMException( + "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", + DOMExceptionNameEnum.invalidCharacterError + ); + } + + let t = ''; + let p = -8; + let a = 0; + let c; + let d; + for (let i = 0; i < str.length; i++) { + if ((c = BASE64_CHARS.indexOf(str.charAt(i))) < 0) { + continue; + } + a = (a << 6) | (c & 63); + if ((p += 6) >= 0) { + d = (a >> p) & 255; + if (c !== 64) { + t += String.fromCharCode(d); + } + a &= 63; + p -= 8; + } + } + return t; + } +} diff --git a/packages/happy-dom/src/nodes/node/NodeUtility.ts b/packages/happy-dom/src/nodes/node/NodeUtility.ts index 958712bbd..970f974f1 100644 --- a/packages/happy-dom/src/nodes/node/NodeUtility.ts +++ b/packages/happy-dom/src/nodes/node/NodeUtility.ts @@ -10,15 +10,18 @@ export default class NodeUtility { /** * Returns boolean indicating if nodeB is an inclusive ancestor of nodeA. * + * Based on: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/helpers/node.js + * * @see https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor - * @param nodeA Node A. - * @param nodeB Node B. + * @param ancestorNode Ancestor node. + * @param referenceNode Reference node. * @returns "true" if following. */ - public static isInclusiveAncestor(nodeA: INode, nodeB: INode): boolean { - let parent: INode = nodeA; + public static isInclusiveAncestor(ancestorNode: INode, referenceNode: INode): boolean { + let parent: INode = referenceNode; while (parent) { - if (parent === nodeB) { + if (ancestorNode === parent) { return true; } parent = parent.parentNode; @@ -29,6 +32,9 @@ export default class NodeUtility { /** * Returns boolean indicating if nodeB is following nodeA in the document tree. * + * Based on: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/helpers/node.js + * * @see https://dom.spec.whatwg.org/#concept-tree-following * @param nodeA Node A. * @param nodeB Node B. @@ -42,13 +48,11 @@ export default class NodeUtility { let current: INode = nodeB; while (current) { - const nextSibling = current.nextSibling; + current = this.following(current); - if (nextSibling === nodeA) { + if (current === nodeA) { return true; } - - current = nextSibling ? nextSibling : current.parentNode; } return false; @@ -57,6 +61,9 @@ export default class NodeUtility { /** * Node length. * + * Based on: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/helpers/node.js + * * @see https://dom.spec.whatwg.org/#concept-node-length * @param node Node. * @returns Node length. @@ -75,4 +82,40 @@ export default class NodeUtility { return node.childNodes.length; } } + + /** + * Returns boolean indicating if nodeB is following nodeA in the document tree. + * + * Based on: + * https://github.com/jsdom/js-symbol-tree/blob/master/lib/SymbolTree.js#L220 + * + * @param node Node. + * @param [root] Root. + * @returns Following node. + */ + private static following(node: INode, root?: INode): INode { + const firstChild = node.firstChild; + + if (firstChild) { + return firstChild; + } + + let current = node; + + while (current) { + if (current === root) { + return null; + } + + const nextSibling = current.nextSibling; + + if (nextSibling) { + return nextSibling; + } + + current = current.parentNode; + } + + return null; + } } diff --git a/packages/happy-dom/src/range/RangeUtility.ts b/packages/happy-dom/src/range/RangeUtility.ts index e85eb7d88..2d1a9cae5 100644 --- a/packages/happy-dom/src/range/RangeUtility.ts +++ b/packages/happy-dom/src/range/RangeUtility.ts @@ -5,6 +5,9 @@ type BoundaryPoint = { node: INode; offset: number }; /** * Range utility. + * + * Based on: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/boundary-point.js. */ export default class RangeUtility { /** diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index d73f322d5..a2bc10084 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -303,4 +303,24 @@ export default interface IWindow extends IEventTarget, NodeJS.Global { * @returns Promise. */ fetch(url: string, init?: IRequestInit): Promise; + + /** + * Creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data). + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/btoa + * @param data Binay data. + * @returns Base64-encoded string. + */ + btoa(data: unknown): string; + + /** + * Decodes a string of data which has been encoded using Base64 encoding. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/atob + * @see https://infra.spec.whatwg.org/#forgiving-base64-encode. + * @see Https://html.spec.whatwg.org/multipage/webappapis.html#btoa. + * @param data Binay string. + * @returns An ASCII string containing decoded data from encodedData. + */ + atob(data: unknown): string; } diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index df6b99128..0fe75ae0c 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -93,7 +93,7 @@ import VMGlobalPropertyScript from './VMGlobalPropertyScript'; import * as PerfHooks from 'perf_hooks'; import VM from 'vm'; import { Buffer } from 'buffer'; -import { atob, btoa } from './WindowBase64'; +import Base64 from '../base64/Base64'; /** * Browser window. @@ -222,10 +222,6 @@ export default class Window extends EventTarget implements IWindow { public readonly localStorage = new Storage(); public readonly performance = PerfHooks.performance; - // Atob & btoa - public atob = atob; - public btoa = btoa; - // Node.js Globals public ArrayBuffer; public Boolean; @@ -530,6 +526,30 @@ export default class Window extends EventTarget implements IWindow { return await FetchHandler.fetch(this.document, url, init); } + /** + * Creates a Base64-encoded ASCII string from a binary string (i.e., a string in which each character in the string is treated as a byte of binary data). + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/btoa + * @param data Binay data. + * @returns Base64-encoded string. + */ + public btoa(data: unknown): string { + return Base64.btoa(data); + } + + /** + * Decodes a string of data which has been encoded using Base64 encoding. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/atob + * @see https://infra.spec.whatwg.org/#forgiving-base64-encode. + * @see Https://html.spec.whatwg.org/multipage/webappapis.html#btoa. + * @param data Binay string. + * @returns An ASCII string containing decoded data from encodedData. + */ + public atob(data: unknown): string { + return Base64.atob(data); + } + /** * Setup of VM context. */ diff --git a/packages/happy-dom/src/window/WindowBase64.ts b/packages/happy-dom/src/window/WindowBase64.ts deleted file mode 100644 index c9ad7b650..000000000 --- a/packages/happy-dom/src/window/WindowBase64.ts +++ /dev/null @@ -1,95 +0,0 @@ -import DOMException from '../exception/DOMException'; -import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum'; - -const base64list = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - -/** - * Btoa. - * - * Reference: - * https://developer.mozilla.org/en-US/docs/Web/API/btoa. - * - * @param data - */ -export const btoa = (data: unknown): string => { - const str = (data).toString(); - if (/[^\u0000-\u00ff]/.test(str)) { - throw new DOMException( - "Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.", - DOMExceptionNameEnum.invalidCharacterError - ); - } - - let t = ''; - let p = -6; - let a = 0; - let i = 0; - let v = 0; - let c; - while (i < str.length || p > -6) { - if (p < 0) { - if (i < str.length) { - c = str.charCodeAt(i++); - v += 8; - } else { - c = 0; - } - a = ((a & 255) << 8) | (c & 255); - p += 8; - } - t += base64list.charAt(v > 0 ? (a >> p) & 63 : 64); - p -= 6; - v -= 6; - } - return t; -}; - -/** - * Atob. - * - * Reference: - * https://infra.spec.whatwg.org/#forgiving-base64-encode. - * Https://html.spec.whatwg.org/multipage/webappapis.html#btoa. - * - * @param data - */ -export const atob = (data: unknown): string => { - const str = (data).toString(); - - if (/[^\u0000-\u00ff]/.test(str)) { - throw new DOMException( - "Failed to execute 'atob' on 'Window': The string to be decoded contains characters outside of the Latin1 range.", - DOMExceptionNameEnum.invalidCharacterError - ); - } - - if (/[^A-Za-z\d+/=]/.test(str) || str.length % 4 == 1) { - throw new DOMException( - "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", - DOMExceptionNameEnum.invalidCharacterError - ); - } - - let t = ''; - let p = -8; - let a = 0; - let c; - let d; - for (let i = 0; i < str.length; i++) { - if ((c = base64list.indexOf(str.charAt(i))) < 0) { - continue; - } - a = (a << 6) | (c & 63); - if ((p += 6) >= 0) { - d = (a >> p) & 255; - if (c !== 64) { - t += String.fromCharCode(d); - } - a &= 63; - p -= 8; - } - } - return t; -}; - -exports = { btoa: btoa, atob: atob }; diff --git a/packages/happy-dom/test/nodes/node/NodeUtility.test.ts b/packages/happy-dom/test/nodes/node/NodeUtility.test.ts new file mode 100644 index 000000000..172081865 --- /dev/null +++ b/packages/happy-dom/test/nodes/node/NodeUtility.test.ts @@ -0,0 +1,106 @@ +import Window from '../../../src/window/Window'; +import Document from '../../../src/nodes/document/Document'; +import NodeUtility from '../../../src/nodes/node/NodeUtility'; +import NodeTypeEnum from '../../../src/nodes/node/NodeTypeEnum'; + +describe('NodeUtility', () => { + let window: Window; + let document: Document; + + beforeEach(() => { + window = new Window(); + document = window.document; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('isInclusiveAncestor()', () => { + it('Returns "true" if referenceNode is the same as ancestorNode.', () => { + const ancestorNode = document.createElement('div'); + const referenceNode = ancestorNode; + expect(NodeUtility.isInclusiveAncestor(ancestorNode, referenceNode)).toBe(true); + }); + + it('Returns "true" if ancestorNode is a parent of referenceNode.', () => { + const ancestorNode = document.createElement('div'); + const ancestorChildNode = document.createElement('div'); + const referenceNode = document.createElement('div'); + + ancestorChildNode.appendChild(referenceNode); + ancestorNode.appendChild(ancestorChildNode); + + expect(NodeUtility.isInclusiveAncestor(ancestorNode, referenceNode)).toBe(true); + }); + }); + + describe('isFollowing()', () => { + it('Returns "false" if nodeA is the same as nodeB.', () => { + const nodeA = document.createElement('div'); + const nodeB = nodeA; + expect(NodeUtility.isFollowing(nodeA, nodeB)).toBe(false); + }); + + it('Returns "true" if nodeA is the next sibling of nodeB.', () => { + const parent = document.createElement('div'); + const nodeA = document.createElement('div'); + const nodeB = document.createElement('div'); + + parent.appendChild(nodeB); + parent.appendChild(nodeA); + + expect(NodeUtility.isFollowing(nodeA, nodeB)).toBe(true); + }); + + it('Returns "true" if nodeA is child of a parent container that is the next sibling of the parent container of nodeB.', () => { + const container = document.createElement('div'); + const parentA = document.createElement('div'); + const parentB = document.createElement('div'); + const nodeA = document.createElement('div'); + const nodeB = document.createElement('div'); + + parentA.appendChild(nodeA); + parentB.appendChild(nodeB); + + container.appendChild(parentB); + container.appendChild(parentA); + + expect(NodeUtility.isFollowing(nodeA, nodeB)).toBe(true); + }); + }); + + describe('getNodeLength()', () => { + it(`Returns 0 if node type is ${NodeTypeEnum.documentTypeNode}.`, () => { + const documentType = document.implementation.createDocumentType( + 'qualifiedName', + 'publicId', + 'systemId' + ); + expect(NodeUtility.getNodeLength(documentType)).toBe(0); + }); + + it(`Returns data length if node type is ${NodeTypeEnum.textNode}.`, () => { + const textNode = document.createTextNode('text'); + expect(NodeUtility.getNodeLength(textNode)).toBe(4); + }); + + it(`Returns data length if node type is ${NodeTypeEnum.commentNode}.`, () => { + const comment = document.createComment('text'); + expect(NodeUtility.getNodeLength(comment)).toBe(4); + }); + + it(`Returns childNodes length as default.`, () => { + const div = document.createComment('div'); + const text1 = document.createTextNode('text'); + const text2 = document.createTextNode('text'); + const text3 = document.createTextNode('text'); + + div.appendChild(text1); + div.appendChild(text2); + div.appendChild(text3); + + expect(NodeUtility.getNodeLength(div)).toBe(3); + }); + }); +}); diff --git a/packages/happy-dom/test/range/Range.test.ts b/packages/happy-dom/test/range/Range.test.ts index 3b0418475..3fe15cbc0 100644 --- a/packages/happy-dom/test/range/Range.test.ts +++ b/packages/happy-dom/test/range/Range.test.ts @@ -153,7 +153,7 @@ describe('Range', () => { range.collapse(true); expect(range.startContainer === span.childNodes[0]).toBe(true); - expect(range.endContainer === span2.childNodes[0]).toBe(true); + expect(range.endContainer === span.childNodes[0]).toBe(true); expect(range.startOffset).toBe(1); expect(range.endOffset).toBe(1); expect(range.collapsed).toBe(true); @@ -220,7 +220,7 @@ describe('Range', () => { sourceRange.setStart(container.children[0].childNodes[0], 1); sourceRange.setEnd(container.children[1].childNodes[0], 10); - expect(range.compareBoundaryPoints(Range.START_TO_END, sourceRange)).toBe(0); + expect(range.compareBoundaryPoints(Range.START_TO_END, sourceRange)).toBe(1); }); }); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 299b9b072..3f5ce31f1 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -11,7 +11,6 @@ import Headers from '../../src/fetch/Headers'; import Response from '../../src/fetch/Response'; import Request from '../../src/fetch/Request'; import Selection from '../../src/selection/Selection'; -import { atob, btoa } from '../../lib/window/WindowBase64'; import DOMException from '../../src/exception/DOMException'; import DOMExceptionNameEnum from '../../src/exception/DOMExceptionNameEnum'; @@ -542,14 +541,14 @@ describe('Window', () => { describe('atob()', () => { it('Decode "hello my happy dom!"', function () { const encoded = 'aGVsbG8gbXkgaGFwcHkgZG9tIQ=='; - const decoded = atob(encoded); + const decoded = window.atob(encoded); expect(decoded).toBe('hello my happy dom!'); }); it('Decode Unicode (throw error)', function () { expect(() => { const data = '😄 hello my happy dom! 🐛'; - atob(data); + window.atob(data); }).toThrowError( new DOMException( "Failed to execute 'atob' on 'Window': The string to be decoded contains characters outside of the Latin1 range.", @@ -561,7 +560,7 @@ describe('Window', () => { it('Data not in base64list', function () { expect(() => { const data = '\x11GVsbG8gbXkgaGFwcHkgZG9tIQ=='; - atob(data); + window.atob(data); }).toThrowError( new DOMException( "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", @@ -572,7 +571,7 @@ describe('Window', () => { it('Data length not valid', function () { expect(() => { const data = 'aGVsbG8gbXkgaGFwcHkgZG9tI'; - atob(data); + window.atob(data); }).toThrowError( new DOMException( "Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.", @@ -585,14 +584,14 @@ describe('Window', () => { describe('btoa()', () => { it('Encode "hello my happy dom!"', function () { const data = 'hello my happy dom!'; - const encoded = btoa(data); + const encoded = window.btoa(data); expect(encoded).toBe('aGVsbG8gbXkgaGFwcHkgZG9tIQ=='); }); it('Encode Unicode (throw error)', function () { expect(() => { const data = '😄 hello my happy dom! 🐛'; - btoa(data); + window.btoa(data); }).toThrowError( new DOMException( "Failed to execute 'btoa' on 'Window': The string to be encoded contains characters outside of the Latin1 range.",