From d40983d02634b1d31885bd4357ff5d14a4f465a0 Mon Sep 17 00:00:00 2001 From: rudywaltz Date: Tue, 31 May 2022 13:24:15 +0200 Subject: [PATCH] #475@minor: Adds support for HTMLMediaElement. --- packages/happy-dom/src/config/ElementTag.ts | 6 +- .../config/NonImplemenetedElementClasses.ts | 3 - packages/happy-dom/src/index.ts | 12 + .../html-audio-element/HTMLAudioElement.ts | 11 + .../html-audio-element/IHTMLAudioElement.ts | 11 + .../html-media-element/HTMLMediaElement.ts | 441 ++++++++++++++++++ .../html-media-element/IHTMLMediaElement.ts | 84 ++++ .../html-video-element/HTMLVideoElement.ts | 11 + .../html-video-element/IHTMLVideoElement.ts | 11 + packages/happy-dom/src/window/IWindow.ts | 6 + packages/happy-dom/src/window/Window.ts | 6 + .../HTMLMediaElement.test.ts | 344 ++++++++++++++ 12 files changed, 941 insertions(+), 5 deletions(-) create mode 100644 packages/happy-dom/src/nodes/html-audio-element/HTMLAudioElement.ts create mode 100644 packages/happy-dom/src/nodes/html-audio-element/IHTMLAudioElement.ts create mode 100644 packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts create mode 100644 packages/happy-dom/src/nodes/html-media-element/IHTMLMediaElement.ts create mode 100644 packages/happy-dom/src/nodes/html-video-element/HTMLVideoElement.ts create mode 100644 packages/happy-dom/src/nodes/html-video-element/IHTMLVideoElement.ts create mode 100644 packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts diff --git a/packages/happy-dom/src/config/ElementTag.ts b/packages/happy-dom/src/config/ElementTag.ts index 3080d5aed..3659ba17d 100644 --- a/packages/happy-dom/src/config/ElementTag.ts +++ b/packages/happy-dom/src/config/ElementTag.ts @@ -18,6 +18,8 @@ import HTMLOptionElement from '../nodes/html-option-element/HTMLOptionElement'; import HTMLOptGroupElement from '../nodes/html-opt-group-element/HTMLOptGroupElement'; import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement'; import HTMLButtonElement from '../nodes/html-button-element/HTMLButtonElement'; +import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement'; +import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement'; export default { A: HTMLElement, @@ -26,7 +28,7 @@ export default { AREA: HTMLElement, ARTICLE: HTMLElement, ASIDE: HTMLElement, - AUDIO: HTMLElement, + AUDIO: HTMLAudioElement, B: HTMLElement, BASE: HTMLBaseElement, BDI: HTMLElement, @@ -145,6 +147,6 @@ export default { U: HTMLElement, UL: HTMLElement, VAR: HTMLElement, - VIDEO: HTMLElement, + VIDEO: HTMLVideoElement, WBR: HTMLElement }; diff --git a/packages/happy-dom/src/config/NonImplemenetedElementClasses.ts b/packages/happy-dom/src/config/NonImplemenetedElementClasses.ts index 6727c9415..3b712841f 100644 --- a/packages/happy-dom/src/config/NonImplemenetedElementClasses.ts +++ b/packages/happy-dom/src/config/NonImplemenetedElementClasses.ts @@ -28,7 +28,6 @@ export default [ 'HTMLLegendElement', 'HTMLMapElement', 'HTMLMarqueeElement', - 'HTMLMediaElement', 'HTMLMeterElement', 'HTMLModElement', 'HTMLOutputElement', @@ -50,7 +49,5 @@ export default [ 'HTMLEmbedElement', 'HTMLObjectElement', 'HTMLParamElement', - 'HTMLVideoElement', - 'HTMLAudioElement', 'HTMLTrackElement' ]; diff --git a/packages/happy-dom/src/index.ts b/packages/happy-dom/src/index.ts index 11cb61668..6caa9b43c 100644 --- a/packages/happy-dom/src/index.ts +++ b/packages/happy-dom/src/index.ts @@ -74,6 +74,12 @@ import HTMLLabelElement from './nodes/html-label-element/HTMLLabelElement'; import IHTMLLabelElement from './nodes/html-label-element/IHTMLLabelElement'; import HTMLMetaElement from './nodes/html-meta-element/HTMLMetaElement'; import IHTMLMetaElement from './nodes/html-meta-element/IHTMLMetaElement'; +import IHTMLMediaElement from './nodes/html-media-element/IHTMLMediaElement'; +import HTMLMediaElement from './nodes/html-media-element/HTMLMediaElement'; +import HTMLAudioElement from './nodes/html-audio-element/HTMLAudioElement'; +import IHTMLAudioElement from './nodes/html-audio-element/IHTMLAudioElement'; +import HTMLVideoElement from './nodes/html-video-element/HTMLVideoElement'; +import IHTMLVideoElement from './nodes/html-video-element/IHTMLVideoElement'; import HTMLBaseElement from './nodes/html-base-element/HTMLBaseElement'; import IHTMLBaseElement from './nodes/html-base-element/IHTMLBaseElement'; import SVGElement from './nodes/svg-element/SVGElement'; @@ -192,6 +198,12 @@ export { IHTMLLabelElement, HTMLMetaElement, IHTMLMetaElement, + HTMLMediaElement, + IHTMLMediaElement, + HTMLAudioElement, + IHTMLAudioElement, + HTMLVideoElement, + IHTMLVideoElement, HTMLBaseElement, IHTMLBaseElement, SVGElement, diff --git a/packages/happy-dom/src/nodes/html-audio-element/HTMLAudioElement.ts b/packages/happy-dom/src/nodes/html-audio-element/HTMLAudioElement.ts new file mode 100644 index 000000000..2069ffc0f --- /dev/null +++ b/packages/happy-dom/src/nodes/html-audio-element/HTMLAudioElement.ts @@ -0,0 +1,11 @@ +import HTMLMediaElement from '../html-media-element/HTMLMediaElement'; +import IHTMLAudioElement from './IHTMLAudioElement'; + +/** + * HTML Audio Element. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement. + * + */ +export default class HTMLAudioElement extends HTMLMediaElement implements IHTMLAudioElement {} diff --git a/packages/happy-dom/src/nodes/html-audio-element/IHTMLAudioElement.ts b/packages/happy-dom/src/nodes/html-audio-element/IHTMLAudioElement.ts new file mode 100644 index 000000000..1ed0379e4 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-audio-element/IHTMLAudioElement.ts @@ -0,0 +1,11 @@ +import IHTMLMediaElement from '../html-media-element/IHTMLMediaElement'; + +/** + * HTML Audio Element. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement. + */ + +type IHTMLAudioElement = IHTMLMediaElement; +export default IHTMLAudioElement; diff --git a/packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts b/packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts new file mode 100644 index 000000000..0cc43042f --- /dev/null +++ b/packages/happy-dom/src/nodes/html-media-element/HTMLMediaElement.ts @@ -0,0 +1,441 @@ +import ErrorEvent from 'src/event/events/ErrorEvent'; +import Event from '../../event/Event'; +import DOMException from '../../exception/DOMException'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum'; +import HTMLElement from '../html-element/HTMLElement'; +import IHTMLMediaElement, { IMediaError } from './IHTMLMediaElement'; + +/** + * + * This implementation coming from jsdom + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/nodes/HTMLMediaElement-impl.js#L7 + * + */ +function getTimeRangeDummy(): object { + return { + length: 0, + start() { + return 0; + }, + end() { + return 0; + } + }; +} +/** + * HTML Media Element. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement. + * + */ +export default class HTMLMediaElement extends HTMLElement implements IHTMLMediaElement { + // Public Properties + public readonly buffered = getTimeRangeDummy(); + public readonly duration = NaN; + public readonly error: IMediaError = null; + public readonly ended = false; + public readonly networkState = 0; + public readonly readyState = 0; + public readonly textTracks = []; + public readonly videoTracks = []; + public readonly seeking = false; + public readonly seekable = getTimeRangeDummy(); + public readonly played = getTimeRangeDummy(); + + // Events + public onabort: (event: Event) => void = null; + public oncanplay: (event: Event) => void = null; + public oncanplaythrough: (event: Event) => void = null; + public ondurationchange: (event: Event) => void = null; + public onemptied: (event: Event) => void = null; + public onended: (event: Event) => void = null; + public onerror: (event: ErrorEvent) => void = null; + public onloadeddata: (event: Event) => void = null; + + #volume = 1; + #paused = true; + #currentTime = 0; + #playbackRate = 1; + #defaultPlaybackRate = 1; + #muted = false; + #defaultMuted = false; + #preservesPitch = true; + /** + * Returns autoplay. + * + * @returns Autoplay. + */ + public get autoplay(): boolean { + return this.getAttributeNS(null, 'autoplay') !== null; + } + + /** + * Sets autoplay. + * + * @param autoplay Autoplay. + */ + public set autoplay(autoplay: boolean) { + if (!autoplay) { + this.removeAttributeNS(null, 'autoplay'); + } else { + this.setAttributeNS(null, 'autoplay', ''); + } + } + + /** + * Returns controls. + * + * @returns Controls. + */ + public get controls(): boolean { + return this.getAttributeNS(null, 'controls') !== null; + } + + /** + * Sets controls. + * + * @param controls Controls. + */ + public set controls(controls: boolean) { + if (!controls) { + this.removeAttributeNS(null, 'controls'); + } else { + this.setAttributeNS(null, 'controls', ''); + } + } + + /** + * Returns loop. + * + * @returns Loop. + */ + public get loop(): boolean { + return this.getAttributeNS(null, 'loop') !== null; + } + + /** + * Sets loop. + * + * @param loop Loop. + */ + public set loop(loop: boolean) { + if (!loop) { + this.removeAttributeNS(null, 'loop'); + } else { + this.setAttributeNS(null, 'loop', ''); + } + } + /** + * Returns muted. + * + * @returns Muted. + */ + public get muted(): boolean { + if (this.#muted) { + return this.#muted; + } + + if (!this.#defaultMuted) { + return this.getAttributeNS(null, 'muted') !== null; + } + + return false; + } + + /** + * Sets muted. + * + * @param muted Muted. + */ + public set muted(muted: boolean) { + this.#muted = !!muted; + if (!muted && !this.#defaultMuted) { + this.removeAttributeNS(null, 'muted'); + } else { + this.setAttributeNS(null, 'muted', ''); + } + } + + /** + * Returns defaultMuted. + * + * @returns DefaultMuted. + */ + public get defaultMuted(): boolean { + return this.#defaultMuted; + } + + /** + * Sets defaultMuted. + * + * @param defaultMuted DefaultMuted. + */ + public set defaultMuted(defaultMuted: boolean) { + this.#defaultMuted = !!defaultMuted; + if (!this.#defaultMuted && !this.#muted) { + this.removeAttributeNS(null, 'muted'); + } else { + this.setAttributeNS(null, 'muted', ''); + } + } + + /** + * Returns src. + * + * @returns Src. + */ + public get src(): string { + return this.getAttributeNS(null, 'src') || ''; + } + + /** + * Sets src. + * + * @param src Src. + */ + public set src(src: string) { + this.setAttributeNS(null, 'src', src); + if (Boolean(src)) { + this.dispatchEvent(new Event('canplay', { bubbles: false, cancelable: false })); + this.dispatchEvent(new Event('durationchange', { bubbles: false, cancelable: false })); + } + } + + /** + * Returns currentSrc. + * + * @returns CurrentrSrc. + */ + public get currentSrc(): string { + return this.src; + } + + /** + * Returns volume. + * + * @returns Volume. + */ + public get volume(): number { + return this.#volume; + } + + /** + * Sets volume. + * + * @param volume Volume. + */ + public set volume(volume: number | string) { + const parsedVolume = Number(volume); + + if (isNaN(parsedVolume)) { + throw new TypeError( + `Failed to set the 'volume' property on 'HTMLMediaElement': The provided double value is non-finite.` + ); + } + if (parsedVolume < 0 || parsedVolume > 1) { + throw new DOMException( + `Failed to set the 'volume' property on 'HTMLMediaElement': The volume provided (${parsedVolume}) is outside the range [0, 1].`, + DOMExceptionNameEnum.indexSizeError + ); + } + // TODO: volumechange event https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/volumechange_event + this.#volume = parsedVolume; + } + + /** + * Returns crossOrigin. + * + * @returns CrossOrigin. + */ + public get crossOrigin(): string { + return this.getAttributeNS(null, 'crossorigin'); + } + + /** + * Sets crossOrigin. + * + * @param crossOrigin CrossOrigin. + */ + public set crossOrigin(crossOrigin: string | null) { + if (crossOrigin === null) { + return; + } + + if (['', 'use-credentials', 'anonymous'].includes(crossOrigin)) { + this.setAttributeNS(null, 'crossorigin', crossOrigin); + } else { + this.setAttributeNS(null, 'crossorigin', 'anonymous'); + } + } + + /** + * Returns currentTime. + * + * @returns CurrentTime. + */ + public get currentTime(): number { + return this.#currentTime; + } + + /** + * Sets currentTime. + * + * @param currentTime CurrentTime. + */ + public set currentTime(currentTime: number | string) { + const parsedCurrentTime = Number(currentTime); + if (isNaN(parsedCurrentTime)) { + throw new TypeError( + `Failed to set the 'currentTime' property on 'HTMLMediaElement': The provided double value is non-finite.` + ); + } + this.#currentTime = parsedCurrentTime; + } + + /** + * Returns playbackRate. + * + * @returns PlaybackRate. + */ + public get playbackRate(): number { + return this.#playbackRate; + } + + /** + * Sets playbackRate. + * + * @param playbackRate PlaybackRate. + */ + public set playbackRate(playbackRate: number | string) { + const parsedPlaybackRate = Number(playbackRate); + if (isNaN(parsedPlaybackRate)) { + throw new TypeError( + `Failed to set the 'playbackRate' property on 'HTMLMediaElement': The provided double value is non-finite.` + ); + } + this.#playbackRate = parsedPlaybackRate; + } + + /** + * Returns defaultPlaybackRate. + * + * @returns DefaultPlaybackRate. + */ + public get defaultPlaybackRate(): number { + return this.#defaultPlaybackRate; + } + + /** + * Sets defaultPlaybackRate. + * + * @param defaultPlaybackRate DefaultPlaybackRate. + */ + public set defaultPlaybackRate(defaultPlaybackRate: number | string) { + const parsedDefaultPlaybackRate = Number(defaultPlaybackRate); + if (isNaN(parsedDefaultPlaybackRate)) { + throw new TypeError( + `Failed to set the 'defaultPlaybackRate' property on 'HTMLMediaElement': The provided double value is non-finite.` + ); + } + this.#defaultPlaybackRate = parsedDefaultPlaybackRate; + } + + /** + * Returns preservesPitch. + * + * @returns PlaybackRate. + */ + public get preservesPitch(): boolean { + return this.#preservesPitch; + } + + /** + * Sets preservesPitch. + * + * @param preservesPitch PreservesPitch. + */ + public set preservesPitch(preservesPitch: boolean) { + this.#preservesPitch = Boolean(preservesPitch); + } + + /** + * Returns preload. + * + * @returns preload. + */ + public get preload(): string { + return this.getAttributeNS(null, 'preload') || 'auto'; + } + + /** + * Sets preload. + * + * @param preload preload. + */ + public set preload(preload: string) { + this.setAttributeNS(null, 'preload', preload); + } + + /** + * Returns paused. + * + * @returns Paused. + */ + public get paused(): boolean { + return this.#paused; + } + + /** + * Pause played media. + */ + public pause(): void { + this.#paused = true; + this.dispatchEvent(new Event('pause', { bubbles: false, cancelable: false })); + } + + /** + * Start playing media. + */ + public async play(): Promise { + this.#paused = false; + return Promise.resolve(); + } + + /** + * + * @param _type + */ + public canPlayType(_type: string): string { + return ''; + } + + /** + * Load media. + */ + public load(): void { + this.dispatchEvent(new Event('emptied', { bubbles: false, cancelable: false })); + } + + /** + * + */ + public captureStream(): object { + return {}; + } + + /** + * Clones a node. + * + * @override + * @param [deep=false] "true" to clone deep. + * @returns Cloned node. + */ + /** + * + * @param deep + */ + public cloneNode(deep = false): IHTMLMediaElement { + return super.cloneNode(deep); + } +} diff --git a/packages/happy-dom/src/nodes/html-media-element/IHTMLMediaElement.ts b/packages/happy-dom/src/nodes/html-media-element/IHTMLMediaElement.ts new file mode 100644 index 000000000..d0e1ff0d2 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-media-element/IHTMLMediaElement.ts @@ -0,0 +1,84 @@ +import IHTMLElement from '../html-element/IHTMLElement'; + +/** + * HTML Media Element. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement. + */ + +export interface IMediaError { + code: number; + message: string; +} +export default interface IHTMLMediaElement extends IHTMLElement { + readonly currentSrc: string; + readonly duration: number; + readonly ended: boolean; + readonly error: IMediaError | null; + readonly networkState: number; + readonly played: object; // TimeRanges https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges + readonly readyState: number; + readonly seekable: object; // TimeRanges https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges + readonly seeking: boolean; + readonly textTracks: object[]; + readonly videoTracks: object[]; + readonly buffered: object; // TimeRanges https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges + autoplay: boolean; + controls: boolean; + crossOrigin: string; // Only anonymus and 'use-credentials' is valid + currentTime: number | string; + defaultMuted: boolean; + defaultPlaybackRate: number | string; + loop: boolean; + muted: boolean; + paused: boolean; + playbackRate: number | string; + preload: string; + preservesPitch: boolean; + src: string; + volume: number | string; + + /** + * A MediaStream object which can be used as a source for audio and/or video data by other media processing code, + * or as a source for WebRTC. + * Https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/captureStream + */ + captureStream(): object; + + /** + * The HTMLMediaElement method canPlayType() reports how likely it is that the current browser will be able to play + * media of a given MIME type. + * Https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType + * possible return value: "" | "probably" | "maybe". + */ + canPlayType(_type: string): string; + + /** + * The HTMLMediaElement method load() resets the media element to its initial state and begins the process of + * selecting a media source and loading the media in preparation for playback to begin at the beginning. + * Https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/load. + */ + load(): void; + + /** + * The HTMLMediaElement.pause() method will pause playback of the media, if the media is already in a paused state + * this method will have no effect. + */ + pause(): void; + + /** + * The HTMLMediaElement play() method attempts to begin playback of the media. It returns a Promise + * which is resolved when playback has been successfully started. + */ + play(): Promise; + + /** + * Clones a node. + * + * @override + * @param [deep=false] "true" to clone deep. + * @returns Cloned node. + */ + cloneNode(deep: boolean): IHTMLMediaElement; +} diff --git a/packages/happy-dom/src/nodes/html-video-element/HTMLVideoElement.ts b/packages/happy-dom/src/nodes/html-video-element/HTMLVideoElement.ts new file mode 100644 index 000000000..8b54a3626 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-video-element/HTMLVideoElement.ts @@ -0,0 +1,11 @@ +import HTMLMediaElement from '../html-media-element/HTMLMediaElement'; +import IHTMLVideoElement from './IHTMLVideoElement'; + +/** + * HTML Video Element. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement. + * + */ +export default class HTMLVideoElement extends HTMLMediaElement implements IHTMLVideoElement {} diff --git a/packages/happy-dom/src/nodes/html-video-element/IHTMLVideoElement.ts b/packages/happy-dom/src/nodes/html-video-element/IHTMLVideoElement.ts new file mode 100644 index 000000000..24f6c4a09 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-video-element/IHTMLVideoElement.ts @@ -0,0 +1,11 @@ +import IHTMLMediaElement from '../html-media-element/IHTMLMediaElement'; + +/** + * HTML Video Element. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement. + */ + +type IHTMLVideoElement = IHTMLMediaElement; +export default IHTMLVideoElement; diff --git a/packages/happy-dom/src/window/IWindow.ts b/packages/happy-dom/src/window/IWindow.ts index d3b83a911..43e4e27f4 100644 --- a/packages/happy-dom/src/window/IWindow.ts +++ b/packages/happy-dom/src/window/IWindow.ts @@ -19,6 +19,9 @@ import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement'; import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement'; import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement'; import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement'; +import HTMLMediaElement from '../nodes/html-media-element/HTMLMediaElement'; +import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement'; +import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement'; import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement'; import SVGSVGElement from '../nodes/svg-element/SVGSVGElement'; import SVGElement from '../nodes/svg-element/SVGElement'; @@ -124,6 +127,9 @@ export default interface IWindow extends IEventTarget, NodeJS.Global { readonly HTMLSlotElement: typeof HTMLSlotElement; readonly HTMLLabelElement: typeof HTMLLabelElement; readonly HTMLMetaElement: typeof HTMLMetaElement; + readonly HTMLMediaElement: typeof HTMLMediaElement; + readonly HTMLAudioElement: typeof HTMLAudioElement; + readonly HTMLVideoElement: typeof HTMLVideoElement; readonly HTMLBaseElement: typeof HTMLBaseElement; readonly HTMLDialogElement: typeof HTMLDialogElement; readonly Attr: typeof Attr; diff --git a/packages/happy-dom/src/window/Window.ts b/packages/happy-dom/src/window/Window.ts index 4f5df2014..bfa663626 100644 --- a/packages/happy-dom/src/window/Window.ts +++ b/packages/happy-dom/src/window/Window.ts @@ -20,6 +20,9 @@ import HTMLStyleElement from '../nodes/html-style-element/HTMLStyleElement'; import HTMLSlotElement from '../nodes/html-slot-element/HTMLSlotElement'; import HTMLLabelElement from '../nodes/html-label-element/HTMLLabelElement'; import HTMLMetaElement from '../nodes/html-meta-element/HTMLMetaElement'; +import HTMLMediaElement from '../nodes/html-media-element/HTMLMediaElement'; +import HTMLAudioElement from '../nodes/html-audio-element/HTMLAudioElement'; +import HTMLVideoElement from '../nodes/html-video-element/HTMLVideoElement'; import HTMLBaseElement from '../nodes/html-base-element/HTMLBaseElement'; import HTMLDialogElement from '../nodes/html-dialog-element/HTMLDialogElement'; import SVGSVGElement from '../nodes/svg-element/SVGSVGElement'; @@ -157,6 +160,9 @@ export default class Window extends EventTarget implements IWindow { public readonly HTMLLabelElement = HTMLLabelElement; public readonly HTMLSlotElement = HTMLSlotElement; public readonly HTMLMetaElement = HTMLMetaElement; + public readonly HTMLMediaElement = HTMLMediaElement; + public readonly HTMLAudioElement = HTMLAudioElement; + public readonly HTMLVideoElement = HTMLVideoElement; public readonly HTMLBaseElement = HTMLBaseElement; public readonly HTMLDialogElement = HTMLDialogElement; public readonly Attr = Attr; diff --git a/packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts b/packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts new file mode 100644 index 000000000..58212f97b --- /dev/null +++ b/packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts @@ -0,0 +1,344 @@ +import Window from '../../../src/window/Window'; +import IWindow from '../../../src/window/IWindow'; +import DOMException from '../../../src/exception/DOMException'; +import DOMExceptionNameEnum from '../../../src/exception/DOMExceptionNameEnum'; +import IDocument from '../../../src/nodes/document/IDocument'; +import IHTMLMediaElement from '../../../src/nodes/html-media-element/IHTMLMediaElement'; +import Event from '../../../src/event/Event'; + +describe('HTMLMediaElement', () => { + let window: IWindow; + let document: IDocument; + let element: IHTMLMediaElement; + + beforeEach(() => { + window = new Window(); + document = window.document; + element = document.createElement('audio'); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + for (const property of ['autoplay', 'controls', 'loop']) { + describe(`get ${property}()`, () => { + it('Returns attribute value.', () => { + expect(element[property]).toBe(false); + element.setAttribute(property, ''); + expect(element[property]).toBe(true); + }); + }); + + describe(`set ${property}()`, () => { + it('Sets attribute value.', () => { + element[property] = true; + expect(element.getAttribute(property)).toBe(''); + }); + + it('Remove attribute value.', () => { + element.setAttribute(property, ''); + element[property] = false; + expect(element.getAttribute(property)).toBeNull(); + }); + }); + } + + for (const property of ['src', 'preload']) { + describe(`get ${property}()`, () => { + it(`Returns the "${property}" attribute.`, () => { + element.setAttribute(property, 'test'); + expect(element[property]).toBe('test'); + }); + }); + + describe(`set ${property}()`, () => { + it(`Sets the attribute "${property}".`, () => { + element[property] = 'test'; + expect(element.getAttribute(property)).toBe('test'); + }); + }); + } + + describe(`get defaultMuted()`, () => { + it('Returns value.', () => { + expect(element.defaultMuted).toBe(false); + element.defaultMuted = true; + expect(element.defaultMuted).toBe(true); + }); + }); + + describe(`set defaultMuted()`, () => { + it('Sets attribute value.', () => { + element.defaultMuted = true; + expect(element.getAttribute('muted')).toBe(''); + }); + + it('Remove attribute value.', () => { + element.defaultMuted = true; + element.defaultMuted = false; + expect(element.getAttribute('muted')).toBeNull(); + }); + }); + + describe(`get muted()`, () => { + it('Returns value.', () => { + element.setAttribute('muted', ''); + expect(element.muted).toBe(true); + }); + it('Returns setter value.', () => { + element.muted = true; + expect(element.muted).toBe(true); + }); + }); + + describe(`set muted()`, () => { + it('Sets attribute value.', () => { + element.muted = true; + expect(element.getAttribute('muted')).toBe(''); + }); + + it('Remove attribute value.', () => { + element.setAttribute('muted', ''); + element.muted = false; + expect(element.getAttribute('muted')).toBeNull(); + }); + + it('Keep attribute value, if default muted true', () => { + element.setAttribute('muted', ''); + element.defaultMuted = true; + element.muted = false; + expect(element.getAttribute('muted')).toBe(''); + expect(element.muted).toBe(false); + }); + }); + + describe('canplay event', () => { + it('Should dispatch after src set', () => { + let dispatchedEvent: Event = null; + element.addEventListener('canplay', (event: Event) => (dispatchedEvent = event)); + element.src = 'https://songURL'; + expect(dispatchedEvent.cancelable).toBe(false); + expect(dispatchedEvent.bubbles).toBe(false); + }); + it('Should not dispatch if src is empty', () => { + let dispatchedEvent: Event = null; + element.addEventListener('canplay', (event: Event) => (dispatchedEvent = event)); + element.src = ''; + expect(dispatchedEvent).toBeNull(); + }); + }); + + describe('currentSrc', () => { + it('Returns the current src', () => { + const src = 'https://src'; + element.src = src; + expect(element.currentSrc).toBe(src); + }); + }); + + describe('paused', () => { + it('Default is true', () => { + expect(element.paused).toBeTruthy(); + }); + + it('Set false with play', () => { + element.play(); + expect(element.paused).toBeFalsy(); + }); + + it('Set true with pause', () => { + element.play(); + element.pause(); + expect(element.paused).toBeTruthy(); + }); + }); + + describe('volume()', () => { + it('Returns default value', () => { + expect(element.volume).toBe(1); + }); + + it('Set value', () => { + element.volume = 0.5; + expect(element.volume).toBe(0.5); + }); + + it('Set parse volmue as a number', () => { + element.volume = '0.5'; + expect(element.volume).toBe(0.5); + }); + + it('Throw type error if volume is not a number', () => { + expect(() => { + element.volume = 'zeropointfive'; + }).toThrowError( + new TypeError( + `Failed to set the 'volume' property on 'HTMLMediaElement': The provided double value is non-finite.` + ) + ); + }); + + for (const volume of [-0.4, 1.3]) { + it(`Throw error if out of range: ${volume}`, () => { + expect(() => { + element.volume = volume; + }).toThrowError( + new DOMException( + `Failed to set the 'volume' property on 'HTMLMediaElement': The volume provided (${volume}) is outside the range [0, 1].`, + DOMExceptionNameEnum.indexSizeError + ) + ); + }); + } + }); + + describe('canPlayType', () => { + it('Returns empty string', () => { + expect(element.canPlayType('notValidMIMEtype')).toBe(''); + }); + }); + + describe('enden', () => { + it('Returns false', () => { + expect(element.ended).toBeFalsy(); + }); + }); + + describe('CrossOrigin', () => { + for (const crossOrigin of ['', null, 'use-credentials', 'anonymous']) { + it(`Set ${crossOrigin} as a valid crossOrigin`, () => { + element.crossOrigin = crossOrigin; + expect(element.getAttribute('crossorigin')).toBe(crossOrigin); + expect(element.crossOrigin).toBe(crossOrigin); + }); + } + + it(`Return 'anonymous' if crossOrigin is not valid`, () => { + element.crossOrigin = 'randomString'; + expect(element.getAttribute('crossorigin')).toBe('anonymous'); + expect(element.crossOrigin).toBe('anonymous'); + }); + }); + + describe('duration', () => { + it('Return NaN by default', () => { + expect(element.duration).toBe(NaN); + }); + }); + + describe('currentTime', () => { + it('Return default value', () => { + expect(element.currentTime).toBe(0); + }); + it('Set value', () => { + element.currentTime = 42; + expect(element.currentTime).toBe(42); + }); + it('Set value as a string', () => { + element.currentTime = '42'; + expect(element.currentTime).toBe(42); + }); + + it('Throw type error if currentTime is not a number', () => { + expect(() => { + element.currentTime = 'zeropointfive'; + }).toThrowError( + new TypeError( + `Failed to set the 'currentTime' property on 'HTMLMediaElement': The provided double value is non-finite.` + ) + ); + }); + }); + + describe('playbackRate', () => { + it('Return default value', () => { + expect(element.playbackRate).toBe(1); + }); + it('Set value', () => { + element.playbackRate = 2.3; + expect(element.playbackRate).toBe(2.3); + }); + it('Set value as a string', () => { + element.playbackRate = '2.3'; + expect(element.playbackRate).toBe(2.3); + }); + + it('Throw type error if playbackRate is not a number', () => { + expect(() => { + element.playbackRate = 'zeropointfive'; + }).toThrowError( + new TypeError( + `Failed to set the 'playbackRate' property on 'HTMLMediaElement': The provided double value is non-finite.` + ) + ); + }); + }); + + describe('defaultPlaybackRate', () => { + it('Return default value', () => { + expect(element.defaultPlaybackRate).toBe(1); + }); + it('Set value', () => { + element.defaultPlaybackRate = 2.3; + expect(element.defaultPlaybackRate).toBe(2.3); + }); + it('Set value as a string', () => { + element.defaultPlaybackRate = '0.3'; + expect(element.defaultPlaybackRate).toBe(0.3); + }); + + it('Throw type error if defaultPlaybackRate is not a number', () => { + expect(() => { + element.defaultPlaybackRate = 'zeropointfive'; + }).toThrowError( + new TypeError( + `Failed to set the 'defaultPlaybackRate' property on 'HTMLMediaElement': The provided double value is non-finite.` + ) + ); + }); + }); + + describe('error', () => { + it('Return null by default', () => { + expect(element.error).toBeNull(); + }); + }); + + describe('networkState', () => { + it('Return 0 by default', () => { + expect(element.networkState).toBe(0); + }); + }); + + describe('preservesPitch', () => { + it('Return true by default', () => { + expect(element.preservesPitch).toBe(true); + }); + + for (const property of [null, undefined, false]) { + it(`Set false with ${property}`, () => { + element.preservesPitch = property; + expect(element.preservesPitch).toBe(false); + }); + } + }); + + describe('readyState', () => { + it('Return 0 by default', () => { + expect(element.readyState).toBe(0); + }); + }); + + describe('load', () => { + it('Dispatch emptied event', () => { + let dispatchedEvent: Event = null; + element.addEventListener('emptied', (event: Event) => (dispatchedEvent = event)); + + element.load(); + + expect(dispatchedEvent.cancelable).toBe(false); + expect(dispatchedEvent.bubbles).toBe(false); + }); + }); +});