diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index 74d7c43e..8f984ea3 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -70,6 +70,7 @@ import DocumentFragment from './nodes/document-fragment/DocumentFragment.js'; import DocumentType from './nodes/document-type/DocumentType.js'; import Document from './nodes/document/Document.js'; import DOMRect from './nodes/element/DOMRect.js'; +import DOMRectReadOnly from './nodes/element/DOMRectReadOnly.js'; import Element from './nodes/element/Element.js'; import HTMLCollection from './nodes/element/HTMLCollection.js'; import HTMLAnchorElement from './nodes/html-anchor-element/HTMLAnchorElement.js'; @@ -211,6 +212,7 @@ export { DOMException, DOMParser, DOMRect, + DOMRectReadOnly, DataTransfer, DataTransferItem, DataTransferItemList, diff --git a/packages/happy-dom/src/nodes/element/DOMRect.ts b/packages/happy-dom/src/nodes/element/DOMRect.ts index 0305962a..86d433a6 100644 --- a/packages/happy-dom/src/nodes/element/DOMRect.ts +++ b/packages/happy-dom/src/nodes/element/DOMRect.ts @@ -1,30 +1,47 @@ +import DOMRectReadOnly, { IDOMRectInit } from './DOMRectReadOnly.js'; +import * as PropertySymbol from '../../PropertySymbol.js'; + +/* eslint-disable jsdoc/require-jsdoc */ + /** * Bounding rect object. * * @see https://developer.mozilla.org/en-US/docs/Web/API/DOMRect */ -export default class DOMRect { - public x = 0; - public y = 0; - public width = 0; - public height = 0; - public top = 0; - public right = 0; - public bottom = 0; - public left = 0; - - /** - * Constructor. - * - * @param [x] X position. - * @param [y] Y position. - * @param [width] Width. - * @param [height] Height. - */ - constructor(x?, y?, width?, height?) { - this.x = x || 0; - this.y = y || 0; - this.width = width || 0; - this.height = height || 0; +export default class DOMRect extends DOMRectReadOnly { + public set x(value: number) { + this[PropertySymbol.x] = value; + } + + public get x(): number { + return this[PropertySymbol.x]; + } + + public set y(value: number) { + this[PropertySymbol.y] = value; + } + + public get y(): number { + return this[PropertySymbol.y]; + } + + public set width(value: number) { + this[PropertySymbol.width] = value; + } + + public get width(): number { + return this[PropertySymbol.width]; + } + + public set height(value: number) { + this[PropertySymbol.height] = value; + } + + public get height(): number { + return this[PropertySymbol.height]; + } + + public static fromRect(other: IDOMRectInit): DOMRect { + return new DOMRect(other.x, other.y, other.width, other.height); } } diff --git a/packages/happy-dom/src/nodes/element/DOMRectReadOnly.ts b/packages/happy-dom/src/nodes/element/DOMRectReadOnly.ts new file mode 100644 index 00000000..65c8bc33 --- /dev/null +++ b/packages/happy-dom/src/nodes/element/DOMRectReadOnly.ts @@ -0,0 +1,86 @@ +import * as PropertySymbol from '../../PropertySymbol.js'; + +/* eslint-disable jsdoc/require-jsdoc */ + +/** + * Bounding rect readonly object. + * + * @see https://drafts.fxtf.org/geometry/#DOMRect + */ +export default class DOMRectReadOnly implements IDOMRectInit { + protected [PropertySymbol.x]: number = 0; + protected [PropertySymbol.y]: number = 0; + protected [PropertySymbol.width]: number = 0; + protected [PropertySymbol.height]: number = 0; + + /** + * Constructor. + * + * @param [x] X position. + * @param [y] Y position. + * @param [width] Width. + * @param [height] Height. + */ + constructor(x?: number | null, y?: number | null, width?: number | null, height?: number | null) { + this[PropertySymbol.x] = x !== undefined && x !== null ? Number(x) : 0; + this[PropertySymbol.y] = y !== undefined && y !== null ? Number(y) : 0; + this[PropertySymbol.width] = width !== undefined && width !== null ? Number(width) : 0; + this[PropertySymbol.height] = height !== undefined && height !== null ? Number(height) : 0; + } + + public get x(): number { + return this[PropertySymbol.x]; + } + + public get y(): number { + return this[PropertySymbol.y]; + } + + public get width(): number { + return this[PropertySymbol.width]; + } + + public get height(): number { + return this[PropertySymbol.height]; + } + + public get top(): number { + return Math.min(this[PropertySymbol.y], this[PropertySymbol.y] + this[PropertySymbol.height]); + } + + public get right(): number { + return Math.max(this[PropertySymbol.x], this[PropertySymbol.x] + this[PropertySymbol.width]); + } + + public get bottom(): number { + return Math.max(this[PropertySymbol.y], this[PropertySymbol.y] + this[PropertySymbol.height]); + } + + public get left(): number { + return Math.min(this[PropertySymbol.x], this[PropertySymbol.x] + this[PropertySymbol.width]); + } + + public toJSON(): object { + return { + x: this.x, + y: this.y, + width: this.width, + height: this.height, + top: this.top, + right: this.right, + bottom: this.bottom, + left: this.left + }; + } + + public static fromRect(other: IDOMRectInit): DOMRectReadOnly { + return new DOMRectReadOnly(other.x, other.y, other.width, other.height); + } +} + +export interface IDOMRectInit { + readonly x: number; + readonly y: number; + readonly width: number; + readonly height: number; +} diff --git a/packages/happy-dom/src/window/BrowserWindow.ts b/packages/happy-dom/src/window/BrowserWindow.ts index b3573f39..70a6e827 100644 --- a/packages/happy-dom/src/window/BrowserWindow.ts +++ b/packages/happy-dom/src/window/BrowserWindow.ts @@ -97,6 +97,7 @@ import Plugin from '../navigator/Plugin.js'; import PluginArray from '../navigator/PluginArray.js'; import Fetch from '../fetch/Fetch.js'; import DOMRect from '../nodes/element/DOMRect.js'; +import DOMRectReadOnly from '../nodes/element/DOMRectReadOnly.js'; import VMGlobalPropertyScript from './VMGlobalPropertyScript.js'; import VM from 'vm'; import { Buffer } from 'buffer'; @@ -372,6 +373,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public readonly PluginArray = PluginArray; public readonly FileList = FileList; public readonly DOMRect = DOMRect; + public readonly DOMRectReadOnly = DOMRectReadOnly; public readonly RadioNodeList = RadioNodeList; public readonly ValidityState = ValidityState; public readonly Headers = Headers; diff --git a/packages/happy-dom/test/nodes/element/DOMRect.test.ts b/packages/happy-dom/test/nodes/element/DOMRect.test.ts new file mode 100644 index 00000000..b8a0b46c --- /dev/null +++ b/packages/happy-dom/test/nodes/element/DOMRect.test.ts @@ -0,0 +1,156 @@ +import { afterEach, describe, it, expect, vi } from 'vitest'; +import DOMRect from '../../../src/nodes/element/DOMRect'; + +describe('DOMRect', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor()', () => { + it('Sets properties.', () => { + const rect = new DOMRect(1, 2, 3, 4); + expect(rect.x).toBe(1); + expect(rect.y).toBe(2); + expect(rect.width).toBe(3); + expect(rect.height).toBe(4); + + const rect2 = new DOMRect(null, null, null, 4); + expect(rect2.x).toBe(0); + expect(rect2.y).toBe(0); + expect(rect2.width).toBe(0); + expect(rect2.height).toBe(4); + + const rect3 = new DOMRect(); + expect(rect3.x).toBe(0); + expect(rect3.y).toBe(0); + expect(rect3.width).toBe(0); + expect(rect3.height).toBe(0); + + const rect4 = new DOMRect( + ('nan'), + ('nan'), + ('nan'), + ('nan') + ); + expect(isNaN(rect4.x)).toBe(true); + expect(isNaN(rect4.y)).toBe(true); + expect(isNaN(rect4.width)).toBe(true); + expect(isNaN(rect4.height)).toBe(true); + }); + }); + + describe('set x()', () => { + it('Sets rect x property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + rect.x = 2; + expect(rect.x).toBe(2); + }); + }); + + describe('get x()', () => { + it('Returns rect x property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + expect(rect.x).toBe(1); + }); + }); + + describe('set y()', () => { + it('Sets rect y property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + rect.y = 3; + expect(rect.y).toBe(3); + }); + }); + + describe('get y()', () => { + it('Returns rect y property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + expect(rect.y).toBe(2); + }); + }); + + describe('set width()', () => { + it('Sets rect y property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + rect.width = 4; + expect(rect.width).toBe(4); + }); + }); + + describe('get width()', () => { + it('Returns rect y property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + expect(rect.width).toBe(3); + }); + }); + + describe('set height()', () => { + it('Sets rect height property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + rect.height = 5; + expect(rect.height).toBe(5); + }); + }); + + describe('get height()', () => { + it('Returns rect height property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + expect(rect.height).toBe(4); + }); + }); + + describe('get top()', () => { + it('Returns rect top property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + expect(rect.top).toBe(2); + }); + }); + + describe('get right()', () => { + it('Returns rect right property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + expect(rect.right).toBe(4); + }); + }); + + describe('get bottom()', () => { + it('Returns rect bottom property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + expect(rect.bottom).toBe(6); + }); + }); + + describe('get left()', () => { + it('Returns rect left property.', () => { + const rect = new DOMRect(1, 2, 3, 4); + expect(rect.left).toBe(1); + }); + }); + + describe('fromRect()', () => { + it('Creates DOMRect instance', () => { + const rect = DOMRect.fromRect({ x: 1, y: 2, width: 3, height: 4 }); + expect(rect instanceof DOMRect).toBe(true); + expect(rect.x).toBe(1); + expect(rect.y).toBe(2); + expect(rect.width).toBe(3); + expect(rect.height).toBe(4); + }); + }); + + describe('toJSON()', () => { + it('Returns rect as JSON.', () => { + const rect = new DOMRect(1, 2, 3, 4); + expect(rect.toJSON()).toEqual({ + x: 1, + y: 2, + width: 3, + height: 4, + top: 2, + right: 4, + bottom: 6, + left: 1 + }); + }); + }); +}); diff --git a/packages/happy-dom/test/nodes/element/DOMRectReadOnly.test.ts b/packages/happy-dom/test/nodes/element/DOMRectReadOnly.test.ts new file mode 100644 index 00000000..c179eeda --- /dev/null +++ b/packages/happy-dom/test/nodes/element/DOMRectReadOnly.test.ts @@ -0,0 +1,124 @@ +import { afterEach, describe, it, expect, vi } from 'vitest'; +import DOMRectReadOnly from '../../../src/nodes/element/DOMRectReadOnly'; + +describe('DOMRectReadOnly', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor()', () => { + it('Sets properties.', () => { + const rect = new DOMRectReadOnly(1, 2, 3, 4); + expect(rect.x).toBe(1); + expect(rect.y).toBe(2); + expect(rect.width).toBe(3); + expect(rect.height).toBe(4); + + const rect2 = new DOMRectReadOnly(null, null, null, 4); + expect(rect2.x).toBe(0); + expect(rect2.y).toBe(0); + expect(rect2.width).toBe(0); + expect(rect2.height).toBe(4); + + const rect3 = new DOMRectReadOnly(); + expect(rect3.x).toBe(0); + expect(rect3.y).toBe(0); + expect(rect3.width).toBe(0); + expect(rect3.height).toBe(0); + + const rect4 = new DOMRectReadOnly( + ('nan'), + ('nan'), + ('nan'), + ('nan') + ); + expect(isNaN(rect4.x)).toBe(true); + expect(isNaN(rect4.y)).toBe(true); + expect(isNaN(rect4.width)).toBe(true); + expect(isNaN(rect4.height)).toBe(true); + }); + }); + + describe('get x()', () => { + it('Returns rect x property.', () => { + const rect = new DOMRectReadOnly(1, 2, 3, 4); + expect(rect.x).toBe(1); + }); + }); + + describe('get y()', () => { + it('Returns rect y property.', () => { + const rect = new DOMRectReadOnly(1, 2, 3, 4); + expect(rect.y).toBe(2); + }); + }); + + describe('get width()', () => { + it('Returns rect y property.', () => { + const rect = new DOMRectReadOnly(1, 2, 3, 4); + expect(rect.width).toBe(3); + }); + }); + + describe('get height()', () => { + it('Returns rect height property.', () => { + const rect = new DOMRectReadOnly(1, 2, 3, 4); + expect(rect.height).toBe(4); + }); + }); + + describe('get top()', () => { + it('Returns rect top property.', () => { + const rect = new DOMRectReadOnly(1, 2, 3, 4); + expect(rect.top).toBe(2); + }); + }); + + describe('get right()', () => { + it('Returns rect right property.', () => { + const rect = new DOMRectReadOnly(1, 2, 3, 4); + expect(rect.right).toBe(4); + }); + }); + + describe('get bottom()', () => { + it('Returns rect bottom property.', () => { + const rect = new DOMRectReadOnly(1, 2, 3, 4); + expect(rect.bottom).toBe(6); + }); + }); + + describe('get left()', () => { + it('Returns rect left property.', () => { + const rect = new DOMRectReadOnly(1, 2, 3, 4); + expect(rect.left).toBe(1); + }); + }); + + describe('fromRect()', () => { + it('Creates DOMRectReadOnly instance', () => { + const rect = DOMRectReadOnly.fromRect({ x: 1, y: 2, width: 3, height: 4 }); + expect(rect instanceof DOMRectReadOnly).toBe(true); + expect(rect.x).toBe(1); + expect(rect.y).toBe(2); + expect(rect.width).toBe(3); + expect(rect.height).toBe(4); + }); + }); + + describe('toJSON()', () => { + it('Returns rect as JSON.', () => { + const rect = new DOMRectReadOnly(1, 2, 3, 4); + expect(rect.toJSON()).toEqual({ + x: 1, + y: 2, + width: 3, + height: 4, + top: 2, + right: 4, + bottom: 6, + left: 1 + }); + }); + }); +});