diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index db4974631..fe5aba889 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3,9 +3,10 @@ on: pull_request jobs: build: runs-on: ubuntu-latest + continue-on-error: true strategy: matrix: - node-version: [16] + node-version: [16, 18, 20] steps: - uses: actions/checkout@v3 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/form-data/FormData.ts b/packages/happy-dom/src/form-data/FormData.ts index 8c5f56809..db88dbfc5 100644 --- a/packages/happy-dom/src/form-data/FormData.ts +++ b/packages/happy-dom/src/form-data/FormData.ts @@ -75,7 +75,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/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 9fe4c4f5a..96cb78469 100644 --- a/packages/happy-dom/src/named-node-map/NamedNodeMap.ts +++ b/packages/happy-dom/src/named-node-map/NamedNodeMap.ts @@ -47,7 +47,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[PropertySymbol.namedItems][name] || null; diff --git a/packages/happy-dom/src/navigator/Navigator.ts b/packages/happy-dom/src/navigator/Navigator.ts index a8dd4b593..a9300746d 100644 --- a/packages/happy-dom/src/navigator/Navigator.ts +++ b/packages/happy-dom/src/navigator/Navigator.ts @@ -4,6 +4,8 @@ import IBrowserWindow from '../window/IBrowserWindow.js'; import Permissions from '../permissions/Permissions.js'; import Clipboard from '../clipboard/Clipboard.js'; import WindowBrowserSettingsReader from '../window/WindowBrowserSettingsReader.js'; +import Blob from '../file/Blob.js'; +import FormData from '../form-data/FormData.js'; /** * Browser Navigator API. @@ -212,6 +214,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/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index b1b829412..9e0a8f73b 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -381,10 +381,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/src/nodes/html-input-element/HTMLInputElementDateUtility.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElementDateUtility.ts index f89e14658..4d0446de4 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,26 @@ 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.getDay() !== 4) { - date.setUTCMonth(0, 1 + ((4 - date.getDay() + 7) % 7)); - } - return ( - date.getUTCFullYear() + - '-W' + - String(1 + Math.ceil((firstThursday - date.getTime()) / 604800000)).padStart(2, '0') + date = typeof date === 'number' ? new Date(date) : date; + // Copy date so don't modify original + 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 + date.setUTCDate(date.getUTCDate() + 4 - (date.getUTCDay() || 7)); + // Get first day of year + const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); + // Calculate full weeks to nearest Thursday + const weekNo = Math.ceil( + (((date) - (yearStart)) / 86400000 + 1) / 7 ); + return `${date.getUTCFullYear()}-W${weekNo < 10 ? '0' : ''}${weekNo}`; } + /** * Returns a date object for monday of given iso week string (\d\d\d\d-W\d\d) * @@ -37,7 +39,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/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]; diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index cb4a75982..2b8eb4e05 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -324,6 +324,7 @@ export default class BrowserWindow extends EventTarget implements IBrowserWindow public readonly TreeWalker = TreeWalker; public readonly MutationObserver = MutationObserver; public readonly MutationRecord = MutationRecord; + public readonly CSSStyleDeclaration = CSSStyleDeclaration; public readonly EventTarget = EventTarget; public readonly MessagePort = MessagePort; public readonly DataTransfer = DataTransfer; diff --git a/packages/happy-dom/src/window/IBrowserWindow.ts b/packages/happy-dom/src/window/IBrowserWindow.ts index 84e062dce..c03672db6 100644 --- a/packages/happy-dom/src/window/IBrowserWindow.ts +++ b/packages/happy-dom/src/window/IBrowserWindow.ts @@ -378,6 +378,7 @@ export default interface IBrowserWindow extends IEventTarget, INodeJSGlobal { readonly DOMParser: new () => DOMParser; readonly MutationObserver: typeof MutationObserver; readonly MutationRecord: typeof MutationRecord; + readonly CSSStyleDeclaration: typeof CSSStyleDeclaration; // Events onload: ((event: Event) => void) | null; 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' }, 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/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts index f0cc9e3ca..15efa77aa 100644 --- a/packages/happy-dom/test/nodes/element/Element.test.ts +++ b/packages/happy-dom/test/nodes/element/Element.test.ts @@ -1549,8 +1549,6 @@ describe('Element', () => { child.className = 'className'; (element.tagName) = 'tagName'; - (element.scrollLeft) = 10; - (element.scrollTop) = 10; // @ts-ignore element.namespaceURI = 'namespaceURI'; @@ -1560,8 +1558,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); 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/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') diff --git a/packages/happy-dom/test/window/BrowserWindow.test.ts b/packages/happy-dom/test/window/BrowserWindow.test.ts index 56c612a89..47ed73686 100644 --- a/packages/happy-dom/test/window/BrowserWindow.test.ts +++ b/packages/happy-dom/test/window/BrowserWindow.test.ts @@ -616,7 +616,7 @@ describe('BrowserWindow', () => { expect(((errorEvent)).error?.message).toBe('Test error'); expect(((errorEvent)).message).toBe('Test error'); resolve(null); - }, 15); + }, 20); }); }); }); @@ -675,7 +675,7 @@ describe('BrowserWindow', () => { expect(((errorEvent)).error?.message).toBe('Test error'); expect(((errorEvent)).message).toBe('Test error'); resolve(null); - }, 10); + }, 20); }); }); }); @@ -773,7 +773,7 @@ describe('BrowserWindow', () => { expect(((errorEvent)).error?.message).toBe('Test error'); expect(((errorEvent)).message).toBe('Test error'); resolve(null); - }, 10); + }, 20); }); }); }); @@ -832,7 +832,7 @@ describe('BrowserWindow', () => { expect(((errorEvent)).error?.message).toBe('Test error'); expect(((errorEvent)).message).toBe('Test error'); resolve(null); - }, 10); + }, 20); }); }); }); @@ -969,7 +969,7 @@ describe('BrowserWindow', () => { setTimeout(() => { expect((loadEvent).target).toBe(document); resolve(null); - }, 10); + }, 20); }); }); @@ -1024,7 +1024,7 @@ describe('BrowserWindow', () => { expect(window['test']).toBe('test'); resolve(null); - }, 10); + }, 20); }); }); @@ -1052,7 +1052,7 @@ describe('BrowserWindow', () => { expect((errorEvents[1].error).message).toBe('Timeout error'); resolve(null); - }, 10); + }, 20); }); }); }); @@ -1152,8 +1152,8 @@ describe('BrowserWindow', () => { expect((triggeredEvent).source).toBe(browserFrame.window); expect((triggeredEvent).lastEventId).toBe(''); resolve(null); - }, 10); - }, 10); + }, 20); + }, 20); }); }); @@ -1172,7 +1172,7 @@ describe('BrowserWindow', () => { setTimeout(() => { expect((triggeredEvent).data).toBe(message); resolve(null); - }, 10); + }, 20); }); });