From 4033abc443e173e28c2301a2109b2c3b3bf76248 Mon Sep 17 00:00:00 2001 From: goring <67476208+goring@users.noreply.github.com> Date: Sat, 7 Oct 2023 12:58:46 +0100 Subject: [PATCH 01/14] #1123@patch: Fixes typo in documentation. --- packages/happy-dom/src/named-node-map/INamedNodeMap.ts | 2 +- packages/happy-dom/src/named-node-map/NamedNodeMap.ts | 2 +- packages/happy-dom/src/query-selector/SelectorParser.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/happy-dom/src/named-node-map/INamedNodeMap.ts b/packages/happy-dom/src/named-node-map/INamedNodeMap.ts index 1cf23829b..395288894 100644 --- a/packages/happy-dom/src/named-node-map/INamedNodeMap.ts +++ b/packages/happy-dom/src/named-node-map/INamedNodeMap.ts @@ -22,7 +22,7 @@ export default interface INamedNodeMap { * Returns named item. * * @param name Name. - * @returns Itme. + * @returns Item. */ getNamedItem(name: string): IAttr | null; diff --git a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts index 7dcf80035..d7522c70b 100644 --- a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts +++ b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts @@ -46,7 +46,7 @@ export default class NamedNodeMap implements INamedNodeMap { * Returns named item. * * @param name Name. - * @returns Itme. + * @returns Item. */ public getNamedItem(name: string): IAttr | null { return this._namedItems[name] || null; diff --git a/packages/happy-dom/src/query-selector/SelectorParser.ts b/packages/happy-dom/src/query-selector/SelectorParser.ts index a384f8120..5f5d05909 100644 --- a/packages/happy-dom/src/query-selector/SelectorParser.ts +++ b/packages/happy-dom/src/query-selector/SelectorParser.ts @@ -63,7 +63,7 @@ export default class SelectorParser { * Parses a selector string and returns an instance of SelectorItem. * * @param selector Selector. - * @returns Selector itme. + * @returns Selector item. */ public static getSelectorItem(selector: string): SelectorItem { return this.getSelectorGroups(selector)[0][0]; From 677a9d5bbcb8c41ddc5a46ce33ed2fe7a6c7651b Mon Sep 17 00:00:00 2001 From: Armand Abric Date: Wed, 18 Oct 2023 12:08:51 +0200 Subject: [PATCH 02/14] #1136@patch: Fix FormData.forEach callback parameters order. --- packages/happy-dom/src/form-data/FormData.ts | 2 +- packages/happy-dom/test/form-data/FormData.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/happy-dom/src/form-data/FormData.ts b/packages/happy-dom/src/form-data/FormData.ts index ba0ae0462..126985ff2 100644 --- a/packages/happy-dom/src/form-data/FormData.ts +++ b/packages/happy-dom/src/form-data/FormData.ts @@ -70,7 +70,7 @@ export default class FormData implements Iterable<[string, string | File]> { */ public forEach(callback: (key: string, value: string | File, thisArg: FormData) => void): void { for (const entry of this._entries) { - callback.call(this, entry.name, entry.value, this); + callback.call(this, entry.value, entry.name, this); } } diff --git a/packages/happy-dom/test/form-data/FormData.test.ts b/packages/happy-dom/test/form-data/FormData.test.ts index b0ab4b893..8c35edcb5 100644 --- a/packages/happy-dom/test/form-data/FormData.test.ts +++ b/packages/happy-dom/test/form-data/FormData.test.ts @@ -92,7 +92,7 @@ describe('FormData', () => { formData.set('key2', 'value2'); const values: Array<{ key: string; value: string | File }> = []; - formData.forEach((key, value) => values.push({ key, value })); + formData.forEach((value, key) => values.push({ key, value })); expect(values).toEqual([ { key: 'key1', value: 'value1' }, From 493dd8045a71053d58ac05aec3578363645467be Mon Sep 17 00:00:00 2001 From: Matthew Lieder Date: Tue, 24 Oct 2023 14:04:14 -0500 Subject: [PATCH 03/14] #1140@patch: Fix Node 18.18.2+ support in global-registrator. --- packages/global-registrator/src/GlobalRegistrator.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/global-registrator/src/GlobalRegistrator.ts b/packages/global-registrator/src/GlobalRegistrator.ts index db03c78c7..0a0c2329d 100644 --- a/packages/global-registrator/src/GlobalRegistrator.ts +++ b/packages/global-registrator/src/GlobalRegistrator.ts @@ -25,7 +25,11 @@ export default class GlobalRegistrator { if (global[key] !== window[key] && !IGNORE_LIST.includes(key)) { this.registered[key] = global[key] !== window[key] && global[key] !== undefined ? global[key] : null; - global[key] = typeof window[key] === 'function' ? window[key].bind(global) : window[key]; + + // Only bind functions that aren't used as classes, since bound functions can't be extended. + const bind = typeof window[key] === 'function' && !isClassLikeName(key); + + global[key] = bind ? window[key].bind(global) : window[key]; } } @@ -56,3 +60,7 @@ export default class GlobalRegistrator { this.registered = null; } } + +function isClassLikeName(name: string): boolean { + return name[0] === name[0].toUpperCase(); +} From 3d1c8c6e0de20ebe5a386a41a6051992a35bcc0a Mon Sep 17 00:00:00 2001 From: David Ortner Date: Wed, 25 Oct 2023 21:24:27 +0200 Subject: [PATCH 04/14] #309@trivial: Starts on implementation. --- .gitignore | 1 + packages/happy-dom/src/navigator/Navigator.ts | 20 ++++ packages/happy-dom/src/window/IWindow.ts | 1 + packages/happy-dom/src/window/Window.ts | 1 + packages/happy-dom/src/worker/Worker.ts | 61 ++++++++++ .../test/navigator/Navigator.test.ts | 110 ++++++++++++++++++ packages/happy-dom/test/window/Window.test.ts | 50 +------- 7 files changed, 196 insertions(+), 48 deletions(-) create mode 100644 packages/happy-dom/src/worker/Worker.ts create mode 100644 packages/happy-dom/test/navigator/Navigator.test.ts diff --git a/.gitignore b/.gitignore index c87bd1984..0fd8f798c 100755 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules tmp lerna-debug.log +.DS_Store .vscode .idea .turbo \ No newline at end of file diff --git a/packages/happy-dom/src/navigator/Navigator.ts b/packages/happy-dom/src/navigator/Navigator.ts index a9d50af99..65bfec7e5 100644 --- a/packages/happy-dom/src/navigator/Navigator.ts +++ b/packages/happy-dom/src/navigator/Navigator.ts @@ -3,6 +3,8 @@ import PluginArray from './PluginArray.js'; import IWindow from '../window/IWindow.js'; import Permissions from '../permissions/Permissions.js'; import Clipboard from '../clipboard/Clipboard.js'; +import Blob from '../file/Blob.js'; +import FormData from '../form-data/FormData.js'; /** * Browser Navigator API. @@ -211,6 +213,24 @@ export default class Navigator { return new PluginArray([]); } + /** + * Sends an HTTP POST request containing a small amount of data to a web server. + * + * @param url URL. + * @param data Data. + * @returns "true" if the user agent successfully queued the data for transfer. Otherwise, it returns "false". + */ + public sendBeacon( + url: string, + data: string | Blob | ArrayBuffer | ArrayBufferView | FormData + ): boolean { + this.#ownerWindow.fetch(url, { + method: 'POST', + body: data + }); + return true; + } + /** * Returns the object as a string. * diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index 7666e7a46..c79151d6f 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -384,6 +384,7 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly DOMParser: typeof DOMParser; readonly MutationObserver: typeof MutationObserver; readonly MutationRecord: typeof MutationRecord; + readonly CSSStyleDeclaration: typeof CSSStyleDeclaration; // Events onload: (event: Event) => void; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index b66b15e11..d94bd337f 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -456,6 +456,7 @@ export default class Window extends EventTarget implements IWindow { public readonly Image; public readonly DocumentFragment; public readonly Audio; + public readonly CSSStyleDeclaration = CSSStyleDeclaration; // Events public onload: (event: Event) => void = null; diff --git a/packages/happy-dom/src/worker/Worker.ts b/packages/happy-dom/src/worker/Worker.ts new file mode 100644 index 000000000..c168c827b --- /dev/null +++ b/packages/happy-dom/src/worker/Worker.ts @@ -0,0 +1,61 @@ +import EventTarget from '../event/EventTarget.js'; +import Event from '../event/Event.js'; +import IEventListener from '../event/IEventListener.js'; +import MediaQueryListEvent from '../event/events/MediaQueryListEvent.js'; +import IDocument from '../nodes/document/IDocument.js'; +import URL from '../url/URL.js'; + +/** + * Worker. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/Worker. + */ +export default class Worker extends EventTarget { + public onchange: (event: Event) => void = null; + + // Needs to be injected by sub-class. + protected _ownerDocument: IDocument; + #url: URL; + #options: { type: 'classic' | 'module'; credentials: 'omit' | 'same-origin' | 'include' }; + #window: IWindow; + + /** + * Constructor. + * + * @param options Options. + * @param options.ownerWindow Owner window. + * @param options.media Media. + * @param [options.rootFontSize] Root font size. + * @param url + * @param options.type + * @param options.credentials + */ + constructor( + url: string, + options: { type: 'classic' | 'module'; credentials: 'omit' | 'same-origin' | 'include' } + ) { + super(); + this.#url = new URL(url); + this.#options = options; + } + + /** + * @override + */ + public addEventListener(type: string, listener: IEventListener | ((event: Event) => void)): void { + super.addEventListener(type, listener); + if (type === 'change') { + let matchesState = false; + const resizeListener = (): void => { + const matches = this.matches; + if (matches !== matchesState) { + matchesState = matches; + this.dispatchEvent(new MediaQueryListEvent('change', { matches, media: this.media })); + } + }; + listener['_windowResizeListener'] = resizeListener; + this._ownerWindow.addEventListener('resize', resizeListener); + } + } +} diff --git a/packages/happy-dom/test/navigator/Navigator.test.ts b/packages/happy-dom/test/navigator/Navigator.test.ts new file mode 100644 index 000000000..dc1054433 --- /dev/null +++ b/packages/happy-dom/test/navigator/Navigator.test.ts @@ -0,0 +1,110 @@ +import Window from '../../src/window/Window.js'; +import IWindow from '../../src/window/IWindow.js'; +import Navigator from '../../src/navigator/Navigator.js'; +import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; +import Permissions from '../../src/permissions/Permissions.js'; +import Clipboard from '../../src/clipboard/Clipboard.js'; +import PackageVersion from '../../src/version.js'; +import IResponse from '../../src/fetch/types/IResponse.js'; +import IRequest from '../../src/fetch/types/IRequest.js'; +import Fetch from '../../src/fetch/Fetch.js'; +import Stream from 'stream'; + +const GET_NAVIGATOR_PLATFORM = (): string => { + return ( + 'X11; ' + + process.platform.charAt(0).toUpperCase() + + process.platform.slice(1) + + ' ' + + process.arch + ); +}; +const PROPERTIES = { + appCodeName: 'Mozilla', + appName: 'Netscape', + appVersion: `5.0 (${GET_NAVIGATOR_PLATFORM()}) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${ + PackageVersion.version + }`, + cookieEnabled: true, + credentials: null, + doNotTrack: 'unspecified', + geolocation: null, + hardwareConcurrency: 8, + language: 'en-US', + languages: ['en-US', 'en'], + locks: null, + maxTouchPoints: 0, + mimeTypes: { + length: 0 + }, + onLine: true, + permissions: new Permissions(), + platform: GET_NAVIGATOR_PLATFORM(), + plugins: { + length: 0 + }, + product: 'Gecko', + productSub: '20100101', + userAgent: `Mozilla/5.0 (${GET_NAVIGATOR_PLATFORM()}) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${ + PackageVersion.version + }`, + vendor: '', + vendorSub: '', + webdriver: true +}; + +describe('Window', () => { + let window: IWindow; + + beforeEach(() => { + window = new Window(); + }); + + afterEach(() => { + resetMockedModules(); + vi.restoreAllMocks(); + }); + + describe('constructor()', () => { + it('Is instanceof Navigator.', () => { + expect(window.navigator instanceof Navigator).toBe(true); + }); + }); + + Object.keys(PROPERTIES).forEach((property) => { + describe(`get ${property}()`, () => { + it('Returns an instance of Navigator with browser data.', () => { + expect(window.navigator[property]).toEqual(PROPERTIES[property]); + }); + }); + }); + + describe('get clipboard()', () => { + it('Returns an instance of Clipboard.', () => { + expect(window.navigator.clipboard).toEqual(new Clipboard(window)); + }); + }); + + describe('sendBeacon()', () => { + it('Sends a beacon request.', async () => { + const expectedURL = 'https://localhost:8080/path/'; + let request: IRequest | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise { + request = this.request; + return Promise.resolve({}); + }); + + window.navigator.sendBeacon(expectedURL, 'test-data'); + + const chunks: Buffer[] = []; + + for await (const chunk of ((request)).body) { + chunks.push(Buffer.from(chunk)); + } + + expect(Buffer.concat(chunks).toString()).toBe('test-data'); + expect(((request)).url).toBe(expectedURL); + }); + }); +}); diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 3af67d868..39815b673 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -6,7 +6,6 @@ import ResourceFetch from '../../src/fetch/ResourceFetch.js'; import IHTMLScriptElement from '../../src/nodes/html-script-element/IHTMLScriptElement.js'; import Window from '../../src/window/Window.js'; import IWindow from '../../src/window/IWindow.js'; -import Navigator from '../../src/navigator/Navigator.js'; import Headers from '../../src/fetch/Headers.js'; import Selection from '../../src/selection/Selection.js'; import DOMException from '../../src/exception/DOMException.js'; @@ -26,8 +25,6 @@ import '../types.d.js'; import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest'; import VirtualConsole from '../../src/console/VirtualConsole.js'; import VirtualConsolePrinter from '../../src/console/VirtualConsolePrinter.js'; -import Permissions from '../../src/permissions/Permissions.js'; -import Clipboard from '../../src/clipboard/Clipboard.js'; import PackageVersion from '../../src/version.js'; import { IHTMLDialogElement } from '../../src/index.js'; @@ -455,49 +452,6 @@ describe('Window', () => { }); }); - describe('get navigator()', () => { - it('Returns an instance of Navigator with browser data.', () => { - const platform = GET_NAVIGATOR_PLATFORM(); - - expect(window.navigator instanceof Navigator).toBe(true); - - const referenceValues = { - appCodeName: 'Mozilla', - appName: 'Netscape', - appVersion: `5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}`, - cookieEnabled: true, - credentials: null, - doNotTrack: 'unspecified', - geolocation: null, - hardwareConcurrency: 8, - language: 'en-US', - languages: ['en-US', 'en'], - locks: null, - maxTouchPoints: 0, - mimeTypes: { - length: 0 - }, - onLine: true, - permissions: new Permissions(), - clipboard: new Clipboard(window), - platform, - plugins: { - length: 0 - }, - product: 'Gecko', - productSub: '20100101', - userAgent: `Mozilla/5.0 (${platform}) AppleWebKit/537.36 (KHTML, like Gecko) HappyDOM/${PackageVersion.version}`, - vendor: '', - vendorSub: '', - webdriver: true - }; - - for (const propertyKey in referenceValues) { - expect(window.navigator[propertyKey]).toEqual(referenceValues[propertyKey]); - } - }); - }); - describe('eval()', () => { it('Respects direct eval.', () => { const result = window.eval(` @@ -1448,7 +1402,7 @@ describe('Window', () => { (window.parent) = parent; - window.addEventListener('message', (event) => (triggeredEvent = event)); + window.addEventListener('message', (event) => (triggeredEvent = event)); window.postMessage(message); expect(triggeredEvent).toBe(null); @@ -1481,7 +1435,7 @@ describe('Window', () => { }; let triggeredEvent: MessageEvent | null = null; - window.addEventListener('message', (event) => (triggeredEvent = event)); + window.addEventListener('message', (event) => (triggeredEvent = event)); window.postMessage(message); expect(triggeredEvent).toBe(null); From d84392388cabb47a8966def655a9cb952bb7338b Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 26 Oct 2023 11:23:05 +0200 Subject: [PATCH 05/14] #309@minor: Adds support for Navigator.sendBeacon(). Adds support for the property Window.CSSStyleDeclaration. --- packages/happy-dom/src/window/IWindow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index c79151d6f..ce21b64d4 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -134,7 +134,7 @@ import ClipboardItem from '../clipboard/ClipboardItem.js'; import ClipboardEvent from '../event/events/ClipboardEvent.js'; /** - * Window without dependencies to server side specific packages. + * Browser window. */ export default interface IWindow extends IEventTarget, INodeJSGlobal { // Happy DOM property. From d91e937080adf08a76341cf2124f8ca5f42d8dd1 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 26 Oct 2023 11:29:10 +0200 Subject: [PATCH 06/14] #309@trivial: Remove Worker class. --- packages/happy-dom/src/worker/Worker.ts | 61 ------------------------- 1 file changed, 61 deletions(-) delete mode 100644 packages/happy-dom/src/worker/Worker.ts diff --git a/packages/happy-dom/src/worker/Worker.ts b/packages/happy-dom/src/worker/Worker.ts deleted file mode 100644 index c168c827b..000000000 --- a/packages/happy-dom/src/worker/Worker.ts +++ /dev/null @@ -1,61 +0,0 @@ -import EventTarget from '../event/EventTarget.js'; -import Event from '../event/Event.js'; -import IEventListener from '../event/IEventListener.js'; -import MediaQueryListEvent from '../event/events/MediaQueryListEvent.js'; -import IDocument from '../nodes/document/IDocument.js'; -import URL from '../url/URL.js'; - -/** - * Worker. - * - * Reference: - * https://developer.mozilla.org/en-US/docs/Web/API/Worker. - */ -export default class Worker extends EventTarget { - public onchange: (event: Event) => void = null; - - // Needs to be injected by sub-class. - protected _ownerDocument: IDocument; - #url: URL; - #options: { type: 'classic' | 'module'; credentials: 'omit' | 'same-origin' | 'include' }; - #window: IWindow; - - /** - * Constructor. - * - * @param options Options. - * @param options.ownerWindow Owner window. - * @param options.media Media. - * @param [options.rootFontSize] Root font size. - * @param url - * @param options.type - * @param options.credentials - */ - constructor( - url: string, - options: { type: 'classic' | 'module'; credentials: 'omit' | 'same-origin' | 'include' } - ) { - super(); - this.#url = new URL(url); - this.#options = options; - } - - /** - * @override - */ - public addEventListener(type: string, listener: IEventListener | ((event: Event) => void)): void { - super.addEventListener(type, listener); - if (type === 'change') { - let matchesState = false; - const resizeListener = (): void => { - const matches = this.matches; - if (matches !== matchesState) { - matchesState = matches; - this.dispatchEvent(new MediaQueryListEvent('change', { matches, media: this.media })); - } - }; - listener['_windowResizeListener'] = resizeListener; - this._ownerWindow.addEventListener('resize', resizeListener); - } - } -} From c43cfcb7b486c3e0d7dc280e769008fa6db0d899 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Thu, 26 Oct 2023 12:05:31 +0200 Subject: [PATCH 07/14] #1136@trivial: Fixes failing unit test. --- packages/happy-dom/test/window/Window.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 39815b673..39a4363af 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -868,7 +868,7 @@ describe('Window', () => { window.setTimeout(async () => { await new Promise((resolve) => setTimeout(resolve, 0)); throw new window.Error('Test error'); - }); + }, 5); setTimeout(() => { expect(((errorEvent)).error).instanceOf(window.Error); expect(((errorEvent)).error?.message).toBe('Test error'); From a0a49546f3d3ac1e9095910db3b0538df79b9111 Mon Sep 17 00:00:00 2001 From: Matthew Lieder Date: Thu, 26 Oct 2023 14:05:21 -0500 Subject: [PATCH 08/14] #1144@patch: Run tests against newer Node.js versions Also fixed tests that were failing locally for me (running on Node.js 18), for various reasons. --- .github/workflows/pull_request.yml | 2 +- .../html-input-element/HTMLInputElementDateUtility.ts | 6 +++--- .../nodes/html-input-element/HTMLInputElement.test.ts | 1 + packages/happy-dom/test/window/Window.test.ts | 4 ++-- .../test/UncaughtExceptionObserver.test.ts | 10 ++++++---- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index db4974631..841bf8bab 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [16] + node-version: [16, 18, 20] steps: - uses: actions/checkout@v3 diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts index f89e14658..0187c1a4a 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts @@ -14,8 +14,8 @@ export default class HTMLInputElementDateUtility { date.setUTCDate(date.getUTCDate() - day + 3); const firstThursday = date.getTime(); date.setUTCMonth(0, 1); - if (date.getDay() !== 4) { - date.setUTCMonth(0, 1 + ((4 - date.getDay() + 7) % 7)); + if (date.getUTCDay() !== 4) { + date.setUTCMonth(0, 1 + ((4 - date.getUTCDay() + 7) % 7)); } return ( date.getUTCFullYear() + @@ -37,7 +37,7 @@ export default class HTMLInputElementDateUtility { } const date = new Date(`${Y}-01-01T00:00Z`); const jan4th = new Date(`${Y}-01-04T00:00Z`); - const jan4thDay = (jan4th.getDay() + 6) % 7; + const jan4thDay = (jan4th.getUTCDay() + 6) % 7; const ordinalDate = 1 + (Number(W) - 1) * 7 - jan4thDay + 3; date.setUTCDate(ordinalDate); if (date.getUTCFullYear() > Number(Y)) { diff --git a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts index abceaa4c8..1c181aa39 100644 --- a/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts +++ b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts @@ -390,6 +390,7 @@ describe('HTMLInputElement', () => { { type: 'time', value: '00:00', want: new Date('1970-01-01T00:00Z') }, { type: 'time', value: '12:00', want: new Date('1970-01-01T12:00Z') }, { type: 'time', value: '18:55', want: new Date('1970-01-01T18:55Z') }, + { type: 'week', value: '1981-W01', want: new Date('1980-12-29T00:00Z') }, { type: 'week', value: '2023-W22', want: new Date('2023-05-29T00:00Z') } ])(`Should return valid date for type $type with valid value`, ({ type, value, want }) => { element.type = type; diff --git a/packages/happy-dom/test/window/Window.test.ts b/packages/happy-dom/test/window/Window.test.ts index 39a4363af..41d2761e3 100644 --- a/packages/happy-dom/test/window/Window.test.ts +++ b/packages/happy-dom/test/window/Window.test.ts @@ -874,7 +874,7 @@ describe('Window', () => { expect(((errorEvent)).error?.message).toBe('Test error'); expect(((errorEvent)).message).toBe('Test error'); resolve(null); - }, 15); + }, 20); }); }); }); @@ -1030,7 +1030,7 @@ describe('Window', () => { expect(((errorEvent)).error?.message).toBe('Test error'); expect(((errorEvent)).message).toBe('Test error'); resolve(null); - }, 10); + }, 20); }); }); }); diff --git a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts index a3b42305f..7d5f9df22 100644 --- a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts +++ b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts @@ -70,15 +70,17 @@ async function itObservesUnhandledJavaScriptFetchRejections(): Promise { } if ( - errorEvent.error.message !== - 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED 127.0.0.1:3000' + !errorEvent.error.message.startsWith( + 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED' + ) ) { throw new Error('Error message not correct.'); } if ( - errorEvent.message !== - 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED 127.0.0.1:3000' + !errorEvent.message.startsWith( + 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED' + ) ) { throw new Error('Error message not correct.'); } From 676e3138182167b2c86b4d1ccb73ab00d4272dd0 Mon Sep 17 00:00:00 2001 From: Matthew Lieder Date: Thu, 26 Oct 2023 14:12:36 -0500 Subject: [PATCH 09/14] #1144@patch: Add continue-on-error. --- .github/workflows/pull_request.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 841bf8bab..fe5aba889 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3,6 +3,7 @@ on: pull_request jobs: build: runs-on: ubuntu-latest + continue-on-error: true strategy: matrix: node-version: [16, 18, 20] From a35f7b274562bbdaf170e11f4c4b83e5d7c173fa Mon Sep 17 00:00:00 2001 From: Matthew Lieder Date: Thu, 26 Oct 2023 14:36:31 -0500 Subject: [PATCH 10/14] #1144@patch: Fix flaky test. --- .../test/UncaughtExceptionObserver.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts index 7d5f9df22..ad55315b9 100644 --- a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts +++ b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts @@ -61,7 +61,12 @@ async function itObservesUnhandledJavaScriptFetchRejections(): Promise { `); - await new Promise((resolve) => setTimeout(resolve, 10)); + for (let i = 0; i < 10; i++) { + await new Promise((resolve) => setTimeout(resolve, 10)); + if (errorEvent) { + break; + } + } observer.disconnect(); From f26b0843b6ab180af7e2dbb452ddd85f093063d6 Mon Sep 17 00:00:00 2001 From: Matthew Lieder Date: Thu, 26 Oct 2023 15:56:59 -0500 Subject: [PATCH 11/14] #1144@patch: Fix test on Node 20. --- .../test/UncaughtExceptionObserver.test.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts index ad55315b9..8d5feaf2f 100644 --- a/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts +++ b/packages/uncaught-exception-observer/test/UncaughtExceptionObserver.test.ts @@ -74,19 +74,11 @@ async function itObservesUnhandledJavaScriptFetchRejections(): Promise { throw new Error('Error event not dispatched.'); } - if ( - !errorEvent.error.message.startsWith( - 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED' - ) - ) { + if (!errorEvent.error.message.startsWith('Fetch to "https://localhost:3000/404.js" failed.')) { throw new Error('Error message not correct.'); } - if ( - !errorEvent.message.startsWith( - 'Fetch to "https://localhost:3000/404.js" failed. Error: connect ECONNREFUSED' - ) - ) { + if (!errorEvent.message.startsWith('Fetch to "https://localhost:3000/404.js" failed.')) { throw new Error('Error message not correct.'); } } From 7b3ce5eb56899328f3c65797b575ee74e9fec991 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 27 Oct 2023 15:47:50 +0200 Subject: [PATCH 12/14] #1148@patch: Element.cloneNode() should not set scroll properties on the clone. --- packages/happy-dom/src/nodes/element/Element.ts | 4 ---- packages/happy-dom/test/nodes/element/Element.test.ts | 4 ---- 2 files changed, 8 deletions(-) diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index 35d47852a..3b807d157 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -379,10 +379,6 @@ export default class Element extends Node implements IElement { } (clone.tagName) = this.tagName; - clone.scrollLeft = this.scrollLeft; - clone.scrollTop = this.scrollTop; - clone.scrollWidth = this.scrollWidth; - clone.scrollHeight = this.scrollHeight; (clone.namespaceURI) = this.namespaceURI; return clone; diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts index 4436ddff3..e216c20ff 100644 --- a/packages/happy-dom/test/nodes/element/Element.test.ts +++ b/packages/happy-dom/test/nodes/element/Element.test.ts @@ -1551,8 +1551,6 @@ describe('Element', () => { child.className = 'className'; (element.tagName) = 'tagName'; - (element.scrollLeft) = 10; - (element.scrollTop) = 10; // @ts-ignore element.namespaceURI = 'namespaceURI'; @@ -1562,8 +1560,6 @@ describe('Element', () => { const clone = element.cloneNode(false); const clone2 = element.cloneNode(true); expect(clone.tagName).toBe('tagName'); - expect(clone.scrollLeft).toBe(10); - expect(clone.scrollTop).toBe(10); expect(clone.namespaceURI).toBe('namespaceURI'); expect(clone.children.length).toEqual(0); expect(clone2.children.length).toBe(1); From 49d489d01473e217bc5040a0824d47d70224d240 Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 27 Oct 2023 23:29:51 +0200 Subject: [PATCH 13/14] #1144@trivial: Updates Date ISO week function to a more battle tested. --- .../HTMLInputElementDateUtility.ts | 28 +++++++++++-------- ...ts => HTMLInputElementDateUtility.test.ts} | 4 +-- 2 files changed, 18 insertions(+), 14 deletions(-) rename packages/happy-dom/test/nodes/html-input-element/{HTMLInputDateUtility.test.ts => HTMLInputElementDateUtility.test.ts} (98%) diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts index 0187c1a4a..7ae7c33bd 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts @@ -5,24 +5,28 @@ export default class HTMLInputElementDateUtility { /** * Returns iso week number from given date * + * @see https://stackoverflow.com/a/6117889 * @param date Date or number. * @returns Iso-week string. */ public static dateIsoWeek(date: Date | number): string { - date = new Date(date); - const day = (date.getUTCDay() + 6) % 7; - date.setUTCDate(date.getUTCDate() - day + 3); - const firstThursday = date.getTime(); - date.setUTCMonth(0, 1); - if (date.getUTCDay() !== 4) { - date.setUTCMonth(0, 1 + ((4 - date.getUTCDay() + 7) % 7)); - } - return ( - date.getUTCFullYear() + - '-W' + - String(1 + Math.ceil((firstThursday - date.getTime()) / 604800000)).padStart(2, '0') + const parsedDate = typeof date === 'number' ? new Date(date) : date; + // Copy date so don't modify original + const newDate = new Date( + Date.UTC(parsedDate.getFullYear(), parsedDate.getMonth(), parsedDate.getDate()) + ); + // Set to nearest Thursday: current date + 4 - current day number + // Make Sunday's day number 7 + newDate.setUTCDate(newDate.getUTCDate() + 4 - (newDate.getUTCDay() || 7)); + // Get first day of year + const yearStart = new Date(Date.UTC(newDate.getUTCFullYear(), 0, 1)); + // Calculate full weeks to nearest Thursday + const weekNo = Math.ceil( + (((newDate) - (yearStart)) / 86400000 + 1) / 7 ); + return `${newDate.getUTCFullYear()}-W${weekNo < 10 ? '0' : ''}${weekNo}`; } + /** * Returns a date object for monday of given iso week string (\d\d\d\d-W\d\d) * diff --git a/packages/happy-dom/test/nodes/html-input-element/HTMLInputDateUtility.test.ts b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElementDateUtility.test.ts similarity index 98% rename from packages/happy-dom/test/nodes/html-input-element/HTMLInputDateUtility.test.ts rename to packages/happy-dom/test/nodes/html-input-element/HTMLInputElementDateUtility.test.ts index e2b4bcb34..72777fdc0 100644 --- a/packages/happy-dom/test/nodes/html-input-element/HTMLInputDateUtility.test.ts +++ b/packages/happy-dom/test/nodes/html-input-element/HTMLInputElementDateUtility.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import HTMLInputElementDateUtility from '../../../src/nodes/html-input-element/HTMLInputElementDateUtility.js'; describe('HTMLInputElementDateUtility', () => { - describe('dateToIsoWeek()', () => { + describe('dateIsoWeek()', () => { it('Returns the ISO week number', () => { expect(HTMLInputElementDateUtility.dateIsoWeek(new Date('2021-01-01'))).toBe('2020-W53'); expect(HTMLInputElementDateUtility.dateIsoWeek(new Date('2021-01-03'))).toBe('2020-W53'); @@ -35,7 +35,7 @@ describe('HTMLInputElementDateUtility', () => { }); }); - describe('IsoWeekToDate()', () => { + describe('isoWeekDate()', () => { it('Returns the ISO week number', () => { expect(HTMLInputElementDateUtility.isoWeekDate('2020-W53')).toEqual( new Date('2020-12-28T00:00Z') From a56ed6a42eb92a34e25140ed97e3026c539d35fd Mon Sep 17 00:00:00 2001 From: David Ortner Date: Fri, 27 Oct 2023 23:32:34 +0200 Subject: [PATCH 14/14] #1144@trivial: Updates Date ISO week function to a more battle tested. --- .../HTMLInputElementDateUtility.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts index 7ae7c33bd..4d0446de4 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts @@ -10,21 +10,19 @@ export default class HTMLInputElementDateUtility { * @returns Iso-week string. */ public static dateIsoWeek(date: Date | number): string { - const parsedDate = typeof date === 'number' ? new Date(date) : date; + date = typeof date === 'number' ? new Date(date) : date; // Copy date so don't modify original - const newDate = new Date( - Date.UTC(parsedDate.getFullYear(), parsedDate.getMonth(), parsedDate.getDate()) - ); + date = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); // Set to nearest Thursday: current date + 4 - current day number // Make Sunday's day number 7 - newDate.setUTCDate(newDate.getUTCDate() + 4 - (newDate.getUTCDay() || 7)); + date.setUTCDate(date.getUTCDate() + 4 - (date.getUTCDay() || 7)); // Get first day of year - const yearStart = new Date(Date.UTC(newDate.getUTCFullYear(), 0, 1)); + const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); // Calculate full weeks to nearest Thursday const weekNo = Math.ceil( - (((newDate) - (yearStart)) / 86400000 + 1) / 7 + (((date) - (yearStart)) / 86400000 + 1) / 7 ); - return `${newDate.getUTCFullYear()}-W${weekNo < 10 ? '0' : ''}${weekNo}`; + return `${date.getUTCFullYear()}-W${weekNo < 10 ? '0' : ''}${weekNo}`; } /**