From 2b6009031f71d7c2936b8fad0905d16a6a6e11de Mon Sep 17 00:00:00 2001 From: David Ortner Date: Mon, 2 Oct 2023 21:52:34 +0200 Subject: [PATCH] #833@minor: Adds support for Window.navigator.clipboard and Window.navigator.permissions. Improves support for DataTransfer. --- packages/happy-dom/src/clipboard/Clipboard.ts | 92 ++++++++++ .../happy-dom/src/clipboard/ClipboardItem.ts | 59 ++++++ .../config/NonImplemenetedElementClasses.ts | 52 ------ packages/happy-dom/src/event/DataTransfer.ts | 16 +- .../happy-dom/src/event/DataTransferItem.ts | 25 ++- .../src/event/DataTransferItemList.ts | 20 ++- .../src/event/NonImplementedEventTypes.ts | 45 ----- .../src/event/events/ClipboardEvent.ts | 22 +++ .../src/event/events/IClipboardEventInit.ts | 6 + packages/happy-dom/src/index.ts | 14 +- packages/happy-dom/src/navigator/Navigator.ts | 34 +++- .../html-input-element/HTMLInputElement.ts | 67 ++++--- .../HTMLTextAreaElement.ts | 2 + .../src/permissions/PermissionStatus.ts | 23 +++ .../happy-dom/src/permissions/Permissions.ts | 60 +++++++ packages/happy-dom/src/window/IWindow.ts | 160 ++++++++++++++--- packages/happy-dom/src/window/Window.ts | 169 ++++++++++++++---- 17 files changed, 661 insertions(+), 205 deletions(-) create mode 100644 packages/happy-dom/src/clipboard/Clipboard.ts create mode 100644 packages/happy-dom/src/clipboard/ClipboardItem.ts delete mode 100644 packages/happy-dom/src/config/NonImplemenetedElementClasses.ts delete mode 100644 packages/happy-dom/src/event/NonImplementedEventTypes.ts create mode 100644 packages/happy-dom/src/event/events/ClipboardEvent.ts create mode 100644 packages/happy-dom/src/event/events/IClipboardEventInit.ts create mode 100644 packages/happy-dom/src/permissions/PermissionStatus.ts create mode 100644 packages/happy-dom/src/permissions/Permissions.ts diff --git a/packages/happy-dom/src/clipboard/Clipboard.ts b/packages/happy-dom/src/clipboard/Clipboard.ts new file mode 100644 index 000000000..82aa7716b --- /dev/null +++ b/packages/happy-dom/src/clipboard/Clipboard.ts @@ -0,0 +1,92 @@ +import DOMException from '../exception/DOMException.js'; +import IWindow from '../window/IWindow.js'; +import ClipboardItem from './ClipboardItem.js'; +import Blob from '../file/Blob.js'; + +/** + * Clipboard API. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/Clipboard. + */ +export default class Clipboard { + #ownerWindow: IWindow; + #data: ClipboardItem[] = []; + + /** + * Constructor. + * + * @param ownerWindow Owner window. + */ + constructor(ownerWindow: IWindow) { + this.#ownerWindow = ownerWindow; + } + + /** + * Returns data. + * + * @returns Data. + */ + public async read(): Promise { + const permissionStatus = await this.#ownerWindow.navigator.permissions.query({ + name: 'clipboard-read' + }); + if (permissionStatus.state === 'denied') { + throw new DOMException(`Failed to execute 'read' on 'Clipboard': The request is not allowed`); + } + return this.#data; + } + + /** + * Returns text. + * + * @returns Text. + */ + public async readText(): Promise { + const permissionStatus = await this.#ownerWindow.navigator.permissions.query({ + name: 'clipboard-read' + }); + if (permissionStatus.state === 'denied') { + throw new DOMException(`Failed to execute 'read' on 'Clipboard': The request is not allowed`); + } + let text = ''; + for (const item of this.#data) { + text += await (await item.getType('text/plain')).text(); + } + return text; + } + + /** + * Writes data. + * + * @param data Data. + */ + public async write(data: ClipboardItem[]): Promise { + const permissionStatus = await this.#ownerWindow.navigator.permissions.query({ + name: 'clipboard-write' + }); + if (permissionStatus.state === 'denied') { + throw new DOMException( + `Failed to execute 'write' on 'Clipboard': The request is not allowed` + ); + } + this.#data = data; + } + + /** + * Writes text. + * + * @param text Text. + */ + public async writeText(text: string): Promise { + const permissionStatus = await this.#ownerWindow.navigator.permissions.query({ + name: 'clipboard-write' + }); + if (permissionStatus.state === 'denied') { + throw new DOMException( + `Failed to execute 'write' on 'Clipboard': The request is not allowed` + ); + } + this.#data = [new ClipboardItem({ 'text/plain': new Blob([text], { type: 'text/plain' }) })]; + } +} diff --git a/packages/happy-dom/src/clipboard/ClipboardItem.ts b/packages/happy-dom/src/clipboard/ClipboardItem.ts new file mode 100644 index 000000000..021fc762f --- /dev/null +++ b/packages/happy-dom/src/clipboard/ClipboardItem.ts @@ -0,0 +1,59 @@ +import DOMException from '../exception/DOMException.js'; +import Blob from '../file/Blob.js'; + +/** + * Clipboard Item API. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem. + */ +export default class ClipboardItem { + public readonly presentationStyle: 'unspecified' | 'inline' | 'attachment' = 'unspecified'; + #data: { [mimeType: string]: Blob }; + + /** + * Constructor. + * + * @param data Data. + * @param [options] Options. + * @param [options.presentationStyle] Presentation style. + */ + constructor( + data: { [mimeType: string]: Blob }, + options?: { presentationStyle?: 'unspecified' | 'inline' | 'attachment' } + ) { + for (const mimeType of Object.keys(data)) { + if (mimeType !== data[mimeType].type) { + throw new DOMException(`Type ${mimeType} does not match the blob's type`); + } + } + this.#data = data; + if (options?.presentationStyle) { + this.presentationStyle = options.presentationStyle; + } + } + + /** + * Returns types. + * + * @returns Types. + */ + public get types(): string[] { + return Object.keys(this.#data); + } + + /** + * Returns data by type. + * + * @param type Type. + * @returns Data. + */ + public async getType(type: string): Promise { + if (!this.#data[type]) { + throw new DOMException( + "Failed to execute 'getType' on 'ClipboardItem': The type was not found" + ); + } + return this.#data[type]; + } +} diff --git a/packages/happy-dom/src/config/NonImplemenetedElementClasses.ts b/packages/happy-dom/src/config/NonImplemenetedElementClasses.ts deleted file mode 100644 index 6622795a7..000000000 --- a/packages/happy-dom/src/config/NonImplemenetedElementClasses.ts +++ /dev/null @@ -1,52 +0,0 @@ -export default [ - 'HTMLHeadElement', - 'HTMLTitleElement', - 'HTMLMetaElement', - 'HTMLBodyElement', - 'HTMLHeadingElement', - 'HTMLParagraphElement', - 'HTMLHRElement', - 'HTMLPreElement', - 'HTMLUListElement', - 'HTMLOListElement', - 'HTMLLIElement', - 'HTMLMenuElement', - 'HTMLDListElement', - 'HTMLDivElement', - 'HTMLAnchorElement', - 'HTMLAreaElement', - 'HTMLBRElement', - 'HTMLButtonElement', - 'HTMLCanvasElement', - 'HTMLDataElement', - 'HTMLDataListElement', - 'HTMLDetailsElement', - 'HTMLDirectoryElement', - 'HTMLFieldSetElement', - 'HTMLFontElement', - 'HTMLHtmlElement', - 'HTMLLegendElement', - 'HTMLMapElement', - 'HTMLMarqueeElement', - 'HTMLMeterElement', - 'HTMLModElement', - 'HTMLOutputElement', - 'HTMLPictureElement', - 'HTMLProgressElement', - 'HTMLQuoteElement', - 'HTMLSourceElement', - 'HTMLSpanElement', - 'HTMLTableCaptionElement', - 'HTMLTableCellElement', - 'HTMLTableColElement', - 'HTMLTableElement', - 'HTMLTimeElement', - 'HTMLTableRowElement', - 'HTMLTableSectionElement', - 'HTMLFrameElement', - 'HTMLFrameSetElement', - 'HTMLEmbedElement', - 'HTMLObjectElement', - 'HTMLParamElement', - 'HTMLTrackElement' -]; diff --git a/packages/happy-dom/src/event/DataTransfer.ts b/packages/happy-dom/src/event/DataTransfer.ts index e307eaad1..768bf47fd 100644 --- a/packages/happy-dom/src/event/DataTransfer.ts +++ b/packages/happy-dom/src/event/DataTransfer.ts @@ -7,7 +7,21 @@ import DataTransferItemList from './DataTransferItemList.js'; export default class DataTransfer { public dropEffect = 'none'; public effectAllowed = 'none'; - public files: File[] = []; public readonly items: DataTransferItemList = new DataTransferItemList(); public readonly types: string[] = []; + + /** + * Returns files. + * + * @returns Files. + */ + public get files(): File[] { + const files = []; + for (const item of this.items) { + if (item.kind === 'file') { + files.push(item.getAsFile()); + } + } + return files; + } } diff --git a/packages/happy-dom/src/event/DataTransferItem.ts b/packages/happy-dom/src/event/DataTransferItem.ts index d18031b48..281315323 100644 --- a/packages/happy-dom/src/event/DataTransferItem.ts +++ b/packages/happy-dom/src/event/DataTransferItem.ts @@ -1,21 +1,26 @@ import File from '../file/File.js'; /** + * Data transfer item. * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem. */ export default class DataTransferItem { - public readonly kind: string = ''; - public readonly type: string = ''; - private _item: string | File = null; + public readonly kind: 'string' | 'file'; + public readonly type: string; + #item: string | File = null; /** * Constructor. * * @param item Item. + * @param type Type. */ - constructor(item: string | File) { + constructor(item: string | File, type?: string) { this.kind = typeof item === 'string' ? 'string' : 'file'; - this._item = item; + this.type = type ?? (this.kind === 'string' ? '' : (item).type); + this.#item = item; } /** @@ -25,16 +30,18 @@ export default class DataTransferItem { if (this.kind === 'string') { return null; } - return this._item; + return this.#item; } /** * Returns string. + * + * @param callback Callback. */ - public getAsString(): string { + public getAsString(callback: (text: string) => void): void { if (this.kind === 'file') { - return null; + callback; } - return this._item; + callback(this.#item); } } diff --git a/packages/happy-dom/src/event/DataTransferItemList.ts b/packages/happy-dom/src/event/DataTransferItemList.ts index 5f3a6a741..e8963657b 100644 --- a/packages/happy-dom/src/event/DataTransferItemList.ts +++ b/packages/happy-dom/src/event/DataTransferItemList.ts @@ -4,16 +4,20 @@ import DataTransferItem from './DataTransferItem.js'; /** * */ -export default class DataTransferItemList { - public readonly DataTransferItem: DataTransferItem[] = []; - +export default class DataTransferItemList extends Array { /** * Adds an item. * * @param item Item. + * @param type Type. */ - public add(item: File | string): void { - this.DataTransferItem.push(new DataTransferItem(item)); + public add(item: File | string, type?: string): void { + if (!type && !(item instanceof File)) { + throw new TypeError( + `Failed to execute 'add' on 'DataTransferItemList': parameter 1 is not of type 'File'.` + ); + } + this.push(new DataTransferItem(item, type)); } /** @@ -22,13 +26,15 @@ export default class DataTransferItemList { * @param index Index. */ public remove(index: number): void { - this.DataTransferItem.splice(index, 1); + this.splice(index, 1); } /** * Clears list. */ public clear(): void { - (this.DataTransferItem) = []; + while (this.length) { + this.pop(); + } } } diff --git a/packages/happy-dom/src/event/NonImplementedEventTypes.ts b/packages/happy-dom/src/event/NonImplementedEventTypes.ts deleted file mode 100644 index 12dda4416..000000000 --- a/packages/happy-dom/src/event/NonImplementedEventTypes.ts +++ /dev/null @@ -1,45 +0,0 @@ -export default [ - 'AudioProcessingEvent', - 'BeforeInputEvent', - 'BeforeUnloadEvent', - 'BlobEvent', - 'ClipboardEvent', - 'CloseEvent', - 'CompositionEvent', - 'CSSFontFaceLoadEvent', - 'DeviceLightEvent', - 'DeviceMotionEvent', - 'DeviceOrientationEvent', - 'DeviceProximityEvent', - 'DOMTransactionEvent', - 'DragEvent', - 'EditingBeforeInputEvent', - 'FetchEvent', - 'GamepadEvent', - 'HashChangeEvent', - 'IDBVersionChangeEvent', - 'MediaStreamEvent', - 'MessageEvent', - 'MutationEvent', - 'OfflineAudioCompletionEvent', - 'OverconstrainedError', - 'PageTransitionEvent', - 'PaymentRequestUpdateEvent', - 'PopStateEvent', - 'ProgressEvent', - 'RelatedEvent', - 'RTCDataChannelEvent', - 'RTCIdentityErrorEvent', - 'RTCIdentityEvent', - 'RTCPeerConnectionIceEvent', - 'SensorEvent', - 'SVGEvent', - 'SVGZoomEvent', - 'TimeEvent', - 'TouchEvent', - 'TrackEvent', - 'TransitionEvent', - 'UserProximityEvent', - 'WebGLContextEvent', - 'TextEvent' -]; diff --git a/packages/happy-dom/src/event/events/ClipboardEvent.ts b/packages/happy-dom/src/event/events/ClipboardEvent.ts new file mode 100644 index 000000000..49d4c05ee --- /dev/null +++ b/packages/happy-dom/src/event/events/ClipboardEvent.ts @@ -0,0 +1,22 @@ +import DataTransfer from '../DataTransfer.js'; +import Event from '../Event.js'; +import IClipboardEventInit from './IClipboardEventInit.js'; + +/** + * + */ +export default class ClipboardEvent extends Event { + public clipboardData: DataTransfer | null; + + /** + * Constructor. + * + * @param type Event type. + * @param [eventInit] Event init. + */ + constructor(type: string, eventInit: IClipboardEventInit | null = null) { + super(type, eventInit); + + this.clipboardData = eventInit?.clipboardData ?? null; + } +} diff --git a/packages/happy-dom/src/event/events/IClipboardEventInit.ts b/packages/happy-dom/src/event/events/IClipboardEventInit.ts new file mode 100644 index 000000000..a9312f384 --- /dev/null +++ b/packages/happy-dom/src/event/events/IClipboardEventInit.ts @@ -0,0 +1,6 @@ +import DataTransfer from '../DataTransfer.js'; +import IEventInit from '../IEventInit.js'; + +export default interface IClipboardEventInit extends IEventInit { + clipboardData?: DataTransfer | null; +} diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index 53661339c..28487d0eb 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -152,6 +152,12 @@ import IVirtualConsoleLogGroup from './console/types/IVirtualConsoleLogGroup.js' import IVirtualConsolePrinter from './console/types/IVirtualConsolePrinter.js'; import VirtualConsole from './console/VirtualConsole.js'; import VirtualConsolePrinter from './console/VirtualConsolePrinter.js'; +import Permissions from './permissions/Permissions.js'; +import PermissionStatus from './permissions/PermissionStatus.js'; +import Clipboard from './clipboard/Clipboard.js'; +import ClipboardItem from './clipboard/ClipboardItem.js'; +import ClipboardEvent from './event/events/ClipboardEvent.js'; +import IClipboardEventInit from './event/events/IClipboardEventInit.js'; export { GlobalWindow, @@ -199,6 +205,8 @@ export { ProgressEvent, SubmitEvent, WheelEvent, + ClipboardEvent, + IClipboardEventInit, DOMParser, Document, IDocument, @@ -307,5 +315,9 @@ export { IVirtualConsoleLogGroup, IVirtualConsolePrinter, VirtualConsole, - VirtualConsolePrinter + VirtualConsolePrinter, + Permissions, + PermissionStatus, + Clipboard, + ClipboardItem }; diff --git a/packages/happy-dom/src/navigator/Navigator.ts b/packages/happy-dom/src/navigator/Navigator.ts index 5e6104991..a9d50af99 100644 --- a/packages/happy-dom/src/navigator/Navigator.ts +++ b/packages/happy-dom/src/navigator/Navigator.ts @@ -1,6 +1,8 @@ import MimeTypeArray from './MimeTypeArray.js'; import PluginArray from './PluginArray.js'; import IWindow from '../window/IWindow.js'; +import Permissions from '../permissions/Permissions.js'; +import Clipboard from '../clipboard/Clipboard.js'; /** * Browser Navigator API. @@ -11,14 +13,19 @@ import IWindow from '../window/IWindow.js'; * https://html.spec.whatwg.org/multipage/system-state.html#dom-navigator. */ export default class Navigator { - private _ownerWindow: IWindow; + #ownerWindow: IWindow; + #clipboard: Clipboard; + #permissions: Permissions; /** + * Constructor. * - * @param ownerWindow + * @param ownerWindow Owner window. */ constructor(ownerWindow: IWindow) { - this._ownerWindow = ownerWindow; + this.#ownerWindow = ownerWindow; + this.#clipboard = new Clipboard(ownerWindow); + this.#permissions = new Permissions(); } /** @@ -144,7 +151,7 @@ export default class Navigator { * "appCodeName/appVersion number (Platform; Security; OS-or-CPU; Localization; rv: revision-version-number) product/productSub Application-Name Application-Name-version". */ public get userAgent(): string { - return this._ownerWindow.happyDOM.settings.navigator.userAgent; + return this.#ownerWindow.happyDOM.settings.navigator.userAgent; } /** @@ -155,10 +162,23 @@ export default class Navigator { } /** - * TODO: Not implemented. + * Returns a Permissions object that can be used to query and update permission status of APIs covered by the Permissions API. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/permissions + * @returns Permissions. */ - public get permissions(): string { - return null; + public get permissions(): Permissions { + return this.#permissions; + } + + /** + * Returns a Clipboard object providing access to the contents of the system clipboard. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/clipboard + * @returns Clipboard. + */ + public get clipboard(): Clipboard { + return this.#clipboard; } /** diff --git a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts index cd74e9de2..39ebbd00b 100644 --- a/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts +++ b/packages/happy-dom/src/nodes/html-input-element/HTMLInputElement.ts @@ -22,6 +22,7 @@ import HTMLInputElementDateUtility from './HTMLInputElementDateUtility.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import HTMLInputElementNamedNodeMap from './HTMLInputElementNamedNodeMap.js'; +import ClipboardEvent from '../../event/events/ClipboardEvent.js'; /** * HTML Input Element. @@ -1167,43 +1168,64 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @override */ public override dispatchEvent(event: Event): boolean { + // Do nothing if the input element is disabled and the event is a click event. if (event.type === 'click' && event.eventPhase === EventPhaseEnum.none && this.disabled) { return false; } + // The checkbox or radio button has to be checked before the click event is dispatched, so that event listeners can check the checked value. + // However, the value has to be restored if preventDefault() is called on the click event. if ( - event.type === 'click' && (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && - (this.type === 'checkbox' || this.type === 'radio') + event.type === 'click' ) { - this._setChecked(this.type === 'checkbox' ? !this.checked : true); + const inputType = this.type; + if (inputType === 'checkbox' || inputType === 'radio') { + this._setChecked(inputType === 'checkbox' ? !this.checked : true); + } } const returnValue = super.dispatchEvent(event); if ( - event.type === 'click' && + !event.defaultPrevented && (event.eventPhase === EventPhaseEnum.atTarget || event.eventPhase === EventPhaseEnum.bubbling) && - (!this.readOnly || this.type === 'checkbox' || this.type === 'radio') + event.type === 'click' ) { - if (this.type === 'checkbox' || this.type === 'radio') { - this.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); - this.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); - } else if (this.type === 'submit') { - const form = this._formNode; - if (form) { - form.requestSubmit(); - } - } else if (this.type === 'reset' && this.isConnected) { - const form = this._formNode; - if (form) { - form.reset(); + const inputType = this.type; + if (!this.readOnly || inputType === 'checkbox' || inputType === 'radio') { + if (inputType === 'checkbox' || inputType === 'radio') { + this.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); + this.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); + } else if (inputType === 'submit') { + const form = this._formNode; + if (form) { + form.requestSubmit(); + } + } else if (inputType === 'reset' && this.isConnected) { + const form = this._formNode; + if (form) { + form.reset(); + } } } } + // Restore checked state if preventDefault() is triggered on the click event. + if ( + event.defaultPrevented && + (event.eventPhase === EventPhaseEnum.atTarget || + event.eventPhase === EventPhaseEnum.bubbling) && + event.type === 'click' + ) { + const inputType = this.type; + if (inputType === 'checkbox' || inputType === 'radio') { + this._setChecked(inputType === 'checkbox' ? !this.checked : true); + } + } + return returnValue; } @@ -1233,12 +1255,13 @@ export default class HTMLInputElement extends HTMLElement implements IHTMLInputE * @returns "true" if selection is supported. */ private _isSelectionSupported(): boolean { + const inputType = this.type; return ( - this.type === 'text' || - this.type === 'search' || - this.type === 'url' || - this.type === 'tel' || - this.type === 'password' + inputType === 'text' || + inputType === 'search' || + inputType === 'url' || + inputType === 'tel' || + inputType === 'password' ); } diff --git a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts index 2139289de..56607990c 100644 --- a/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts +++ b/packages/happy-dom/src/nodes/html-text-area-element/HTMLTextAreaElement.ts @@ -14,6 +14,8 @@ import IHTMLLabelElement from '../html-label-element/IHTMLLabelElement.js'; import HTMLLabelElementUtility from '../html-label-element/HTMLLabelElementUtility.js'; import INamedNodeMap from '../../named-node-map/INamedNodeMap.js'; import HTMLTextAreaElementNamedNodeMap from './HTMLTextAreaElementNamedNodeMap.js'; +import EventPhaseEnum from '../../event/EventPhaseEnum.js'; +import ClipboardEvent from '../../event/events/ClipboardEvent.js'; /** * HTML Text Area Element. diff --git a/packages/happy-dom/src/permissions/PermissionStatus.ts b/packages/happy-dom/src/permissions/PermissionStatus.ts new file mode 100644 index 000000000..96fd2aa3d --- /dev/null +++ b/packages/happy-dom/src/permissions/PermissionStatus.ts @@ -0,0 +1,23 @@ +import EventTarget from '../event/EventTarget.js'; +import Event from '../event/Event.js'; + +/** + * Permission status. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/PermissionStatus + */ +export default class PermissionStatus extends EventTarget { + public readonly state: 'granted' | 'denied' | 'prompt'; + public onchange: ((event: Event) => void) | null = null; + + /** + * Constructor. + * + * @param [state] State. + */ + constructor(state: 'granted' | 'denied' | 'prompt' = 'granted') { + super(); + this.state = state; + } +} diff --git a/packages/happy-dom/src/permissions/Permissions.ts b/packages/happy-dom/src/permissions/Permissions.ts new file mode 100644 index 000000000..2d9c471a4 --- /dev/null +++ b/packages/happy-dom/src/permissions/Permissions.ts @@ -0,0 +1,60 @@ +import PermissionStatus from './PermissionStatus.js'; + +/** + * Permissions API. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/Permissions. + */ +export default class Permissions { + /** + * Returns scroll restoration. + * + * @param permissionDescriptor Permission descriptor. + * @param permissionDescriptor.name Permission name. + * @param permissionDescriptor.userVisibleOnly User visible only. + * @param permissionDescriptor.sysex Sysex. + * @returns Permission status. + */ + public async query(permissionDescriptor: { + name: string; + userVisibleOnly?: boolean; + sysex?: boolean; + }): Promise { + switch (permissionDescriptor.name) { + case 'geolocation': + case 'notifications': + case 'push': + case 'midi': + case 'camera': + case 'microphone': + case 'background-fetch': + case 'background-sync': + case 'persistent-storage': + case 'ambient-light-sensor': + case 'accelerometer': + case 'gyroscope': + case 'magnetometer': + case 'screen-wake-lock': + case 'nfc': + case 'display-capture': + case 'accessibility-events': + case 'clipboard-read': + case 'clipboard-write': + case 'payment-handler': + case 'idle-detection': + case 'periodic-background-sync': + case 'system-wake-lock': + case 'storage-access': + case 'window-management': + case 'window-placement': + case 'local-fonts': + case 'top-level-storage-access': + return new PermissionStatus('granted'); + } + + throw new Error( + `Failed to execute 'query' on 'Permissions': Failed to read the 'name' property from 'PermissionDescriptor': The provided value '${permissionDescriptor.name}' is not a valid enum value of type PermissionName.` + ); + } +} diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index 716c96ebf..7ec890227 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -126,12 +126,17 @@ import RadioNodeList from '../nodes/html-form-element/RadioNodeList.js'; import ValidityState from '../validity-state/ValidityState.js'; import INodeJSGlobal from './INodeJSGlobal.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; +import Permissions from '../permissions/Permissions.js'; +import PermissionStatus from '../permissions/PermissionStatus.js'; +import Clipboard from '../clipboard/Clipboard.js'; +import ClipboardItem from '../clipboard/ClipboardItem.js'; +import ClipboardEvent from '../event/events/ClipboardEvent.js'; /** * Window without dependencies to server side specific packages. */ export default interface IWindow extends IEventTarget, INodeJSGlobal { - // Public Properties + // Happy DOM property. readonly happyDOM: { whenAsyncComplete: () => Promise; cancelAsync: () => void; @@ -152,8 +157,24 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { setInnerHeight: (height: number) => void; }; - // Global classes + // Nodes readonly Node: typeof Node; + readonly Attr: typeof Attr; + readonly SVGSVGElement: typeof SVGSVGElement; + readonly SVGElement: typeof SVGElement; + readonly Text: typeof Text; + readonly Comment: typeof Comment; + readonly ShadowRoot: typeof ShadowRoot; + readonly Element: typeof Element; + readonly DocumentFragment: typeof DocumentFragment; + readonly CharacterData: typeof CharacterData; + readonly ProcessingInstruction: typeof ProcessingInstruction; + readonly Document: typeof Document; + readonly HTMLDocument: typeof HTMLDocument; + readonly XMLDocument: typeof XMLDocument; + readonly SVGDocument: typeof SVGDocument; + + // Element classes readonly HTMLElement: typeof HTMLElement; readonly HTMLUnknownElement: typeof HTMLUnknownElement; readonly HTMLTemplateElement: typeof HTMLTemplateElement; @@ -174,27 +195,61 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly HTMLBaseElement: typeof HTMLBaseElement; readonly HTMLIFrameElement: typeof HTMLIFrameElement; readonly HTMLDialogElement: typeof HTMLDialogElement; - readonly Attr: typeof Attr; - readonly NamedNodeMap: typeof NamedNodeMap; - readonly SVGSVGElement: typeof SVGSVGElement; - readonly SVGElement: typeof SVGElement; - readonly Image: typeof Image; - readonly Text: typeof Text; - readonly Comment: typeof Comment; - readonly ShadowRoot: typeof ShadowRoot; - readonly Element: typeof Element; - readonly DocumentFragment: typeof DocumentFragment; - readonly CharacterData: typeof CharacterData; - readonly ProcessingInstruction: typeof ProcessingInstruction; - readonly NodeFilter: typeof NodeFilter; - readonly NodeIterator: typeof NodeIterator; - readonly TreeWalker: typeof TreeWalker; - readonly DOMParser: typeof DOMParser; - readonly MutationObserver: typeof MutationObserver; - readonly Document: typeof Document; - readonly HTMLDocument: typeof HTMLDocument; - readonly XMLDocument: typeof XMLDocument; - readonly SVGDocument: typeof SVGDocument; + + /** + * Non-implemented element classes + */ + readonly HTMLHeadElement: typeof HTMLElement; + readonly HTMLTitleElement: typeof HTMLElement; + readonly HTMLBodyElement: typeof HTMLElement; + readonly HTMLHeadingElement: typeof HTMLElement; + readonly HTMLParagraphElement: typeof HTMLElement; + readonly HTMLHRElement: typeof HTMLElement; + readonly HTMLPreElement: typeof HTMLElement; + readonly HTMLUListElement: typeof HTMLElement; + readonly HTMLOListElement: typeof HTMLElement; + readonly HTMLLIElement: typeof HTMLElement; + readonly HTMLMenuElement: typeof HTMLElement; + readonly HTMLDListElement: typeof HTMLElement; + readonly HTMLDivElement: typeof HTMLElement; + readonly HTMLAnchorElement: typeof HTMLElement; + readonly HTMLAreaElement: typeof HTMLElement; + readonly HTMLBRElement: typeof HTMLElement; + readonly HTMLButtonElement: typeof HTMLElement; + readonly HTMLCanvasElement: typeof HTMLElement; + readonly HTMLDataElement: typeof HTMLElement; + readonly HTMLDataListElement: typeof HTMLElement; + readonly HTMLDetailsElement: typeof HTMLElement; + readonly HTMLDirectoryElement: typeof HTMLElement; + readonly HTMLFieldSetElement: typeof HTMLElement; + readonly HTMLFontElement: typeof HTMLElement; + readonly HTMLHtmlElement: typeof HTMLElement; + readonly HTMLLegendElement: typeof HTMLElement; + readonly HTMLMapElement: typeof HTMLElement; + readonly HTMLMarqueeElement: typeof HTMLElement; + readonly HTMLMeterElement: typeof HTMLElement; + readonly HTMLModElement: typeof HTMLElement; + readonly HTMLOutputElement: typeof HTMLElement; + readonly HTMLPictureElement: typeof HTMLElement; + readonly HTMLProgressElement: typeof HTMLElement; + readonly HTMLQuoteElement: typeof HTMLElement; + readonly HTMLSourceElement: typeof HTMLElement; + readonly HTMLSpanElement: typeof HTMLElement; + readonly HTMLTableCaptionElement: typeof HTMLElement; + readonly HTMLTableCellElement: typeof HTMLElement; + readonly HTMLTableColElement: typeof HTMLElement; + readonly HTMLTableElement: typeof HTMLElement; + readonly HTMLTimeElement: typeof HTMLElement; + readonly HTMLTableRowElement: typeof HTMLElement; + readonly HTMLTableSectionElement: typeof HTMLElement; + readonly HTMLFrameElement: typeof HTMLElement; + readonly HTMLFrameSetElement: typeof HTMLElement; + readonly HTMLEmbedElement: typeof HTMLElement; + readonly HTMLObjectElement: typeof HTMLElement; + readonly HTMLParamElement: typeof HTMLElement; + readonly HTMLTrackElement: typeof HTMLElement; + + // Event classes readonly Event: typeof Event; readonly UIEvent: typeof UIEvent; readonly CustomEvent: typeof CustomEvent; @@ -212,6 +267,55 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly MessagePort: typeof MessagePort; readonly ProgressEvent: typeof ProgressEvent; readonly MediaQueryListEvent: typeof MediaQueryListEvent; + readonly ClipboardEvent: typeof ClipboardEvent; + + /** + * Non-implemented event classes + */ + readonly AudioProcessingEvent: typeof Event; + readonly BeforeInputEvent: typeof Event; + readonly BeforeUnloadEvent: typeof Event; + readonly BlobEvent: typeof Event; + readonly CloseEvent: typeof Event; + readonly CompositionEvent: typeof Event; + readonly CSSFontFaceLoadEvent: typeof Event; + readonly DeviceLightEvent: typeof Event; + readonly DeviceMotionEvent: typeof Event; + readonly DeviceOrientationEvent: typeof Event; + readonly DeviceProximityEvent: typeof Event; + readonly DOMTransactionEvent: typeof Event; + readonly DragEvent: typeof Event; + readonly EditingBeforeInputEvent: typeof Event; + readonly FetchEvent: typeof Event; + readonly GamepadEvent: typeof Event; + readonly HashChangeEvent: typeof Event; + readonly IDBVersionChangeEvent: typeof Event; + readonly MediaStreamEvent: typeof Event; + readonly MutationEvent: typeof Event; + readonly OfflineAudioCompletionEvent: typeof Event; + readonly OverconstrainedError: typeof Event; + readonly PageTransitionEvent: typeof Event; + readonly PaymentRequestUpdateEvent: typeof Event; + readonly PopStateEvent: typeof Event; + readonly RelatedEvent: typeof Event; + readonly RTCDataChannelEvent: typeof Event; + readonly RTCIdentityErrorEvent: typeof Event; + readonly RTCIdentityEvent: typeof Event; + readonly RTCPeerConnectionIceEvent: typeof Event; + readonly SensorEvent: typeof Event; + readonly SVGEvent: typeof Event; + readonly SVGZoomEvent: typeof Event; + readonly TimeEvent: typeof Event; + readonly TouchEvent: typeof Event; + readonly TrackEvent: typeof Event; + readonly TransitionEvent: typeof Event; + readonly UserProximityEvent: typeof Event; + readonly WebGLContextEvent: typeof Event; + readonly TextEvent: typeof Event; + + // Other classes + readonly Image: typeof Image; + readonly NamedNodeMap: typeof NamedNodeMap; readonly EventTarget: typeof EventTarget; readonly DataTransfer: typeof DataTransfer; readonly DataTransferItem: typeof DataTransferItem; @@ -268,6 +372,16 @@ export default interface IWindow extends IEventTarget, INodeJSGlobal { readonly AbortSignal: typeof AbortSignal; readonly RadioNodeList: typeof RadioNodeList; readonly ValidityState: typeof ValidityState; + readonly Permissions: typeof Permissions; + readonly PermissionStatus: typeof PermissionStatus; + readonly Clipboard: typeof Clipboard; + readonly ClipboardItem: typeof ClipboardItem; + + readonly NodeFilter: typeof NodeFilter; + readonly NodeIterator: typeof NodeIterator; + readonly TreeWalker: typeof TreeWalker; + readonly DOMParser: typeof DOMParser; + readonly MutationObserver: typeof MutationObserver; // Events onload: (event: Event) => void; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 981327672..c36793e15 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -50,9 +50,7 @@ import MessagePort from '../event/MessagePort.js'; import { URLSearchParams } from 'url'; import URL from '../url/URL.js'; import Location from '../location/Location.js'; -import NonImplementedEventTypes from '../event/NonImplementedEventTypes.js'; import MutationObserver from '../mutation-observer/MutationObserver.js'; -import NonImplemenetedElementClasses from '../config/NonImplemenetedElementClasses.js'; import DOMParserImplementation from '../dom-parser/DOMParser.js'; import XMLSerializer from '../xml-serializer/XMLSerializer.js'; import ResizeObserver from '../resize-observer/ResizeObserver.js'; @@ -142,6 +140,11 @@ import VirtualConsole from '../console/VirtualConsole.js'; import VirtualConsolePrinter from '../console/VirtualConsolePrinter.js'; import IHappyDOMSettings from './IHappyDOMSettings.js'; import PackageVersion from '../version.js'; +import Permissions from '../permissions/Permissions.js'; +import PermissionStatus from '../permissions/PermissionStatus.js'; +import Clipboard from '../clipboard/Clipboard.js'; +import ClipboardItem from '../clipboard/ClipboardItem.js'; +import ClipboardEvent from '../event/events/ClipboardEvent.js'; const ORIGINAL_SET_TIMEOUT = setTimeout; const ORIGINAL_CLEAR_TIMEOUT = clearTimeout; @@ -156,7 +159,7 @@ const ORIGINAL_QUEUE_MICROTASK = queueMicrotask; * https://developer.mozilla.org/en-US/docs/Web/API/Window. */ export default class Window extends EventTarget implements IWindow { - // The Happy DOM property + // Happy DOM property. public readonly happyDOM: { whenAsyncComplete: () => Promise; cancelAsync: () => void; @@ -227,8 +230,24 @@ export default class Window extends EventTarget implements IWindow { setInnerHeight: (height: number): void => this.happyDOM.setWindowSize({ height }) }; - // Global classes + // Nodes public readonly Node = Node; + public readonly Attr = Attr; + public readonly SVGSVGElement = SVGSVGElement; + public readonly SVGElement = SVGElement; + public readonly SVGGraphicsElement = SVGGraphicsElement; + public readonly Text = Text; + public readonly Comment = Comment; + public readonly ShadowRoot = ShadowRoot; + public readonly ProcessingInstruction = ProcessingInstruction; + public readonly Element = Element; + public readonly CharacterData = CharacterData; + public readonly Document = Document; + public readonly HTMLDocument = HTMLDocument; + public readonly XMLDocument = XMLDocument; + public readonly SVGDocument = SVGDocument; + + // Element classes public readonly HTMLElement = HTMLElement; public readonly HTMLUnknownElement = HTMLUnknownElement; public readonly HTMLTemplateElement = HTMLTemplateElement; @@ -249,25 +268,59 @@ export default class Window extends EventTarget implements IWindow { public readonly HTMLBaseElement = HTMLBaseElement; public readonly HTMLIFrameElement = HTMLIFrameElement; public readonly HTMLDialogElement = HTMLDialogElement; - public readonly Attr = Attr; - public readonly NamedNodeMap = NamedNodeMap; - public readonly SVGSVGElement = SVGSVGElement; - public readonly SVGElement = SVGElement; - public readonly SVGGraphicsElement = SVGGraphicsElement; - public readonly Text = Text; - public readonly Comment = Comment; - public readonly ShadowRoot = ShadowRoot; - public readonly ProcessingInstruction = ProcessingInstruction; - public readonly Element = Element; - public readonly CharacterData = CharacterData; - public readonly NodeFilter = NodeFilter; - public readonly NodeIterator = NodeIterator; - public readonly TreeWalker = TreeWalker; - public readonly MutationObserver = MutationObserver; - public readonly Document = Document; - public readonly HTMLDocument = HTMLDocument; - public readonly XMLDocument = XMLDocument; - public readonly SVGDocument = SVGDocument; + + // Non-implemented element classes + public readonly HTMLHeadElement = HTMLElement; + public readonly HTMLTitleElement = HTMLElement; + public readonly HTMLBodyElement = HTMLElement; + public readonly HTMLHeadingElement = HTMLElement; + public readonly HTMLParagraphElement = HTMLElement; + public readonly HTMLHRElement = HTMLElement; + public readonly HTMLPreElement = HTMLElement; + public readonly HTMLUListElement = HTMLElement; + public readonly HTMLOListElement = HTMLElement; + public readonly HTMLLIElement = HTMLElement; + public readonly HTMLMenuElement = HTMLElement; + public readonly HTMLDListElement = HTMLElement; + public readonly HTMLDivElement = HTMLElement; + public readonly HTMLAnchorElement = HTMLElement; + public readonly HTMLAreaElement = HTMLElement; + public readonly HTMLBRElement = HTMLElement; + public readonly HTMLButtonElement = HTMLElement; + public readonly HTMLCanvasElement = HTMLElement; + public readonly HTMLDataElement = HTMLElement; + public readonly HTMLDataListElement = HTMLElement; + public readonly HTMLDetailsElement = HTMLElement; + public readonly HTMLDirectoryElement = HTMLElement; + public readonly HTMLFieldSetElement = HTMLElement; + public readonly HTMLFontElement = HTMLElement; + public readonly HTMLHtmlElement = HTMLElement; + public readonly HTMLLegendElement = HTMLElement; + public readonly HTMLMapElement = HTMLElement; + public readonly HTMLMarqueeElement = HTMLElement; + public readonly HTMLMeterElement = HTMLElement; + public readonly HTMLModElement = HTMLElement; + public readonly HTMLOutputElement = HTMLElement; + public readonly HTMLPictureElement = HTMLElement; + public readonly HTMLProgressElement = HTMLElement; + public readonly HTMLQuoteElement = HTMLElement; + public readonly HTMLSourceElement = HTMLElement; + public readonly HTMLSpanElement = HTMLElement; + public readonly HTMLTableCaptionElement = HTMLElement; + public readonly HTMLTableCellElement = HTMLElement; + public readonly HTMLTableColElement = HTMLElement; + public readonly HTMLTableElement = HTMLElement; + public readonly HTMLTimeElement = HTMLElement; + public readonly HTMLTableRowElement = HTMLElement; + public readonly HTMLTableSectionElement = HTMLElement; + public readonly HTMLFrameElement = HTMLElement; + public readonly HTMLFrameSetElement = HTMLElement; + public readonly HTMLEmbedElement = HTMLElement; + public readonly HTMLObjectElement = HTMLElement; + public readonly HTMLParamElement = HTMLElement; + public readonly HTMLTrackElement = HTMLElement; + + // Events classes public readonly Event = Event; public readonly UIEvent = UIEvent; public readonly CustomEvent = CustomEvent; @@ -284,6 +337,56 @@ export default class Window extends EventTarget implements IWindow { public readonly SubmitEvent = SubmitEvent; public readonly ProgressEvent = ProgressEvent; public readonly MediaQueryListEvent = MediaQueryListEvent; + public readonly ClipboardEvent = ClipboardEvent; + + // Non-implemented event classes + public readonly AudioProcessingEvent = Event; + public readonly BeforeInputEvent = Event; + public readonly BeforeUnloadEvent = Event; + public readonly BlobEvent = Event; + public readonly CloseEvent = Event; + public readonly CompositionEvent = Event; + public readonly CSSFontFaceLoadEvent = Event; + public readonly DeviceLightEvent = Event; + public readonly DeviceMotionEvent = Event; + public readonly DeviceOrientationEvent = Event; + public readonly DeviceProximityEvent = Event; + public readonly DOMTransactionEvent = Event; + public readonly DragEvent = Event; + public readonly EditingBeforeInputEvent = Event; + public readonly FetchEvent = Event; + public readonly GamepadEvent = Event; + public readonly HashChangeEvent = Event; + public readonly IDBVersionChangeEvent = Event; + public readonly MediaStreamEvent = Event; + public readonly MutationEvent = Event; + public readonly OfflineAudioCompletionEvent = Event; + public readonly OverconstrainedError = Event; + public readonly PageTransitionEvent = Event; + public readonly PaymentRequestUpdateEvent = Event; + public readonly PopStateEvent = Event; + public readonly RelatedEvent = Event; + public readonly RTCDataChannelEvent = Event; + public readonly RTCIdentityErrorEvent = Event; + public readonly RTCIdentityEvent = Event; + public readonly RTCPeerConnectionIceEvent = Event; + public readonly SensorEvent = Event; + public readonly SVGEvent = Event; + public readonly SVGZoomEvent = Event; + public readonly TimeEvent = Event; + public readonly TouchEvent = Event; + public readonly TrackEvent = Event; + public readonly TransitionEvent = Event; + public readonly UserProximityEvent = Event; + public readonly WebGLContextEvent = Event; + public readonly TextEvent = Event; + + // Other classes + public readonly NamedNodeMap = NamedNodeMap; + public readonly NodeFilter = NodeFilter; + public readonly NodeIterator = NodeIterator; + public readonly TreeWalker = TreeWalker; + public readonly MutationObserver = MutationObserver; public readonly EventTarget = EventTarget; public readonly MessagePort = MessagePort; public readonly DataTransfer = DataTransfer; @@ -340,6 +443,10 @@ export default class Window extends EventTarget implements IWindow { public readonly AbortController = AbortController; public readonly AbortSignal = AbortSignal; public readonly FormData = FormData; + public readonly Permissions = Permissions; + public readonly PermissionStatus = PermissionStatus; + public readonly Clipboard = Clipboard; + public readonly ClipboardItem = ClipboardItem; public readonly XMLHttpRequest; public readonly DOMParser: typeof DOMParserImplementation; public readonly Range; @@ -352,7 +459,7 @@ export default class Window extends EventTarget implements IWindow { public onload: (event: Event) => void = null; public onerror: (event: ErrorEvent) => void = null; - // Public Properties + // Public properties. public readonly document: Document; public readonly customElements: CustomElementRegistry; public readonly location: Location; @@ -521,20 +628,6 @@ export default class Window extends EventTarget implements IWindow { this._clearInterval = ORIGINAL_CLEAR_INTERVAL; this._queueMicrotask = ORIGINAL_QUEUE_MICROTASK; - // Non-implemented event types - for (const eventType of NonImplementedEventTypes) { - if (!this[eventType]) { - this[eventType] = Event; - } - } - - // Non implemented element classes - for (const className of NonImplemenetedElementClasses) { - if (!this[className]) { - this[className] = HTMLElement; - } - } - // Binds all methods to "this", so that it will use the correct context when called globally. for (const key of Object.getOwnPropertyNames(Window.prototype).concat( Object.getOwnPropertyNames(EventTarget.prototype)