diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index c13aebf1..e3b7fc15 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -158,3 +158,4 @@ export const navigator = Symbol('navigator'); export const screen = Symbol('screen'); export const sessionStorage = Symbol('sessionStorage'); export const localStorage = Symbol('localStorage'); +export const sandbox = Symbol('sandbox'); diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts index 1cffbdb1..d43fb63e 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElement.ts @@ -9,6 +9,7 @@ import HTMLIFrameElementNamedNodeMap from './HTMLIFrameElementNamedNodeMap.js'; import CrossOriginBrowserWindow from '../../window/CrossOriginBrowserWindow.js'; import IBrowserFrame from '../../browser/types/IBrowserFrame.js'; import HTMLIFrameElementPageLoader from './HTMLIFrameElementPageLoader.js'; +import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; /** * HTML Iframe Element. @@ -23,7 +24,7 @@ export default class HTMLIFrameElement extends HTMLElement { // Internal properties public override [PropertySymbol.attributes]: NamedNodeMap; - + public [PropertySymbol.sandbox]: DOMTokenList = null; // Private properties #contentWindowContainer: { window: BrowserWindow | CrossOriginBrowserWindow | null } = { window: null @@ -140,14 +141,15 @@ export default class HTMLIFrameElement extends HTMLElement { * * @returns Sandbox. */ - public get sandbox(): string { - return this.getAttribute('sandbox') || ''; + public get sandbox(): DOMTokenList { + if (!this[PropertySymbol.sandbox]) { + this[PropertySymbol.sandbox] = new DOMTokenList(this, 'sandbox'); + } + return this[PropertySymbol.sandbox]; } /** * Sets sandbox. - * - * @param sandbox Sandbox. */ public set sandbox(sandbox: string) { this.setAttribute('sandbox', sandbox); @@ -163,7 +165,7 @@ export default class HTMLIFrameElement extends HTMLElement { } /** - * Sets sandbox. + * Sets srcdoc. * * @param srcdoc Srcdoc. */ diff --git a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts index 3a6329e4..28a8be90 100644 --- a/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts +++ b/packages/happy-dom/src/nodes/html-iframe-element/HTMLIFrameElementNamedNodeMap.ts @@ -3,6 +3,23 @@ import Element from '../element/Element.js'; import HTMLElementNamedNodeMap from '../html-element/HTMLElementNamedNodeMap.js'; import HTMLIFrameElementPageLoader from './HTMLIFrameElementPageLoader.js'; import * as PropertySymbol from '../../PropertySymbol.js'; +import DOMTokenList from '../../dom-token-list/DOMTokenList.js'; + +const SANDBOX_FLAGS = [ + 'allow-downloads', + 'allow-forms', + 'allow-modals', + 'allow-orientation-lock', + 'allow-pointer-lock', + 'allow-popups', + 'allow-popups-to-escape-sandbox', + 'allow-presentation', + 'allow-same-origin', + 'allow-scripts', + 'allow-top-navigation', + 'allow-top-navigation-by-user-activation', + 'allow-top-navigation-to-custom-protocols' +]; /** * Named Node Map. @@ -37,6 +54,49 @@ export default class HTMLIFrameElementNamedNodeMap extends HTMLElementNamedNodeM this.#pageLoader.loadPage(); } + if (item[PropertySymbol.name] === 'sandbox') { + if (!this[PropertySymbol.ownerElement][PropertySymbol.sandbox]) { + this[PropertySymbol.ownerElement][PropertySymbol.sandbox] = new DOMTokenList( + this[PropertySymbol.ownerElement], + 'sandbox' + ); + } else { + this[PropertySymbol.ownerElement][PropertySymbol.sandbox][PropertySymbol.updateIndices](); + } + + this.#validateSandboxFlags(); + } + return replacedAttribute || null; } + + /** + * + * @param tokens + * @param vconsole + */ + #validateSandboxFlags(): void { + const window = + this[PropertySymbol.ownerElement][PropertySymbol.ownerDocument][PropertySymbol.ownerWindow]; + const values = this[PropertySymbol.ownerElement][PropertySymbol.sandbox].values(); + const invalidFlags: string[] = []; + + for (const token of values) { + if (!SANDBOX_FLAGS.includes(token)) { + invalidFlags.push(token); + } + } + + if (invalidFlags.length === 1) { + window.console.error( + `Error while parsing the 'sandbox' attribute: '${invalidFlags[0]}' is an invalid sandbox flag.` + ); + } else if (invalidFlags.length > 1) { + window.console.error( + `Error while parsing the 'sandbox' attribute: '${invalidFlags.join( + `', '` + )}' are invalid sandbox flags.` + ); + } + } } diff --git a/packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts b/packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts index ddc8c717..5fa2f00a 100644 --- a/packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts +++ b/packages/happy-dom/test/dom-token-list/DOMTokenList.test.ts @@ -85,6 +85,7 @@ describe('DOMTokenList', () => { it('Sets the attribute value.', () => { classList.value = 'class1 class2 class3'; expect(element.className).toBe('class1 class2 class3'); + expect(classList[2]).toBe('class3'); }); }); diff --git a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts index fdcad0c6..74d75024 100644 --- a/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts +++ b/packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts @@ -1,6 +1,5 @@ import Window from '../../../src/window/Window.js'; import BrowserWindow from '../../../src/window/BrowserWindow.js'; -import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; import HTMLIFrameElement from '../../../src/nodes/html-iframe-element/HTMLIFrameElement.js'; import Response from '../../../src/fetch/Response.js'; @@ -13,6 +12,7 @@ import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest'; import IRequestInfo from '../../../src/fetch/types/IRequestInfo.js'; import Headers from '../../../src/fetch/Headers.js'; import Browser from '../../../src/browser/Browser.js'; +import DOMTokenList from '../../../src/dom-token-list/DOMTokenList.js'; describe('HTMLIFrameElement', () => { let window: Window; @@ -35,7 +35,7 @@ describe('HTMLIFrameElement', () => { }); }); - for (const property of ['src', 'allow', 'height', 'width', 'name', 'sandbox', 'srcdoc']) { + for (const property of ['src', 'allow', 'height', 'width', 'name', 'srcdoc']) { describe(`get ${property}()`, () => { it(`Returns the "${property}" attribute.`, () => { element.setAttribute(property, 'value'); @@ -51,6 +51,81 @@ describe('HTMLIFrameElement', () => { }); } + describe('get sandbox()', () => { + it('Returns DOMTokenList', () => { + expect(element.sandbox).toBeInstanceOf(DOMTokenList); + element.sandbox.add('allow-forms', 'allow-scripts'); + expect(element.sandbox.toString()).toBe('allow-forms allow-scripts'); + }); + + it('Returns values from attribute', () => { + element.setAttribute('sandbox', 'allow-forms allow-scripts'); + expect(element.sandbox.toString()).toBe('allow-forms allow-scripts'); + }); + }); + + describe('set sandbox()', () => { + it('Sets attribute', () => { + element.sandbox = 'allow-forms allow-scripts'; + expect(element.getAttribute('sandbox')).toBe('allow-forms allow-scripts'); + + element.sandbox = + 'allow-downloads allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts allow-top-navigation allow-top-navigation-by-user-activation allow-top-navigation-to-custom-protocols'; + expect(element.sandbox.toString()).toBe( + 'allow-downloads allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts allow-top-navigation allow-top-navigation-by-user-activation allow-top-navigation-to-custom-protocols' + ); + }); + + it('Updates the DOMTokenList indicies when setting the sandbox attribute', () => { + element.sandbox = 'allow-forms allow-scripts'; + expect(element.sandbox.length).toBe(2); + expect(element.sandbox[0]).toBe('allow-forms'); + expect(element.sandbox[1]).toBe('allow-scripts'); + + element.sandbox = 'allow-scripts allow-forms'; + expect(element.sandbox.length).toBe(2); + expect(element.sandbox[0]).toBe('allow-scripts'); + expect(element.sandbox[1]).toBe('allow-forms'); + + element.sandbox = 'allow-forms'; + expect(element.sandbox.length).toBe(1); + expect(element.sandbox[0]).toBe('allow-forms'); + expect(element.sandbox[1]).toBe(undefined); + + element.sandbox = ''; + + expect(element.sandbox.length).toBe(0); + expect(element.sandbox[0]).toBe(undefined); + + element.sandbox = 'allow-forms allow-scripts allow-forms'; + expect(element.sandbox.length).toBe(2); + expect(element.sandbox[0]).toBe('allow-forms'); + expect(element.sandbox[1]).toBe('allow-scripts'); + + element.sandbox = 'allow-forms allow-scripts allow-modals'; + expect(element.sandbox.length).toBe(3); + expect(element.sandbox[0]).toBe('allow-forms'); + expect(element.sandbox[1]).toBe('allow-scripts'); + expect(element.sandbox[2]).toBe('allow-modals'); + }); + + it('Console error occurs when add an invalid sandbox flag', () => { + element.sandbox = 'invalid'; + expect(window.happyDOM.virtualConsolePrinter.readAsString()).toBe( + `Error while parsing the 'sandbox' attribute: 'invalid' is an invalid sandbox flag.\n` + ); + expect(element.sandbox.toString()).toBe('invalid'); + expect(element.getAttribute('sandbox')).toBe('invalid'); + + element.setAttribute('sandbox', 'first-invalid second-invalid'); + expect(window.happyDOM.virtualConsolePrinter.readAsString()).toBe( + `Error while parsing the 'sandbox' attribute: 'first-invalid', 'second-invalid' are invalid sandbox flags.\n` + ); + expect(element.sandbox.toString()).toBe('first-invalid second-invalid'); + expect(element.getAttribute('sandbox')).toBe('first-invalid second-invalid'); + }); + }); + describe('get contentWindow()', () => { it('Returns content window for "about:blank".', () => { element.src = 'about:blank';