From 69b2150381c8774c1306ccba6a6930bba9073552 Mon Sep 17 00:00:00 2001 From: Matthew Lieder Date: Sun, 30 Oct 2022 14:21:02 -0500 Subject: [PATCH] #204@minor: Add support for HTMLAnchorElement. --- packages/happy-dom/src/config/ElementTag.ts | 3 +- .../html-anchor-element/HTMLAnchorElement.ts | 465 ++++++++++++++++++ .../html-anchor-element/IHTMLAnchorElement.ts | 21 + .../IHTMLHyperlinkElementUtils.ts | 19 + .../HTMLAnchorElement.test.ts | 298 +++++++++++ 5 files changed, 805 insertions(+), 1 deletion(-) create mode 100644 packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts create mode 100644 packages/happy-dom/src/nodes/html-anchor-element/IHTMLAnchorElement.ts create mode 100644 packages/happy-dom/src/nodes/html-anchor-element/IHTMLHyperlinkElementUtils.ts create mode 100644 packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts diff --git a/packages/happy-dom/src/config/ElementTag.ts b/packages/happy-dom/src/config/ElementTag.ts index 3659ba17d..49f15720a 100644 --- a/packages/happy-dom/src/config/ElementTag.ts +++ b/packages/happy-dom/src/config/ElementTag.ts @@ -20,9 +20,10 @@ 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'; +import HTMLAnchorElement from '../nodes/html-anchor-element/HTMLAnchorElement'; export default { - A: HTMLElement, + A: HTMLAnchorElement, ABBR: HTMLElement, ADDRESS: HTMLElement, AREA: HTMLElement, diff --git a/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts new file mode 100644 index 000000000..c2c4da280 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-anchor-element/HTMLAnchorElement.ts @@ -0,0 +1,465 @@ +import HTMLElement from '../html-element/HTMLElement'; +import DOMTokenList from '../../dom-token-list/DOMTokenList'; +import IDOMTokenList from '../../dom-token-list/IDOMTokenList'; +import IHTMLAnchorElement from './IHTMLAnchorElement'; +import { URL } from 'url'; + +/** + * HTML Anchor Element. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement. + */ +export default class HTMLAnchorElement extends HTMLElement implements IHTMLAnchorElement { + private _relList: DOMTokenList = null; + private _url: URL | null = null; + + /** + * Constructor + */ + constructor() { + super(); + + this._setUrl(); + } + + /** + * Returns download. + * + * @returns download. + */ + public get download(): string { + return this.getAttribute('download') || ''; + } + + /** + * Sets download. + * + * @param download Download. + */ + public set download(download: string) { + this.setAttributeNS(null, 'download', download); + } + + /** + * Returns hash. + * + * @returns Hash. + */ + public get hash(): string { + this._reinitializeUrl(); + return this._url?.hash ?? ''; + } + + /** + * Sets hash. + * + * @param hash Hash. + */ + public set hash(hash: string) { + this._reinitializeUrl(); + if (this._url) { + this._url.hash = hash; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns href. + * + * @returns Href. + */ + public get href(): string | null { + this._reinitializeUrl(); + + if (this._url) { + return this._url.toString(); + } + + return this.getAttributeNS(null, 'href') || ''; + } + + /** + * Sets href. + * + * @param href Href. + */ + public set href(href: string) { + this.setAttributeNS(null, 'href', href); + + this._setUrl(); + } + + /** + * Returns hreflang. + * + * @returns Hreflang. + */ + public get hreflang(): string { + return this.getAttributeNS(null, 'hreflang') || ''; + } + + /** + * Sets hreflang. + * + * @param hreflang Hreflang. + */ + public set hreflang(hreflang: string) { + this.setAttributeNS(null, 'hreflang', hreflang); + } + + /** + * Returns the hyperlink's URL's origin. + * + * @returns Origin. + */ + public get origin(): string { + this._reinitializeUrl(); + return this._url?.origin ?? ''; + } + + /** + * Returns ping. + * + * @returns Ping. + */ + public get ping(): string { + return this.getAttributeNS(null, 'ping') || ''; + } + + /** + * Sets ping. + * + * @param ping Ping. + */ + public set ping(ping: string) { + this.setAttributeNS(null, 'ping', ping); + } + + /** + * Returns protocol. + * + * @returns Protocol. + */ + public get protocol(): string { + this._reinitializeUrl(); + return this._url?.protocol ?? ''; + } + + /** + * Sets protocol. + * + * @param protocol Protocol. + */ + public set protocol(protocol: string) { + this._reinitializeUrl(); + if (this._url) { + this._url.protocol = protocol; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns username. + * + * @returns Username. + */ + public get username(): string { + this._reinitializeUrl(); + return this._url?.username ?? ''; + } + + /** + * Sets username. + * + * @param username Username. + */ + public set username(username: string) { + this._reinitializeUrl(); + if (this._url && this._url.host && this._url.protocol != 'file') { + this._url.username = username; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns password. + * + * @returns Password. + */ + public get password(): string { + this._reinitializeUrl(); + return this._url?.password ?? ''; + } + + /** + * Sets password. + * + * @param password Password. + */ + public set password(password: string) { + this._reinitializeUrl(); + if (this._url && this._url.host && this._url.protocol != 'file') { + this._url.password = password; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns pathname. + * + * @returns Pathname. + */ + public get pathname(): string { + this._reinitializeUrl(); + return this._url?.pathname ?? ''; + } + + /** + * Sets pathname. + * + * @param pathname Pathname. + */ + public set pathname(pathname: string) { + this._reinitializeUrl(); + if (this._url) { + this._url.pathname = pathname; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns port. + * + * @returns Port. + */ + public get port(): string { + this._reinitializeUrl(); + return this._url?.port ?? ''; + } + + /** + * Sets port. + * + * @param port Port. + */ + public set port(port: string) { + this._reinitializeUrl(); + if (this._url && this._url.host && this._url.protocol != 'file') { + this._url.port = port; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns host. + * + * @returns Host. + */ + public get host(): string { + this._reinitializeUrl(); + return this._url?.host ?? ''; + } + + /** + * Sets host. + * + * @param host Host. + */ + public set host(host: string) { + this._reinitializeUrl(); + if (this._url) { + this._url.host = host; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns hostname. + * + * @returns Hostname. + */ + public get hostname(): string { + this._reinitializeUrl(); + return this._url?.hostname ?? ''; + } + + /** + * Sets hostname. + * + * @param hostname Hostname. + */ + public set hostname(hostname: string) { + this._reinitializeUrl(); + if (this._url) { + this._url.hostname = hostname; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns referrerPolicy. + * + * @returns Referrer Policy. + */ + public get referrerPolicy(): string { + return this.getAttributeNS(null, 'referrerPolicy') || ''; + } + + /** + * Sets referrerPolicy. + * + * @param referrerPolicy Referrer Policy. + */ + public set referrerPolicy(referrerPolicy: string) { + this.setAttributeNS(null, 'referrerPolicy', referrerPolicy); + } + + /** + * Returns rel. + * + * @returns Rel. + */ + public get rel(): string { + return this.getAttributeNS(null, 'rel') || ''; + } + + /** + * Sets rel. + * + * @param rel Rel. + */ + public set rel(rel: string) { + this.setAttributeNS(null, 'rel', rel); + } + + /** + * Returns rel list. + * + * @returns Rel list. + */ + public get relList(): IDOMTokenList { + if (!this._relList) { + this._relList = new DOMTokenList(this, 'rel'); + } + return this._relList; + } + + /** + * Returns search. + * + * @returns Search. + */ + public get search(): string { + this._reinitializeUrl(); + return this._url?.search ?? ''; + } + + /** + * Sets search. + * + * @param search Search. + */ + public set search(search: string) { + this._reinitializeUrl(); + if (this._url) { + this._url.search = search; + this.setAttributeNS(null, 'href', this._url.toString()); + } + } + + /** + * Returns target. + * + * @returns target. + */ + public get target(): string { + return this.getAttributeNS(null, 'target') || ''; + } + + /** + * Sets target. + * + * @param target Target. + */ + public set target(target: string) { + this.setAttributeNS(null, 'target', target); + } + + /** + * Returns text. + * + * @returns text. + */ + public get text(): string { + return this.textContent; + } + + /** + * Sets text. + * + * @param text Text. + */ + public set text(text: string) { + this.textContent = text; + } + + /** + * Returns type. + * + * @returns Type. + */ + public get type(): string { + return this.getAttributeNS(null, 'type') || ''; + } + + /** + * Sets type. + * + * @param type Type. + */ + public set type(type: string) { + this.setAttributeNS(null, 'type', type); + } + + /** + * Updates DOM list indices. + */ + protected override _updateDomListIndices(): void { + super._updateDomListIndices(); + + if (this._relList) { + this._relList._updateIndices(); + } + } + + /** + * Reinitialize URL from href attribute + */ + private _reinitializeUrl(): void { + // If element's url is non-null, its scheme is "blob", and it has an opaque path, then terminate these steps. + if (this._url?.protocol === 'blob' && this._url.pathname.length > 1) { + return; + } + + this._setUrl(); + } + + /** + * Initialize URL from href attribute + */ + private _setUrl(): void { + const hrefAttr = this.getAttributeNS(null, 'href'); + if (!hrefAttr) { + this._url = null; + } + + const documentUrl = this.ownerDocument?.location?.href; + + try { + this._url = new URL(hrefAttr.trim(), documentUrl); + } catch (TypeError) { + this._url = null; + } + } +} diff --git a/packages/happy-dom/src/nodes/html-anchor-element/IHTMLAnchorElement.ts b/packages/happy-dom/src/nodes/html-anchor-element/IHTMLAnchorElement.ts new file mode 100644 index 000000000..b436b9c90 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-anchor-element/IHTMLAnchorElement.ts @@ -0,0 +1,21 @@ +import IDOMTokenList from '../../dom-token-list/IDOMTokenList'; +import IHTMLElement from '../html-element/IHTMLElement'; +import IHTMLHyperlinkElementUtils from './IHTMLHyperlinkElementUtils'; + +/** + * HTML Anchor Element. + * + * Reference: + * https://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement. + */ +export default interface IHTMLAnchorElement extends IHTMLElement, IHTMLHyperlinkElementUtils { + readonly relList: IDOMTokenList; + download: string; + ping: string; + hreflang: string; + referrerPolicy: string; + rel: string; + target: string; + text: string; + type: string; +} diff --git a/packages/happy-dom/src/nodes/html-anchor-element/IHTMLHyperlinkElementUtils.ts b/packages/happy-dom/src/nodes/html-anchor-element/IHTMLHyperlinkElementUtils.ts new file mode 100644 index 000000000..6d23f0564 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-anchor-element/IHTMLHyperlinkElementUtils.ts @@ -0,0 +1,19 @@ +/** + * HTMLHyperlinkElementUtils. + * + * Reference: + * https://html.spec.whatwg.org/multipage/links.html#htmlhyperlinkelementutils. + */ +export default interface IHTMLHyperlinkElementUtils { + readonly origin: string; + href: string; + protocol: string; + username: string; + password: string; + host: string; + hostname: string; + port: string; + pathname: string; + search: string; + hash: string; +} diff --git a/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts new file mode 100644 index 000000000..1a856cfce --- /dev/null +++ b/packages/happy-dom/test/nodes/html-anchor-element/HTMLAnchorElement.test.ts @@ -0,0 +1,298 @@ +import Window from '../../../src/window/Window'; +import IWindow from '../../../src/window/IWindow'; +import IDocument from '../../../src/nodes/document/IDocument'; +import IHTMLAnchorElement from '../../../src/nodes/html-anchor-element/IHTMLAnchorElement'; + +describe('HTMLAnchorElement', () => { + let window: IWindow; + let document: IDocument; + + beforeEach(() => { + window = new Window({ url: 'https://www.somesite.com/test.html' }); + document = window.document; + }); + + describe('Object.prototype.toString', () => { + it('Returns `[object HTMLAnchorElement]`', () => { + const element = document.createElement('a'); + expect(Object.prototype.toString.call(element)).toBe('[object HTMLAnchorElement]'); + }); + }); + + for (const property of [ + 'download', + 'hreflang', + 'ping', + 'target', + 'referrerPolicy', + 'rel', + 'type' + ]) { + describe(`get ${property}()`, () => { + it(`Returns the "${property}" attribute.`, () => { + const element = document.createElement('a'); + element.setAttribute(property, 'test'); + expect(element[property]).toBe('test'); + }); + }); + + describe(`set ${property}()`, () => { + it(`Sets the attribute "${property}".`, () => { + const element = document.createElement('a'); + element[property] = 'test'; + expect(element.getAttribute(property)).toBe('test'); + }); + }); + } + + describe('get href()', () => { + it('Returns the "href" attribute.', () => { + const element = document.createElement('a'); + element.setAttribute('href', 'test'); + expect(element.href).toBe('https://www.somesite.com/test'); + }); + + it('Returns the "href" attribute when scheme is http.', () => { + const element = document.createElement('a'); + element.setAttribute('href', 'http://www.example.com'); + expect(element.href).toBe('http://www.example.com/'); + }); + + it('Returns the "href" attribute when scheme is tel.', () => { + const element = document.createElement('a'); + element.setAttribute('href', 'tel:+123456789'); + expect(element.href).toBe('tel:+123456789'); + }); + + it('Returns the "href" attribute when scheme-relative', () => { + const element = document.createElement('a'); + element.setAttribute('href', '//example.com'); + expect(element.href).toBe('https://example.com/'); + }); + + it('Returns empty string if "href" attribute is empty.', () => { + const element = document.createElement('a'); + expect(element.href).toBe(''); + }); + }); + + describe('set href()', () => { + it('Sets the attribute "href".', () => { + const element = document.createElement('a'); + element.href = 'test'; + expect(element.getAttribute('href')).toBe('test'); + }); + }); + + describe('get origin()', () => { + it("Returns the href URL's origin.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.origin).toBe('https://www.example.com'); + }); + + it("Returns the href URL's origin with port when non-standard.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'http://www.example.com:8080/path?q1=a#xyz'); + expect(element.origin).toBe('http://www.example.com:8080'); + }); + + it("Returns the page's origin when href is relative.", () => { + const element = document.createElement('a'); + element.setAttribute('href', '/path?q1=a#xyz'); + expect(element.origin).toBe('https://www.somesite.com'); + }); + }); + + describe('get protocol()', () => { + it("Returns the href URL's protocol.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.protocol).toBe('https:'); + }); + }); + + describe('set protocol()', () => { + it("Sets the href URL's protocol.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.protocol).toBe('https:'); + + element.protocol = 'http'; + expect(element.protocol).toBe('http:'); + expect(element.href).toBe('http://www.example.com/path?q1=a#xyz'); + }); + }); + + describe('get username()', () => { + it("Returns the href URL's username.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); + expect(element.username).toBe('user'); + }); + }); + + describe('set username()', () => { + it("Sets the href URL's username.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); + + expect(element.username).toBe('user'); + + element.username = 'user2'; + expect(element.username).toBe('user2'); + expect(element.href).toBe('https://user2:pw@www.example.com/path?q1=a#xyz'); + }); + }); + + describe('get password()', () => { + it("Returns the href URL's password.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); + expect(element.password).toBe('pw'); + }); + }); + + describe('set password()', () => { + it("Sets the href URL's password.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://user:pw@www.example.com:443/path?q1=a#xyz'); + + expect(element.password).toBe('pw'); + + element.password = 'pw2'; + expect(element.password).toBe('pw2'); + expect(element.href).toBe('https://user:pw2@www.example.com/path?q1=a#xyz'); + }); + }); + + describe('get host()', () => { + it("Returns the href URL's host.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.host).toBe('www.example.com'); + }); + }); + + describe('set host()', () => { + it("Sets the href URL's host.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.host).toBe('www.example.com'); + + element.host = 'abc.example2.com'; + expect(element.host).toBe('abc.example2.com'); + expect(element.href).toBe('https://abc.example2.com/path?q1=a#xyz'); + }); + }); + + describe('get hostname()', () => { + it("Returns the href URL's hostname.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.hostname).toBe('www.example.com'); + }); + }); + + describe('set hostname()', () => { + it("Sets the href URL's hostname.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.hostname).toBe('www.example.com'); + + element.hostname = 'abc.example2.com'; + expect(element.hostname).toBe('abc.example2.com'); + expect(element.href).toBe('https://abc.example2.com/path?q1=a#xyz'); + }); + }); + + describe('get port()', () => { + it("Returns the href URL's port.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.port).toBe(''); + + element.setAttribute('href', 'https://www.example.com:444/path?q1=a#xyz'); + expect(element.port).toBe('444'); + }); + }); + + describe('set port()', () => { + it("Sets the href URL's port.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.port).toBe(''); + + element.port = '8080'; + expect(element.port).toBe('8080'); + expect(element.href).toBe('https://www.example.com:8080/path?q1=a#xyz'); + }); + }); + + describe('get pathname()', () => { + it("Returns the href URL's pathname.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.pathname).toBe('/path'); + }); + }); + + describe('set pathname()', () => { + it("Sets the href URL's pathname.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.pathname).toBe('/path'); + + element.pathname = '/path2'; + expect(element.pathname).toBe('/path2'); + expect(element.href).toBe('https://www.example.com/path2?q1=a#xyz'); + }); + }); + + describe('get search()', () => { + it("Returns the href URL's search.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.search).toBe('?q1=a'); + }); + }); + + describe('set search()', () => { + it("Sets the href URL's search.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.search).toBe('?q1=a'); + + element.search = '?q1=b'; + expect(element.search).toBe('?q1=b'); + expect(element.href).toBe('https://www.example.com/path?q1=b#xyz'); + }); + }); + + describe('get hash()', () => { + it("Returns the href URL's hash.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + expect(element.hash).toBe('#xyz'); + }); + }); + + describe('set hash()', () => { + it("Sets the href URL's hash.", () => { + const element = document.createElement('a'); + element.setAttribute('href', 'https://www.example.com:443/path?q1=a#xyz'); + + expect(element.hash).toBe('#xyz'); + + element.hash = '#fgh'; + expect(element.hash).toBe('#fgh'); + expect(element.href).toBe('https://www.example.com/path?q1=a#fgh'); + }); + }); +});