CVE-2025-61927
Description
Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. Happy DOM v19 and lower contains a security vulnerability that puts the owner system at the risk of RCE (Remote Code Execution) attacks. A Node.js VM Context is not an isolated environment, and if the user runs untrusted JavaScript code within the Happy DOM VM Context, it may escape the VM and get access to process level functionality. It seems like what the attacker can get control over depends on if the process is using ESM or CommonJS. With CommonJS the attacker can get hold of the require() function to import modules. Happy DOM has JavaScript evaluation enabled by default. This may not be obvious to the consumer of Happy DOM and can potentially put the user at risk if untrusted code is executed within the environment. Version 20.0.0 patches the issue by changing JavaScript evaluation to be disabled by default.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
happy-domnpm | < 20.0.0 | 20.0.0 |
Affected products
1- Range: v0.0.1, v0.1.0, v0.10.0, …
Patches
2819d15ba2894BREAKING CHANGE: [#0] Changes JavaScript evaluation to be disabled by default (#1930)
59 files changed · +1304 −644
integration-test/package.json+1 −1 modified@@ -10,7 +10,7 @@ "access": "restricted" }, "scripts": { - "test": "cd ./test && ls | node --test && node ./browser-exception-observer/BrowserExceptionObserver.test.js", + "test": "cd ./test && ls | node --disallow-code-generation-from-strings --test && node --disallow-code-generation-from-strings ./browser-exception-observer/BrowserExceptionObserver.test.js", "test:debug": "cd ./test && ls | node --inspect-brk" }, "devDependencies": {
integration-test/test/browser-exception-observer/BrowserExceptionObserver.test.js+8 −2 modified@@ -21,7 +21,10 @@ await describe('BrowserExceptionObserver', async () => { await describe('observe()', async () => { await it('Observes unhandled fetch rejections.', async () => { const browser = new Browser({ - settings: { errorCapture: BrowserErrorCaptureEnum.processLevel } + settings: { + errorCapture: BrowserErrorCaptureEnum.processLevel, + enableJavaScriptEvaluation: true + } }); const page = browser.newPage(); const window = page.mainFrame.window; @@ -58,7 +61,10 @@ await describe('BrowserExceptionObserver', async () => { await it('Observes uncaught exceptions.', async () => { const browser = new Browser({ - settings: { errorCapture: BrowserErrorCaptureEnum.processLevel } + settings: { + errorCapture: BrowserErrorCaptureEnum.processLevel, + enableJavaScriptEvaluation: true + } }); const page = browser.newPage(); const window = page.mainFrame.window;
integration-test/test/Browser.test.js+3 −1 modified@@ -7,6 +7,7 @@ describe('Browser', () => { const browser = new Browser({ settings: { errorCapture: BrowserErrorCaptureEnum.processLevel, + enableJavaScriptEvaluation: true, // Github.com has a timer that is very long (hours) and a timer loop that never ends. timer: { @@ -47,7 +48,8 @@ describe('Browser', () => { it('Goes to "npmjs.com".', async () => { const browser = new Browser({ settings: { - errorCapture: BrowserErrorCaptureEnum.processLevel + errorCapture: BrowserErrorCaptureEnum.processLevel, + enableJavaScriptEvaluation: true } }); const page = browser.newPage();
integration-test/test/WindowGlobals.test.js+3 −3 modified@@ -4,7 +4,7 @@ import { describe, it } from 'node:test'; describe('WindowGlobals', () => { it('Functions should have the constructor global.Function.', () => { - const window = new Window(); + const window = new Window({ settings: { enableJavaScriptEvaluation: true } }); let error = null; window.addEventListener('error', (event) => (error = event.error)); window.document.write(` @@ -18,7 +18,7 @@ describe('WindowGlobals', () => { }); it('Object should have the constructor global.Object.', () => { - const window = new Window(); + const window = new Window({ settings: { enableJavaScriptEvaluation: true } }); let error = null; window.addEventListener('error', (event) => (error = event.error)); window.document.write(` @@ -32,7 +32,7 @@ describe('WindowGlobals', () => { }); it('Binds global methods to the Window context.', () => { - const window = new Window(); + const window = new Window({ settings: { enableJavaScriptEvaluation: true } }); let error = null; window.addEventListener('error', (event) => (error = event.error)); window.document.write(`
packages/@happy-dom/server-renderer/src/config/DefaultServerRendererConfiguration.ts+7 −1 modified@@ -5,7 +5,13 @@ import OS from 'os'; import { BrowserErrorCaptureEnum } from 'happy-dom'; export default <IServerRendererConfiguration>{ - browser: { ...DefaultBrowserSettings, errorCapture: BrowserErrorCaptureEnum.processLevel }, + browser: { + ...DefaultBrowserSettings, + errorCapture: BrowserErrorCaptureEnum.processLevel, + // This is enabled by default as the entire point of this package is to server-render client side JavaScript. + // "--disallow-code-generation-from-strings" is enabled on workers to prevent escape of the VM context. + enableJavaScriptEvaluation: true + }, outputDirectory: './happy-dom/render', logLevel: ServerRendererLogLevelEnum.info, debug: false,
packages/@happy-dom/server-renderer/src/ServerRenderer.ts+1 −0 modified@@ -197,6 +197,7 @@ export default class ServerRenderer { return; } const worker = new Worker(new URL('ServerRendererWorker.js', import.meta.url), { + execArgv: ['--disallow-code-generation-from-strings'], workerData: { configuration: configuration }
packages/@happy-dom/server-renderer/src/utilities/HelpPrinterRows.ts+8 −1 modified@@ -126,12 +126,19 @@ export default [ '1' ], [ - '--browser.disableJavascriptEvaluation', + '--browser.disableJavaScriptEvaluation', '', 'boolean', 'Disables JavaScript evaluation.', 'false' ], + [ + '--browser.suppressCodeGenerationFromStringsWarning', + '', + 'boolean', + 'Suppresses the warning that is printed when code generation from strings is enabled at process level', + 'false' + ], [ '--browser.disableJavaScriptFileLoading', '',
packages/@happy-dom/server-renderer/src/utilities/ProcessArgumentsParser.ts+3 −1 modified@@ -40,7 +40,9 @@ export default class ProcessArgumentsParser { } else if (arg === '--help' || arg === '-h') { config.help = true; } else if (arg === '--browser.disableJavaScriptEvaluation') { - config.browser.disableJavaScriptEvaluation = true; + config.browser.enableJavaScriptEvaluation = false; + } else if (arg === '--browser.suppressCodeGenerationFromStringsWarning') { + config.browser.suppressCodeGenerationFromStringsWarning = true; } else if (arg === '--browser.disableJavaScriptFileLoading') { config.browser.disableJavaScriptFileLoading = true; } else if (arg === '--browser.disableCSSFileLoading') {
packages/@happy-dom/server-renderer/test/MockedWorker.ts+13 −7 modified@@ -2,13 +2,16 @@ import IServerRendererConfiguration from '../src/types/IServerRendererConfigurat import IServerRendererItem from '../src/types/IServerRendererItem'; import IServerRendererResult from '../src/types/IServerRendererResult'; +type TEvent = 'message' | 'error' | 'exit'; + /** - * + * Mocked worker. */ export default class MockedWorker { public static openWorkers: MockedWorker[] = []; public static terminatedWorkers: MockedWorker[] = []; public scriptPath: string; + public execArgv: string[] = []; public workerData: { configuration: IServerRendererConfiguration; }; @@ -25,13 +28,16 @@ export default class MockedWorker { public isTerminated: boolean = false; /** + * Constructor. * - * @param scriptPath - * @param options - * @param options.workerData + * @param scriptPath Script path. + * @param options Options. + * @param options.execArgv Exec arguments. + * @param options.workerData Worker data. */ - constructor(scriptPath: string, options: { workerData: any }) { + constructor(scriptPath: string, options: { execArgv: string[]; workerData: any }) { this.scriptPath = scriptPath; + this.execArgv = options.execArgv; this.workerData = options.workerData; (<typeof MockedWorker>this.constructor).openWorkers.push(this); } @@ -41,7 +47,7 @@ export default class MockedWorker { * @param event * @param listener */ - public on(event: string, listener: any): void { + public on(event: TEvent, listener: any): void { this.listeners[event].push(listener); } @@ -50,7 +56,7 @@ export default class MockedWorker { * @param event * @param listener */ - public off(event: string, listener: any): void { + public off(event: TEvent, listener: any): void { const index = this.listeners[event].indexOf(listener); this.listeners[event].splice(index, 1); }
packages/@happy-dom/server-renderer/test/ServerRendererBrowser.test.ts+25 −9 modified@@ -34,7 +34,9 @@ describe('ServerRendererBrowser', () => { return Promise.resolve(); }); const browser = new ServerRendererBrowser( - ServerRendererConfigurationFactory.createConfiguration() + ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true } + }) ); const results = await browser.render([{ url: 'https://example.com/gb/en/' }]); @@ -70,7 +72,9 @@ describe('ServerRendererBrowser', () => { return Promise.resolve(); }); const browser = new ServerRendererBrowser( - ServerRendererConfigurationFactory.createConfiguration() + ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true } + }) ); const results = await browser.render(MockedURLList.slice(0, 15).map((url) => ({ url }))); @@ -97,9 +101,9 @@ describe('ServerRendererBrowser', () => { const writtenFiles: { filePath: string; content: string }[] = []; const requestHeaders: Array<{ [key: string]: string }> = []; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function () { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function (this: Fetch) { const headers: { [key: string]: string } = {}; - for (const [key, value] of this.request.headers) { + for (const [key, value] of (<any>this).request.headers) { headers[key] = value; } requestHeaders.push(headers); @@ -131,7 +135,9 @@ describe('ServerRendererBrowser', () => { ); const browser = new ServerRendererBrowser( - ServerRendererConfigurationFactory.createConfiguration() + ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true } + }) ); const results = await browser.render( MockedURLList.slice(0, 15).map((url, index) => ({ @@ -193,7 +199,9 @@ describe('ServerRendererBrowser', () => { }; }); const browser = new ServerRendererBrowser( - ServerRendererConfigurationFactory.createConfiguration() + ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true } + }) ); const results = await browser.render([{ url: 'https://example.com/gb/en/' }]); @@ -225,6 +233,7 @@ describe('ServerRendererBrowser', () => { const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, render: { timeout: 100 } @@ -264,6 +273,7 @@ The page may contain scripts with timer loops that prevent it from completing. Y const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, render: { timeout: 100 }, @@ -321,7 +331,9 @@ Timer #1 }); const browser = new ServerRendererBrowser( - ServerRendererConfigurationFactory.createConfiguration() + ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true } + }) ); const results = await browser.render([{ url: 'https://example.com/gb/en/' }]); (<any>results[0]).pageConsole = results[0].pageConsole @@ -338,14 +350,14 @@ Timer #1 headers: { key1: 'value' }, outputFile: null, pageConsole: `Error: Error - at eval (https://example.com/gb/en/:0:0) + at https://example.com/gb/en/:1:26 at Timeout._onTimeout (/window/BrowserWindow.ts:0:0) at listOnTimeout (node:internal/timers:0:0) at processTimers (node:internal/timers:0:0) `, pageErrors: [ `Error: Error - at eval (https://example.com/gb/en/:0:0) + at https://example.com/gb/en/:1:26 at Timeout._onTimeout (/window/BrowserWindow.ts:0:0) at listOnTimeout (node:internal/timers:0:0) at processTimers (node:internal/timers:0:0)` @@ -383,6 +395,7 @@ Timer #1 const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, render: { allShadowRoots: true } @@ -453,6 +466,7 @@ Timer #1 const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, render: { serializableShadowRoots: true } @@ -533,6 +547,7 @@ Timer #1 const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, render: { allShadowRoots: true, excludeShadowRootTags: ['custom-element'] @@ -601,6 +616,7 @@ Timer #1 ); const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, cache: { disable: true }
packages/@happy-dom/server-renderer/test/ServerRenderer.test.ts+4 −0 modified@@ -58,6 +58,8 @@ describe('ServerRenderer', () => { 'file://' + Path.resolve(Path.join('src', 'ServerRendererWorker.js')) ); + expect(worker.execArgv).toEqual(['--disallow-code-generation-from-strings']); + expect(worker.workerData.configuration.cache.directory).toBe( Path.resolve(Path.join('happy-dom', 'cache')) ); @@ -214,6 +216,8 @@ describe('ServerRenderer', () => { 'file://' + Path.resolve(Path.join('src', 'ServerRendererWorker.js')) ); + expect(worker.execArgv).toEqual(['--disallow-code-generation-from-strings']); + expect(worker.workerData.configuration.cache.directory).toBe( Path.resolve(Path.join('happy-dom', 'cache')) );
packages/@happy-dom/server-renderer/test/utilities/MockedConfiguration.ts+3 −1 modified@@ -4,7 +4,9 @@ import ServerRendererLogLevelEnum from '../../src/enums/ServerRendererLogLevelEn export default <IServerRendererConfiguration>{ browser: { - disableJavaScriptEvaluation: true, + disableJavaScriptEvaluation: false, + enableJavaScriptEvaluation: false, + suppressCodeGenerationFromStringsWarning: true, disableJavaScriptFileLoading: true, disableCSSFileLoading: true, disableIframePageLoading: false,
packages/@happy-dom/server-renderer/test/utilities/ProcessArgumentsParser.test.ts+37 −0 modified@@ -308,13 +308,49 @@ describe('ProcessArgumentsParser', () => { ); }); + it('Returns configuration with javascript evaluation disabled.', async () => { + const expectedConfig = { + ...DefaultServerRendererConfiguration, + browser: { + ...DefaultServerRendererConfiguration.browser, + enableJavaScriptEvaluation: false + } + }; + expect( + await ProcessArgumentsParser.getConfiguration([ + 'node', + 'script.js', + '--browser.disableJavaScriptEvaluation' + ]) + ).toEqual(expectedConfig); + }); + + it('Returns configuration with suppressed code generation warning.', async () => { + const expectedConfig = { + ...DefaultServerRendererConfiguration, + browser: { + ...DefaultServerRendererConfiguration.browser, + suppressCodeGenerationFromStringsWarning: true + } + }; + expect( + await ProcessArgumentsParser.getConfiguration([ + 'node', + 'script.js', + '--browser.suppressCodeGenerationFromStringsWarning' + ]) + ).toEqual(expectedConfig); + }); + it('Returns configuration for all options.', async () => { const specialArguments = [ // Deprecated 'browser.disableErrorCapturing', 'browser.enableFileSystemHttpRequests', 'browser.disableIframePageLoading', + 'browser.disableJavaScriptEvaluation', // Special handling + 'browser.enableJavaScriptEvaluation', 'browser.fetch.requestHeaders', 'browser.fetch.virtualServers', 'urls' @@ -345,6 +381,7 @@ describe('ProcessArgumentsParser', () => { args.unshift('script.js'); args.unshift('node'); + args.push('--browser.disableJavaScriptEvaluation'); args.push('--browser.fetch.requestHeaders="X-Custom-Header-1:Value-1"'); args.push('--browser.fetch.requestHeaders="https://example.com/|X-Custom-Header-2:Value-2"'); args.push('--browser.fetch.virtualServers="https://example.com/path/|./virtual-server/path"');
packages/happy-dom/src/browser/DefaultBrowserSettings.ts+2 −0 modified@@ -5,6 +5,7 @@ import IBrowserSettings from './types/IBrowserSettings.js'; export default <IBrowserSettings>{ disableJavaScriptEvaluation: false, + enableJavaScriptEvaluation: false, disableJavaScriptFileLoading: false, disableCSSFileLoading: false, disableIframePageLoading: false, @@ -13,6 +14,7 @@ export default <IBrowserSettings>{ disableErrorCapturing: false, errorCapture: BrowserErrorCaptureEnum.tryAndCatch, enableFileSystemHttpRequests: false, + suppressCodeGenerationFromStringsWarning: false, timer: { maxTimeout: -1, maxIntervalTime: -1,
packages/happy-dom/src/browser/types/IBrowserSettings.ts+18 −1 modified@@ -11,9 +11,23 @@ import BrowserWindow from '../../window/BrowserWindow.js'; * Browser settings. */ export default interface IBrowserSettings { - /** Disables JavaScript evaluation. */ + /** + * Disables JavaScript evaluation. + * + * @deprecated Javascript evaluation is now disabled by default. Use "enableJavaScriptEvaluation" if you want to enable it. + */ disableJavaScriptEvaluation: boolean; + /** + * Enables JavaScript evaluation. + * + * A VM Context is not an isolated environment, and if you run untrusted code you are at risk of RCE (Remote Code Execution) attacks. + * It is recommended to disable code generation at process level by running node with the "--disallow-code-generation-from-strings" flag enabled to protect against these types of attacks. + * + * @see https://github.com/capricorn86/happy-dom/wiki/Code-Generation-From-Strings-Warning + */ + enableJavaScriptEvaluation: boolean; + /** Disables JavaScript file loading. */ disableJavaScriptFileLoading: boolean; @@ -26,6 +40,9 @@ export default interface IBrowserSettings { /** Handle disabled resource loading as success */ handleDisabledFileLoadingAsSuccess: boolean; + /** Suppresses the warning that is printed when code generation from strings is enabled at process level. */ + suppressCodeGenerationFromStringsWarning: boolean; + /** * Settings for timers */
packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts+18 −1 modified@@ -8,9 +8,23 @@ import IOptionalTimerLoopsLimit from '../../window/IOptionalTimerLoopsLimit.js'; import BrowserWindow from '../../window/BrowserWindow.js'; export default interface IOptionalBrowserSettings { - /** Disables JavaScript evaluation. */ + /** + * Disables JavaScript evaluation. + * + * @deprecated Javascript evaluation is now disabled by default. Use "enableJavaScriptEvaluation" if you want to enable it. + */ disableJavaScriptEvaluation?: boolean; + /** + * Enables JavaScript evaluation. + * + * A VM Context is not an isolated environment, and if you run untrusted code you are at risk of RCE (Remote Code Execution) attacks. + * It is recommended to disable code generation at process level by running node with the "--disallow-code-generation-from-strings" flag enabled to protect against these types of attacks. + * + * @see https://github.com/capricorn86/happy-dom/wiki/Code-Generation-From-Strings-Warning + */ + enableJavaScriptEvaluation?: boolean; + /** Disables JavaScript file loading. */ disableJavaScriptFileLoading?: boolean; @@ -23,6 +37,9 @@ export default interface IOptionalBrowserSettings { /** Handle disabled file loading as success */ handleDisabledFileLoadingAsSuccess?: boolean; + /** Suppresses the warning that is printed when code generation from strings is enabled at process level. */ + suppressCodeGenerationFromStringsWarning?: boolean; + /** Settings for timers */ timer?: { maxTimeout?: number;
packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts+3 −3 modified@@ -89,12 +89,12 @@ export default class BrowserFrameNavigator { // Javascript protocol if (targetURL.protocol === 'javascript:') { - if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { + if (frame && frame.page.context.browser.settings.enableJavaScriptEvaluation) { const readyStateManager = frame.window[PropertySymbol.readyStateManager]; const asyncTaskManager = frame[PropertySymbol.asyncTaskManager]; const taskID = readyStateManager.startTask(); - const code = `${targetURL.href.replace('javascript:', '')}\n//# sourceURL=${frame.url}`; + const code = targetURL.href.replace('javascript:', ''); // The browser will wait for the next tick before executing the script. // Fixes issue where evaluating the response can throw an error. @@ -110,7 +110,7 @@ export default class BrowserFrameNavigator { clearImmediate(immediate); resolve(null); }); - frame.window.eval(code); + frame.window[PropertySymbol.evaluateScript](code, { filename: frame.url }); }); });
packages/happy-dom/src/html-parser/HTMLParser.ts+3 −2 modified@@ -776,10 +776,11 @@ export default class HTMLParser { // However, they are allowed to be executed when document.write() is used. // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement if (upperTagName === 'SCRIPT') { - (<HTMLScriptElement>this.currentNode)[PropertySymbol.evaluateScript] = this.evaluateScripts; + (<HTMLScriptElement>this.currentNode)[PropertySymbol.disableEvaluation] = + !this.evaluateScripts; } else if (upperTagName === 'LINK') { // An assumption that the same rule should be applied for the HTMLLinkElement is made here. - (<HTMLLinkElement>this.currentNode)[PropertySymbol.evaluateCSS] = this.evaluateScripts; + (<HTMLLinkElement>this.currentNode)[PropertySymbol.disableEvaluation] = !this.evaluateScripts; } // Plain text elements such as <script> and <style> should only contain text.
packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts+6 −2 modified@@ -401,10 +401,14 @@ export default class ECMAScriptModuleCompiler { } newCode += '})'; - newCode += `\n//# sourceURL=${sourceURL || moduleURL}`; try { - return { imports, execute: this.window.eval(newCode) }; + return { + imports, + execute: this.window[PropertySymbol.evaluateScript](newCode, { + filename: sourceURL || moduleURL + }) + }; } catch (e) { const errorMessage = this.getError(moduleURL, code, sourceURL) || (<Error>e).message; const error = new this.window.SyntaxError(
packages/happy-dom/src/nodes/element/ElementEventAttributeUtility.ts+5 −4 modified@@ -32,7 +32,7 @@ export default class ElementEventAttributeUtility { const browserSettings = new WindowBrowserContext(window).getSettings(); - if (!browserSettings) { + if (!browserSettings || !browserSettings.enableJavaScriptEvaluation) { return null; } @@ -63,13 +63,14 @@ export default class ElementEventAttributeUtility { } newCode += '})'; - newCode += `\n//# sourceURL=${window.location.href}`; let listener: ((event: Event) => void) | null = null; try { - listener = window.eval(newCode).bind(element, { - dispatchError: window[PropertySymbol.dispatchError] + listener = window[PropertySymbol.evaluateScript](newCode, { + filename: window.location.href + }).bind(element, { + dispatchError: window[PropertySymbol.dispatchError].bind(window) }); } catch (e) { const error = new window.SyntaxError(
packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts+4 −4 modified@@ -24,7 +24,7 @@ import IResourceFetchResponse from '../../fetch/types/IResourceFetchResponse.js' export default class HTMLLinkElement extends HTMLElement { // Internal properties public [PropertySymbol.sheet]: CSSStyleSheet | null = null; - public [PropertySymbol.evaluateCSS] = true; + public [PropertySymbol.disableEvaluation] = false; public [PropertySymbol.relList]: DOMTokenList | null = null; #loadedStyleSheetURL: string | null = null; @@ -305,7 +305,7 @@ export default class HTMLLinkElement extends HTMLElement { !browserSettings || !this[PropertySymbol.isConnected] || browserSettings.disableJavaScriptFileLoading || - browserSettings.disableJavaScriptEvaluation + !browserSettings.enableJavaScriptEvaluation ) { return; } @@ -351,7 +351,7 @@ export default class HTMLLinkElement extends HTMLElement { if ( as === 'script' && - (browserSettings.disableJavaScriptFileLoading || browserSettings.disableJavaScriptEvaluation) + (browserSettings.disableJavaScriptFileLoading || !browserSettings.enableJavaScriptEvaluation) ) { return; } @@ -422,7 +422,7 @@ export default class HTMLLinkElement extends HTMLElement { const browserSettings = browserFrame.page.context.browser.settings; - if (!this[PropertySymbol.evaluateCSS] || !this[PropertySymbol.isConnected]) { + if (this[PropertySymbol.disableEvaluation] || !this[PropertySymbol.isConnected]) { return; }
packages/happy-dom/src/nodes/html-media-element/TextTrackList.ts+3 −0 modified@@ -9,6 +9,9 @@ import ClassMethodBinder from '../../utilities/ClassMethodBinder.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrackList */ export default class TextTrackList extends EventTarget { + // Index signature + [index: number]: TextTrack | undefined; + // Internal properties public [PropertySymbol.items]: TextTrack[] = [];
packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts+51 −42 modified@@ -25,7 +25,7 @@ export default class HTMLScriptElement extends HTMLElement { public declare cloneNode: (deep?: boolean) => HTMLScriptElement; // Internal properties - public [PropertySymbol.evaluateScript] = true; + public [PropertySymbol.disableEvaluation] = false; public [PropertySymbol.blocking]: DOMTokenList | null = null; // Private properties @@ -349,32 +349,34 @@ export default class HTMLScriptElement extends HTMLElement { super[PropertySymbol.connectedToDocument](); - if (this[PropertySymbol.evaluateScript]) { - const src = this.getAttribute('src'); + if (this[PropertySymbol.disableEvaluation]) { + return; + } - if (src !== null) { - if (this.getAttribute('type') === 'module') { - this.#loadModule(src); - } else { - this.#loadScript(src); - } - } else if (browserSettings && !browserSettings.disableJavaScriptEvaluation) { - const source = this.textContent; - const type = this.getAttribute('type'); - - if (source) { - if (type === 'module') { - this.#evaluateModule(source); - } else if (type === 'importmap') { - this.#evaluateImportMap(source); - } else if ( - type === null || - type === 'application/x-ecmascript' || - type === 'application/x-javascript' || - type.startsWith('text/javascript') - ) { - this.#evaluateScript(source); - } + const src = this.getAttribute('src'); + + if (src !== null) { + if (this.getAttribute('type') === 'module') { + this.#loadModule(src); + } else { + this.#loadScript(src); + } + } else if (browserSettings && browserSettings.enableJavaScriptEvaluation) { + const source = this.textContent; + const type = this.getAttribute('type'); + + if (source) { + if (type === 'module') { + this.#evaluateModule(source); + } else if (type === 'importmap') { + this.#evaluateImportMap(source); + } else if ( + type === null || + type === 'application/x-ecmascript' || + type === 'application/x-javascript' || + type.startsWith('text/javascript') + ) { + this.#evaluateScript(source); } } } @@ -413,7 +415,7 @@ export default class HTMLScriptElement extends HTMLElement { const browserSettings = new WindowBrowserContext(window).getSettings(); const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); - if (!browserFrame || !browserSettings) { + if (!browserFrame || !browserSettings || !browserSettings.enableJavaScriptEvaluation) { return; } @@ -446,7 +448,12 @@ export default class HTMLScriptElement extends HTMLElement { const browserSettings = new WindowBrowserContext(window).getSettings(); const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); - if (!browserFrame || !browserSettings || window[PropertySymbol.moduleImportMap]) { + if ( + !browserFrame || + !browserSettings || + window[PropertySymbol.moduleImportMap] || + !browserSettings.enableJavaScriptEvaluation + ) { return; } @@ -510,9 +517,9 @@ export default class HTMLScriptElement extends HTMLElement { /** * Evaluates a script. * - * @param source Source. + * @param code Code. */ - #evaluateScript(source: string): void { + #evaluateScript(code: string): void { const window = this[PropertySymbol.window]; const browserSettings = new WindowBrowserContext(window).getSettings(); @@ -522,16 +529,18 @@ export default class HTMLScriptElement extends HTMLElement { this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = this; - const code = `${source}\n//# sourceURL=${this[PropertySymbol.ownerDocument].location.href}`; - if ( browserSettings.disableErrorCapturing || browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch ) { - window.eval(code); + window[PropertySymbol.evaluateScript](code, { + filename: this[PropertySymbol.ownerDocument].location.href + }); } else { try { - window.eval(code); + window[PropertySymbol.evaluateScript](code, { + filename: this[PropertySymbol.ownerDocument].location.href + }); } catch (error) { window[PropertySymbol.dispatchError](<Error>error); } @@ -560,7 +569,7 @@ export default class HTMLScriptElement extends HTMLElement { if ( browserSettings && - (browserSettings.disableJavaScriptFileLoading || browserSettings.disableJavaScriptEvaluation) + (browserSettings.disableJavaScriptFileLoading || !browserSettings.enableJavaScriptEvaluation) ) { if (browserSettings.handleDisabledFileLoadingAsSuccess) { this.dispatchEvent(new Event('load')); @@ -639,7 +648,7 @@ export default class HTMLScriptElement extends HTMLElement { if ( browserSettings && - (browserSettings.disableJavaScriptFileLoading || browserSettings.disableJavaScriptEvaluation) + (browserSettings.disableJavaScriptFileLoading || !browserSettings.enableJavaScriptEvaluation) ) { if (browserSettings.handleDisabledFileLoadingAsSuccess) { this.dispatchEvent(new Event('load')); @@ -693,18 +702,18 @@ export default class HTMLScriptElement extends HTMLElement { this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = this; - const code = `${response.content}\n//# sourceURL=${ - response.virtualServerFile || absoluteURLString - }`; - if ( browserSettings.disableErrorCapturing || browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch ) { - this[PropertySymbol.window].eval(code); + this[PropertySymbol.window][PropertySymbol.evaluateScript](response.content, { + filename: response.virtualServerFile || absoluteURLString + }); } else { try { - this[PropertySymbol.window].eval(code); + this[PropertySymbol.window][PropertySymbol.evaluateScript](response.content, { + filename: response.virtualServerFile || absoluteURLString + }); } catch (error) { this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = null; window[PropertySymbol.dispatchError](<Error>error);
packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts+3 −0 modified@@ -25,6 +25,9 @@ import BrowserWindow from '../../window/BrowserWindow.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement. */ export default class HTMLSelectElement extends HTMLElement { + // Index signature + [index: number]: HTMLOptionElement | undefined; + // Injected by WindowContextClassExtender protected declare [PropertySymbol.window]: BrowserWindow;
packages/happy-dom/src/PropertySymbol.ts+1 −0 modified@@ -404,3 +404,4 @@ export const cssRule = Symbol('cssRule'); export const rulePrefix = Symbol('rulePrefix'); export const virtualServerFile = Symbol('virtualServerFile'); export const frames = Symbol('frames'); +export const disableEvaluation = Symbol('disableEvaluation');
packages/happy-dom/src/window/BrowserWindow.ts+38 −0 modified@@ -331,6 +331,16 @@ const TIMER = { const IS_NODE_JS_TIMEOUT_ENVIRONMENT = setTimeout.toString().includes('new Timeout'); +const IS_PROCESS_LEVEL_CODE_GENERATION_FROM_STRINGS_ALLOWED = (() => { + try { + // eslint-disable-next-line no-new-func + new Function('return true;')(); + return true; + } catch { + return false; + } +})(); + /** * Class for PerformanceObserverEntryList as it is only available as an interface from Node.js. */ @@ -779,6 +789,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public declare encodeURI: typeof encodeURI; public declare encodeURIComponent: typeof encodeURIComponent; public declare eval: typeof eval; + /** * @deprecated */ @@ -847,6 +858,21 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal constructor(browserFrame: IBrowserFrame, options?: { url?: string }) { super(); + if ( + IS_PROCESS_LEVEL_CODE_GENERATION_FROM_STRINGS_ALLOWED && + browserFrame.page.context.browser.settings.enableJavaScriptEvaluation && + !browserFrame.page.context.browser.settings.suppressCodeGenerationFromStringsWarning + ) { + // eslint-disable-next-line no-console + console.warn( + '\nWarning! Happy DOM has JavaScript evaluation enabled and is running in an environment with code generation from strings (eval) enabled at process level.' + + '\n\nA VM Context is not an isolated environment, and if you run untrusted code you are at risk of RCE (Remote Code Execution) attacks. The attacker can use code generation to escape the VM and run code at process level.' + + '\n\nIt is recommended to disable code generation at process level by running node with the "--disallow-code-generation-from-strings" flag enabled when Javascript evaluation is enabled in Happy DOM.' + + ' You can suppress this warning by setting "suppressCodeGenerationFromStringsWarning" to "true" at your own risk.' + + '\n\nFor more information, see https://github.com/capricorn86/happy-dom/wiki/Code-Generation-From-Strings-Warning\n\n' + ); + } + this.#browserFrame = browserFrame; this.console = browserFrame.page.console; @@ -1806,6 +1832,18 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal this.dispatchEvent(new ErrorEvent('error', { message: error.message, error })); } + /** + * Evaluates code in a VM context. + * + * @param code Code. + * @param [options] Options. + * @param [options.filename] Filename. + * @returns any. + */ + public [PropertySymbol.evaluateScript](code: string, options?: { filename?: string }): any { + return new VM.Script(code, options).runInContext(this); + } + /** * Setup of VM context. */
packages/happy-dom/src/window/GlobalWindow.ts+14 −1 modified@@ -69,7 +69,20 @@ export default class GlobalWindow extends Window { public unescape: (str: string) => string = globalThis.unescape; /** - * Setup of VM context. + * @override + */ + public override [PropertySymbol.evaluateScript]( + code: string, + options?: { filename?: string } + ): any { + if (options?.filename) { + return this.eval(`${code}\n//# sourceURL=${options.filename}`); + } + return this.eval(code); + } + + /** + * @override */ protected override [PropertySymbol.setupVMContext](): void { // Do nothing
packages/happy-dom/src/xml-parser/XMLParser.ts+0 −3 modified@@ -109,9 +109,6 @@ export default class XMLParser { * Constructor. * * @param window Window. - * @param [options] Options. - * @param [options.mode] Mode. Defaults to "htmlFragment". - * @param [options.evaluateScripts] Set to "true" to enable script execution */ constructor(window: BrowserWindow) { this.window = window;
packages/happy-dom/test/browser/BrowserFrame.test.ts+48 −27 modified@@ -152,9 +152,9 @@ describe('BrowserFrame', () => { frame1.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); await page.waitUntilComplete(); - expect(page.mainFrame.window['test']).toBe(1); - expect(frame1.window['test']).toBe(2); - expect(frame2.window['test']).toBe(3); + expect((<any>page.mainFrame.window)['test']).toBe(1); + expect((<any>frame1.window)['test']).toBe(2); + expect((<any>frame2.window)['test']).toBe(3); }); it('Traces never ending timeout when calling waitUntilComplete() with the setting "debug.traceWaitUntilComplete" set to a time in ms.', async () => { @@ -177,7 +177,7 @@ describe('BrowserFrame', () => { try { await page.waitUntilComplete(); } catch (e) { - error = e; + error = <Error>e; } expect(error?.toString().replace(STACK_TRACE_REGEXP, '') + '> testFunction (test.js:1:1)\n') .toBe(`Error: The maximum time was reached for "waitUntilComplete()". @@ -210,7 +210,7 @@ Timer #1 try { await page.waitUntilComplete(); } catch (e) { - error = e; + error = <Error>e; } expect(error?.toString().replace(STACK_TRACE_REGEXP, '') + '> testFunction (test.js:1:1)\n') .toBe(`Error: The maximum time was reached for "waitUntilComplete()". @@ -272,9 +272,9 @@ Task #1 frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); page.abort(); await new Promise((resolve) => setTimeout(resolve, 50)); - expect(page.mainFrame.window['test']).toBeUndefined(); - expect(frame1.window['test']).toBeUndefined(); - expect(frame2.window['test']).toBeUndefined(); + expect((<any>page.mainFrame.window)['test']).toBeUndefined(); + expect((<any>frame1.window)['test']).toBeUndefined(); + expect((<any>frame2.window)['test']).toBeUndefined(); }); }); @@ -283,22 +283,24 @@ Task #1 const browser = new Browser(); const page = browser.newPage(); expect(page.mainFrame.evaluate('globalThis.test = 1')).toBe(1); - expect(page.mainFrame.window['test']).toBe(1); + expect((<any>page.mainFrame.window)['test']).toBe(1); }); it("Evaluates a VM script in the frame's context.", () => { const browser = new Browser(); const page = browser.newPage(); expect(page.mainFrame.evaluate(new Script('globalThis.test = 1'))).toBe(1); - expect(page.mainFrame.window['test']).toBe(1); + expect((<any>page.mainFrame.window)['test']).toBe(1); }); }); describe('goto()', () => { it('Navigates to a URL.', async () => { let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = (<any>this).request; return Promise.resolve(<Response>{ url: request?.url, text: () => @@ -326,8 +328,10 @@ Task #1 it('Triggers "beforeContentCallback" before content is loaded into the document', async () => { let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = (<any>this).request; return Promise.resolve(<Response>{ url: request?.url, text: () => @@ -353,8 +357,10 @@ Task #1 it('Triggers "browser.settings.navigation.beforeContentCallback" before content is loaded into the document', async () => { let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = (<any>this).request; return Promise.resolve(<Response>{ url: request?.url, text: () => @@ -383,7 +389,12 @@ Task #1 }); it('Navigates to a URL with "javascript:" as protocol.', async () => { - const browser = new Browser(); + const browser = new Browser({ + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } + }); const page = browser.newPage(); const oldWindow = page.mainFrame.window; const response = await page.mainFrame.goto('javascript:document.write("test");'); @@ -416,7 +427,7 @@ Task #1 timeout: 1 }); } catch (e) { - error = e; + error = <Error>e; } expect(error).toEqual( @@ -433,8 +444,10 @@ Task #1 it('Handles error status code in response.', async () => { let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = (<any>this).request; return Promise.resolve(<Response>{ url: request?.url, status: 404, @@ -474,7 +487,7 @@ Task #1 try { await page.mainFrame.goto('http://localhost:9999'); } catch (e) { - error = e; + error = <Error>e; } expect(error).toEqual(new Error('Error')); @@ -919,6 +932,8 @@ Task #1 it('Navigates to a virtual server page.', async () => { const browser = new Browser({ settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ { @@ -989,15 +1004,17 @@ Task #1 describe('goBack()', () => { it('Navigates back in history.', async () => { - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { return Promise.resolve(<Response>{ status: 200, text: () => new Promise((resolve) => setTimeout( () => resolve( - this.request.url === 'http://localhost:3000/' + (<any>this).request.url === 'http://localhost:3000/' ? '<a href="http://localhost:3000/navigated/">' : '<b>Navigated</b>' ), @@ -1043,15 +1060,17 @@ Task #1 describe('goForward()', () => { it('Navigates forward in history.', async () => { - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { return Promise.resolve(<Response>{ status: 200, text: () => new Promise((resolve) => setTimeout( () => resolve( - this.request.url === 'http://localhost:3000/' + (<any>this).request.url === 'http://localhost:3000/' ? '<a href="http://localhost:3000/navigated/">' : '<b>Navigated</b>' ), @@ -1098,15 +1117,17 @@ Task #1 describe('goSteps()', () => { it('Navigates a delta in history.', async () => { - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { return Promise.resolve(<Response>{ status: 200, text: () => new Promise((resolve) => setTimeout( () => resolve( - this.request.url === 'http://localhost:3000/' + (<any>this).request.url === 'http://localhost:3000/' ? '<a href="http://localhost:3000/navigated/">' : '<b>Navigated</b>' ),
packages/happy-dom/test/browser/BrowserPage.test.ts+6 −4 modified@@ -184,6 +184,8 @@ describe('BrowserPage', () => { it('Clears modules when closing.', async () => { const browser = new Browser({ settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ { @@ -251,8 +253,8 @@ describe('BrowserPage', () => { frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); await page.waitUntilComplete(); - expect(frame1.window['test']).toBe(1); - expect(frame2.window['test']).toBe(2); + expect((<any>frame1.window)['test']).toBe(1); + expect((<any>frame2.window)['test']).toBe(2); }); }); @@ -283,8 +285,8 @@ describe('BrowserPage', () => { frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); page.abort(); await new Promise((resolve) => setTimeout(resolve, 50)); - expect(frame1.window['test']).toBeUndefined(); - expect(frame2.window['test']).toBeUndefined(); + expect((<any>frame1.window)['test']).toBeUndefined(); + expect((<any>frame2.window)['test']).toBeUndefined(); }); });
packages/happy-dom/test/browser/Browser.test.ts+7 −7 modified@@ -65,7 +65,7 @@ describe('Browser', () => { it('Returns the settings with custom settings.', () => { const settings = { - disableJavaScriptEvaluation: true, + enableJavaScriptEvaluation: true, navigator: { userAgent: 'test' } @@ -135,9 +135,9 @@ describe('Browser', () => { page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); page3.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); await browser.waitUntilComplete(); - expect(page1.mainFrame.window['test']).toBe(1); - expect(page2.mainFrame.window['test']).toBe(2); - expect(page3.mainFrame.window['test']).toBe(3); + expect((<any>page1.mainFrame.window)['test']).toBe(1); + expect((<any>page2.mainFrame.window)['test']).toBe(2); + expect((<any>page3.mainFrame.window)['test']).toBe(3); }); }); @@ -152,9 +152,9 @@ describe('Browser', () => { page3.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); browser.abort(); await new Promise((resolve) => setTimeout(resolve, 50)); - expect(page1.mainFrame.window['test']).toBeUndefined(); - expect(page2.mainFrame.window['test']).toBeUndefined(); - expect(page3.mainFrame.window['test']).toBeUndefined(); + expect((<any>page1.mainFrame.window)['test']).toBeUndefined(); + expect((<any>page2.mainFrame.window)['test']).toBeUndefined(); + expect((<any>page3.mainFrame.window)['test']).toBeUndefined(); }); });
packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts+8 −1 modified@@ -362,7 +362,12 @@ describe('DetachedBrowserFrame', () => { }); it('Navigates to a URL with "javascript:" as protocol.', async () => { - const browser = new DetachedBrowser(BrowserWindow); + const browser = new DetachedBrowser(BrowserWindow, { + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } + }); browser.defaultContext.pages[0].mainFrame.window = new BrowserWindow( browser.defaultContext.pages[0].mainFrame ); @@ -1015,6 +1020,8 @@ describe('DetachedBrowserFrame', () => { it('Navigates to a virtual server page.', async () => { const browser = new DetachedBrowser(BrowserWindow, { settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ {
packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts+1 −1 modified@@ -46,7 +46,7 @@ describe('DetachedBrowser', () => { it('Returns the settings with custom settings.', () => { const settings = { - disableJavaScriptEvaluation: true, + enableJavaScriptEvaluation: true, navigator: { userAgent: 'test' }
packages/happy-dom/test/dom-parser/DOMParser.test.ts+0 −1 modified@@ -16,7 +16,6 @@ describe('DOMParser', () => { window = new Window({ settings: { disableJavaScriptFileLoading: true, - disableJavaScriptEvaluation: true, disableCSSFileLoading: true, enableFileSystemHttpRequests: false }
packages/happy-dom/test/html-serializer/HTMLSerializer.test.ts+3 −1 modified@@ -10,7 +10,9 @@ describe('HTMLSerializer', () => { let serializer: HTMLSerializer; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; serializer = new HTMLSerializer();
packages/happy-dom/test/nodes/document/Document.test.ts+3 −1 modified@@ -46,7 +46,9 @@ describe('Document', () => { let document: Document; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; });
packages/happy-dom/test/nodes/element/Element.test.ts+172 −166 modified@@ -30,7 +30,9 @@ describe('Element', () => { let element: Element; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <Element>document.createElement('div'); window.customElements.define('custom-element', CustomElement); @@ -52,20 +54,20 @@ describe('Element', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -106,63 +108,63 @@ describe('Element', () => { it('Adds the "id" attribute as a property to Window.', () => { element.setAttribute('id', 'element1'); - expect(window['element1']).toBeUndefined(); + expect((<any>window)['element1']).toBeUndefined(); document.body.appendChild(element); - expect(window['element1']).toBe(element); + expect((<any>window)['element1']).toBe(element); document.body.removeChild(element); - expect(window['element1']).toBeUndefined(); + expect((<any>window)['element1']).toBeUndefined(); document.body.appendChild(element); - expect(window['element1']).toBe(element); + expect((<any>window)['element1']).toBe(element); element.setAttribute('id', 'element2'); - expect(window['element1']).toBeUndefined(); - expect(window['element2']).toBe(element); + expect((<any>window)['element1']).toBeUndefined(); + expect((<any>window)['element2']).toBe(element); const element2 = document.createElement('div'); element2.id = 'element2'; document.body.appendChild(element2); - expect(window['element2']).toBeInstanceOf(HTMLCollection); - expect(window['element2'].length).toBe(2); - expect(window['element2'][0]).toBe(element); - expect(window['element2'][1]).toBe(element2); + expect((<any>window)['element2']).toBeInstanceOf(HTMLCollection); + expect((<any>window)['element2'].length).toBe(2); + expect((<any>window)['element2'][0]).toBe(element); + expect((<any>window)['element2'][1]).toBe(element2); document.body.removeChild(element2); - expect(window['element2']).toBe(element); + expect((<any>window)['element2']).toBe(element); document.body.appendChild(element2); - expect(window['element2']).toBeInstanceOf(HTMLCollection); - expect(window['element2'].length).toBe(2); + expect((<any>window)['element2']).toBeInstanceOf(HTMLCollection); + expect((<any>window)['element2'].length).toBe(2); element2.removeAttribute('id'); - expect(window['element2']).toBe(element); + expect((<any>window)['element2']).toBe(element); element.removeAttribute('id'); - expect(window['element2']).toBe(undefined); + expect((<any>window)['element2']).toBe(undefined); }); it(`Doesn't add the "id" attribute as a property to Window if it collides with Window properties.`, () => { element.setAttribute('id', 'document'); document.body.appendChild(element); - expect(window['document']).toBe(document); + expect((<any>window)['document']).toBe(document); }); it(`Doesn't add the "opener" attribute as a property to Window when the property value is null (#1841).`, () => { document.body.appendChild(element); element.id = 'opener'; - expect(window['opener']).toBe(null); + expect((<any>window)['opener']).toBe(null); }); }); @@ -355,8 +357,12 @@ describe('Element', () => { element.appendChild(document.createElement('div')); div.appendChild(textNode); - vi.spyOn(HTMLParser.prototype, 'parse').mockImplementation(function (html, rootNode) { - expect(this.window).toBe(window); + vi.spyOn(HTMLParser.prototype, 'parse').mockImplementation(function ( + this: HTMLParser, + html, + rootNode + ) { + expect((<any>this).window).toBe(window); expect(html).toBe('SOME_HTML'); expect(rootNode).toBe(element); element.appendChild(div); @@ -437,26 +443,26 @@ describe('Element', () => { expect(element.attributes[2].ownerElement === element).toBe(true); expect(element.attributes[2].ownerDocument === document).toBe(true); - expect(element.attributes['key1'].name).toBe('key1'); - expect(element.attributes['key1'].value).toBe('value1'); - expect(element.attributes['key1'].namespaceURI).toBe(null); - expect(element.attributes['key1'].specified).toBe(true); - expect(element.attributes['key1'].ownerElement === element).toBe(true); - expect(element.attributes['key1'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key1'].name).toBe('key1'); + expect((<any>element).attributes['key1'].value).toBe('value1'); + expect((<any>element).attributes['key1'].namespaceURI).toBe(null); + expect((<any>element).attributes['key1'].specified).toBe(true); + expect((<any>element).attributes['key1'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key1'].ownerDocument === document).toBe(true); - expect(element.attributes['key2'].name).toBe('key2'); - expect(element.attributes['key2'].value).toBe('value2'); - expect(element.attributes['key2'].namespaceURI).toBe(null); - expect(element.attributes['key2'].specified).toBe(true); - expect(element.attributes['key2'].ownerElement === element).toBe(true); - expect(element.attributes['key2'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key2'].name).toBe('key2'); + expect((<any>element).attributes['key2'].value).toBe('value2'); + expect((<any>element).attributes['key2'].namespaceURI).toBe(null); + expect((<any>element).attributes['key2'].specified).toBe(true); + expect((<any>element).attributes['key2'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key2'].ownerDocument === document).toBe(true); - expect(element.attributes['key3'].name).toBe('key3'); - expect(element.attributes['key3'].value).toBe('value3'); - expect(element.attributes['key3'].namespaceURI).toBe(null); - expect(element.attributes['key3'].specified).toBe(true); - expect(element.attributes['key3'].ownerElement === element).toBe(true); - expect(element.attributes['key3'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key3'].name).toBe('key3'); + expect((<any>element).attributes['key3'].value).toBe('value3'); + expect((<any>element).attributes['key3'].namespaceURI).toBe(null); + expect((<any>element).attributes['key3'].specified).toBe(true); + expect((<any>element).attributes['key3'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key3'].ownerDocument === document).toBe(true); }); }); @@ -1287,10 +1293,10 @@ describe('Element', () => { expect(otherParent.children[0] === otherDiv).toBe(true); expect(otherParent.children[1] === div).toBe(true); expect(otherParent.children[2] === otherSpan).toBe(true); - expect(otherParent.children['div1'] === div).toBe(true); - expect(otherParent.children['div2'] === div).toBe(true); - expect(otherParent.children['otherDiv'] === otherDiv).toBe(true); - expect(otherParent.children['otherSpan'] === otherSpan).toBe(true); + expect((<any>otherParent.children)['div1'] === div).toBe(true); + expect((<any>otherParent.children)['div2'] === div).toBe(true); + expect((<any>otherParent.children)['otherDiv'] === otherDiv).toBe(true); + expect((<any>otherParent.children)['otherSpan'] === otherSpan).toBe(true); element.appendChild(document.createComment('test')); element.appendChild(div); @@ -1300,17 +1306,17 @@ describe('Element', () => { expect(otherParent.children.length).toBe(2); expect(otherParent.children[0] === otherDiv).toBe(true); expect(otherParent.children[1] === otherSpan).toBe(true); - expect(otherParent.children['div1'] === undefined).toBe(true); - expect(otherParent.children['div2'] === undefined).toBe(true); - expect(otherParent.children['otherDiv'] === otherDiv).toBe(true); - expect(otherParent.children['otherSpan'] === otherSpan).toBe(true); + expect((<any>otherParent.children)['div1'] === undefined).toBe(true); + expect((<any>otherParent.children)['div2'] === undefined).toBe(true); + expect((<any>otherParent.children)['otherDiv'] === otherDiv).toBe(true); + expect((<any>otherParent.children)['otherSpan'] === otherSpan).toBe(true); expect(element.children.length).toBe(2); expect(element.children[0] === div).toBe(true); expect(element.children[1] === span).toBe(true); - expect(element.children['div1'] === div).toBe(true); - expect(element.children['div2'] === div).toBe(true); - expect(element.children['span'] === span).toBe(true); + expect((<any>element.children)['div1'] === div).toBe(true); + expect((<any>element.children)['div2'] === div).toBe(true); + expect((<any>element.children)['span'] === span).toBe(true); }); }); @@ -1327,15 +1333,15 @@ describe('Element', () => { element.appendChild(document.createComment('test')); element.appendChild(span); - expect(element.children['div'] === div).toBe(true); - expect(element.children['span'] === span).toBe(true); + expect((<any>element.children)['div'] === div).toBe(true); + expect((<any>element.children)['span'] === span).toBe(true); element.removeChild(div); expect(element.children.length).toBe(1); expect(element.children[0] === span).toBe(true); - expect(element.children['div'] === undefined).toBe(true); - expect(element.children['span'] === span).toBe(true); + expect((<any>element.children)['div'] === undefined).toBe(true); + expect((<any>element.children)['span'] === span).toBe(true); }); }); @@ -1456,9 +1462,9 @@ describe('Element', () => { expect(otherParent.children[0] === otherSpan1).toBe(true); expect(otherParent.children[1] === div).toBe(true); expect(otherParent.children[2] === otherSpan2).toBe(true); - expect(otherParent.children['otherSpan1'] === otherSpan1).toBe(true); - expect(otherParent.children['div'] === div).toBe(true); - expect(otherParent.children['otherSpan2'] === otherSpan2).toBe(true); + expect((<any>otherParent.children)['otherSpan1'] === otherSpan1).toBe(true); + expect((<any>otherParent.children)['div'] === div).toBe(true); + expect((<any>otherParent.children)['otherSpan2'] === otherSpan2).toBe(true); element.appendChild(document.createComment('test')); element.appendChild(span1); @@ -1472,17 +1478,17 @@ describe('Element', () => { expect(otherParent.children.length).toBe(2); expect(otherParent.children[0] === otherSpan1).toBe(true); expect(otherParent.children[1] === otherSpan2).toBe(true); - expect(otherParent.children['div'] === undefined).toBe(true); - expect(otherParent.children['otherSpan1'] === otherSpan1).toBe(true); - expect(otherParent.children['otherSpan2'] === otherSpan2).toBe(true); + expect((<any>otherParent.children)['div'] === undefined).toBe(true); + expect((<any>otherParent.children)['otherSpan1'] === otherSpan1).toBe(true); + expect((<any>otherParent.children)['otherSpan2'] === otherSpan2).toBe(true); expect(element.children.length).toBe(3); expect(element.children[0] === span1).toBe(true); expect(element.children[1] === div).toBe(true); expect(element.children[2] === span2).toBe(true); - expect(element.children['span1'] === span1).toBe(true); - expect(element.children['div'] === div).toBe(true); - expect(element.children['span2'] === span2).toBe(true); + expect((<any>element.children)['span1'] === span1).toBe(true); + expect((<any>element.children)['div'] === div).toBe(true); + expect((<any>element.children)['span2'] === span2).toBe(true); }); it('Inserts correctly with comment reference node', () => { @@ -1612,19 +1618,19 @@ describe('Element', () => { expect(element.attributes[1].ownerElement === element).toBe(true); expect(element.attributes[1].ownerDocument === document).toBe(true); - expect(element.attributes['key1'].name).toBe('key1'); - expect(element.attributes['key1'].value).toBe('value1'); - expect(element.attributes['key1'].namespaceURI).toBe(null); - expect(element.attributes['key1'].specified).toBe(true); - expect(element.attributes['key1'].ownerElement === element).toBe(true); - expect(element.attributes['key1'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key1'].name).toBe('key1'); + expect((<any>element).attributes['key1'].value).toBe('value1'); + expect((<any>element).attributes['key1'].namespaceURI).toBe(null); + expect((<any>element).attributes['key1'].specified).toBe(true); + expect((<any>element).attributes['key1'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key1'].ownerDocument === document).toBe(true); - expect(element.attributes['key2'].name).toBe('key2'); - expect(element.attributes['key2'].value).toBe(''); - expect(element.attributes['key2'].namespaceURI).toBe(null); - expect(element.attributes['key2'].specified).toBe(true); - expect(element.attributes['key2'].ownerElement === element).toBe(true); - expect(element.attributes['key2'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key2'].name).toBe('key2'); + expect((<any>element).attributes['key2'].value).toBe(''); + expect((<any>element).attributes['key2'].namespaceURI).toBe(null); + expect((<any>element).attributes['key2'].specified).toBe(true); + expect((<any>element).attributes['key2'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key2'].ownerDocument === document).toBe(true); }); it('Sets valid attribute names', () => { @@ -1748,58 +1754,58 @@ describe('Element', () => { try { element.setAttribute('☺', '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { // eslint-disable-next-line element.setAttribute({} as string, '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute('', '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute('=', '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(' ', '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute('"', '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(`'`, '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(`>`, '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(`\/`, '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(`\u007F`, '1'); // control character delete } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(`\u9FFFE`, '1'); // non character } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } }); }); @@ -1825,19 +1831,19 @@ describe('Element', () => { expect(element.attributes[1].ownerElement === element).toBe(true); expect(element.attributes[1].ownerDocument === document).toBe(true); - expect(element.attributes['global:local1'].name).toBe('global:local1'); - expect(element.attributes['global:local1'].value).toBe('value1'); - expect(element.attributes['global:local1'].namespaceURI).toBe(NAMESPACE_URI); - expect(element.attributes['global:local1'].specified).toBe(true); - expect(element.attributes['global:local1'].ownerElement === element).toBe(true); - expect(element.attributes['global:local1'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['global:local1'].name).toBe('global:local1'); + expect((<any>element).attributes['global:local1'].value).toBe('value1'); + expect((<any>element).attributes['global:local1'].namespaceURI).toBe(NAMESPACE_URI); + expect((<any>element).attributes['global:local1'].specified).toBe(true); + expect((<any>element).attributes['global:local1'].ownerElement === element).toBe(true); + expect((<any>element).attributes['global:local1'].ownerDocument === document).toBe(true); - expect(element.attributes['global:local2'].name).toBe('global:local2'); - expect(element.attributes['global:local2'].value).toBe(''); - expect(element.attributes['global:local2'].namespaceURI).toBe(NAMESPACE_URI); - expect(element.attributes['global:local2'].specified).toBe(true); - expect(element.attributes['global:local2'].ownerElement === element).toBe(true); - expect(element.attributes['global:local2'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['global:local2'].name).toBe('global:local2'); + expect((<any>element).attributes['global:local2'].value).toBe(''); + expect((<any>element).attributes['global:local2'].namespaceURI).toBe(NAMESPACE_URI); + expect((<any>element).attributes['global:local2'].specified).toBe(true); + expect((<any>element).attributes['global:local2'].ownerElement === element).toBe(true); + expect((<any>element).attributes['global:local2'].ownerDocument === document).toBe(true); }); }); @@ -1962,39 +1968,39 @@ describe('Element', () => { for (const functionName of ['scroll', 'scrollTo']) { describe(`${functionName}()`, () => { it('Sets the properties scrollTop and scrollLeft.', () => { - element[functionName](50, 60); + (<any>element)[functionName](50, 60); expect(element.scrollLeft).toBe(50); expect(element.scrollTop).toBe(60); }); }); describe(`${functionName}()`, () => { it('Sets the properties scrollTop and scrollLeft using object.', () => { - element[functionName]({ left: 50, top: 60 }); + (<any>element)[functionName]({ left: 50, top: 60 }); expect(element.scrollLeft).toBe(50); expect(element.scrollTop).toBe(60); }); }); describe(`${functionName}()`, () => { it('Sets only the property scrollTop.', () => { - element[functionName]({ top: 60 }); + (<any>element)[functionName]({ top: 60 }); expect(element.scrollLeft).toBe(0); expect(element.scrollTop).toBe(60); }); }); describe(`${functionName}()`, () => { it('Sets only the property scrollLeft.', () => { - element[functionName]({ left: 60 }); + (<any>element)[functionName]({ left: 60 }); expect(element.scrollLeft).toBe(60); expect(element.scrollTop).toBe(0); }); }); describe(`${functionName}()`, () => { it('Sets the properties scrollTop and scrollLeft with animation.', async () => { - element[functionName]({ left: 50, top: 60, behavior: 'smooth' }); + (<any>element)[functionName]({ left: 50, top: 60, behavior: 'smooth' }); expect(element.scrollLeft).toBe(0); expect(element.scrollTop).toBe(0); await window.happyDOM?.waitUntilComplete(); @@ -2127,9 +2133,9 @@ describe('Element', () => { attribute2.value = 'value2'; attribute3.value = 'value3'; - element[method](attribute1); - element[method](attribute2); - element[method](attribute3); + (<any>element)[method](attribute1); + (<any>element)[method](attribute2); + (<any>element)[method](attribute3); expect(element.attributes.length).toBe(3); @@ -2155,32 +2161,32 @@ describe('Element', () => { expect(element.attributes[2].ownerDocument === document).toBe(true); // "undefined" as the key is in upper case which should not be considered as a named item when the element is in the HTML namespace - expect(element.attributes['key1']).toBe(undefined); - expect(element.attributes['KEY1']).toBe(undefined); + expect((<any>element).attributes['key1']).toBe(undefined); + expect((<any>element).attributes['KEY1']).toBe(undefined); // Lower case SVG namespace key is fine - expect(element.attributes['key2'].name).toBe('key2'); - expect(element.attributes['key2'].namespaceURI).toBe(NamespaceURI.svg); - expect(element.attributes['key2'].value).toBe('value2'); - expect(element.attributes['key2'].specified).toBe(true); - expect(element.attributes['key2'].ownerElement === element).toBe(true); - expect(element.attributes['key2'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key2'].name).toBe('key2'); + expect((<any>element).attributes['key2'].namespaceURI).toBe(NamespaceURI.svg); + expect((<any>element).attributes['key2'].value).toBe('value2'); + expect((<any>element).attributes['key2'].specified).toBe(true); + expect((<any>element).attributes['key2'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key2'].ownerDocument === document).toBe(true); // Matches the key in the HTML namespace - expect(element.attributes['key3'].name).toBe('key3'); - expect(element.attributes['key3'].namespaceURI).toBe(null); - expect(element.attributes['key3'].value).toBe('value3'); - expect(element.attributes['key3'].specified).toBe(true); - expect(element.attributes['key3'].ownerElement === element).toBe(true); - expect(element.attributes['key3'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key3'].name).toBe('key3'); + expect((<any>element).attributes['key3'].namespaceURI).toBe(null); + expect((<any>element).attributes['key3'].value).toBe('value3'); + expect((<any>element).attributes['key3'].specified).toBe(true); + expect((<any>element).attributes['key3'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key3'].ownerDocument === document).toBe(true); // Is converted to lower case through the Proxy in the HTML namespace - expect(element.attributes['KeY3'].name).toBe('key3'); - expect(element.attributes['KeY3'].namespaceURI).toBe(null); - expect(element.attributes['KeY3'].value).toBe('value3'); - expect(element.attributes['KeY3'].specified).toBe(true); - expect(element.attributes['KeY3'].ownerElement === element).toBe(true); - expect(element.attributes['KeY3'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['KeY3'].name).toBe('key3'); + expect((<any>element).attributes['KeY3'].namespaceURI).toBe(null); + expect((<any>element).attributes['KeY3'].value).toBe('value3'); + expect((<any>element).attributes['KeY3'].specified).toBe(true); + expect((<any>element).attributes['KeY3'].ownerElement === element).toBe(true); + expect((<any>element).attributes['KeY3'].ownerDocument === document).toBe(true); }); it('Sets an Attr node on an <svg> element.', () => { @@ -2191,47 +2197,47 @@ describe('Element', () => { attribute1.value = 'value1'; attribute2.value = 'value2'; - svg[method](attribute1); - svg[method](attribute2); + (<any>svg)[method](attribute1); + (<any>svg)[method](attribute2); expect(svg.attributes.length).toBe(2); - expect(svg.attributes[0].name).toBe('KEY1'); - expect(svg.attributes[0].namespaceURI).toBe(NamespaceURI.svg); - expect(svg.attributes[0].value).toBe('value1'); - expect(svg.attributes[0].specified).toBe(true); - expect(svg.attributes[0].ownerElement === svg).toBe(true); - expect(svg.attributes[0].ownerDocument).toBe(document); + expect((<any>svg.attributes)[0].name).toBe('KEY1'); + expect((<any>svg.attributes)[0].namespaceURI).toBe(NamespaceURI.svg); + expect((<any>svg.attributes)[0].value).toBe('value1'); + expect((<any>svg.attributes)[0].specified).toBe(true); + expect((<any>svg.attributes)[0].ownerElement === svg).toBe(true); + expect((<any>svg.attributes)[0].ownerDocument).toBe(document); - expect(svg.attributes[1].name).toBe('key2'); - expect(svg.attributes[1].namespaceURI).toBe(null); - expect(svg.attributes[1].value).toBe('value2'); - expect(svg.attributes[1].specified).toBe(true); - expect(svg.attributes[1].ownerElement === svg).toBe(true); - expect(svg.attributes[1].ownerDocument).toBe(document); + expect((<any>svg.attributes)[1].name).toBe('key2'); + expect((<any>svg.attributes)[1].namespaceURI).toBe(null); + expect((<any>svg.attributes)[1].value).toBe('value2'); + expect((<any>svg.attributes)[1].specified).toBe(true); + expect((<any>svg.attributes)[1].ownerElement === svg).toBe(true); + expect((<any>svg.attributes)[1].ownerDocument).toBe(document); // "undefined" as the SVG namespace should not lowercase the key - expect(svg.attributes['key1']).toBe(undefined); - expect(svg.attributes['kEy1']).toBe(undefined); + expect((<any>svg.attributes)['key1']).toBe(undefined); + expect((<any>svg.attributes)['kEy1']).toBe(undefined); // Matching key is fine in the SVG namespace - expect(svg.attributes['KEY1'].name).toBe('KEY1'); - expect(svg.attributes['KEY1'].namespaceURI).toBe(NamespaceURI.svg); - expect(svg.attributes['KEY1'].value).toBe('value1'); - expect(svg.attributes['KEY1'].specified).toBe(true); - expect(svg.attributes['KEY1'].ownerElement === svg).toBe(true); - expect(svg.attributes['KEY1'].ownerDocument).toBe(document); + expect((<any>svg.attributes)['KEY1'].name).toBe('KEY1'); + expect((<any>svg.attributes)['KEY1'].namespaceURI).toBe(NamespaceURI.svg); + expect((<any>svg.attributes)['KEY1'].value).toBe('value1'); + expect((<any>svg.attributes)['KEY1'].specified).toBe(true); + expect((<any>svg.attributes)['KEY1'].ownerElement === svg).toBe(true); + expect((<any>svg.attributes)['KEY1'].ownerDocument).toBe(document); // "undefined" as the SVG namespace should not lowercase the key - expect(svg.attributes['KeY2']).toBe(undefined); + expect((<any>svg.attributes)['KeY2']).toBe(undefined); // Works when matching in the SVG namespace - expect(svg.attributes['key2'].name).toBe('key2'); - expect(svg.attributes['key2'].namespaceURI).toBe(null); - expect(svg.attributes['key2'].value).toBe('value2'); - expect(svg.attributes['key2'].specified).toBe(true); - expect(svg.attributes['key2'].ownerElement === svg).toBe(true); - expect(svg.attributes['key2'].ownerDocument).toBe(document); + expect((<any>svg.attributes)['key2'].name).toBe('key2'); + expect((<any>svg.attributes)['key2'].namespaceURI).toBe(null); + expect((<any>svg.attributes)['key2'].value).toBe('value2'); + expect((<any>svg.attributes)['key2'].specified).toBe(true); + expect((<any>svg.attributes)['key2'].ownerElement === svg).toBe(true); + expect((<any>svg.attributes)['key2'].ownerDocument).toBe(document); }); }); } @@ -2347,7 +2353,7 @@ describe('Element', () => { div.scrollLeft = 10; div.scrollTop = 15; - div[functionName](20, 30); + (<any>div)[functionName](20, 30); expect(div.scrollLeft).toBe(20); expect(div.scrollTop).toBe(30); @@ -2359,7 +2365,7 @@ describe('Element', () => { div.scrollLeft = 10; div.scrollTop = 15; - div[functionName]({ left: 20, top: 30 }); + (<any>div)[functionName]({ left: 20, top: 30 }); expect(div.scrollLeft).toBe(20); expect(div.scrollTop).toBe(30); @@ -2371,7 +2377,7 @@ describe('Element', () => { div.scrollLeft = 10; div.scrollTop = 15; - div[functionName]({ left: 20, top: 30, behavior: 'smooth' }); + (<any>div)[functionName]({ left: 20, top: 30, behavior: 'smooth' }); expect(div.scrollLeft).toBe(10); expect(div.scrollTop).toBe(15); @@ -2384,7 +2390,7 @@ describe('Element', () => { it('Throws an exception if the there is only one argument and it is not an object.', () => { const div = document.createElement('div'); - expect(() => div[functionName](10)).toThrow( + expect(() => (<any>div)[functionName](10)).toThrow( new TypeError( `Failed to execute '${functionName}' on 'Element': The provided value is not of type 'ScrollToOptions'.` ) @@ -2450,15 +2456,15 @@ describe('Element', () => { const div = document.createElement('div'); div.setAttribute('onclick', 'divClicked = true'); div.dispatchEvent(new Event('click')); - expect(window['divClicked']).toBe(true); + expect((<any>window)['divClicked']).toBe(true); }); it("Doesn't evaluate attribute event listener is immediate propagation has been stopped.", () => { const div = document.createElement('div'); div.addEventListener('click', (e: Event) => e.stopImmediatePropagation()); div.setAttribute('onclick', 'divClicked = true'); div.dispatchEvent(new Event('click')); - expect(window['divClicked']).toBe(undefined); + expect((<any>window)['divClicked']).toBe(undefined); }); }); });
packages/happy-dom/test/nodes/html-body-element/HTMLBodyElement.test.ts+9 −7 modified@@ -10,7 +10,9 @@ describe('HTMLBodyElement', () => { let element: HTMLBodyElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElement('body'); }); @@ -44,20 +46,20 @@ describe('HTMLBodyElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts+9 −7 modified@@ -16,7 +16,9 @@ describe('HTMLCanvasElement', () => { let element: HTMLCanvasElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElement('canvas'); }); @@ -37,20 +39,20 @@ describe('HTMLCanvasElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts+9 −7 modified@@ -11,7 +11,9 @@ describe('HTMLDetailsElement', () => { let element: HTMLDetailsElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElement('details'); }); @@ -26,20 +28,20 @@ describe('HTMLDetailsElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/html-dialog-element/HTMLDialogElement.test.ts+9 −7 modified@@ -11,7 +11,9 @@ describe('HTMLDialogElement', () => { let element: HTMLDialogElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLDialogElement>document.createElement('dialog'); }); @@ -26,20 +28,20 @@ describe('HTMLDialogElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts+12 −10 modified@@ -17,7 +17,9 @@ describe('HTMLElement', () => { let element: HTMLElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLElement>document.createElement('div'); }); @@ -91,20 +93,20 @@ describe('HTMLElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -129,7 +131,7 @@ describe('HTMLElement', () => { describe(`get ${property}()`, () => { it('Returns "0".', () => { const div = document.createElement('div'); - expect(div[property]).toBe(0); + expect((<any>div)[property]).toBe(0); }); }); } @@ -619,14 +621,14 @@ describe('HTMLElement', () => { it(`Returns the attribute "${property}".`, () => { const div = document.createElement('div'); div.setAttribute(property, 'value'); - expect(div[property]).toBe('value'); + expect((<any>div)[property]).toBe('value'); }); }); describe(`set ${property}()`, () => { it(`Sets the attribute "${property}".`, () => { const div = document.createElement('div'); - div[property] = 'value'; + (<any>div)[property] = 'value'; expect(div.getAttribute(property)).toBe('value'); }); });
packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts+23 −15 modified@@ -19,7 +19,9 @@ describe('HTMLIFrameElement', () => { let element: HTMLIFrameElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLIFrameElement>document.createElement('iframe'); }); @@ -38,20 +40,20 @@ describe('HTMLIFrameElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -60,13 +62,13 @@ describe('HTMLIFrameElement', () => { describe(`get ${property}()`, () => { it(`Returns the "${property}" attribute.`, () => { element.setAttribute(property, 'value'); - expect(element[property]).toBe('value'); + expect((<any>element)[property]).toBe('value'); }); }); describe(`set ${property}()`, () => { it(`Sets the attribute "${property}".`, () => { - element[property] = 'value'; + (<any>element)[property] = 'value'; expect(element.getAttribute(property)).toBe('value'); }); }); @@ -382,8 +384,10 @@ describe('HTMLIFrameElement', () => { const responseHTML = '<html><head></head><body>Test</body></html>'; let fetchedURL: string | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function () { - fetchedURL = <string>this.request.url; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + fetchedURL = <string>(<any>this).request.url; return new Promise((resolve) => { setTimeout(() => { resolve(<Response>(<unknown>{ @@ -418,8 +422,10 @@ describe('HTMLIFrameElement', () => { const responseHTML = '<html><head></head><body>Test</body></html>'; let fetchedURL: string | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function () { - fetchedURL = <string>this.request.url; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + fetchedURL = <string>(<any>this).request.url; return Promise.resolve(<Response>(<unknown>{ text: () => Promise.resolve(responseHTML), ok: true, @@ -516,8 +522,10 @@ describe('HTMLIFrameElement', () => { page.mainFrame.url = documentOrigin; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - fetchedURL = <string>this.request.url; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + fetchedURL = <string>(<any>this).request.url; return new Promise((resolve) => { setTimeout(() => { resolve(<Response>(<unknown>{
packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts+32 −30 modified@@ -21,7 +21,9 @@ describe('HTMLInputElement', () => { let element: HTMLInputElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLInputElement>document.createElement('input'); }); @@ -36,20 +38,20 @@ describe('HTMLInputElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -751,15 +753,15 @@ describe('HTMLInputElement', () => { for (const property of ['disabled', 'autofocus', 'required', 'multiple', 'readOnly']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(false); + expect((<any>element)[property]).toBe(false); element.setAttribute(property, ''); - expect(element[property]).toBe(true); + expect((<any>element)[property]).toBe(true); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = true; + (<any>element)[property] = true; expect(element.getAttribute(property)).toBe(''); }); }); @@ -781,15 +783,15 @@ describe('HTMLInputElement', () => { ]) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(''); + expect((<any>element)[property]).toBe(''); element.setAttribute(property, 'value'); - expect(element[property]).toBe('value'); + expect((<any>element)[property]).toBe('value'); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = 'value'; + (<any>element)[property] = 'value'; expect(element.getAttribute(property)).toBe('value'); }); }); @@ -798,18 +800,18 @@ describe('HTMLInputElement', () => { for (const property of ['height', 'width']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(0); - element[property] = 20; - expect(element[property]).toBe(20); + expect((<any>element)[property]).toBe(0); + (<any>element)[property] = 20; + expect((<any>element)[property]).toBe(20); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { element.setAttribute(property, '50'); - expect(element[property]).toBe(0); - element[property] = 50; - expect(element[property]).toBe(50); + expect((<any>element)[property]).toBe(0); + (<any>element)[property] = 50; + expect((<any>element)[property]).toBe(50); expect(element.getAttribute(property)).toBe('50'); }); }); @@ -818,16 +820,16 @@ describe('HTMLInputElement', () => { for (const property of ['minLength', 'maxLength']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(-1); + expect((<any>element)[property]).toBe(-1); element.setAttribute(property, '50'); - expect(element[property]).toBe(50); + expect((<any>element)[property]).toBe(50); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = 50; - expect(element[property]).toBe(50); + (<any>element)[property] = 50; + expect((<any>element)[property]).toBe(50); expect(element.getAttribute(property)).toBe('50'); }); }); @@ -1243,43 +1245,43 @@ describe('HTMLInputElement', () => { it('Returns "true" if the field is "disabled".', () => { element.required = true; element.disabled = true; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "true" if the field is "readOnly".', () => { element.required = true; element.readOnly = true; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "true" if the field type is "hidden".', () => { element.required = true; element.type = 'hidden'; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "true" if the field type is "reset".', () => { element.required = true; element.type = 'reset'; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "true" if the field type is "button".', () => { element.required = true; element.type = 'button'; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "false" if invalid.', () => { element.required = true; - expect(element[method]()).toBe(false); + expect((<any>element)[method]()).toBe(false); }); it('Triggers an "invalid" event when invalid.', () => { element.required = true; let dispatchedEvent: Event | null = null; element.addEventListener('invalid', (event: Event) => (dispatchedEvent = event)); - element[method](); + (<any>element)[method](); expect((<Event>(<unknown>dispatchedEvent)).type).toBe('invalid'); }); });
packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts+32 −18 modified@@ -13,7 +13,9 @@ describe('HTMLLinkElement', () => { let document: Document; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; }); @@ -33,21 +35,21 @@ describe('HTMLLinkElement', () => { it('Returns the event listener.', () => { const element = document.createElement('link'); element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { const element = document.createElement('link'); - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -66,14 +68,14 @@ describe('HTMLLinkElement', () => { it(`Returns the "${property}" attribute.`, () => { const element = document.createElement('link'); element.setAttribute(property, 'test'); - expect(element[property]).toBe('test'); + expect((<any>element)[property]).toBe('test'); }); }); describe(`set ${property}()`, () => { it(`Sets the attribute "${property}".`, () => { const element = document.createElement('link'); - element[property] = 'test'; + (<any>element)[property] = 'test'; expect(element.getAttribute(property)).toBe('test'); }); }); @@ -136,9 +138,10 @@ describe('HTMLLinkElement', () => { let loadEventCurrentTarget: EventTarget | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function ( + this: ResourceFetch, url: string | URL ) { - loadedWindow = this.window; + loadedWindow = (<any>this).window; loadedURL = <string>url; return { content: css, virtualServerFile: null }; }); @@ -158,8 +161,8 @@ describe('HTMLLinkElement', () => { expect(loadedWindow).toBe(window); expect(loadedURL).toBe('https://localhost:8080/test/path/file.css'); - expect(element.sheet.cssRules.length).toBe(1); - expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); + expect(element.sheet!.cssRules.length).toBe(1); + expect(element.sheet!.cssRules[0].cssText).toBe('div { background: red; }'); expect((<Event>(<unknown>loadEvent)).target).toBe(element); expect(loadEventTarget).toBe(element); expect(loadEventCurrentTarget).toBe(element); @@ -199,9 +202,10 @@ describe('HTMLLinkElement', () => { let loadedURL: string | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function ( + this: ResourceFetch, url: string | URL ) { - loadedWindow = this.window; + loadedWindow = (<any>this).window; loadedURL = <string>url; return { content: css, virtualServerFile: null }; }); @@ -225,9 +229,10 @@ describe('HTMLLinkElement', () => { let loadedURL: string | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function ( + this: ResourceFetch, url: string | URL ) { - loadedWindow = this.window; + loadedWindow = (<any>this).window; loadedURL = <string>url; return { content: css, virtualServerFile: null }; }); @@ -246,8 +251,8 @@ describe('HTMLLinkElement', () => { expect(loadedWindow).toBe(window); expect(loadedURL).toBe('https://localhost:8080/test/path/file.css'); - expect(element.sheet.cssRules.length).toBe(1); - expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); + expect(element.sheet!.cssRules.length).toBe(1); + expect(element.sheet!.cssRules[0].cssText).toBe('div { background: red; }'); expect((<Event>(<unknown>loadEvent)).target).toBe(element); expect(loadEventTarget).toBe(element); expect(loadEventCurrentTarget).toBe(element); @@ -286,9 +291,10 @@ describe('HTMLLinkElement', () => { let loadedURL: string | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function ( + this: ResourceFetch, url: string | URL ) { - loadedWindow = this.window; + loadedWindow = (<any>this).window; loadedURL = <string>url; return { content: css, virtualServerFile: null }; }); @@ -353,6 +359,8 @@ describe('HTMLLinkElement', () => { const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, errorCapture: BrowserErrorCaptureEnum.disabled, fetch: { interceptor: { @@ -403,7 +411,7 @@ describe('HTMLLinkElement', () => { 'https://localhost:8080/base/js/utilities/stringUtility.js' ]); - expect(window['moduleLoadOrder']).toEqual([ + expect((<any>window)['moduleLoadOrder']).toEqual([ 'apostrophWrapper.js', 'StringUtilityClass.js', 'stringUtility.js', @@ -426,6 +434,8 @@ describe('HTMLLinkElement', () => { const window = new Window({ url: 'https://localhost:8080/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, errorCapture: BrowserErrorCaptureEnum.disabled, fetch: { interceptor: { @@ -505,6 +515,8 @@ describe('HTMLLinkElement', () => { const window = new Window({ url: 'https://localhost:8080/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, errorCapture: BrowserErrorCaptureEnum.disabled, fetch: { interceptor: { @@ -585,6 +597,8 @@ describe('HTMLLinkElement', () => { const window = new Window({ url: 'https://localhost:8080/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, errorCapture: BrowserErrorCaptureEnum.disabled, fetch: { interceptor: {
packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts+22 −20 modified@@ -21,7 +21,9 @@ describe('HTMLMediaElement', () => { let element: HTMLMediaElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLMediaElement>document.createElement('audio'); }); @@ -41,7 +43,7 @@ describe('HTMLMediaElement', () => { expect(audio.localName).toBe('audio'); expect(audio.namespaceURI).toBe(NamespaceURI.html); - expect(window['Video']).toBe(undefined); + expect((<any>window)['Video']).toBe(undefined); }); }); @@ -79,20 +81,20 @@ describe('HTMLMediaElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -174,21 +176,21 @@ describe('HTMLMediaElement', () => { for (const property of ['autoplay', 'controls', 'loop']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(false); + expect((<any>element)[property]).toBe(false); element.setAttribute(property, ''); - expect(element[property]).toBe(true); + expect((<any>element)[property]).toBe(true); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = true; + (<any>element)[property] = true; expect(element.getAttribute(property)).toBe(''); }); it('Remove attribute value.', () => { element.setAttribute(property, ''); - element[property] = false; + (<any>element)[property] = false; expect(element.getAttribute(property)).toBeNull(); }); }); @@ -392,15 +394,15 @@ describe('HTMLMediaElement', () => { expect(element.textTracks.length).toBe(4); - expect(element.textTracks[2].id).toBe('track1'); - expect(element.textTracks[2].kind).toBe('captions'); - expect(element.textTracks[2].label).toBe('English'); - expect(element.textTracks[2].language).toBe('en'); + expect(element.textTracks[2]!.id).toBe('track1'); + expect(element.textTracks[2]!.kind).toBe('captions'); + expect(element.textTracks[2]!.label).toBe('English'); + expect(element.textTracks[2]!.language).toBe('en'); - expect(element.textTracks[3].id).toBe('track2'); - expect(element.textTracks[3].kind).toBe('metadata'); - expect(element.textTracks[3].label).toBe('French'); - expect(element.textTracks[3].language).toBe('fr'); + expect(element.textTracks[3]!.id).toBe('track2'); + expect(element.textTracks[3]!.kind).toBe('metadata'); + expect(element.textTracks[3]!.label).toBe('French'); + expect(element.textTracks[3]!.language).toBe('fr'); }); });
packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts+358 −68 modified@@ -18,7 +18,9 @@ describe('HTMLScriptElement', () => { let document: Document; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; }); @@ -38,21 +40,21 @@ describe('HTMLScriptElement', () => { it('Returns the event listener.', () => { const element = document.createElement('script'); element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { const element = document.createElement('script'); - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -62,14 +64,14 @@ describe('HTMLScriptElement', () => { it(`Returns the "${property}" attribute.`, () => { const element = document.createElement('script'); element.setAttribute(property, 'test'); - expect(element[property]).toBe('test'); + expect((<any>element)[property]).toBe('test'); }); }); describe(`set ${property}()`, () => { it(`Sets the attribute "${property}".`, () => { const element = document.createElement('script'); - element[property] = 'test'; + (<any>element)[property] = 'test'; expect(element.getAttribute(property)).toBe('test'); }); }); @@ -80,14 +82,14 @@ describe('HTMLScriptElement', () => { it(`Returns "true" if the "${property}" attribute is defined.`, () => { const element = document.createElement('script'); element.setAttribute(property, ''); - expect(element[property]).toBe(true); + expect((<any>element)[property]).toBe(true); }); }); describe(`set ${property}()`, () => { it(`Sets the "${property}" attribute to an empty string if set to "true".`, () => { const element = document.createElement('script'); - element[property] = true; + (<any>element)[property] = true; expect(element.getAttribute(property)).toBe(''); }); }); @@ -211,13 +213,62 @@ describe('HTMLScriptElement', () => { }); describe('set src()', () => { + it('Does not load external script when JavaScript is disabled.', async () => { + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + + vi.spyOn(Fetch.prototype, 'send').mockImplementation( + async () => + <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true, + status: 200 + } + ); + + document.body.appendChild(element); + + element.async = true; + element.src = 'https://localhost:8080/path/to/script.js'; + + await window.happyDOM?.waitUntilComplete(); + + expect((<any>window)['test']).toBe(undefined); + }); + + it('Does not load external module when JavaScript is disabled.', async () => { + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + + element.type = 'module'; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation( + async () => + <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true, + status: 200 + } + ); + + document.body.appendChild(element); + + element.src = 'https://localhost:8080/path/to/script.js'; + + await window.happyDOM?.waitUntilComplete(); + + expect((<any>window)['test']).toBe(undefined); + }); + it('Sets the attribute "src".', () => { const element = document.createElement('script'); element.src = 'test'; expect(element.getAttribute('src')).toBe('test'); }); - it('Loads and evaluates an external script when the attribute "src" is set and the element is connected to DOM.', async () => { + it('Loads and evaluates an external script when the property "src" is set and the element is connected to DOM.', async () => { const element = document.createElement('script'); vi.spyOn(Fetch.prototype, 'send').mockImplementation( @@ -236,7 +287,7 @@ describe('HTMLScriptElement', () => { await window.happyDOM?.waitUntilComplete(); - expect(window['test']).toBe('test'); + expect((<any>window)['test']).toBe('test'); }); it('Does not evaluate script if the element is not connected to DOM.', async () => { @@ -256,7 +307,52 @@ describe('HTMLScriptElement', () => { await window.happyDOM?.waitUntilComplete(); - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); + }); + + it('Loads and evaluates an external script when the attribute "src" is set and the element is connected to DOM.', async () => { + const element = document.createElement('script'); + + vi.spyOn(Fetch.prototype, 'send').mockImplementation( + async () => + <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true, + status: 200 + } + ); + + document.body.appendChild(element); + + element.async = true; + element.setAttribute('src', 'https://localhost:8080/path/to/script.js'); + + await window.happyDOM?.waitUntilComplete(); + + expect((<any>window)['test']).toBe('test'); + }); + + it('Loads and evaluates an external module script when the attribute "src" is set and the element is connected to DOM.', async () => { + const element = document.createElement('script'); + + element.type = 'module'; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation( + async () => + <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true, + status: 200 + } + ); + + document.body.appendChild(element); + + element.setAttribute('src', 'https://localhost:8080/path/to/script.js'); + + await window.happyDOM?.waitUntilComplete(); + + expect((<any>window)['test']).toBe('test'); }); }); @@ -278,12 +374,65 @@ describe('HTMLScriptElement', () => { }); describe('set isConnected()', () => { + it('Does not execute script when Javascript is disabled.', () => { + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + element.text = 'globalThis.test = "test";globalThis.currentScript = document.currentScript;'; + document.body.appendChild(element); + expect((<any>window)['test']).toBe(undefined); + expect((<any>window)['currentScript']).toBe(undefined); + }); + + it('Does not load script when Javascript is disabled.', () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function (this: Fetch) { + return <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true + }; + }); + + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + element.src = 'https://localhost:8080/path/to/script.js'; + document.body.appendChild(element); + expect((<any>window)['test']).toBe(undefined); + }); + + it('Does not execute module script when Javascript is disabled.', () => { + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + element.type = 'module'; + element.text = 'globalThis.test = "test";'; + document.body.appendChild(element); + expect((<any>window)['test']).toBe(undefined); + }); + + it('Does not load module script when Javascript is disabled.', () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function (this: Fetch) { + return <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true + }; + }); + + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + element.type = 'module'; + element.src = 'https://localhost:8080/path/to/script.js'; + document.body.appendChild(element); + expect((<any>window)['test']).toBe(undefined); + }); + it('Evaluates the text content as code when appended to an element that is connected to the document.', () => { const element = document.createElement('script'); element.text = 'globalThis.test = "test";globalThis.currentScript = document.currentScript;'; document.body.appendChild(element); - expect(window['test']).toBe('test'); - expect(window['currentScript']).toBe(element); + expect((<any>window)['test']).toBe('test'); + expect((<any>window)['currentScript']).toBe(element); }); it('Evaluates the text content as code when inserted before an element that is connected to the document.', () => { @@ -299,8 +448,8 @@ describe('HTMLScriptElement', () => { document.body.insertBefore(div1, div2); document.body.appendChild(element); - expect(window['test']).toBe('test'); - expect(window['currentScript']).toBe(element); + expect((<any>window)['test']).toBe('test'); + expect((<any>window)['currentScript']).toBe(element); }); for (const attribute of [ @@ -313,8 +462,8 @@ describe('HTMLScriptElement', () => { let loadEventTarget: EventTarget | null = null; let loadEventCurrentTarget: EventTarget | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function () { - fetchedURL = this.request.url; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function (this: Fetch) { + fetchedURL = (<any>this).request.url; return <Response>{ text: async () => 'globalThis.test = "test";globalThis.currentScript = document.currentScript;', @@ -339,8 +488,8 @@ describe('HTMLScriptElement', () => { expect(loadEventTarget).toBe(script); expect(loadEventCurrentTarget).toBe(script); expect(fetchedURL).toBe('https://localhost:8080/path/to/script.js'); - expect(window['test']).toBe('test'); - expect(window['currentScript']).toBe(script); + expect((<any>window)['test']).toBe('test'); + expect((<any>window)['currentScript']).toBe(script); }); it(`Triggers error event when loading external script asynchronously when the attribute "${attribute.name}" is set to "${attribute.value}".`, async () => { @@ -379,15 +528,24 @@ describe('HTMLScriptElement', () => { } it('Loads external script synchronously with relative URL.', async () => { - const window = new Window({ url: 'https://localhost:8080/base/' }); + const window = new Window({ + url: 'https://localhost:8080/base/', + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } + }); let fetchedWindow: BrowserWindow | null = null; let fetchedURL: string | null = null; let loadEvent: Event | null = null; let loadEventTarget: EventTarget | null = null; let loadEventCurrentTarget: EventTarget | null = null; - vi.spyOn(ResourceFetch.prototype, 'fetchSync').mockImplementation(function (url: string) { - fetchedWindow = this.window; + vi.spyOn(ResourceFetch.prototype, 'fetchSync').mockImplementation(function ( + this: ResourceFetch, + url: string + ) { + fetchedWindow = (<any>this).window; fetchedURL = url; return { content: 'globalThis.test = "test";globalThis.currentScript = document.currentScript;', @@ -410,12 +568,18 @@ describe('HTMLScriptElement', () => { expect(loadEventCurrentTarget).toBe(script); expect(fetchedWindow).toBe(window); expect(fetchedURL).toBe('https://localhost:8080/base/path/to/script.js'); - expect(window['test']).toBe('test'); - expect(window['currentScript']).toBe(script); + expect((<any>window)['test']).toBe('test'); + expect((<any>window)['currentScript']).toBe(script); }); it('Triggers error event when loading external script synchronously with relative URL.', () => { - const window = new Window({ url: 'https://localhost:8080/base/' }); + const window = new Window({ + url: 'https://localhost:8080/base/', + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } + }); const thrownError = new Error('error'); let errorEvent: Event | null = null; @@ -451,27 +615,27 @@ describe('HTMLScriptElement', () => { const div = document.createElement('div'); div.innerHTML = '<script>globalThis.test = "test";</script>'; document.body.appendChild(div); - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); }); it('Does not evaluate code when added as outerHTML.', () => { const div = document.createElement('div'); document.body.appendChild(div); div.outerHTML = '<script>globalThis.test = "test";</script>'; - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); }); it('Does not evaluate code if the element is not connected to DOM.', () => { const div = document.createElement('div'); const element = document.createElement('script'); element.text = 'window.test = "test";'; div.appendChild(element); - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); }); it('Evaluates the text content as code when using document.write().', () => { document.write('<script>globalThis.test = "test";</script>'); - expect(window['test']).toBe('test'); + expect((<any>window)['test']).toBe('test'); }); it("Doesn't evaluate the text content as code when using DOMParser.parseFromString().", () => { @@ -480,9 +644,9 @@ describe('HTMLScriptElement', () => { '<script>globalThis.test = "test";</script>', 'text/html' ); - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); document.body.appendChild(result); - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); }); it('Loads and evaluates an external script when "src" attribute has been set, but does not evaluate text content.', () => { @@ -498,8 +662,8 @@ describe('HTMLScriptElement', () => { document.body.appendChild(element); - expect(window['testFetch']).toBe('test'); - expect(window['testContent']).toBe(undefined); + expect((<any>window)['testFetch']).toBe('test'); + expect((<any>window)['testContent']).toBe(undefined); }); it('Does not load external scripts when "src" attribute has been set if the element is not connected to DOM.', () => { @@ -513,13 +677,17 @@ describe('HTMLScriptElement', () => { element.src = 'https://localhost:8080/path/to/script.js'; element.text = 'globalThis.test = "test";'; - expect(window['testFetch']).toBe(undefined); - expect(window['testContent']).toBe(undefined); + expect((<any>window)['testFetch']).toBe(undefined); + expect((<any>window)['testContent']).toBe(undefined); }); - it('Triggers an error event when attempting to perform an asynchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + it('Triggers an error event when attempting to perform an asynchronous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { window = new Window({ - settings: { disableJavaScriptFileLoading: true } + settings: { + disableJavaScriptFileLoading: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -545,9 +713,14 @@ describe('HTMLScriptElement', () => { ).toBe(true); }); - it('Triggers a load event when attempting to perform an asynchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" and "handleDisabledFileLoadingAsSuccess" is set to "true".', () => { + it('Triggers a load event when attempting to perform an asynchronous request and the Happy DOM setting "disableJavaScriptFileLoading" and "handleDisabledFileLoadingAsSuccess" is set to "true".', () => { window = new Window({ - settings: { disableJavaScriptFileLoading: true, handleDisabledFileLoadingAsSuccess: true } + settings: { + disableJavaScriptFileLoading: true, + handleDisabledFileLoadingAsSuccess: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -565,9 +738,13 @@ describe('HTMLScriptElement', () => { expect((<Event>(<unknown>loadEvent)).type).toBe('load'); }); - it('Triggers an error event when attempting to perform a synchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + it('Triggers an error event when attempting to perform a synchronous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { window = new Window({ - settings: { disableJavaScriptFileLoading: true } + settings: { + disableJavaScriptFileLoading: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -592,9 +769,13 @@ describe('HTMLScriptElement', () => { ).toBe(true); }); - it('Triggers an error event when attempting to perform an asynchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + it('Triggers an error event when attempting to perform an asynchronous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { window = new Window({ - settings: { disableJavaScriptFileLoading: true } + settings: { + disableJavaScriptFileLoading: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -620,9 +801,13 @@ describe('HTMLScriptElement', () => { ).toBe(true); }); - it('Triggers an error event when attempting to perform a synchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + it('Triggers an error event when attempting to perform a synchronous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { window = new Window({ - settings: { disableJavaScriptFileLoading: true } + settings: { + disableJavaScriptFileLoading: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -646,7 +831,7 @@ describe('HTMLScriptElement', () => { ).toBe(true); }); - it('Triggers an error event on Window when attempting to perform an asynchrounous request containing invalid JavaScript.', async () => { + it('Triggers an error event on Window when attempting to perform an asynchronous request containing invalid JavaScript.', async () => { let errorEvent: ErrorEvent | null = null; vi.spyOn(Fetch.prototype, 'send').mockImplementation( @@ -672,12 +857,16 @@ describe('HTMLScriptElement', () => { ); const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || ''; - expect(consoleOutput.startsWith('SyntaxError: Invalid regular expression: missing /')).toBe( - true - ); + expect( + consoleOutput.startsWith(`https://localhost:8080/base/path/to/script/:1 +globalThis.test = /; + ^ + +SyntaxError: Invalid regular expression: missing /`) + ).toBe(true); }); - it('Triggers an error event on Window when attempting to perform a synchrounous request containing invalid JavaScript.', () => { + it('Triggers an error event on Window when attempting to perform a synchronous request containing invalid JavaScript.', () => { let errorEvent: ErrorEvent | null = null; vi.spyOn(ResourceFetch.prototype, 'fetchSync').mockImplementation(() => ({ @@ -697,9 +886,13 @@ describe('HTMLScriptElement', () => { ); const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || ''; - expect(consoleOutput.startsWith('SyntaxError: Invalid regular expression: missing /')).toBe( - true - ); + expect( + consoleOutput.startsWith(`https://localhost:8080/base/path/to/script/:1 +globalThis.test = /; + ^ + +SyntaxError: Invalid regular expression: missing /`) + ).toBe(true); }); it('Triggers an error event on Window when appending an element that contains invalid Javascript.', () => { @@ -717,14 +910,22 @@ describe('HTMLScriptElement', () => { ); const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || ''; - expect(consoleOutput.startsWith('SyntaxError: Invalid regular expression: missing /')).toBe( - true - ); + expect( + consoleOutput.startsWith(`about:blank:1 +globalThis.test = /; + ^ + +SyntaxError: Invalid regular expression: missing /`) + ).toBe(true); }); it('Throws an exception when appending an element that contains invalid Javascript and the Happy DOM setting "disableErrorCapturing" is set to true.', () => { window = new Window({ - settings: { disableErrorCapturing: true } + settings: { + disableErrorCapturing: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -734,12 +935,16 @@ describe('HTMLScriptElement', () => { expect(() => { document.body.appendChild(element); - }).toThrow(new TypeError('Invalid regular expression: missing /')); + }).toThrow(new SyntaxError('Invalid regular expression: missing /')); }); it('Throws an exception when appending an element that contains invalid Javascript and the Happy DOM setting "errorCapture" is set to "disabled".', () => { window = new Window({ - settings: { errorCapture: BrowserErrorCaptureEnum.disabled } + settings: { + errorCapture: BrowserErrorCaptureEnum.disabled, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -749,14 +954,16 @@ describe('HTMLScriptElement', () => { expect(() => { document.body.appendChild(element); - }).toThrow(new TypeError('Invalid regular expression: missing /')); + }).toThrow(new SyntaxError('Invalid regular expression: missing /')); }); - it('Handles loading of a modules.', async () => { + it('Handles loading of a modules with "src" attribute.', async () => { const requests: string[] = []; const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { interceptor: { beforeAsyncRequest: async ({ request }) => { @@ -780,7 +987,7 @@ describe('HTMLScriptElement', () => { script.src = 'https://localhost:8080/base/js/TestModuleElement.js'; script.type = 'module'; script.addEventListener('load', () => { - modulesLoadedAfterLoadEvent = window['moduleLoadOrder'].slice(); + modulesLoadedAfterLoadEvent = (<any>window)['moduleLoadOrder'].slice(); }); document.body.appendChild(script); @@ -803,7 +1010,7 @@ describe('HTMLScriptElement', () => { 'https://localhost:8080/base/js/utilities/lazyload.js' ]); - expect(window['moduleLoadOrder']).toEqual([ + expect((<any>window)['moduleLoadOrder']).toEqual([ 'apostrophWrapper.js', 'StringUtilityClass.js', 'stringUtility.js', @@ -836,11 +1043,88 @@ describe('HTMLScriptElement', () => { ).toBe('red'); }); + it('Handles loading of a modules by code.', async () => { + const requests: string[] = []; + const window = new Window({ + url: 'https://localhost:8080/base/', + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, + fetch: { + interceptor: { + beforeAsyncRequest: async ({ request }) => { + requests.push(request.url); + } + }, + virtualServers: [ + { + url: 'https://localhost:8080/base/js/', + directory: './test/nodes/html-script-element/modules/' + } + ] + } + }, + console + }); + const document = window.document; + const script = document.createElement('script'); + + script.type = 'module'; + script.textContent = `import('./js/TestModuleElement.js');`; + + document.body.appendChild(script); + + await window.happyDOM?.waitUntilComplete(); + + const testModule = document.createElement('test-module'); + + document.body.appendChild(testModule); + + await window.happyDOM?.waitUntilComplete(); + + expect(requests).toEqual([ + 'https://localhost:8080/base/js/TestModuleElement.js', + 'https://localhost:8080/base/js/utilities/StringUtilityClass.js', + 'https://localhost:8080/base/js/utilities/stringUtility.js', + 'https://localhost:8080/base/js/json/data.json', + 'https://localhost:8080/base/js/css/style.css', + 'https://localhost:8080/base/js/utilities/apostrophWrapper.js', + 'https://localhost:8080/base/js/utilities/lazyload.js' + ]); + + expect((<any>window)['moduleLoadOrder']).toEqual([ + 'apostrophWrapper.js', + 'StringUtilityClass.js', + 'stringUtility.js', + 'TestModuleElement.js', + 'lazyload.js' + ]); + + expect(testModule.shadowRoot?.innerHTML).toBe(`<div> + Expect lower case: "value" + Expect upper case: "VALUE" + Expect lower case. "value" + Expect trimmed lower case: "value" + Import URL: https://localhost:8080/base/js/TestModuleElement.js + Resolved URL: https://localhost:8080/base/js/Resolved.js + </div><div>Lazy-loaded module: true</div>`); + + expect(testModule.shadowRoot?.adoptedStyleSheets[0].cssRules[0].cssText).toBe( + 'div { background: red; }' + ); + expect( + window.getComputedStyle(<HTMLElement>testModule.shadowRoot?.querySelector('div')) + .backgroundColor + ).toBe('red'); + }); + it('Handles modules using an import map.', async () => { const requests: string[] = []; const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { interceptor: { beforeAsyncRequest: async ({ request }) => { @@ -915,7 +1199,7 @@ describe('HTMLScriptElement', () => { 'https://localhost:8080/base/js/utilities/lazyload.js' ]); - expect(window['moduleLoadOrder']).toEqual([ + expect((<any>window)['moduleLoadOrder']).toEqual([ 'apostrophWrapper.js', 'StringUtilityClass.js', 'stringUtility.js', @@ -944,6 +1228,8 @@ describe('HTMLScriptElement', () => { const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ { @@ -983,6 +1269,8 @@ DOMException: Failed to perform request to "https://localhost:8080/base/js/utili const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ { @@ -1024,6 +1312,8 @@ DOMException: Failed to perform request to "https://localhost:8080/base/js/utili const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ { @@ -1053,7 +1343,7 @@ DOMException: Failed to perform request to "https://localhost:8080/base/js/utili expect((<ErrorEvent>(<unknown>errorEvent)).type).toBe('error'); expect((<ErrorEvent>(<unknown>errorEvent)).bubbles).toBe(false); expect( - /^ReferenceError: notFound is not defined\n at eval \((.+?)\/nodes\/html-script-element\/modules-with-evaluation-error\/utilities\/stringUtility.js:10:14\)/.test( + /^ReferenceError: notFound is not defined\n at (.+?)\/nodes\/html-script-element\/modules-with-evaluation-error\/utilities\/stringUtility.js:10:14/.test( window.happyDOM?.virtualConsolePrinter.readAsString() ) ).toBe(true);
packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts+18 −16 modified@@ -14,7 +14,9 @@ describe('HTMLSelectElement', () => { let element: HTMLSelectElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLSelectElement>document.createElement('select'); }); @@ -37,21 +39,21 @@ describe('HTMLSelectElement', () => { it('Returns the event listener.', () => { const element = document.createElement('script'); element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { const element = document.createElement('script'); - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -147,15 +149,15 @@ describe('HTMLSelectElement', () => { for (const property of ['disabled', 'autofocus', 'required', 'multiple']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(false); + expect((<any>element)[property]).toBe(false); element.setAttribute(property, ''); - expect(element[property]).toBe(true); + expect((<any>element)[property]).toBe(true); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = true; + (<any>element)[property] = true; expect(element.getAttribute(property)).toBe(''); }); }); @@ -268,14 +270,14 @@ describe('HTMLSelectElement', () => { describe('get symbol()', () => { it('returns existing symbol properties', () => { const symbol = Symbol('test'); - element[symbol] = 'test'; - expect(element[symbol]).toBe('test'); + (<any>element)[symbol] = 'test'; + expect((<any>element)[symbol]).toBe('test'); }); it('ignores missing symbol properties', () => { const symbol = Symbol('other-test'); - expect(element[symbol]).toBe(undefined); + expect((<any>element)[symbol]).toBe(undefined); // https://github.com/capricorn86/happy-dom/issues/1526 expect(symbol in element).toBe(false); @@ -799,7 +801,7 @@ describe('HTMLSelectElement', () => { element.required = true; element.disabled = true; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "false" if invalid.', () => { @@ -809,7 +811,7 @@ describe('HTMLSelectElement', () => { element.required = true; - expect(element[method]()).toBe(false); + expect((<any>element)[method]()).toBe(false); }); it('Triggers an "invalid" event when invalid.', () => { @@ -822,7 +824,7 @@ describe('HTMLSelectElement', () => { let dispatchedEvent: Event | null = null; element.addEventListener('invalid', (event: Event) => (dispatchedEvent = event)); - element[method](); + (<any>element)[method](); expect((<Event>(<unknown>dispatchedEvent)).type).toBe('invalid'); });
packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts+9 −7 modified@@ -15,7 +15,9 @@ describe('HTMLSlotElement', () => { let customElementWithSlot: CustomElementWithSlot; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; window.customElements.define('custom-element-with-named-slots', CustomElementWithNamedSlots); @@ -37,21 +39,21 @@ describe('HTMLSlotElement', () => { it('Returns the event listener.', () => { const element = <HTMLSlotElement>customElementWithSlot.shadowRoot?.querySelector('slot'); element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { const element = <HTMLSlotElement>customElementWithSlot.shadowRoot?.querySelector('slot'); - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts+23 −21 modified@@ -14,7 +14,9 @@ describe('HTMLTextAreaElement', () => { let element: HTMLTextAreaElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLTextAreaElement>document.createElement('textarea'); }); @@ -29,20 +31,20 @@ describe('HTMLTextAreaElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -169,15 +171,15 @@ describe('HTMLTextAreaElement', () => { for (const property of ['disabled', 'autofocus', 'required', 'readOnly']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(false); + expect((<any>element)[property]).toBe(false); element.setAttribute(property, ''); - expect(element[property]).toBe(true); + expect((<any>element)[property]).toBe(true); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = true; + (<any>element)[property] = true; expect(element.getAttribute(property)).toBe(''); }); }); @@ -186,15 +188,15 @@ describe('HTMLTextAreaElement', () => { for (const property of ['name', 'autocomplete', 'cols', 'rows', 'placeholder', 'inputMode']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(''); + expect((<any>element)[property]).toBe(''); element.setAttribute(property, 'value'); - expect(element[property]).toBe('value'); + expect((<any>element)[property]).toBe('value'); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = 'value'; + (<any>element)[property] = 'value'; expect(element.getAttribute(property)).toBe('value'); }); }); @@ -203,16 +205,16 @@ describe('HTMLTextAreaElement', () => { for (const property of ['minLength', 'maxLength']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(-1); + expect((<any>element)[property]).toBe(-1); element.setAttribute(property, '50'); - expect(element[property]).toBe(50); + expect((<any>element)[property]).toBe(50); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = 50; - expect(element[property]).toBe(50); + (<any>element)[property] = 50; + expect((<any>element)[property]).toBe(50); expect(element.getAttribute(property)).toBe('50'); }); }); @@ -285,25 +287,25 @@ describe('HTMLTextAreaElement', () => { it('Returns "true" if the field is "disabled".', () => { element.required = true; element.disabled = true; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "true" if the field is "readOnly".', () => { element.required = true; element.readOnly = true; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "false" if invalid.', () => { element.required = true; - expect(element[method]()).toBe(false); + expect((<any>element)[method]()).toBe(false); }); it('Triggers an "invalid" event when invalid.', () => { element.required = true; let triggeredEvent: Event | null = null; element.addEventListener('invalid', (event: Event) => (triggeredEvent = event)); - element[method](); + (<any>element)[method](); expect((<Event>(<unknown>triggeredEvent)).type).toBe('invalid'); }); });
packages/happy-dom/test/nodes/html-track-element/HTMLTrackElement.test.ts+9 −7 modified@@ -11,7 +11,9 @@ describe('HTMLTrackElement', () => { let element: HTMLTrackElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElement('track'); }); @@ -26,20 +28,20 @@ describe('HTMLTrackElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/svg-animation-element/SVGAnimationElement.test.ts+9 −7 modified@@ -11,7 +11,9 @@ describe('SVGAnimationElement', () => { let element: SVGAnimationElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElementNS('http://www.w3.org/2000/svg', 'animate'); }); @@ -30,20 +32,20 @@ describe('SVGAnimationElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/svg-element/SVGElement.test.ts+9 −9 modified@@ -1,7 +1,5 @@ import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; -import SVGSVGElement from '../../../src/nodes/svg-svg-element/SVGSVGElement.js'; -import NamespaceURI from '../../../src/config/NamespaceURI.js'; import SVGElement from '../../../src/nodes/svg-element/SVGElement.js'; import HTMLElementUtility from '../../../src/nodes/html-element/HTMLElementUtility.js'; import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest'; @@ -15,7 +13,9 @@ describe('SVGElement', () => { let element: SVGElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <SVGElement>document.createElementNS('http://www.w3.org/2000/svg', 'unknown'); }); @@ -126,20 +126,20 @@ describe('SVGElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/svg-graphics-element/SVGGraphicsElement.test.ts+39 −37 modified@@ -13,7 +13,9 @@ describe('SVGGraphicsElement', () => { let element: SVGGraphicsElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElementNS('http://www.w3.org/2000/svg', 'g'); }); @@ -32,20 +34,20 @@ describe('SVGGraphicsElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -129,38 +131,38 @@ describe('SVGGraphicsElement', () => { element.setAttribute('transform', 'matrix(1 2 3 4 5 6) translate(10 20)'); expect(element.transform.baseVal.numberOfItems).toBe(2); - expect(element.transform.baseVal.getItem(0).type).toBe(SVGTransformTypeEnum.matrix); - expect(element.transform.baseVal.getItem(0).matrix.a).toBe(1); - expect(element.transform.baseVal.getItem(0).matrix.b).toBe(2); - expect(element.transform.baseVal.getItem(0).matrix.c).toBe(3); - expect(element.transform.baseVal.getItem(0).matrix.d).toBe(4); - expect(element.transform.baseVal.getItem(0).matrix.e).toBe(5); - expect(element.transform.baseVal.getItem(0).matrix.f).toBe(6); - - expect(element.transform.baseVal.getItem(1).type).toBe(SVGTransformTypeEnum.translate); - expect(element.transform.baseVal.getItem(1).matrix.a).toBe(1); - expect(element.transform.baseVal.getItem(1).matrix.b).toBe(0); - expect(element.transform.baseVal.getItem(1).matrix.c).toBe(0); - expect(element.transform.baseVal.getItem(1).matrix.d).toBe(1); - expect(element.transform.baseVal.getItem(1).matrix.e).toBe(10); - expect(element.transform.baseVal.getItem(1).matrix.f).toBe(20); + expect(element.transform.baseVal.getItem(0)!.type).toBe(SVGTransformTypeEnum.matrix); + expect(element.transform.baseVal.getItem(0)!.matrix.a).toBe(1); + expect(element.transform.baseVal.getItem(0)!.matrix.b).toBe(2); + expect(element.transform.baseVal.getItem(0)!.matrix.c).toBe(3); + expect(element.transform.baseVal.getItem(0)!.matrix.d).toBe(4); + expect(element.transform.baseVal.getItem(0)!.matrix.e).toBe(5); + expect(element.transform.baseVal.getItem(0)!.matrix.f).toBe(6); + + expect(element.transform.baseVal.getItem(1)!.type).toBe(SVGTransformTypeEnum.translate); + expect(element.transform.baseVal.getItem(1)!.matrix.a).toBe(1); + expect(element.transform.baseVal.getItem(1)!.matrix.b).toBe(0); + expect(element.transform.baseVal.getItem(1)!.matrix.c).toBe(0); + expect(element.transform.baseVal.getItem(1)!.matrix.d).toBe(1); + expect(element.transform.baseVal.getItem(1)!.matrix.e).toBe(10); + expect(element.transform.baseVal.getItem(1)!.matrix.f).toBe(20); expect(element.transform.animVal.numberOfItems).toBe(2); - expect(element.transform.animVal.getItem(0).type).toBe(SVGTransformTypeEnum.matrix); - expect(element.transform.animVal.getItem(0).matrix.a).toBe(1); - expect(element.transform.animVal.getItem(0).matrix.b).toBe(2); - expect(element.transform.animVal.getItem(0).matrix.c).toBe(3); - expect(element.transform.animVal.getItem(0).matrix.d).toBe(4); - expect(element.transform.animVal.getItem(0).matrix.e).toBe(5); - expect(element.transform.animVal.getItem(0).matrix.f).toBe(6); - - expect(element.transform.animVal.getItem(1).type).toBe(SVGTransformTypeEnum.translate); - expect(element.transform.animVal.getItem(1).matrix.a).toBe(1); - expect(element.transform.animVal.getItem(1).matrix.b).toBe(0); - expect(element.transform.animVal.getItem(1).matrix.c).toBe(0); - expect(element.transform.animVal.getItem(1).matrix.d).toBe(1); - expect(element.transform.animVal.getItem(1).matrix.e).toBe(10); - expect(element.transform.animVal.getItem(1).matrix.f).toBe(20); + expect(element.transform.animVal.getItem(0)!.type).toBe(SVGTransformTypeEnum.matrix); + expect(element.transform.animVal.getItem(0)!.matrix.a).toBe(1); + expect(element.transform.animVal.getItem(0)!.matrix.b).toBe(2); + expect(element.transform.animVal.getItem(0)!.matrix.c).toBe(3); + expect(element.transform.animVal.getItem(0)!.matrix.d).toBe(4); + expect(element.transform.animVal.getItem(0)!.matrix.e).toBe(5); + expect(element.transform.animVal.getItem(0)!.matrix.f).toBe(6); + + expect(element.transform.animVal.getItem(1)!.type).toBe(SVGTransformTypeEnum.translate); + expect(element.transform.animVal.getItem(1)!.matrix.a).toBe(1); + expect(element.transform.animVal.getItem(1)!.matrix.b).toBe(0); + expect(element.transform.animVal.getItem(1)!.matrix.c).toBe(0); + expect(element.transform.animVal.getItem(1)!.matrix.d).toBe(1); + expect(element.transform.animVal.getItem(1)!.matrix.e).toBe(10); + expect(element.transform.animVal.getItem(1)!.matrix.f).toBe(20); const transform = new window.SVGTransform(PropertySymbol.illegalConstructor, window);
packages/happy-dom/test/nodes/svg-svg-element/SVGSVGElement.test.ts+9 −7 modified@@ -25,7 +25,9 @@ describe('SVGSVGElement', () => { let element: SVGSVGElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); }); @@ -53,20 +55,20 @@ describe('SVGSVGElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/window/BrowserWindow.test.ts+89 −27 modified@@ -58,7 +58,9 @@ describe('BrowserWindow', () => { let document: Document; beforeEach(() => { - browser = new Browser(); + browser = new Browser({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); browserPage = browser.newPage(); browserFrame = browserPage.mainFrame; window = browserFrame.window; @@ -75,9 +77,58 @@ describe('BrowserWindow', () => { vi.restoreAllMocks(); }); + describe('constructor()', () => { + it('Outputs a warning if "enableJavaScriptEvaluation" is enabled in an environment with code generation enabled.', () => { + const consoleWarn: string[] = []; + vi.spyOn(globalThis.console, 'warn').mockImplementation((...args: any[]) => + consoleWarn.push(args.join(' ')) + ); + const browser = new Browser({ settings: { enableJavaScriptEvaluation: true } }); + + browser.newPage(); + + expect(consoleWarn).toEqual([ + '\nWarning! Happy DOM has JavaScript evaluation enabled and is running in an environment with code generation from strings (eval) enabled at process level.' + + '\n\nA VM Context is not an isolated environment, and if you run untrusted code you are at risk of RCE (Remote Code Execution) attacks. The attacker can use code generation to escape the VM and run code at process level.' + + '\n\nIt is recommended to disable code generation at process level by running node with the "--disallow-code-generation-from-strings" flag enabled when Javascript evaluation is enabled in Happy DOM.' + + ' You can suppress this warning by setting "suppressCodeGenerationFromStringsWarning" to "true" at your own risk.' + + '\n\nFor more information, see https://github.com/capricorn86/happy-dom/wiki/Code-Generation-From-Strings-Warning\n\n' + ]); + }); + + it('Does not output a warning if "enableJavaScriptEvaluation" is disabled.', () => { + const consoleWarn: string[] = []; + vi.spyOn(globalThis.console, 'warn').mockImplementation((...args: any[]) => + consoleWarn.push(args.join(' ')) + ); + const browser = new Browser(); + + new BrowserWindow(browser.newPage().mainFrame); + + expect(consoleWarn.length).toBe(0); + }); + + it('Does not output a warning if "suppressCodeGenerationFromStringsWarning" is enabled.', () => { + const consoleWarn: string[] = []; + vi.spyOn(globalThis.console, 'warn').mockImplementation((...args: any[]) => + consoleWarn.push(args.join(' ')) + ); + const browser = new Browser({ + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } + }); + + new BrowserWindow(browser.newPage().mainFrame); + + expect(consoleWarn.length).toBe(0); + }); + }); + describe('get happyDOM()', () => { it('Returns "undefined" for an attached browser.', () => { - expect(browserFrame.window['happyDOM']).toBeUndefined(); + expect((<any>browserFrame.window)['happyDOM']).toBeUndefined(); }); }); @@ -114,18 +165,22 @@ describe('BrowserWindow', () => { }); describe('get Function()', () => { - it('Is not the same as (() => {}).constructorr when inside the VM.', () => { + it('Is not the same as (() => {}).constructor when inside the VM.', () => { expect(typeof window.Function).toBe('function'); expect((() => {}).constructor).not.toBe(window.Function); }); it('Is the same as (() => {}).constructor when using eval().', () => { - expect(window.eval('(() => {}).constructor === window.Function')).toBe(true); + expect(window.Function('return (() => {}).constructor === window.Function')()).toBe(true); + }); + + it('Does not execute unsafe code using import', () => { + expect(() => window.Function('return import("process")')()).rejects.toThrow(); }); }); describe('get Array()', () => { - it('Is not the same as [].constructorr when inside the VM.', () => { + it('Is not the same as [].constructor when inside the VM.', () => { expect(typeof window.Array).toBe('function'); expect([].constructor).not.toBe(window.Array); }); @@ -182,15 +237,15 @@ describe('BrowserWindow', () => { describe('get {ElementClass}()', () => { for (const tagName of Object.keys(HTMLElementConfig)) { it(`Exposes the HTML element class "${HTMLElementConfig[tagName].className}" for tag name "${tagName}"`, () => { - expect(window[HTMLElementConfig[tagName].className].name).toBe( + expect((<any>window)[HTMLElementConfig[tagName].className].name).toBe( HTMLElementConfig[tagName].className ); }); } for (const tagName of Object.keys(SVGElementConfig)) { it(`Exposes the SVG element class "${SVGElementConfig[tagName]}" for tag name "${tagName}"`, () => { - expect(window[SVGElementConfig[tagName].className].name).toBe( + expect((<any>window)[SVGElementConfig[tagName].className].name).toBe( SVGElementConfig[tagName].className ); }); @@ -205,7 +260,7 @@ describe('BrowserWindow', () => { describe('get process()', () => { it('Returns undefined.', () => { - expect(window['process']).toBeUndefined(); + expect((<any>window)['process']).toBeUndefined(); }); }); @@ -283,7 +338,7 @@ describe('BrowserWindow', () => { }; for (const propertyKey in referenceValues) { - expect(window.navigator[propertyKey]).toEqual(referenceValues[propertyKey]); + expect((<any>window.navigator)[propertyKey]).toEqual((<any>referenceValues)[propertyKey]); } }); }); @@ -408,7 +463,7 @@ describe('BrowserWindow', () => { return eval('variable'); })()`); expect(result).toBe('locally defined'); - expect(window['variable']).toBe('globally defined'); + expect((<any>window)['variable']).toBe('globally defined'); }); it('Respects indirect eval.', () => { @@ -419,12 +474,12 @@ describe('BrowserWindow', () => { return (0,eval)('variable'); })()`); expect(result).toBe('globally defined'); - expect(window['variable']).toBe('globally defined'); + expect((<any>window)['variable']).toBe('globally defined'); }); it('Has access to the window and document.', () => { window.eval(`window.variable = document.characterSet;`); - expect(window['variable']).toBe('UTF-8'); + expect((<any>window)['variable']).toBe('UTF-8'); }); }); @@ -1751,8 +1806,10 @@ describe('BrowserWindow', () => { }; let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = <Request>this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = <Request>(<any>this).request; return Promise.resolve(expectedResponse); }); @@ -1767,7 +1824,7 @@ describe('BrowserWindow', () => { for (const functionName of ['scroll', 'scrollTo']) { describe(`${functionName}()`, () => { it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset', () => { - window[functionName](50, 60); + (<any>window)[functionName](50, 60); expect(window.document.documentElement.scrollLeft).toBe(50); expect(window.document.documentElement.scrollTop).toBe(60); expect(window.pageXOffset).toBe(50); @@ -1777,7 +1834,7 @@ describe('BrowserWindow', () => { }); it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset using object.', () => { - window[functionName]({ left: 50, top: 60 }); + (<any>window)[functionName]({ left: 50, top: 60 }); expect(window.document.documentElement.scrollLeft).toBe(50); expect(window.document.documentElement.scrollTop).toBe(60); expect(window.pageXOffset).toBe(50); @@ -1787,7 +1844,7 @@ describe('BrowserWindow', () => { }); it('Sets only the property scrollTop, pageYOffset, and scrollY', () => { - window[functionName]({ top: 60 }); + (<any>window)[functionName]({ top: 60 }); expect(window.document.documentElement.scrollLeft).toBe(0); expect(window.document.documentElement.scrollTop).toBe(60); expect(window.pageXOffset).toBe(0); @@ -1797,7 +1854,7 @@ describe('BrowserWindow', () => { }); it('Sets only the property scrollLeft, pageXOffset, and scrollX', () => { - window[functionName]({ left: 60 }); + (<any>window)[functionName]({ left: 60 }); expect(window.document.documentElement.scrollLeft).toBe(60); expect(window.document.documentElement.scrollTop).toBe(0); expect(window.document.documentElement.scrollLeft).toBe(60); @@ -1809,7 +1866,7 @@ describe('BrowserWindow', () => { }); it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset with animation.', async () => { - window[functionName]({ left: 50, top: 60, behavior: 'smooth' }); + (<any>window)[functionName]({ left: 50, top: 60, behavior: 'smooth' }); expect(window.document.documentElement.scrollLeft).toBe(0); expect(window.document.documentElement.scrollTop).toBe(0); expect(window.pageXOffset).toBe(0); @@ -1826,7 +1883,7 @@ describe('BrowserWindow', () => { }); it('Throws an exception if the there is only one argument and it is not an object.', () => { - expect(() => window[functionName](10)).toThrow( + expect(() => (<any>window)[functionName](10)).toThrow( new TypeError( `Failed to execute '${functionName}' on 'Window': The provided value is not of type 'ScrollToOptions'.` ) @@ -1930,15 +1987,16 @@ describe('BrowserWindow', () => { let loadEventCurrentTarget: EventTarget | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function ( + this: ResourceFetch, url: string | URL ) { if ((<string>url).endsWith('.css')) { - resourceFetchCSSWindow = this.window; + resourceFetchCSSWindow = (<any>this).window; resourceFetchCSSURL = <string>url; return { content: cssResponse, virtualServerFile: null }; } - resourceFetchJSWindow = this.window; + resourceFetchJSWindow = (<any>this).window; resourceFetchJSURL = <string>url; return { content: jsResponse, virtualServerFile: null }; }); @@ -1973,7 +2031,7 @@ describe('BrowserWindow', () => { expect(document.styleSheets.length).toBe(1); expect(document.styleSheets[0].cssRules[0].cssText).toBe(cssResponse); - expect(window['test']).toBe('test'); + expect((<any>window)['test']).toBe('test'); resolve(null); }, 20); @@ -2185,8 +2243,10 @@ describe('BrowserWindow', () => { const html = '<html><body>Test</body></html>'; let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = <Request>this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = <Request>(<any>this).request; return Promise.resolve(<Response>{ text: () => new Promise((resolve) => setTimeout(() => resolve(html))) }); @@ -2267,8 +2327,10 @@ describe('BrowserWindow', () => { const html = '<html><body>Test</body></html>'; let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = <Request>this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = <Request>(<any>this).request; return Promise.resolve(<Response>{ text: () => new Promise((resolve) => setTimeout(() => resolve(html))) });
packages/happy-dom/test/window/DetachedWindowAPI.test.ts+4 −4 modified@@ -21,16 +21,16 @@ describe('DetachedWindowAPI', () => { describe('get settings()', () => { it('Returns browser settings.', () => { - const window = new Window({ settings: { disableJavaScriptEvaluation: true } }); + const window = new Window({ settings: { enableJavaScriptEvaluation: true } }); expect(window.happyDOM?.settings).toEqual({ ...DefaultBrowserSettings, - disableJavaScriptEvaluation: true + enableJavaScriptEvaluation: true }); }); it('Supports editing setting properties.', () => { - const window = new Window({ settings: { disableJavaScriptEvaluation: true } }); - (<DetachedWindowAPI>window.happyDOM).settings.disableJavaScriptEvaluation = false; + const window = new Window({ settings: { enableJavaScriptEvaluation: true } }); + (<DetachedWindowAPI>window.happyDOM).settings.enableJavaScriptEvaluation = false; expect(window.happyDOM?.settings).toEqual(DefaultBrowserSettings); }); });
packages/happy-dom/test/window/GlobalWindow.test.ts+35 −14 modified@@ -1,12 +1,14 @@ import GlobalWindow from '../../src/window/GlobalWindow.js'; -import Window from '../../src/window/Window.js'; +import * as PropertySymbol from '../../src/PropertySymbol.js'; import { beforeEach, describe, it, expect } from 'vitest'; describe('GlobalWindow', () => { - let window: Window; + let window: GlobalWindow; beforeEach(() => { - window = new GlobalWindow(); + window = new GlobalWindow({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); }); describe('get Object()', () => { @@ -15,9 +17,9 @@ describe('GlobalWindow', () => { }); it('Is the same as {}.constructor when using eval().', () => { - global['globalWindow'] = window; + (<any>global)['globalWindow'] = window; expect(window.eval('({}).constructor === globalWindow.Object')).toBe(true); - delete global['globalWindow']; + delete (<any>global)['globalWindow']; }); }); @@ -27,9 +29,11 @@ describe('GlobalWindow', () => { }); it('Is the same as (() => {}).constructor when using eval().', () => { - global['globalWindow'] = window; - expect(window.eval('(() => {}).constructor === globalWindow.Function')).toBe(true); - delete global['globalWindow']; + expect(window.Function('return (() => {}).constructor === globalThis.Function')()).toBe(true); + }); + + it('Does not execute unsafe code using import', () => { + expect(() => window.Function('return import("process")')()).rejects.toThrow(); }); }); @@ -39,9 +43,9 @@ describe('GlobalWindow', () => { }); it('Is the same as [].constructor when using eval().', () => { - global['globalWindow'] = window; + (<any>global)['globalWindow'] = window; expect(window.eval('[].constructor === globalWindow.Array')).toBe(true); - delete global['globalWindow']; + delete (<any>global)['globalWindow']; }); }); @@ -55,9 +59,9 @@ describe('GlobalWindow', () => { })()`); expect(result).toBe('locally defined'); - expect(globalThis['variable']).toBe('globally defined'); + expect((<any>globalThis)['variable']).toBe('globally defined'); - delete globalThis['variable']; + delete (<any>globalThis)['variable']; }); it('Respects indirect eval.', () => { @@ -69,9 +73,26 @@ describe('GlobalWindow', () => { })()`); expect(result).toBe('globally defined'); - expect(globalThis['variable']).toBe('globally defined'); + expect((<any>globalThis)['variable']).toBe('globally defined'); + + delete (<any>globalThis)['variable']; + }); + }); + + describe('[PropertySymbol.evaluateScript]()', () => { + it('Evaluates script.', () => { + const result = window[PropertySymbol.evaluateScript]('1 + 1;', { + filename: 'filename.js' + }); + expect(result).toBe(2); + }); - delete globalThis['variable']; + it('Evaluates code from script elements', () => { + const script = window.document.createElement('script'); + script.textContent = 'globalThis["$scriptResult"] = 1 + 1;'; + window.document.body.appendChild(script); + expect((<any>globalThis)['$scriptResult']).toBe(2); + delete (<any>globalThis)['$scriptResult']; }); });
packages/happy-dom/test/window/Window.test.ts+3 −0 modified@@ -149,6 +149,7 @@ describe('Window', () => { console: globalThis.console, settings: { disableJavaScriptEvaluation: true, + enableJavaScriptEvaluation: true, navigator: { userAgent: 'test' }, @@ -169,6 +170,7 @@ describe('Window', () => { VirtualConsolePrinter ); expect(windowWithOptions.happyDOM?.settings.disableJavaScriptEvaluation).toBe(true); + expect(windowWithOptions.happyDOM?.settings.enableJavaScriptEvaluation).toBe(true); expect(windowWithOptions.happyDOM?.settings.disableJavaScriptFileLoading).toBe(false); expect(windowWithOptions.happyDOM?.settings.disableCSSFileLoading).toBe(false); expect(windowWithOptions.happyDOM?.settings.disableIframePageLoading).toBe(false); @@ -191,6 +193,7 @@ describe('Window', () => { VirtualConsolePrinter ); expect(windowWithoutOptions.happyDOM?.settings.disableJavaScriptEvaluation).toBe(false); + expect(windowWithoutOptions.happyDOM?.settings.enableJavaScriptEvaluation).toBe(false); expect(windowWithoutOptions.happyDOM?.settings.disableJavaScriptFileLoading).toBe(false); expect(windowWithoutOptions.happyDOM?.settings.disableCSSFileLoading).toBe(false); expect(windowWithoutOptions.happyDOM?.settings.disableIframePageLoading).toBe(false);
de438ad72921BREAKING CHANGE: [#0] Changes JavaScript evaluation to be disabled by default
59 files changed · +1304 −644
integration-test/package.json+1 −1 modified@@ -10,7 +10,7 @@ "access": "restricted" }, "scripts": { - "test": "cd ./test && ls | node --test && node ./browser-exception-observer/BrowserExceptionObserver.test.js", + "test": "cd ./test && ls | node --disallow-code-generation-from-strings --test && node --disallow-code-generation-from-strings ./browser-exception-observer/BrowserExceptionObserver.test.js", "test:debug": "cd ./test && ls | node --inspect-brk" }, "devDependencies": {
integration-test/test/browser-exception-observer/BrowserExceptionObserver.test.js+8 −2 modified@@ -21,7 +21,10 @@ await describe('BrowserExceptionObserver', async () => { await describe('observe()', async () => { await it('Observes unhandled fetch rejections.', async () => { const browser = new Browser({ - settings: { errorCapture: BrowserErrorCaptureEnum.processLevel } + settings: { + errorCapture: BrowserErrorCaptureEnum.processLevel, + enableJavaScriptEvaluation: true + } }); const page = browser.newPage(); const window = page.mainFrame.window; @@ -58,7 +61,10 @@ await describe('BrowserExceptionObserver', async () => { await it('Observes uncaught exceptions.', async () => { const browser = new Browser({ - settings: { errorCapture: BrowserErrorCaptureEnum.processLevel } + settings: { + errorCapture: BrowserErrorCaptureEnum.processLevel, + enableJavaScriptEvaluation: true + } }); const page = browser.newPage(); const window = page.mainFrame.window;
integration-test/test/Browser.test.js+3 −1 modified@@ -7,6 +7,7 @@ describe('Browser', () => { const browser = new Browser({ settings: { errorCapture: BrowserErrorCaptureEnum.processLevel, + enableJavaScriptEvaluation: true, // Github.com has a timer that is very long (hours) and a timer loop that never ends. timer: { @@ -47,7 +48,8 @@ describe('Browser', () => { it('Goes to "npmjs.com".', async () => { const browser = new Browser({ settings: { - errorCapture: BrowserErrorCaptureEnum.processLevel + errorCapture: BrowserErrorCaptureEnum.processLevel, + enableJavaScriptEvaluation: true } }); const page = browser.newPage();
integration-test/test/WindowGlobals.test.js+3 −3 modified@@ -4,7 +4,7 @@ import { describe, it } from 'node:test'; describe('WindowGlobals', () => { it('Functions should have the constructor global.Function.', () => { - const window = new Window(); + const window = new Window({ settings: { enableJavaScriptEvaluation: true } }); let error = null; window.addEventListener('error', (event) => (error = event.error)); window.document.write(` @@ -18,7 +18,7 @@ describe('WindowGlobals', () => { }); it('Object should have the constructor global.Object.', () => { - const window = new Window(); + const window = new Window({ settings: { enableJavaScriptEvaluation: true } }); let error = null; window.addEventListener('error', (event) => (error = event.error)); window.document.write(` @@ -32,7 +32,7 @@ describe('WindowGlobals', () => { }); it('Binds global methods to the Window context.', () => { - const window = new Window(); + const window = new Window({ settings: { enableJavaScriptEvaluation: true } }); let error = null; window.addEventListener('error', (event) => (error = event.error)); window.document.write(`
packages/@happy-dom/server-renderer/src/config/DefaultServerRendererConfiguration.ts+7 −1 modified@@ -5,7 +5,13 @@ import OS from 'os'; import { BrowserErrorCaptureEnum } from 'happy-dom'; export default <IServerRendererConfiguration>{ - browser: { ...DefaultBrowserSettings, errorCapture: BrowserErrorCaptureEnum.processLevel }, + browser: { + ...DefaultBrowserSettings, + errorCapture: BrowserErrorCaptureEnum.processLevel, + // This is enabled by default as the entire point of this package is to server-render client side JavaScript. + // "--disallow-code-generation-from-strings" is enabled on workers to prevent escape of the VM context. + enableJavaScriptEvaluation: true + }, outputDirectory: './happy-dom/render', logLevel: ServerRendererLogLevelEnum.info, debug: false,
packages/@happy-dom/server-renderer/src/ServerRenderer.ts+1 −0 modified@@ -197,6 +197,7 @@ export default class ServerRenderer { return; } const worker = new Worker(new URL('ServerRendererWorker.js', import.meta.url), { + execArgv: ['--disallow-code-generation-from-strings'], workerData: { configuration: configuration }
packages/@happy-dom/server-renderer/src/utilities/HelpPrinterRows.ts+8 −1 modified@@ -126,12 +126,19 @@ export default [ '1' ], [ - '--browser.disableJavascriptEvaluation', + '--browser.disableJavaScriptEvaluation', '', 'boolean', 'Disables JavaScript evaluation.', 'false' ], + [ + '--browser.suppressCodeGenerationFromStringsWarning', + '', + 'boolean', + 'Suppresses the warning that is printed when code generation from strings is enabled at process level', + 'false' + ], [ '--browser.disableJavaScriptFileLoading', '',
packages/@happy-dom/server-renderer/src/utilities/ProcessArgumentsParser.ts+3 −1 modified@@ -40,7 +40,9 @@ export default class ProcessArgumentsParser { } else if (arg === '--help' || arg === '-h') { config.help = true; } else if (arg === '--browser.disableJavaScriptEvaluation') { - config.browser.disableJavaScriptEvaluation = true; + config.browser.enableJavaScriptEvaluation = false; + } else if (arg === '--browser.suppressCodeGenerationFromStringsWarning') { + config.browser.suppressCodeGenerationFromStringsWarning = true; } else if (arg === '--browser.disableJavaScriptFileLoading') { config.browser.disableJavaScriptFileLoading = true; } else if (arg === '--browser.disableCSSFileLoading') {
packages/@happy-dom/server-renderer/test/MockedWorker.ts+13 −7 modified@@ -2,13 +2,16 @@ import IServerRendererConfiguration from '../src/types/IServerRendererConfigurat import IServerRendererItem from '../src/types/IServerRendererItem'; import IServerRendererResult from '../src/types/IServerRendererResult'; +type TEvent = 'message' | 'error' | 'exit'; + /** - * + * Mocked worker. */ export default class MockedWorker { public static openWorkers: MockedWorker[] = []; public static terminatedWorkers: MockedWorker[] = []; public scriptPath: string; + public execArgv: string[] = []; public workerData: { configuration: IServerRendererConfiguration; }; @@ -25,13 +28,16 @@ export default class MockedWorker { public isTerminated: boolean = false; /** + * Constructor. * - * @param scriptPath - * @param options - * @param options.workerData + * @param scriptPath Script path. + * @param options Options. + * @param options.execArgv Exec arguments. + * @param options.workerData Worker data. */ - constructor(scriptPath: string, options: { workerData: any }) { + constructor(scriptPath: string, options: { execArgv: string[]; workerData: any }) { this.scriptPath = scriptPath; + this.execArgv = options.execArgv; this.workerData = options.workerData; (<typeof MockedWorker>this.constructor).openWorkers.push(this); } @@ -41,7 +47,7 @@ export default class MockedWorker { * @param event * @param listener */ - public on(event: string, listener: any): void { + public on(event: TEvent, listener: any): void { this.listeners[event].push(listener); } @@ -50,7 +56,7 @@ export default class MockedWorker { * @param event * @param listener */ - public off(event: string, listener: any): void { + public off(event: TEvent, listener: any): void { const index = this.listeners[event].indexOf(listener); this.listeners[event].splice(index, 1); }
packages/@happy-dom/server-renderer/test/ServerRendererBrowser.test.ts+25 −9 modified@@ -34,7 +34,9 @@ describe('ServerRendererBrowser', () => { return Promise.resolve(); }); const browser = new ServerRendererBrowser( - ServerRendererConfigurationFactory.createConfiguration() + ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true } + }) ); const results = await browser.render([{ url: 'https://example.com/gb/en/' }]); @@ -70,7 +72,9 @@ describe('ServerRendererBrowser', () => { return Promise.resolve(); }); const browser = new ServerRendererBrowser( - ServerRendererConfigurationFactory.createConfiguration() + ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true } + }) ); const results = await browser.render(MockedURLList.slice(0, 15).map((url) => ({ url }))); @@ -97,9 +101,9 @@ describe('ServerRendererBrowser', () => { const writtenFiles: { filePath: string; content: string }[] = []; const requestHeaders: Array<{ [key: string]: string }> = []; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function () { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function (this: Fetch) { const headers: { [key: string]: string } = {}; - for (const [key, value] of this.request.headers) { + for (const [key, value] of (<any>this).request.headers) { headers[key] = value; } requestHeaders.push(headers); @@ -131,7 +135,9 @@ describe('ServerRendererBrowser', () => { ); const browser = new ServerRendererBrowser( - ServerRendererConfigurationFactory.createConfiguration() + ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true } + }) ); const results = await browser.render( MockedURLList.slice(0, 15).map((url, index) => ({ @@ -193,7 +199,9 @@ describe('ServerRendererBrowser', () => { }; }); const browser = new ServerRendererBrowser( - ServerRendererConfigurationFactory.createConfiguration() + ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true } + }) ); const results = await browser.render([{ url: 'https://example.com/gb/en/' }]); @@ -225,6 +233,7 @@ describe('ServerRendererBrowser', () => { const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, render: { timeout: 100 } @@ -264,6 +273,7 @@ The page may contain scripts with timer loops that prevent it from completing. Y const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, render: { timeout: 100 }, @@ -321,7 +331,9 @@ Timer #1 }); const browser = new ServerRendererBrowser( - ServerRendererConfigurationFactory.createConfiguration() + ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true } + }) ); const results = await browser.render([{ url: 'https://example.com/gb/en/' }]); (<any>results[0]).pageConsole = results[0].pageConsole @@ -338,14 +350,14 @@ Timer #1 headers: { key1: 'value' }, outputFile: null, pageConsole: `Error: Error - at eval (https://example.com/gb/en/:0:0) + at https://example.com/gb/en/:1:26 at Timeout._onTimeout (/window/BrowserWindow.ts:0:0) at listOnTimeout (node:internal/timers:0:0) at processTimers (node:internal/timers:0:0) `, pageErrors: [ `Error: Error - at eval (https://example.com/gb/en/:0:0) + at https://example.com/gb/en/:1:26 at Timeout._onTimeout (/window/BrowserWindow.ts:0:0) at listOnTimeout (node:internal/timers:0:0) at processTimers (node:internal/timers:0:0)` @@ -383,6 +395,7 @@ Timer #1 const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, render: { allShadowRoots: true } @@ -453,6 +466,7 @@ Timer #1 const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, render: { serializableShadowRoots: true } @@ -533,6 +547,7 @@ Timer #1 const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, render: { allShadowRoots: true, excludeShadowRootTags: ['custom-element'] @@ -601,6 +616,7 @@ Timer #1 ); const browser = new ServerRendererBrowser( ServerRendererConfigurationFactory.createConfiguration({ + browser: { suppressCodeGenerationFromStringsWarning: true }, cache: { disable: true }
packages/@happy-dom/server-renderer/test/ServerRenderer.test.ts+4 −0 modified@@ -58,6 +58,8 @@ describe('ServerRenderer', () => { 'file://' + Path.resolve(Path.join('src', 'ServerRendererWorker.js')) ); + expect(worker.execArgv).toEqual(['--disallow-code-generation-from-strings']); + expect(worker.workerData.configuration.cache.directory).toBe( Path.resolve(Path.join('happy-dom', 'cache')) ); @@ -214,6 +216,8 @@ describe('ServerRenderer', () => { 'file://' + Path.resolve(Path.join('src', 'ServerRendererWorker.js')) ); + expect(worker.execArgv).toEqual(['--disallow-code-generation-from-strings']); + expect(worker.workerData.configuration.cache.directory).toBe( Path.resolve(Path.join('happy-dom', 'cache')) );
packages/@happy-dom/server-renderer/test/utilities/MockedConfiguration.ts+3 −1 modified@@ -4,7 +4,9 @@ import ServerRendererLogLevelEnum from '../../src/enums/ServerRendererLogLevelEn export default <IServerRendererConfiguration>{ browser: { - disableJavaScriptEvaluation: true, + disableJavaScriptEvaluation: false, + enableJavaScriptEvaluation: false, + suppressCodeGenerationFromStringsWarning: true, disableJavaScriptFileLoading: true, disableCSSFileLoading: true, disableIframePageLoading: false,
packages/@happy-dom/server-renderer/test/utilities/ProcessArgumentsParser.test.ts+37 −0 modified@@ -308,13 +308,49 @@ describe('ProcessArgumentsParser', () => { ); }); + it('Returns configuration with javascript evaluation disabled.', async () => { + const expectedConfig = { + ...DefaultServerRendererConfiguration, + browser: { + ...DefaultServerRendererConfiguration.browser, + enableJavaScriptEvaluation: false + } + }; + expect( + await ProcessArgumentsParser.getConfiguration([ + 'node', + 'script.js', + '--browser.disableJavaScriptEvaluation' + ]) + ).toEqual(expectedConfig); + }); + + it('Returns configuration with suppressed code generation warning.', async () => { + const expectedConfig = { + ...DefaultServerRendererConfiguration, + browser: { + ...DefaultServerRendererConfiguration.browser, + suppressCodeGenerationFromStringsWarning: true + } + }; + expect( + await ProcessArgumentsParser.getConfiguration([ + 'node', + 'script.js', + '--browser.suppressCodeGenerationFromStringsWarning' + ]) + ).toEqual(expectedConfig); + }); + it('Returns configuration for all options.', async () => { const specialArguments = [ // Deprecated 'browser.disableErrorCapturing', 'browser.enableFileSystemHttpRequests', 'browser.disableIframePageLoading', + 'browser.disableJavaScriptEvaluation', // Special handling + 'browser.enableJavaScriptEvaluation', 'browser.fetch.requestHeaders', 'browser.fetch.virtualServers', 'urls' @@ -345,6 +381,7 @@ describe('ProcessArgumentsParser', () => { args.unshift('script.js'); args.unshift('node'); + args.push('--browser.disableJavaScriptEvaluation'); args.push('--browser.fetch.requestHeaders="X-Custom-Header-1:Value-1"'); args.push('--browser.fetch.requestHeaders="https://example.com/|X-Custom-Header-2:Value-2"'); args.push('--browser.fetch.virtualServers="https://example.com/path/|./virtual-server/path"');
packages/happy-dom/src/browser/DefaultBrowserSettings.ts+2 −0 modified@@ -5,6 +5,7 @@ import IBrowserSettings from './types/IBrowserSettings.js'; export default <IBrowserSettings>{ disableJavaScriptEvaluation: false, + enableJavaScriptEvaluation: false, disableJavaScriptFileLoading: false, disableCSSFileLoading: false, disableIframePageLoading: false, @@ -13,6 +14,7 @@ export default <IBrowserSettings>{ disableErrorCapturing: false, errorCapture: BrowserErrorCaptureEnum.tryAndCatch, enableFileSystemHttpRequests: false, + suppressCodeGenerationFromStringsWarning: false, timer: { maxTimeout: -1, maxIntervalTime: -1,
packages/happy-dom/src/browser/types/IBrowserSettings.ts+18 −1 modified@@ -11,9 +11,23 @@ import BrowserWindow from '../../window/BrowserWindow.js'; * Browser settings. */ export default interface IBrowserSettings { - /** Disables JavaScript evaluation. */ + /** + * Disables JavaScript evaluation. + * + * @deprecated Javascript evaluation is now disabled by default. Use "enableJavaScriptEvaluation" if you want to enable it. + */ disableJavaScriptEvaluation: boolean; + /** + * Enables JavaScript evaluation. + * + * A VM Context is not an isolated environment, and if you run untrusted code you are at risk of RCE (Remote Code Execution) attacks. + * It is recommended to disable code generation at process level by running node with the "--disallow-code-generation-from-strings" flag enabled to protect against these types of attacks. + * + * @see https://github.com/capricorn86/happy-dom/wiki/Code-Generation-From-Strings-Warning + */ + enableJavaScriptEvaluation: boolean; + /** Disables JavaScript file loading. */ disableJavaScriptFileLoading: boolean; @@ -26,6 +40,9 @@ export default interface IBrowserSettings { /** Handle disabled resource loading as success */ handleDisabledFileLoadingAsSuccess: boolean; + /** Suppresses the warning that is printed when code generation from strings is enabled at process level. */ + suppressCodeGenerationFromStringsWarning: boolean; + /** * Settings for timers */
packages/happy-dom/src/browser/types/IOptionalBrowserSettings.ts+18 −1 modified@@ -8,9 +8,23 @@ import IOptionalTimerLoopsLimit from '../../window/IOptionalTimerLoopsLimit.js'; import BrowserWindow from '../../window/BrowserWindow.js'; export default interface IOptionalBrowserSettings { - /** Disables JavaScript evaluation. */ + /** + * Disables JavaScript evaluation. + * + * @deprecated Javascript evaluation is now disabled by default. Use "enableJavaScriptEvaluation" if you want to enable it. + */ disableJavaScriptEvaluation?: boolean; + /** + * Enables JavaScript evaluation. + * + * A VM Context is not an isolated environment, and if you run untrusted code you are at risk of RCE (Remote Code Execution) attacks. + * It is recommended to disable code generation at process level by running node with the "--disallow-code-generation-from-strings" flag enabled to protect against these types of attacks. + * + * @see https://github.com/capricorn86/happy-dom/wiki/Code-Generation-From-Strings-Warning + */ + enableJavaScriptEvaluation?: boolean; + /** Disables JavaScript file loading. */ disableJavaScriptFileLoading?: boolean; @@ -23,6 +37,9 @@ export default interface IOptionalBrowserSettings { /** Handle disabled file loading as success */ handleDisabledFileLoadingAsSuccess?: boolean; + /** Suppresses the warning that is printed when code generation from strings is enabled at process level. */ + suppressCodeGenerationFromStringsWarning?: boolean; + /** Settings for timers */ timer?: { maxTimeout?: number;
packages/happy-dom/src/browser/utilities/BrowserFrameNavigator.ts+3 −3 modified@@ -89,12 +89,12 @@ export default class BrowserFrameNavigator { // Javascript protocol if (targetURL.protocol === 'javascript:') { - if (frame && !frame.page.context.browser.settings.disableJavaScriptEvaluation) { + if (frame && frame.page.context.browser.settings.enableJavaScriptEvaluation) { const readyStateManager = frame.window[PropertySymbol.readyStateManager]; const asyncTaskManager = frame[PropertySymbol.asyncTaskManager]; const taskID = readyStateManager.startTask(); - const code = `${targetURL.href.replace('javascript:', '')}\n//# sourceURL=${frame.url}`; + const code = targetURL.href.replace('javascript:', ''); // The browser will wait for the next tick before executing the script. // Fixes issue where evaluating the response can throw an error. @@ -110,7 +110,7 @@ export default class BrowserFrameNavigator { clearImmediate(immediate); resolve(null); }); - frame.window.eval(code); + frame.window[PropertySymbol.evaluateScript](code, { filename: frame.url }); }); });
packages/happy-dom/src/html-parser/HTMLParser.ts+3 −2 modified@@ -776,10 +776,11 @@ export default class HTMLParser { // However, they are allowed to be executed when document.write() is used. // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement if (upperTagName === 'SCRIPT') { - (<HTMLScriptElement>this.currentNode)[PropertySymbol.evaluateScript] = this.evaluateScripts; + (<HTMLScriptElement>this.currentNode)[PropertySymbol.disableEvaluation] = + !this.evaluateScripts; } else if (upperTagName === 'LINK') { // An assumption that the same rule should be applied for the HTMLLinkElement is made here. - (<HTMLLinkElement>this.currentNode)[PropertySymbol.evaluateCSS] = this.evaluateScripts; + (<HTMLLinkElement>this.currentNode)[PropertySymbol.disableEvaluation] = !this.evaluateScripts; } // Plain text elements such as <script> and <style> should only contain text.
packages/happy-dom/src/module/ECMAScriptModuleCompiler.ts+6 −2 modified@@ -401,10 +401,14 @@ export default class ECMAScriptModuleCompiler { } newCode += '})'; - newCode += `\n//# sourceURL=${sourceURL || moduleURL}`; try { - return { imports, execute: this.window.eval(newCode) }; + return { + imports, + execute: this.window[PropertySymbol.evaluateScript](newCode, { + filename: sourceURL || moduleURL + }) + }; } catch (e) { const errorMessage = this.getError(moduleURL, code, sourceURL) || (<Error>e).message; const error = new this.window.SyntaxError(
packages/happy-dom/src/nodes/element/ElementEventAttributeUtility.ts+5 −4 modified@@ -32,7 +32,7 @@ export default class ElementEventAttributeUtility { const browserSettings = new WindowBrowserContext(window).getSettings(); - if (!browserSettings) { + if (!browserSettings || !browserSettings.enableJavaScriptEvaluation) { return null; } @@ -63,13 +63,14 @@ export default class ElementEventAttributeUtility { } newCode += '})'; - newCode += `\n//# sourceURL=${window.location.href}`; let listener: ((event: Event) => void) | null = null; try { - listener = window.eval(newCode).bind(element, { - dispatchError: window[PropertySymbol.dispatchError] + listener = window[PropertySymbol.evaluateScript](newCode, { + filename: window.location.href + }).bind(element, { + dispatchError: window[PropertySymbol.dispatchError].bind(window) }); } catch (e) { const error = new window.SyntaxError(
packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts+4 −4 modified@@ -24,7 +24,7 @@ import IResourceFetchResponse from '../../fetch/types/IResourceFetchResponse.js' export default class HTMLLinkElement extends HTMLElement { // Internal properties public [PropertySymbol.sheet]: CSSStyleSheet | null = null; - public [PropertySymbol.evaluateCSS] = true; + public [PropertySymbol.disableEvaluation] = false; public [PropertySymbol.relList]: DOMTokenList | null = null; #loadedStyleSheetURL: string | null = null; @@ -305,7 +305,7 @@ export default class HTMLLinkElement extends HTMLElement { !browserSettings || !this[PropertySymbol.isConnected] || browserSettings.disableJavaScriptFileLoading || - browserSettings.disableJavaScriptEvaluation + !browserSettings.enableJavaScriptEvaluation ) { return; } @@ -351,7 +351,7 @@ export default class HTMLLinkElement extends HTMLElement { if ( as === 'script' && - (browserSettings.disableJavaScriptFileLoading || browserSettings.disableJavaScriptEvaluation) + (browserSettings.disableJavaScriptFileLoading || !browserSettings.enableJavaScriptEvaluation) ) { return; } @@ -422,7 +422,7 @@ export default class HTMLLinkElement extends HTMLElement { const browserSettings = browserFrame.page.context.browser.settings; - if (!this[PropertySymbol.evaluateCSS] || !this[PropertySymbol.isConnected]) { + if (this[PropertySymbol.disableEvaluation] || !this[PropertySymbol.isConnected]) { return; }
packages/happy-dom/src/nodes/html-media-element/TextTrackList.ts+3 −0 modified@@ -9,6 +9,9 @@ import ClassMethodBinder from '../../utilities/ClassMethodBinder.js'; * @see https://developer.mozilla.org/en-US/docs/Web/API/TextTrackList */ export default class TextTrackList extends EventTarget { + // Index signature + [index: number]: TextTrack | undefined; + // Internal properties public [PropertySymbol.items]: TextTrack[] = [];
packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts+51 −42 modified@@ -25,7 +25,7 @@ export default class HTMLScriptElement extends HTMLElement { public declare cloneNode: (deep?: boolean) => HTMLScriptElement; // Internal properties - public [PropertySymbol.evaluateScript] = true; + public [PropertySymbol.disableEvaluation] = false; public [PropertySymbol.blocking]: DOMTokenList | null = null; // Private properties @@ -349,32 +349,34 @@ export default class HTMLScriptElement extends HTMLElement { super[PropertySymbol.connectedToDocument](); - if (this[PropertySymbol.evaluateScript]) { - const src = this.getAttribute('src'); + if (this[PropertySymbol.disableEvaluation]) { + return; + } - if (src !== null) { - if (this.getAttribute('type') === 'module') { - this.#loadModule(src); - } else { - this.#loadScript(src); - } - } else if (browserSettings && !browserSettings.disableJavaScriptEvaluation) { - const source = this.textContent; - const type = this.getAttribute('type'); - - if (source) { - if (type === 'module') { - this.#evaluateModule(source); - } else if (type === 'importmap') { - this.#evaluateImportMap(source); - } else if ( - type === null || - type === 'application/x-ecmascript' || - type === 'application/x-javascript' || - type.startsWith('text/javascript') - ) { - this.#evaluateScript(source); - } + const src = this.getAttribute('src'); + + if (src !== null) { + if (this.getAttribute('type') === 'module') { + this.#loadModule(src); + } else { + this.#loadScript(src); + } + } else if (browserSettings && browserSettings.enableJavaScriptEvaluation) { + const source = this.textContent; + const type = this.getAttribute('type'); + + if (source) { + if (type === 'module') { + this.#evaluateModule(source); + } else if (type === 'importmap') { + this.#evaluateImportMap(source); + } else if ( + type === null || + type === 'application/x-ecmascript' || + type === 'application/x-javascript' || + type.startsWith('text/javascript') + ) { + this.#evaluateScript(source); } } } @@ -413,7 +415,7 @@ export default class HTMLScriptElement extends HTMLElement { const browserSettings = new WindowBrowserContext(window).getSettings(); const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); - if (!browserFrame || !browserSettings) { + if (!browserFrame || !browserSettings || !browserSettings.enableJavaScriptEvaluation) { return; } @@ -446,7 +448,12 @@ export default class HTMLScriptElement extends HTMLElement { const browserSettings = new WindowBrowserContext(window).getSettings(); const browserFrame = new WindowBrowserContext(window).getBrowserFrame(); - if (!browserFrame || !browserSettings || window[PropertySymbol.moduleImportMap]) { + if ( + !browserFrame || + !browserSettings || + window[PropertySymbol.moduleImportMap] || + !browserSettings.enableJavaScriptEvaluation + ) { return; } @@ -510,9 +517,9 @@ export default class HTMLScriptElement extends HTMLElement { /** * Evaluates a script. * - * @param source Source. + * @param code Code. */ - #evaluateScript(source: string): void { + #evaluateScript(code: string): void { const window = this[PropertySymbol.window]; const browserSettings = new WindowBrowserContext(window).getSettings(); @@ -522,16 +529,18 @@ export default class HTMLScriptElement extends HTMLElement { this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = this; - const code = `${source}\n//# sourceURL=${this[PropertySymbol.ownerDocument].location.href}`; - if ( browserSettings.disableErrorCapturing || browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch ) { - window.eval(code); + window[PropertySymbol.evaluateScript](code, { + filename: this[PropertySymbol.ownerDocument].location.href + }); } else { try { - window.eval(code); + window[PropertySymbol.evaluateScript](code, { + filename: this[PropertySymbol.ownerDocument].location.href + }); } catch (error) { window[PropertySymbol.dispatchError](<Error>error); } @@ -560,7 +569,7 @@ export default class HTMLScriptElement extends HTMLElement { if ( browserSettings && - (browserSettings.disableJavaScriptFileLoading || browserSettings.disableJavaScriptEvaluation) + (browserSettings.disableJavaScriptFileLoading || !browserSettings.enableJavaScriptEvaluation) ) { if (browserSettings.handleDisabledFileLoadingAsSuccess) { this.dispatchEvent(new Event('load')); @@ -639,7 +648,7 @@ export default class HTMLScriptElement extends HTMLElement { if ( browserSettings && - (browserSettings.disableJavaScriptFileLoading || browserSettings.disableJavaScriptEvaluation) + (browserSettings.disableJavaScriptFileLoading || !browserSettings.enableJavaScriptEvaluation) ) { if (browserSettings.handleDisabledFileLoadingAsSuccess) { this.dispatchEvent(new Event('load')); @@ -693,18 +702,18 @@ export default class HTMLScriptElement extends HTMLElement { this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = this; - const code = `${response.content}\n//# sourceURL=${ - response.virtualServerFile || absoluteURLString - }`; - if ( browserSettings.disableErrorCapturing || browserSettings.errorCapture !== BrowserErrorCaptureEnum.tryAndCatch ) { - this[PropertySymbol.window].eval(code); + this[PropertySymbol.window][PropertySymbol.evaluateScript](response.content, { + filename: response.virtualServerFile || absoluteURLString + }); } else { try { - this[PropertySymbol.window].eval(code); + this[PropertySymbol.window][PropertySymbol.evaluateScript](response.content, { + filename: response.virtualServerFile || absoluteURLString + }); } catch (error) { this[PropertySymbol.ownerDocument][PropertySymbol.currentScript] = null; window[PropertySymbol.dispatchError](<Error>error);
packages/happy-dom/src/nodes/html-select-element/HTMLSelectElement.ts+3 −0 modified@@ -25,6 +25,9 @@ import BrowserWindow from '../../window/BrowserWindow.js'; * https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement. */ export default class HTMLSelectElement extends HTMLElement { + // Index signature + [index: number]: HTMLOptionElement | undefined; + // Injected by WindowContextClassExtender protected declare [PropertySymbol.window]: BrowserWindow;
packages/happy-dom/src/PropertySymbol.ts+1 −0 modified@@ -404,3 +404,4 @@ export const cssRule = Symbol('cssRule'); export const rulePrefix = Symbol('rulePrefix'); export const virtualServerFile = Symbol('virtualServerFile'); export const frames = Symbol('frames'); +export const disableEvaluation = Symbol('disableEvaluation');
packages/happy-dom/src/window/BrowserWindow.ts+38 −0 modified@@ -331,6 +331,16 @@ const TIMER = { const IS_NODE_JS_TIMEOUT_ENVIRONMENT = setTimeout.toString().includes('new Timeout'); +const IS_PROCESS_LEVEL_CODE_GENERATION_FROM_STRINGS_ALLOWED = (() => { + try { + // eslint-disable-next-line no-new-func + new Function('return true;')(); + return true; + } catch { + return false; + } +})(); + /** * Class for PerformanceObserverEntryList as it is only available as an interface from Node.js. */ @@ -779,6 +789,7 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal public declare encodeURI: typeof encodeURI; public declare encodeURIComponent: typeof encodeURIComponent; public declare eval: typeof eval; + /** * @deprecated */ @@ -847,6 +858,21 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal constructor(browserFrame: IBrowserFrame, options?: { url?: string }) { super(); + if ( + IS_PROCESS_LEVEL_CODE_GENERATION_FROM_STRINGS_ALLOWED && + browserFrame.page.context.browser.settings.enableJavaScriptEvaluation && + !browserFrame.page.context.browser.settings.suppressCodeGenerationFromStringsWarning + ) { + // eslint-disable-next-line no-console + console.warn( + '\nWarning! Happy DOM has JavaScript evaluation enabled and is running in an environment with code generation from strings (eval) enabled at process level.' + + '\n\nA VM Context is not an isolated environment, and if you run untrusted code you are at risk of RCE (Remote Code Execution) attacks. The attacker can use code generation to escape the VM and run code at process level.' + + '\n\nIt is recommended to disable code generation at process level by running node with the "--disallow-code-generation-from-strings" flag enabled when Javascript evaluation is enabled in Happy DOM.' + + ' You can suppress this warning by setting "suppressCodeGenerationFromStringsWarning" to "true" at your own risk.' + + '\n\nFor more information, see https://github.com/capricorn86/happy-dom/wiki/Code-Generation-From-Strings-Warning\n\n' + ); + } + this.#browserFrame = browserFrame; this.console = browserFrame.page.console; @@ -1806,6 +1832,18 @@ export default class BrowserWindow extends EventTarget implements INodeJSGlobal this.dispatchEvent(new ErrorEvent('error', { message: error.message, error })); } + /** + * Evaluates code in a VM context. + * + * @param code Code. + * @param [options] Options. + * @param [options.filename] Filename. + * @returns any. + */ + public [PropertySymbol.evaluateScript](code: string, options?: { filename?: string }): any { + return new VM.Script(code, options).runInContext(this); + } + /** * Setup of VM context. */
packages/happy-dom/src/window/GlobalWindow.ts+14 −1 modified@@ -69,7 +69,20 @@ export default class GlobalWindow extends Window { public unescape: (str: string) => string = globalThis.unescape; /** - * Setup of VM context. + * @override + */ + public override [PropertySymbol.evaluateScript]( + code: string, + options?: { filename?: string } + ): any { + if (options?.filename) { + return this.eval(`${code}\n//# sourceURL=${options.filename}`); + } + return this.eval(code); + } + + /** + * @override */ protected override [PropertySymbol.setupVMContext](): void { // Do nothing
packages/happy-dom/src/xml-parser/XMLParser.ts+0 −3 modified@@ -109,9 +109,6 @@ export default class XMLParser { * Constructor. * * @param window Window. - * @param [options] Options. - * @param [options.mode] Mode. Defaults to "htmlFragment". - * @param [options.evaluateScripts] Set to "true" to enable script execution */ constructor(window: BrowserWindow) { this.window = window;
packages/happy-dom/test/browser/BrowserFrame.test.ts+48 −27 modified@@ -152,9 +152,9 @@ describe('BrowserFrame', () => { frame1.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); await page.waitUntilComplete(); - expect(page.mainFrame.window['test']).toBe(1); - expect(frame1.window['test']).toBe(2); - expect(frame2.window['test']).toBe(3); + expect((<any>page.mainFrame.window)['test']).toBe(1); + expect((<any>frame1.window)['test']).toBe(2); + expect((<any>frame2.window)['test']).toBe(3); }); it('Traces never ending timeout when calling waitUntilComplete() with the setting "debug.traceWaitUntilComplete" set to a time in ms.', async () => { @@ -177,7 +177,7 @@ describe('BrowserFrame', () => { try { await page.waitUntilComplete(); } catch (e) { - error = e; + error = <Error>e; } expect(error?.toString().replace(STACK_TRACE_REGEXP, '') + '> testFunction (test.js:1:1)\n') .toBe(`Error: The maximum time was reached for "waitUntilComplete()". @@ -210,7 +210,7 @@ Timer #1 try { await page.waitUntilComplete(); } catch (e) { - error = e; + error = <Error>e; } expect(error?.toString().replace(STACK_TRACE_REGEXP, '') + '> testFunction (test.js:1:1)\n') .toBe(`Error: The maximum time was reached for "waitUntilComplete()". @@ -272,9 +272,9 @@ Task #1 frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); page.abort(); await new Promise((resolve) => setTimeout(resolve, 50)); - expect(page.mainFrame.window['test']).toBeUndefined(); - expect(frame1.window['test']).toBeUndefined(); - expect(frame2.window['test']).toBeUndefined(); + expect((<any>page.mainFrame.window)['test']).toBeUndefined(); + expect((<any>frame1.window)['test']).toBeUndefined(); + expect((<any>frame2.window)['test']).toBeUndefined(); }); }); @@ -283,22 +283,24 @@ Task #1 const browser = new Browser(); const page = browser.newPage(); expect(page.mainFrame.evaluate('globalThis.test = 1')).toBe(1); - expect(page.mainFrame.window['test']).toBe(1); + expect((<any>page.mainFrame.window)['test']).toBe(1); }); it("Evaluates a VM script in the frame's context.", () => { const browser = new Browser(); const page = browser.newPage(); expect(page.mainFrame.evaluate(new Script('globalThis.test = 1'))).toBe(1); - expect(page.mainFrame.window['test']).toBe(1); + expect((<any>page.mainFrame.window)['test']).toBe(1); }); }); describe('goto()', () => { it('Navigates to a URL.', async () => { let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = (<any>this).request; return Promise.resolve(<Response>{ url: request?.url, text: () => @@ -326,8 +328,10 @@ Task #1 it('Triggers "beforeContentCallback" before content is loaded into the document', async () => { let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = (<any>this).request; return Promise.resolve(<Response>{ url: request?.url, text: () => @@ -353,8 +357,10 @@ Task #1 it('Triggers "browser.settings.navigation.beforeContentCallback" before content is loaded into the document', async () => { let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = (<any>this).request; return Promise.resolve(<Response>{ url: request?.url, text: () => @@ -383,7 +389,12 @@ Task #1 }); it('Navigates to a URL with "javascript:" as protocol.', async () => { - const browser = new Browser(); + const browser = new Browser({ + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } + }); const page = browser.newPage(); const oldWindow = page.mainFrame.window; const response = await page.mainFrame.goto('javascript:document.write("test");'); @@ -416,7 +427,7 @@ Task #1 timeout: 1 }); } catch (e) { - error = e; + error = <Error>e; } expect(error).toEqual( @@ -433,8 +444,10 @@ Task #1 it('Handles error status code in response.', async () => { let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = (<any>this).request; return Promise.resolve(<Response>{ url: request?.url, status: 404, @@ -474,7 +487,7 @@ Task #1 try { await page.mainFrame.goto('http://localhost:9999'); } catch (e) { - error = e; + error = <Error>e; } expect(error).toEqual(new Error('Error')); @@ -919,6 +932,8 @@ Task #1 it('Navigates to a virtual server page.', async () => { const browser = new Browser({ settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ { @@ -989,15 +1004,17 @@ Task #1 describe('goBack()', () => { it('Navigates back in history.', async () => { - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { return Promise.resolve(<Response>{ status: 200, text: () => new Promise((resolve) => setTimeout( () => resolve( - this.request.url === 'http://localhost:3000/' + (<any>this).request.url === 'http://localhost:3000/' ? '<a href="http://localhost:3000/navigated/">' : '<b>Navigated</b>' ), @@ -1043,15 +1060,17 @@ Task #1 describe('goForward()', () => { it('Navigates forward in history.', async () => { - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { return Promise.resolve(<Response>{ status: 200, text: () => new Promise((resolve) => setTimeout( () => resolve( - this.request.url === 'http://localhost:3000/' + (<any>this).request.url === 'http://localhost:3000/' ? '<a href="http://localhost:3000/navigated/">' : '<b>Navigated</b>' ), @@ -1098,15 +1117,17 @@ Task #1 describe('goSteps()', () => { it('Navigates a delta in history.', async () => { - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { return Promise.resolve(<Response>{ status: 200, text: () => new Promise((resolve) => setTimeout( () => resolve( - this.request.url === 'http://localhost:3000/' + (<any>this).request.url === 'http://localhost:3000/' ? '<a href="http://localhost:3000/navigated/">' : '<b>Navigated</b>' ),
packages/happy-dom/test/browser/BrowserPage.test.ts+6 −4 modified@@ -184,6 +184,8 @@ describe('BrowserPage', () => { it('Clears modules when closing.', async () => { const browser = new Browser({ settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ { @@ -251,8 +253,8 @@ describe('BrowserPage', () => { frame1.evaluate('setTimeout(() => { globalThis.test = 1; }, 10);'); frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); await page.waitUntilComplete(); - expect(frame1.window['test']).toBe(1); - expect(frame2.window['test']).toBe(2); + expect((<any>frame1.window)['test']).toBe(1); + expect((<any>frame2.window)['test']).toBe(2); }); }); @@ -283,8 +285,8 @@ describe('BrowserPage', () => { frame2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); page.abort(); await new Promise((resolve) => setTimeout(resolve, 50)); - expect(frame1.window['test']).toBeUndefined(); - expect(frame2.window['test']).toBeUndefined(); + expect((<any>frame1.window)['test']).toBeUndefined(); + expect((<any>frame2.window)['test']).toBeUndefined(); }); });
packages/happy-dom/test/browser/Browser.test.ts+7 −7 modified@@ -65,7 +65,7 @@ describe('Browser', () => { it('Returns the settings with custom settings.', () => { const settings = { - disableJavaScriptEvaluation: true, + enableJavaScriptEvaluation: true, navigator: { userAgent: 'test' } @@ -135,9 +135,9 @@ describe('Browser', () => { page2.evaluate('setTimeout(() => { globalThis.test = 2; }, 10);'); page3.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); await browser.waitUntilComplete(); - expect(page1.mainFrame.window['test']).toBe(1); - expect(page2.mainFrame.window['test']).toBe(2); - expect(page3.mainFrame.window['test']).toBe(3); + expect((<any>page1.mainFrame.window)['test']).toBe(1); + expect((<any>page2.mainFrame.window)['test']).toBe(2); + expect((<any>page3.mainFrame.window)['test']).toBe(3); }); }); @@ -152,9 +152,9 @@ describe('Browser', () => { page3.evaluate('setTimeout(() => { globalThis.test = 3; }, 10);'); browser.abort(); await new Promise((resolve) => setTimeout(resolve, 50)); - expect(page1.mainFrame.window['test']).toBeUndefined(); - expect(page2.mainFrame.window['test']).toBeUndefined(); - expect(page3.mainFrame.window['test']).toBeUndefined(); + expect((<any>page1.mainFrame.window)['test']).toBeUndefined(); + expect((<any>page2.mainFrame.window)['test']).toBeUndefined(); + expect((<any>page3.mainFrame.window)['test']).toBeUndefined(); }); });
packages/happy-dom/test/browser/detached-browser/DetachedBrowserFrame.test.ts+8 −1 modified@@ -362,7 +362,12 @@ describe('DetachedBrowserFrame', () => { }); it('Navigates to a URL with "javascript:" as protocol.', async () => { - const browser = new DetachedBrowser(BrowserWindow); + const browser = new DetachedBrowser(BrowserWindow, { + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } + }); browser.defaultContext.pages[0].mainFrame.window = new BrowserWindow( browser.defaultContext.pages[0].mainFrame ); @@ -1015,6 +1020,8 @@ describe('DetachedBrowserFrame', () => { it('Navigates to a virtual server page.', async () => { const browser = new DetachedBrowser(BrowserWindow, { settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ {
packages/happy-dom/test/browser/detached-browser/DetachedBrowser.test.ts+1 −1 modified@@ -46,7 +46,7 @@ describe('DetachedBrowser', () => { it('Returns the settings with custom settings.', () => { const settings = { - disableJavaScriptEvaluation: true, + enableJavaScriptEvaluation: true, navigator: { userAgent: 'test' }
packages/happy-dom/test/dom-parser/DOMParser.test.ts+0 −1 modified@@ -16,7 +16,6 @@ describe('DOMParser', () => { window = new Window({ settings: { disableJavaScriptFileLoading: true, - disableJavaScriptEvaluation: true, disableCSSFileLoading: true, enableFileSystemHttpRequests: false }
packages/happy-dom/test/html-serializer/HTMLSerializer.test.ts+3 −1 modified@@ -10,7 +10,9 @@ describe('HTMLSerializer', () => { let serializer: HTMLSerializer; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; serializer = new HTMLSerializer();
packages/happy-dom/test/nodes/document/Document.test.ts+3 −1 modified@@ -46,7 +46,9 @@ describe('Document', () => { let document: Document; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; });
packages/happy-dom/test/nodes/element/Element.test.ts+172 −166 modified@@ -30,7 +30,9 @@ describe('Element', () => { let element: Element; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <Element>document.createElement('div'); window.customElements.define('custom-element', CustomElement); @@ -52,20 +54,20 @@ describe('Element', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -106,63 +108,63 @@ describe('Element', () => { it('Adds the "id" attribute as a property to Window.', () => { element.setAttribute('id', 'element1'); - expect(window['element1']).toBeUndefined(); + expect((<any>window)['element1']).toBeUndefined(); document.body.appendChild(element); - expect(window['element1']).toBe(element); + expect((<any>window)['element1']).toBe(element); document.body.removeChild(element); - expect(window['element1']).toBeUndefined(); + expect((<any>window)['element1']).toBeUndefined(); document.body.appendChild(element); - expect(window['element1']).toBe(element); + expect((<any>window)['element1']).toBe(element); element.setAttribute('id', 'element2'); - expect(window['element1']).toBeUndefined(); - expect(window['element2']).toBe(element); + expect((<any>window)['element1']).toBeUndefined(); + expect((<any>window)['element2']).toBe(element); const element2 = document.createElement('div'); element2.id = 'element2'; document.body.appendChild(element2); - expect(window['element2']).toBeInstanceOf(HTMLCollection); - expect(window['element2'].length).toBe(2); - expect(window['element2'][0]).toBe(element); - expect(window['element2'][1]).toBe(element2); + expect((<any>window)['element2']).toBeInstanceOf(HTMLCollection); + expect((<any>window)['element2'].length).toBe(2); + expect((<any>window)['element2'][0]).toBe(element); + expect((<any>window)['element2'][1]).toBe(element2); document.body.removeChild(element2); - expect(window['element2']).toBe(element); + expect((<any>window)['element2']).toBe(element); document.body.appendChild(element2); - expect(window['element2']).toBeInstanceOf(HTMLCollection); - expect(window['element2'].length).toBe(2); + expect((<any>window)['element2']).toBeInstanceOf(HTMLCollection); + expect((<any>window)['element2'].length).toBe(2); element2.removeAttribute('id'); - expect(window['element2']).toBe(element); + expect((<any>window)['element2']).toBe(element); element.removeAttribute('id'); - expect(window['element2']).toBe(undefined); + expect((<any>window)['element2']).toBe(undefined); }); it(`Doesn't add the "id" attribute as a property to Window if it collides with Window properties.`, () => { element.setAttribute('id', 'document'); document.body.appendChild(element); - expect(window['document']).toBe(document); + expect((<any>window)['document']).toBe(document); }); it(`Doesn't add the "opener" attribute as a property to Window when the property value is null (#1841).`, () => { document.body.appendChild(element); element.id = 'opener'; - expect(window['opener']).toBe(null); + expect((<any>window)['opener']).toBe(null); }); }); @@ -355,8 +357,12 @@ describe('Element', () => { element.appendChild(document.createElement('div')); div.appendChild(textNode); - vi.spyOn(HTMLParser.prototype, 'parse').mockImplementation(function (html, rootNode) { - expect(this.window).toBe(window); + vi.spyOn(HTMLParser.prototype, 'parse').mockImplementation(function ( + this: HTMLParser, + html, + rootNode + ) { + expect((<any>this).window).toBe(window); expect(html).toBe('SOME_HTML'); expect(rootNode).toBe(element); element.appendChild(div); @@ -437,26 +443,26 @@ describe('Element', () => { expect(element.attributes[2].ownerElement === element).toBe(true); expect(element.attributes[2].ownerDocument === document).toBe(true); - expect(element.attributes['key1'].name).toBe('key1'); - expect(element.attributes['key1'].value).toBe('value1'); - expect(element.attributes['key1'].namespaceURI).toBe(null); - expect(element.attributes['key1'].specified).toBe(true); - expect(element.attributes['key1'].ownerElement === element).toBe(true); - expect(element.attributes['key1'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key1'].name).toBe('key1'); + expect((<any>element).attributes['key1'].value).toBe('value1'); + expect((<any>element).attributes['key1'].namespaceURI).toBe(null); + expect((<any>element).attributes['key1'].specified).toBe(true); + expect((<any>element).attributes['key1'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key1'].ownerDocument === document).toBe(true); - expect(element.attributes['key2'].name).toBe('key2'); - expect(element.attributes['key2'].value).toBe('value2'); - expect(element.attributes['key2'].namespaceURI).toBe(null); - expect(element.attributes['key2'].specified).toBe(true); - expect(element.attributes['key2'].ownerElement === element).toBe(true); - expect(element.attributes['key2'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key2'].name).toBe('key2'); + expect((<any>element).attributes['key2'].value).toBe('value2'); + expect((<any>element).attributes['key2'].namespaceURI).toBe(null); + expect((<any>element).attributes['key2'].specified).toBe(true); + expect((<any>element).attributes['key2'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key2'].ownerDocument === document).toBe(true); - expect(element.attributes['key3'].name).toBe('key3'); - expect(element.attributes['key3'].value).toBe('value3'); - expect(element.attributes['key3'].namespaceURI).toBe(null); - expect(element.attributes['key3'].specified).toBe(true); - expect(element.attributes['key3'].ownerElement === element).toBe(true); - expect(element.attributes['key3'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key3'].name).toBe('key3'); + expect((<any>element).attributes['key3'].value).toBe('value3'); + expect((<any>element).attributes['key3'].namespaceURI).toBe(null); + expect((<any>element).attributes['key3'].specified).toBe(true); + expect((<any>element).attributes['key3'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key3'].ownerDocument === document).toBe(true); }); }); @@ -1287,10 +1293,10 @@ describe('Element', () => { expect(otherParent.children[0] === otherDiv).toBe(true); expect(otherParent.children[1] === div).toBe(true); expect(otherParent.children[2] === otherSpan).toBe(true); - expect(otherParent.children['div1'] === div).toBe(true); - expect(otherParent.children['div2'] === div).toBe(true); - expect(otherParent.children['otherDiv'] === otherDiv).toBe(true); - expect(otherParent.children['otherSpan'] === otherSpan).toBe(true); + expect((<any>otherParent.children)['div1'] === div).toBe(true); + expect((<any>otherParent.children)['div2'] === div).toBe(true); + expect((<any>otherParent.children)['otherDiv'] === otherDiv).toBe(true); + expect((<any>otherParent.children)['otherSpan'] === otherSpan).toBe(true); element.appendChild(document.createComment('test')); element.appendChild(div); @@ -1300,17 +1306,17 @@ describe('Element', () => { expect(otherParent.children.length).toBe(2); expect(otherParent.children[0] === otherDiv).toBe(true); expect(otherParent.children[1] === otherSpan).toBe(true); - expect(otherParent.children['div1'] === undefined).toBe(true); - expect(otherParent.children['div2'] === undefined).toBe(true); - expect(otherParent.children['otherDiv'] === otherDiv).toBe(true); - expect(otherParent.children['otherSpan'] === otherSpan).toBe(true); + expect((<any>otherParent.children)['div1'] === undefined).toBe(true); + expect((<any>otherParent.children)['div2'] === undefined).toBe(true); + expect((<any>otherParent.children)['otherDiv'] === otherDiv).toBe(true); + expect((<any>otherParent.children)['otherSpan'] === otherSpan).toBe(true); expect(element.children.length).toBe(2); expect(element.children[0] === div).toBe(true); expect(element.children[1] === span).toBe(true); - expect(element.children['div1'] === div).toBe(true); - expect(element.children['div2'] === div).toBe(true); - expect(element.children['span'] === span).toBe(true); + expect((<any>element.children)['div1'] === div).toBe(true); + expect((<any>element.children)['div2'] === div).toBe(true); + expect((<any>element.children)['span'] === span).toBe(true); }); }); @@ -1327,15 +1333,15 @@ describe('Element', () => { element.appendChild(document.createComment('test')); element.appendChild(span); - expect(element.children['div'] === div).toBe(true); - expect(element.children['span'] === span).toBe(true); + expect((<any>element.children)['div'] === div).toBe(true); + expect((<any>element.children)['span'] === span).toBe(true); element.removeChild(div); expect(element.children.length).toBe(1); expect(element.children[0] === span).toBe(true); - expect(element.children['div'] === undefined).toBe(true); - expect(element.children['span'] === span).toBe(true); + expect((<any>element.children)['div'] === undefined).toBe(true); + expect((<any>element.children)['span'] === span).toBe(true); }); }); @@ -1456,9 +1462,9 @@ describe('Element', () => { expect(otherParent.children[0] === otherSpan1).toBe(true); expect(otherParent.children[1] === div).toBe(true); expect(otherParent.children[2] === otherSpan2).toBe(true); - expect(otherParent.children['otherSpan1'] === otherSpan1).toBe(true); - expect(otherParent.children['div'] === div).toBe(true); - expect(otherParent.children['otherSpan2'] === otherSpan2).toBe(true); + expect((<any>otherParent.children)['otherSpan1'] === otherSpan1).toBe(true); + expect((<any>otherParent.children)['div'] === div).toBe(true); + expect((<any>otherParent.children)['otherSpan2'] === otherSpan2).toBe(true); element.appendChild(document.createComment('test')); element.appendChild(span1); @@ -1472,17 +1478,17 @@ describe('Element', () => { expect(otherParent.children.length).toBe(2); expect(otherParent.children[0] === otherSpan1).toBe(true); expect(otherParent.children[1] === otherSpan2).toBe(true); - expect(otherParent.children['div'] === undefined).toBe(true); - expect(otherParent.children['otherSpan1'] === otherSpan1).toBe(true); - expect(otherParent.children['otherSpan2'] === otherSpan2).toBe(true); + expect((<any>otherParent.children)['div'] === undefined).toBe(true); + expect((<any>otherParent.children)['otherSpan1'] === otherSpan1).toBe(true); + expect((<any>otherParent.children)['otherSpan2'] === otherSpan2).toBe(true); expect(element.children.length).toBe(3); expect(element.children[0] === span1).toBe(true); expect(element.children[1] === div).toBe(true); expect(element.children[2] === span2).toBe(true); - expect(element.children['span1'] === span1).toBe(true); - expect(element.children['div'] === div).toBe(true); - expect(element.children['span2'] === span2).toBe(true); + expect((<any>element.children)['span1'] === span1).toBe(true); + expect((<any>element.children)['div'] === div).toBe(true); + expect((<any>element.children)['span2'] === span2).toBe(true); }); it('Inserts correctly with comment reference node', () => { @@ -1612,19 +1618,19 @@ describe('Element', () => { expect(element.attributes[1].ownerElement === element).toBe(true); expect(element.attributes[1].ownerDocument === document).toBe(true); - expect(element.attributes['key1'].name).toBe('key1'); - expect(element.attributes['key1'].value).toBe('value1'); - expect(element.attributes['key1'].namespaceURI).toBe(null); - expect(element.attributes['key1'].specified).toBe(true); - expect(element.attributes['key1'].ownerElement === element).toBe(true); - expect(element.attributes['key1'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key1'].name).toBe('key1'); + expect((<any>element).attributes['key1'].value).toBe('value1'); + expect((<any>element).attributes['key1'].namespaceURI).toBe(null); + expect((<any>element).attributes['key1'].specified).toBe(true); + expect((<any>element).attributes['key1'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key1'].ownerDocument === document).toBe(true); - expect(element.attributes['key2'].name).toBe('key2'); - expect(element.attributes['key2'].value).toBe(''); - expect(element.attributes['key2'].namespaceURI).toBe(null); - expect(element.attributes['key2'].specified).toBe(true); - expect(element.attributes['key2'].ownerElement === element).toBe(true); - expect(element.attributes['key2'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key2'].name).toBe('key2'); + expect((<any>element).attributes['key2'].value).toBe(''); + expect((<any>element).attributes['key2'].namespaceURI).toBe(null); + expect((<any>element).attributes['key2'].specified).toBe(true); + expect((<any>element).attributes['key2'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key2'].ownerDocument === document).toBe(true); }); it('Sets valid attribute names', () => { @@ -1748,58 +1754,58 @@ describe('Element', () => { try { element.setAttribute('☺', '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { // eslint-disable-next-line element.setAttribute({} as string, '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute('', '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute('=', '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(' ', '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute('"', '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(`'`, '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(`>`, '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(`\/`, '1'); } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(`\u007F`, '1'); // control character delete } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } try { element.setAttribute(`\u9FFFE`, '1'); // non character } catch (error) { - expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError); + expect((<any>error).name).toBe(DOMExceptionNameEnum.invalidCharacterError); } }); }); @@ -1825,19 +1831,19 @@ describe('Element', () => { expect(element.attributes[1].ownerElement === element).toBe(true); expect(element.attributes[1].ownerDocument === document).toBe(true); - expect(element.attributes['global:local1'].name).toBe('global:local1'); - expect(element.attributes['global:local1'].value).toBe('value1'); - expect(element.attributes['global:local1'].namespaceURI).toBe(NAMESPACE_URI); - expect(element.attributes['global:local1'].specified).toBe(true); - expect(element.attributes['global:local1'].ownerElement === element).toBe(true); - expect(element.attributes['global:local1'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['global:local1'].name).toBe('global:local1'); + expect((<any>element).attributes['global:local1'].value).toBe('value1'); + expect((<any>element).attributes['global:local1'].namespaceURI).toBe(NAMESPACE_URI); + expect((<any>element).attributes['global:local1'].specified).toBe(true); + expect((<any>element).attributes['global:local1'].ownerElement === element).toBe(true); + expect((<any>element).attributes['global:local1'].ownerDocument === document).toBe(true); - expect(element.attributes['global:local2'].name).toBe('global:local2'); - expect(element.attributes['global:local2'].value).toBe(''); - expect(element.attributes['global:local2'].namespaceURI).toBe(NAMESPACE_URI); - expect(element.attributes['global:local2'].specified).toBe(true); - expect(element.attributes['global:local2'].ownerElement === element).toBe(true); - expect(element.attributes['global:local2'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['global:local2'].name).toBe('global:local2'); + expect((<any>element).attributes['global:local2'].value).toBe(''); + expect((<any>element).attributes['global:local2'].namespaceURI).toBe(NAMESPACE_URI); + expect((<any>element).attributes['global:local2'].specified).toBe(true); + expect((<any>element).attributes['global:local2'].ownerElement === element).toBe(true); + expect((<any>element).attributes['global:local2'].ownerDocument === document).toBe(true); }); }); @@ -1962,39 +1968,39 @@ describe('Element', () => { for (const functionName of ['scroll', 'scrollTo']) { describe(`${functionName}()`, () => { it('Sets the properties scrollTop and scrollLeft.', () => { - element[functionName](50, 60); + (<any>element)[functionName](50, 60); expect(element.scrollLeft).toBe(50); expect(element.scrollTop).toBe(60); }); }); describe(`${functionName}()`, () => { it('Sets the properties scrollTop and scrollLeft using object.', () => { - element[functionName]({ left: 50, top: 60 }); + (<any>element)[functionName]({ left: 50, top: 60 }); expect(element.scrollLeft).toBe(50); expect(element.scrollTop).toBe(60); }); }); describe(`${functionName}()`, () => { it('Sets only the property scrollTop.', () => { - element[functionName]({ top: 60 }); + (<any>element)[functionName]({ top: 60 }); expect(element.scrollLeft).toBe(0); expect(element.scrollTop).toBe(60); }); }); describe(`${functionName}()`, () => { it('Sets only the property scrollLeft.', () => { - element[functionName]({ left: 60 }); + (<any>element)[functionName]({ left: 60 }); expect(element.scrollLeft).toBe(60); expect(element.scrollTop).toBe(0); }); }); describe(`${functionName}()`, () => { it('Sets the properties scrollTop and scrollLeft with animation.', async () => { - element[functionName]({ left: 50, top: 60, behavior: 'smooth' }); + (<any>element)[functionName]({ left: 50, top: 60, behavior: 'smooth' }); expect(element.scrollLeft).toBe(0); expect(element.scrollTop).toBe(0); await window.happyDOM?.waitUntilComplete(); @@ -2127,9 +2133,9 @@ describe('Element', () => { attribute2.value = 'value2'; attribute3.value = 'value3'; - element[method](attribute1); - element[method](attribute2); - element[method](attribute3); + (<any>element)[method](attribute1); + (<any>element)[method](attribute2); + (<any>element)[method](attribute3); expect(element.attributes.length).toBe(3); @@ -2155,32 +2161,32 @@ describe('Element', () => { expect(element.attributes[2].ownerDocument === document).toBe(true); // "undefined" as the key is in upper case which should not be considered as a named item when the element is in the HTML namespace - expect(element.attributes['key1']).toBe(undefined); - expect(element.attributes['KEY1']).toBe(undefined); + expect((<any>element).attributes['key1']).toBe(undefined); + expect((<any>element).attributes['KEY1']).toBe(undefined); // Lower case SVG namespace key is fine - expect(element.attributes['key2'].name).toBe('key2'); - expect(element.attributes['key2'].namespaceURI).toBe(NamespaceURI.svg); - expect(element.attributes['key2'].value).toBe('value2'); - expect(element.attributes['key2'].specified).toBe(true); - expect(element.attributes['key2'].ownerElement === element).toBe(true); - expect(element.attributes['key2'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key2'].name).toBe('key2'); + expect((<any>element).attributes['key2'].namespaceURI).toBe(NamespaceURI.svg); + expect((<any>element).attributes['key2'].value).toBe('value2'); + expect((<any>element).attributes['key2'].specified).toBe(true); + expect((<any>element).attributes['key2'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key2'].ownerDocument === document).toBe(true); // Matches the key in the HTML namespace - expect(element.attributes['key3'].name).toBe('key3'); - expect(element.attributes['key3'].namespaceURI).toBe(null); - expect(element.attributes['key3'].value).toBe('value3'); - expect(element.attributes['key3'].specified).toBe(true); - expect(element.attributes['key3'].ownerElement === element).toBe(true); - expect(element.attributes['key3'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['key3'].name).toBe('key3'); + expect((<any>element).attributes['key3'].namespaceURI).toBe(null); + expect((<any>element).attributes['key3'].value).toBe('value3'); + expect((<any>element).attributes['key3'].specified).toBe(true); + expect((<any>element).attributes['key3'].ownerElement === element).toBe(true); + expect((<any>element).attributes['key3'].ownerDocument === document).toBe(true); // Is converted to lower case through the Proxy in the HTML namespace - expect(element.attributes['KeY3'].name).toBe('key3'); - expect(element.attributes['KeY3'].namespaceURI).toBe(null); - expect(element.attributes['KeY3'].value).toBe('value3'); - expect(element.attributes['KeY3'].specified).toBe(true); - expect(element.attributes['KeY3'].ownerElement === element).toBe(true); - expect(element.attributes['KeY3'].ownerDocument === document).toBe(true); + expect((<any>element).attributes['KeY3'].name).toBe('key3'); + expect((<any>element).attributes['KeY3'].namespaceURI).toBe(null); + expect((<any>element).attributes['KeY3'].value).toBe('value3'); + expect((<any>element).attributes['KeY3'].specified).toBe(true); + expect((<any>element).attributes['KeY3'].ownerElement === element).toBe(true); + expect((<any>element).attributes['KeY3'].ownerDocument === document).toBe(true); }); it('Sets an Attr node on an <svg> element.', () => { @@ -2191,47 +2197,47 @@ describe('Element', () => { attribute1.value = 'value1'; attribute2.value = 'value2'; - svg[method](attribute1); - svg[method](attribute2); + (<any>svg)[method](attribute1); + (<any>svg)[method](attribute2); expect(svg.attributes.length).toBe(2); - expect(svg.attributes[0].name).toBe('KEY1'); - expect(svg.attributes[0].namespaceURI).toBe(NamespaceURI.svg); - expect(svg.attributes[0].value).toBe('value1'); - expect(svg.attributes[0].specified).toBe(true); - expect(svg.attributes[0].ownerElement === svg).toBe(true); - expect(svg.attributes[0].ownerDocument).toBe(document); + expect((<any>svg.attributes)[0].name).toBe('KEY1'); + expect((<any>svg.attributes)[0].namespaceURI).toBe(NamespaceURI.svg); + expect((<any>svg.attributes)[0].value).toBe('value1'); + expect((<any>svg.attributes)[0].specified).toBe(true); + expect((<any>svg.attributes)[0].ownerElement === svg).toBe(true); + expect((<any>svg.attributes)[0].ownerDocument).toBe(document); - expect(svg.attributes[1].name).toBe('key2'); - expect(svg.attributes[1].namespaceURI).toBe(null); - expect(svg.attributes[1].value).toBe('value2'); - expect(svg.attributes[1].specified).toBe(true); - expect(svg.attributes[1].ownerElement === svg).toBe(true); - expect(svg.attributes[1].ownerDocument).toBe(document); + expect((<any>svg.attributes)[1].name).toBe('key2'); + expect((<any>svg.attributes)[1].namespaceURI).toBe(null); + expect((<any>svg.attributes)[1].value).toBe('value2'); + expect((<any>svg.attributes)[1].specified).toBe(true); + expect((<any>svg.attributes)[1].ownerElement === svg).toBe(true); + expect((<any>svg.attributes)[1].ownerDocument).toBe(document); // "undefined" as the SVG namespace should not lowercase the key - expect(svg.attributes['key1']).toBe(undefined); - expect(svg.attributes['kEy1']).toBe(undefined); + expect((<any>svg.attributes)['key1']).toBe(undefined); + expect((<any>svg.attributes)['kEy1']).toBe(undefined); // Matching key is fine in the SVG namespace - expect(svg.attributes['KEY1'].name).toBe('KEY1'); - expect(svg.attributes['KEY1'].namespaceURI).toBe(NamespaceURI.svg); - expect(svg.attributes['KEY1'].value).toBe('value1'); - expect(svg.attributes['KEY1'].specified).toBe(true); - expect(svg.attributes['KEY1'].ownerElement === svg).toBe(true); - expect(svg.attributes['KEY1'].ownerDocument).toBe(document); + expect((<any>svg.attributes)['KEY1'].name).toBe('KEY1'); + expect((<any>svg.attributes)['KEY1'].namespaceURI).toBe(NamespaceURI.svg); + expect((<any>svg.attributes)['KEY1'].value).toBe('value1'); + expect((<any>svg.attributes)['KEY1'].specified).toBe(true); + expect((<any>svg.attributes)['KEY1'].ownerElement === svg).toBe(true); + expect((<any>svg.attributes)['KEY1'].ownerDocument).toBe(document); // "undefined" as the SVG namespace should not lowercase the key - expect(svg.attributes['KeY2']).toBe(undefined); + expect((<any>svg.attributes)['KeY2']).toBe(undefined); // Works when matching in the SVG namespace - expect(svg.attributes['key2'].name).toBe('key2'); - expect(svg.attributes['key2'].namespaceURI).toBe(null); - expect(svg.attributes['key2'].value).toBe('value2'); - expect(svg.attributes['key2'].specified).toBe(true); - expect(svg.attributes['key2'].ownerElement === svg).toBe(true); - expect(svg.attributes['key2'].ownerDocument).toBe(document); + expect((<any>svg.attributes)['key2'].name).toBe('key2'); + expect((<any>svg.attributes)['key2'].namespaceURI).toBe(null); + expect((<any>svg.attributes)['key2'].value).toBe('value2'); + expect((<any>svg.attributes)['key2'].specified).toBe(true); + expect((<any>svg.attributes)['key2'].ownerElement === svg).toBe(true); + expect((<any>svg.attributes)['key2'].ownerDocument).toBe(document); }); }); } @@ -2347,7 +2353,7 @@ describe('Element', () => { div.scrollLeft = 10; div.scrollTop = 15; - div[functionName](20, 30); + (<any>div)[functionName](20, 30); expect(div.scrollLeft).toBe(20); expect(div.scrollTop).toBe(30); @@ -2359,7 +2365,7 @@ describe('Element', () => { div.scrollLeft = 10; div.scrollTop = 15; - div[functionName]({ left: 20, top: 30 }); + (<any>div)[functionName]({ left: 20, top: 30 }); expect(div.scrollLeft).toBe(20); expect(div.scrollTop).toBe(30); @@ -2371,7 +2377,7 @@ describe('Element', () => { div.scrollLeft = 10; div.scrollTop = 15; - div[functionName]({ left: 20, top: 30, behavior: 'smooth' }); + (<any>div)[functionName]({ left: 20, top: 30, behavior: 'smooth' }); expect(div.scrollLeft).toBe(10); expect(div.scrollTop).toBe(15); @@ -2384,7 +2390,7 @@ describe('Element', () => { it('Throws an exception if the there is only one argument and it is not an object.', () => { const div = document.createElement('div'); - expect(() => div[functionName](10)).toThrow( + expect(() => (<any>div)[functionName](10)).toThrow( new TypeError( `Failed to execute '${functionName}' on 'Element': The provided value is not of type 'ScrollToOptions'.` ) @@ -2450,15 +2456,15 @@ describe('Element', () => { const div = document.createElement('div'); div.setAttribute('onclick', 'divClicked = true'); div.dispatchEvent(new Event('click')); - expect(window['divClicked']).toBe(true); + expect((<any>window)['divClicked']).toBe(true); }); it("Doesn't evaluate attribute event listener is immediate propagation has been stopped.", () => { const div = document.createElement('div'); div.addEventListener('click', (e: Event) => e.stopImmediatePropagation()); div.setAttribute('onclick', 'divClicked = true'); div.dispatchEvent(new Event('click')); - expect(window['divClicked']).toBe(undefined); + expect((<any>window)['divClicked']).toBe(undefined); }); }); });
packages/happy-dom/test/nodes/html-body-element/HTMLBodyElement.test.ts+9 −7 modified@@ -10,7 +10,9 @@ describe('HTMLBodyElement', () => { let element: HTMLBodyElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElement('body'); }); @@ -44,20 +46,20 @@ describe('HTMLBodyElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/html-canvas-element/HTMLCanvasElement.test.ts+9 −7 modified@@ -16,7 +16,9 @@ describe('HTMLCanvasElement', () => { let element: HTMLCanvasElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElement('canvas'); }); @@ -37,20 +39,20 @@ describe('HTMLCanvasElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/html-details-element/HTMLDetailsElement.test.ts+9 −7 modified@@ -11,7 +11,9 @@ describe('HTMLDetailsElement', () => { let element: HTMLDetailsElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElement('details'); }); @@ -26,20 +28,20 @@ describe('HTMLDetailsElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/html-dialog-element/HTMLDialogElement.test.ts+9 −7 modified@@ -11,7 +11,9 @@ describe('HTMLDialogElement', () => { let element: HTMLDialogElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLDialogElement>document.createElement('dialog'); }); @@ -26,20 +28,20 @@ describe('HTMLDialogElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/html-element/HTMLElement.test.ts+12 −10 modified@@ -17,7 +17,9 @@ describe('HTMLElement', () => { let element: HTMLElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLElement>document.createElement('div'); }); @@ -91,20 +93,20 @@ describe('HTMLElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -129,7 +131,7 @@ describe('HTMLElement', () => { describe(`get ${property}()`, () => { it('Returns "0".', () => { const div = document.createElement('div'); - expect(div[property]).toBe(0); + expect((<any>div)[property]).toBe(0); }); }); } @@ -619,14 +621,14 @@ describe('HTMLElement', () => { it(`Returns the attribute "${property}".`, () => { const div = document.createElement('div'); div.setAttribute(property, 'value'); - expect(div[property]).toBe('value'); + expect((<any>div)[property]).toBe('value'); }); }); describe(`set ${property}()`, () => { it(`Sets the attribute "${property}".`, () => { const div = document.createElement('div'); - div[property] = 'value'; + (<any>div)[property] = 'value'; expect(div.getAttribute(property)).toBe('value'); }); });
packages/happy-dom/test/nodes/html-iframe-element/HTMLIFrameElement.test.ts+23 −15 modified@@ -19,7 +19,9 @@ describe('HTMLIFrameElement', () => { let element: HTMLIFrameElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLIFrameElement>document.createElement('iframe'); }); @@ -38,20 +40,20 @@ describe('HTMLIFrameElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -60,13 +62,13 @@ describe('HTMLIFrameElement', () => { describe(`get ${property}()`, () => { it(`Returns the "${property}" attribute.`, () => { element.setAttribute(property, 'value'); - expect(element[property]).toBe('value'); + expect((<any>element)[property]).toBe('value'); }); }); describe(`set ${property}()`, () => { it(`Sets the attribute "${property}".`, () => { - element[property] = 'value'; + (<any>element)[property] = 'value'; expect(element.getAttribute(property)).toBe('value'); }); }); @@ -382,8 +384,10 @@ describe('HTMLIFrameElement', () => { const responseHTML = '<html><head></head><body>Test</body></html>'; let fetchedURL: string | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function () { - fetchedURL = <string>this.request.url; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + fetchedURL = <string>(<any>this).request.url; return new Promise((resolve) => { setTimeout(() => { resolve(<Response>(<unknown>{ @@ -418,8 +422,10 @@ describe('HTMLIFrameElement', () => { const responseHTML = '<html><head></head><body>Test</body></html>'; let fetchedURL: string | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function () { - fetchedURL = <string>this.request.url; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + fetchedURL = <string>(<any>this).request.url; return Promise.resolve(<Response>(<unknown>{ text: () => Promise.resolve(responseHTML), ok: true, @@ -516,8 +522,10 @@ describe('HTMLIFrameElement', () => { page.mainFrame.url = documentOrigin; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - fetchedURL = <string>this.request.url; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + fetchedURL = <string>(<any>this).request.url; return new Promise((resolve) => { setTimeout(() => { resolve(<Response>(<unknown>{
packages/happy-dom/test/nodes/html-input-element/HTMLInputElement.test.ts+32 −30 modified@@ -21,7 +21,9 @@ describe('HTMLInputElement', () => { let element: HTMLInputElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLInputElement>document.createElement('input'); }); @@ -36,20 +38,20 @@ describe('HTMLInputElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -751,15 +753,15 @@ describe('HTMLInputElement', () => { for (const property of ['disabled', 'autofocus', 'required', 'multiple', 'readOnly']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(false); + expect((<any>element)[property]).toBe(false); element.setAttribute(property, ''); - expect(element[property]).toBe(true); + expect((<any>element)[property]).toBe(true); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = true; + (<any>element)[property] = true; expect(element.getAttribute(property)).toBe(''); }); }); @@ -781,15 +783,15 @@ describe('HTMLInputElement', () => { ]) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(''); + expect((<any>element)[property]).toBe(''); element.setAttribute(property, 'value'); - expect(element[property]).toBe('value'); + expect((<any>element)[property]).toBe('value'); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = 'value'; + (<any>element)[property] = 'value'; expect(element.getAttribute(property)).toBe('value'); }); }); @@ -798,18 +800,18 @@ describe('HTMLInputElement', () => { for (const property of ['height', 'width']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(0); - element[property] = 20; - expect(element[property]).toBe(20); + expect((<any>element)[property]).toBe(0); + (<any>element)[property] = 20; + expect((<any>element)[property]).toBe(20); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { element.setAttribute(property, '50'); - expect(element[property]).toBe(0); - element[property] = 50; - expect(element[property]).toBe(50); + expect((<any>element)[property]).toBe(0); + (<any>element)[property] = 50; + expect((<any>element)[property]).toBe(50); expect(element.getAttribute(property)).toBe('50'); }); }); @@ -818,16 +820,16 @@ describe('HTMLInputElement', () => { for (const property of ['minLength', 'maxLength']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(-1); + expect((<any>element)[property]).toBe(-1); element.setAttribute(property, '50'); - expect(element[property]).toBe(50); + expect((<any>element)[property]).toBe(50); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = 50; - expect(element[property]).toBe(50); + (<any>element)[property] = 50; + expect((<any>element)[property]).toBe(50); expect(element.getAttribute(property)).toBe('50'); }); }); @@ -1243,43 +1245,43 @@ describe('HTMLInputElement', () => { it('Returns "true" if the field is "disabled".', () => { element.required = true; element.disabled = true; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "true" if the field is "readOnly".', () => { element.required = true; element.readOnly = true; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "true" if the field type is "hidden".', () => { element.required = true; element.type = 'hidden'; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "true" if the field type is "reset".', () => { element.required = true; element.type = 'reset'; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "true" if the field type is "button".', () => { element.required = true; element.type = 'button'; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "false" if invalid.', () => { element.required = true; - expect(element[method]()).toBe(false); + expect((<any>element)[method]()).toBe(false); }); it('Triggers an "invalid" event when invalid.', () => { element.required = true; let dispatchedEvent: Event | null = null; element.addEventListener('invalid', (event: Event) => (dispatchedEvent = event)); - element[method](); + (<any>element)[method](); expect((<Event>(<unknown>dispatchedEvent)).type).toBe('invalid'); }); });
packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts+32 −18 modified@@ -13,7 +13,9 @@ describe('HTMLLinkElement', () => { let document: Document; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; }); @@ -33,21 +35,21 @@ describe('HTMLLinkElement', () => { it('Returns the event listener.', () => { const element = document.createElement('link'); element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { const element = document.createElement('link'); - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -66,14 +68,14 @@ describe('HTMLLinkElement', () => { it(`Returns the "${property}" attribute.`, () => { const element = document.createElement('link'); element.setAttribute(property, 'test'); - expect(element[property]).toBe('test'); + expect((<any>element)[property]).toBe('test'); }); }); describe(`set ${property}()`, () => { it(`Sets the attribute "${property}".`, () => { const element = document.createElement('link'); - element[property] = 'test'; + (<any>element)[property] = 'test'; expect(element.getAttribute(property)).toBe('test'); }); }); @@ -136,9 +138,10 @@ describe('HTMLLinkElement', () => { let loadEventCurrentTarget: EventTarget | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function ( + this: ResourceFetch, url: string | URL ) { - loadedWindow = this.window; + loadedWindow = (<any>this).window; loadedURL = <string>url; return { content: css, virtualServerFile: null }; }); @@ -158,8 +161,8 @@ describe('HTMLLinkElement', () => { expect(loadedWindow).toBe(window); expect(loadedURL).toBe('https://localhost:8080/test/path/file.css'); - expect(element.sheet.cssRules.length).toBe(1); - expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); + expect(element.sheet!.cssRules.length).toBe(1); + expect(element.sheet!.cssRules[0].cssText).toBe('div { background: red; }'); expect((<Event>(<unknown>loadEvent)).target).toBe(element); expect(loadEventTarget).toBe(element); expect(loadEventCurrentTarget).toBe(element); @@ -199,9 +202,10 @@ describe('HTMLLinkElement', () => { let loadedURL: string | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function ( + this: ResourceFetch, url: string | URL ) { - loadedWindow = this.window; + loadedWindow = (<any>this).window; loadedURL = <string>url; return { content: css, virtualServerFile: null }; }); @@ -225,9 +229,10 @@ describe('HTMLLinkElement', () => { let loadedURL: string | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function ( + this: ResourceFetch, url: string | URL ) { - loadedWindow = this.window; + loadedWindow = (<any>this).window; loadedURL = <string>url; return { content: css, virtualServerFile: null }; }); @@ -246,8 +251,8 @@ describe('HTMLLinkElement', () => { expect(loadedWindow).toBe(window); expect(loadedURL).toBe('https://localhost:8080/test/path/file.css'); - expect(element.sheet.cssRules.length).toBe(1); - expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); + expect(element.sheet!.cssRules.length).toBe(1); + expect(element.sheet!.cssRules[0].cssText).toBe('div { background: red; }'); expect((<Event>(<unknown>loadEvent)).target).toBe(element); expect(loadEventTarget).toBe(element); expect(loadEventCurrentTarget).toBe(element); @@ -286,9 +291,10 @@ describe('HTMLLinkElement', () => { let loadedURL: string | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function ( + this: ResourceFetch, url: string | URL ) { - loadedWindow = this.window; + loadedWindow = (<any>this).window; loadedURL = <string>url; return { content: css, virtualServerFile: null }; }); @@ -353,6 +359,8 @@ describe('HTMLLinkElement', () => { const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, errorCapture: BrowserErrorCaptureEnum.disabled, fetch: { interceptor: { @@ -403,7 +411,7 @@ describe('HTMLLinkElement', () => { 'https://localhost:8080/base/js/utilities/stringUtility.js' ]); - expect(window['moduleLoadOrder']).toEqual([ + expect((<any>window)['moduleLoadOrder']).toEqual([ 'apostrophWrapper.js', 'StringUtilityClass.js', 'stringUtility.js', @@ -426,6 +434,8 @@ describe('HTMLLinkElement', () => { const window = new Window({ url: 'https://localhost:8080/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, errorCapture: BrowserErrorCaptureEnum.disabled, fetch: { interceptor: { @@ -505,6 +515,8 @@ describe('HTMLLinkElement', () => { const window = new Window({ url: 'https://localhost:8080/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, errorCapture: BrowserErrorCaptureEnum.disabled, fetch: { interceptor: { @@ -585,6 +597,8 @@ describe('HTMLLinkElement', () => { const window = new Window({ url: 'https://localhost:8080/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, errorCapture: BrowserErrorCaptureEnum.disabled, fetch: { interceptor: {
packages/happy-dom/test/nodes/html-media-element/HTMLMediaElement.test.ts+22 −20 modified@@ -21,7 +21,9 @@ describe('HTMLMediaElement', () => { let element: HTMLMediaElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLMediaElement>document.createElement('audio'); }); @@ -41,7 +43,7 @@ describe('HTMLMediaElement', () => { expect(audio.localName).toBe('audio'); expect(audio.namespaceURI).toBe(NamespaceURI.html); - expect(window['Video']).toBe(undefined); + expect((<any>window)['Video']).toBe(undefined); }); }); @@ -79,20 +81,20 @@ describe('HTMLMediaElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -174,21 +176,21 @@ describe('HTMLMediaElement', () => { for (const property of ['autoplay', 'controls', 'loop']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(false); + expect((<any>element)[property]).toBe(false); element.setAttribute(property, ''); - expect(element[property]).toBe(true); + expect((<any>element)[property]).toBe(true); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = true; + (<any>element)[property] = true; expect(element.getAttribute(property)).toBe(''); }); it('Remove attribute value.', () => { element.setAttribute(property, ''); - element[property] = false; + (<any>element)[property] = false; expect(element.getAttribute(property)).toBeNull(); }); }); @@ -392,15 +394,15 @@ describe('HTMLMediaElement', () => { expect(element.textTracks.length).toBe(4); - expect(element.textTracks[2].id).toBe('track1'); - expect(element.textTracks[2].kind).toBe('captions'); - expect(element.textTracks[2].label).toBe('English'); - expect(element.textTracks[2].language).toBe('en'); + expect(element.textTracks[2]!.id).toBe('track1'); + expect(element.textTracks[2]!.kind).toBe('captions'); + expect(element.textTracks[2]!.label).toBe('English'); + expect(element.textTracks[2]!.language).toBe('en'); - expect(element.textTracks[3].id).toBe('track2'); - expect(element.textTracks[3].kind).toBe('metadata'); - expect(element.textTracks[3].label).toBe('French'); - expect(element.textTracks[3].language).toBe('fr'); + expect(element.textTracks[3]!.id).toBe('track2'); + expect(element.textTracks[3]!.kind).toBe('metadata'); + expect(element.textTracks[3]!.label).toBe('French'); + expect(element.textTracks[3]!.language).toBe('fr'); }); });
packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts+358 −68 modified@@ -18,7 +18,9 @@ describe('HTMLScriptElement', () => { let document: Document; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; }); @@ -38,21 +40,21 @@ describe('HTMLScriptElement', () => { it('Returns the event listener.', () => { const element = document.createElement('script'); element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { const element = document.createElement('script'); - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -62,14 +64,14 @@ describe('HTMLScriptElement', () => { it(`Returns the "${property}" attribute.`, () => { const element = document.createElement('script'); element.setAttribute(property, 'test'); - expect(element[property]).toBe('test'); + expect((<any>element)[property]).toBe('test'); }); }); describe(`set ${property}()`, () => { it(`Sets the attribute "${property}".`, () => { const element = document.createElement('script'); - element[property] = 'test'; + (<any>element)[property] = 'test'; expect(element.getAttribute(property)).toBe('test'); }); }); @@ -80,14 +82,14 @@ describe('HTMLScriptElement', () => { it(`Returns "true" if the "${property}" attribute is defined.`, () => { const element = document.createElement('script'); element.setAttribute(property, ''); - expect(element[property]).toBe(true); + expect((<any>element)[property]).toBe(true); }); }); describe(`set ${property}()`, () => { it(`Sets the "${property}" attribute to an empty string if set to "true".`, () => { const element = document.createElement('script'); - element[property] = true; + (<any>element)[property] = true; expect(element.getAttribute(property)).toBe(''); }); }); @@ -211,13 +213,62 @@ describe('HTMLScriptElement', () => { }); describe('set src()', () => { + it('Does not load external script when JavaScript is disabled.', async () => { + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + + vi.spyOn(Fetch.prototype, 'send').mockImplementation( + async () => + <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true, + status: 200 + } + ); + + document.body.appendChild(element); + + element.async = true; + element.src = 'https://localhost:8080/path/to/script.js'; + + await window.happyDOM?.waitUntilComplete(); + + expect((<any>window)['test']).toBe(undefined); + }); + + it('Does not load external module when JavaScript is disabled.', async () => { + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + + element.type = 'module'; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation( + async () => + <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true, + status: 200 + } + ); + + document.body.appendChild(element); + + element.src = 'https://localhost:8080/path/to/script.js'; + + await window.happyDOM?.waitUntilComplete(); + + expect((<any>window)['test']).toBe(undefined); + }); + it('Sets the attribute "src".', () => { const element = document.createElement('script'); element.src = 'test'; expect(element.getAttribute('src')).toBe('test'); }); - it('Loads and evaluates an external script when the attribute "src" is set and the element is connected to DOM.', async () => { + it('Loads and evaluates an external script when the property "src" is set and the element is connected to DOM.', async () => { const element = document.createElement('script'); vi.spyOn(Fetch.prototype, 'send').mockImplementation( @@ -236,7 +287,7 @@ describe('HTMLScriptElement', () => { await window.happyDOM?.waitUntilComplete(); - expect(window['test']).toBe('test'); + expect((<any>window)['test']).toBe('test'); }); it('Does not evaluate script if the element is not connected to DOM.', async () => { @@ -256,7 +307,52 @@ describe('HTMLScriptElement', () => { await window.happyDOM?.waitUntilComplete(); - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); + }); + + it('Loads and evaluates an external script when the attribute "src" is set and the element is connected to DOM.', async () => { + const element = document.createElement('script'); + + vi.spyOn(Fetch.prototype, 'send').mockImplementation( + async () => + <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true, + status: 200 + } + ); + + document.body.appendChild(element); + + element.async = true; + element.setAttribute('src', 'https://localhost:8080/path/to/script.js'); + + await window.happyDOM?.waitUntilComplete(); + + expect((<any>window)['test']).toBe('test'); + }); + + it('Loads and evaluates an external module script when the attribute "src" is set and the element is connected to DOM.', async () => { + const element = document.createElement('script'); + + element.type = 'module'; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation( + async () => + <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true, + status: 200 + } + ); + + document.body.appendChild(element); + + element.setAttribute('src', 'https://localhost:8080/path/to/script.js'); + + await window.happyDOM?.waitUntilComplete(); + + expect((<any>window)['test']).toBe('test'); }); }); @@ -278,12 +374,65 @@ describe('HTMLScriptElement', () => { }); describe('set isConnected()', () => { + it('Does not execute script when Javascript is disabled.', () => { + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + element.text = 'globalThis.test = "test";globalThis.currentScript = document.currentScript;'; + document.body.appendChild(element); + expect((<any>window)['test']).toBe(undefined); + expect((<any>window)['currentScript']).toBe(undefined); + }); + + it('Does not load script when Javascript is disabled.', () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function (this: Fetch) { + return <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true + }; + }); + + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + element.src = 'https://localhost:8080/path/to/script.js'; + document.body.appendChild(element); + expect((<any>window)['test']).toBe(undefined); + }); + + it('Does not execute module script when Javascript is disabled.', () => { + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + element.type = 'module'; + element.text = 'globalThis.test = "test";'; + document.body.appendChild(element); + expect((<any>window)['test']).toBe(undefined); + }); + + it('Does not load module script when Javascript is disabled.', () => { + vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function (this: Fetch) { + return <Response>{ + text: async () => 'globalThis.test = "test";', + ok: true + }; + }); + + const window = new Window(); + const document = window.document; + const element = document.createElement('script'); + element.type = 'module'; + element.src = 'https://localhost:8080/path/to/script.js'; + document.body.appendChild(element); + expect((<any>window)['test']).toBe(undefined); + }); + it('Evaluates the text content as code when appended to an element that is connected to the document.', () => { const element = document.createElement('script'); element.text = 'globalThis.test = "test";globalThis.currentScript = document.currentScript;'; document.body.appendChild(element); - expect(window['test']).toBe('test'); - expect(window['currentScript']).toBe(element); + expect((<any>window)['test']).toBe('test'); + expect((<any>window)['currentScript']).toBe(element); }); it('Evaluates the text content as code when inserted before an element that is connected to the document.', () => { @@ -299,8 +448,8 @@ describe('HTMLScriptElement', () => { document.body.insertBefore(div1, div2); document.body.appendChild(element); - expect(window['test']).toBe('test'); - expect(window['currentScript']).toBe(element); + expect((<any>window)['test']).toBe('test'); + expect((<any>window)['currentScript']).toBe(element); }); for (const attribute of [ @@ -313,8 +462,8 @@ describe('HTMLScriptElement', () => { let loadEventTarget: EventTarget | null = null; let loadEventCurrentTarget: EventTarget | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function () { - fetchedURL = this.request.url; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function (this: Fetch) { + fetchedURL = (<any>this).request.url; return <Response>{ text: async () => 'globalThis.test = "test";globalThis.currentScript = document.currentScript;', @@ -339,8 +488,8 @@ describe('HTMLScriptElement', () => { expect(loadEventTarget).toBe(script); expect(loadEventCurrentTarget).toBe(script); expect(fetchedURL).toBe('https://localhost:8080/path/to/script.js'); - expect(window['test']).toBe('test'); - expect(window['currentScript']).toBe(script); + expect((<any>window)['test']).toBe('test'); + expect((<any>window)['currentScript']).toBe(script); }); it(`Triggers error event when loading external script asynchronously when the attribute "${attribute.name}" is set to "${attribute.value}".`, async () => { @@ -379,15 +528,24 @@ describe('HTMLScriptElement', () => { } it('Loads external script synchronously with relative URL.', async () => { - const window = new Window({ url: 'https://localhost:8080/base/' }); + const window = new Window({ + url: 'https://localhost:8080/base/', + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } + }); let fetchedWindow: BrowserWindow | null = null; let fetchedURL: string | null = null; let loadEvent: Event | null = null; let loadEventTarget: EventTarget | null = null; let loadEventCurrentTarget: EventTarget | null = null; - vi.spyOn(ResourceFetch.prototype, 'fetchSync').mockImplementation(function (url: string) { - fetchedWindow = this.window; + vi.spyOn(ResourceFetch.prototype, 'fetchSync').mockImplementation(function ( + this: ResourceFetch, + url: string + ) { + fetchedWindow = (<any>this).window; fetchedURL = url; return { content: 'globalThis.test = "test";globalThis.currentScript = document.currentScript;', @@ -410,12 +568,18 @@ describe('HTMLScriptElement', () => { expect(loadEventCurrentTarget).toBe(script); expect(fetchedWindow).toBe(window); expect(fetchedURL).toBe('https://localhost:8080/base/path/to/script.js'); - expect(window['test']).toBe('test'); - expect(window['currentScript']).toBe(script); + expect((<any>window)['test']).toBe('test'); + expect((<any>window)['currentScript']).toBe(script); }); it('Triggers error event when loading external script synchronously with relative URL.', () => { - const window = new Window({ url: 'https://localhost:8080/base/' }); + const window = new Window({ + url: 'https://localhost:8080/base/', + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } + }); const thrownError = new Error('error'); let errorEvent: Event | null = null; @@ -451,27 +615,27 @@ describe('HTMLScriptElement', () => { const div = document.createElement('div'); div.innerHTML = '<script>globalThis.test = "test";</script>'; document.body.appendChild(div); - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); }); it('Does not evaluate code when added as outerHTML.', () => { const div = document.createElement('div'); document.body.appendChild(div); div.outerHTML = '<script>globalThis.test = "test";</script>'; - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); }); it('Does not evaluate code if the element is not connected to DOM.', () => { const div = document.createElement('div'); const element = document.createElement('script'); element.text = 'window.test = "test";'; div.appendChild(element); - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); }); it('Evaluates the text content as code when using document.write().', () => { document.write('<script>globalThis.test = "test";</script>'); - expect(window['test']).toBe('test'); + expect((<any>window)['test']).toBe('test'); }); it("Doesn't evaluate the text content as code when using DOMParser.parseFromString().", () => { @@ -480,9 +644,9 @@ describe('HTMLScriptElement', () => { '<script>globalThis.test = "test";</script>', 'text/html' ); - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); document.body.appendChild(result); - expect(window['test']).toBe(undefined); + expect((<any>window)['test']).toBe(undefined); }); it('Loads and evaluates an external script when "src" attribute has been set, but does not evaluate text content.', () => { @@ -498,8 +662,8 @@ describe('HTMLScriptElement', () => { document.body.appendChild(element); - expect(window['testFetch']).toBe('test'); - expect(window['testContent']).toBe(undefined); + expect((<any>window)['testFetch']).toBe('test'); + expect((<any>window)['testContent']).toBe(undefined); }); it('Does not load external scripts when "src" attribute has been set if the element is not connected to DOM.', () => { @@ -513,13 +677,17 @@ describe('HTMLScriptElement', () => { element.src = 'https://localhost:8080/path/to/script.js'; element.text = 'globalThis.test = "test";'; - expect(window['testFetch']).toBe(undefined); - expect(window['testContent']).toBe(undefined); + expect((<any>window)['testFetch']).toBe(undefined); + expect((<any>window)['testContent']).toBe(undefined); }); - it('Triggers an error event when attempting to perform an asynchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + it('Triggers an error event when attempting to perform an asynchronous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { window = new Window({ - settings: { disableJavaScriptFileLoading: true } + settings: { + disableJavaScriptFileLoading: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -545,9 +713,14 @@ describe('HTMLScriptElement', () => { ).toBe(true); }); - it('Triggers a load event when attempting to perform an asynchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" and "handleDisabledFileLoadingAsSuccess" is set to "true".', () => { + it('Triggers a load event when attempting to perform an asynchronous request and the Happy DOM setting "disableJavaScriptFileLoading" and "handleDisabledFileLoadingAsSuccess" is set to "true".', () => { window = new Window({ - settings: { disableJavaScriptFileLoading: true, handleDisabledFileLoadingAsSuccess: true } + settings: { + disableJavaScriptFileLoading: true, + handleDisabledFileLoadingAsSuccess: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -565,9 +738,13 @@ describe('HTMLScriptElement', () => { expect((<Event>(<unknown>loadEvent)).type).toBe('load'); }); - it('Triggers an error event when attempting to perform a synchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + it('Triggers an error event when attempting to perform a synchronous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { window = new Window({ - settings: { disableJavaScriptFileLoading: true } + settings: { + disableJavaScriptFileLoading: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -592,9 +769,13 @@ describe('HTMLScriptElement', () => { ).toBe(true); }); - it('Triggers an error event when attempting to perform an asynchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + it('Triggers an error event when attempting to perform an asynchronous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { window = new Window({ - settings: { disableJavaScriptFileLoading: true } + settings: { + disableJavaScriptFileLoading: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -620,9 +801,13 @@ describe('HTMLScriptElement', () => { ).toBe(true); }); - it('Triggers an error event when attempting to perform a synchrounous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { + it('Triggers an error event when attempting to perform a synchronous request and the Happy DOM setting "disableJavaScriptFileLoading" is set to "true".', () => { window = new Window({ - settings: { disableJavaScriptFileLoading: true } + settings: { + disableJavaScriptFileLoading: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -646,7 +831,7 @@ describe('HTMLScriptElement', () => { ).toBe(true); }); - it('Triggers an error event on Window when attempting to perform an asynchrounous request containing invalid JavaScript.', async () => { + it('Triggers an error event on Window when attempting to perform an asynchronous request containing invalid JavaScript.', async () => { let errorEvent: ErrorEvent | null = null; vi.spyOn(Fetch.prototype, 'send').mockImplementation( @@ -672,12 +857,16 @@ describe('HTMLScriptElement', () => { ); const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || ''; - expect(consoleOutput.startsWith('SyntaxError: Invalid regular expression: missing /')).toBe( - true - ); + expect( + consoleOutput.startsWith(`https://localhost:8080/base/path/to/script/:1 +globalThis.test = /; + ^ + +SyntaxError: Invalid regular expression: missing /`) + ).toBe(true); }); - it('Triggers an error event on Window when attempting to perform a synchrounous request containing invalid JavaScript.', () => { + it('Triggers an error event on Window when attempting to perform a synchronous request containing invalid JavaScript.', () => { let errorEvent: ErrorEvent | null = null; vi.spyOn(ResourceFetch.prototype, 'fetchSync').mockImplementation(() => ({ @@ -697,9 +886,13 @@ describe('HTMLScriptElement', () => { ); const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || ''; - expect(consoleOutput.startsWith('SyntaxError: Invalid regular expression: missing /')).toBe( - true - ); + expect( + consoleOutput.startsWith(`https://localhost:8080/base/path/to/script/:1 +globalThis.test = /; + ^ + +SyntaxError: Invalid regular expression: missing /`) + ).toBe(true); }); it('Triggers an error event on Window when appending an element that contains invalid Javascript.', () => { @@ -717,14 +910,22 @@ describe('HTMLScriptElement', () => { ); const consoleOutput = window.happyDOM?.virtualConsolePrinter.readAsString() || ''; - expect(consoleOutput.startsWith('SyntaxError: Invalid regular expression: missing /')).toBe( - true - ); + expect( + consoleOutput.startsWith(`about:blank:1 +globalThis.test = /; + ^ + +SyntaxError: Invalid regular expression: missing /`) + ).toBe(true); }); it('Throws an exception when appending an element that contains invalid Javascript and the Happy DOM setting "disableErrorCapturing" is set to true.', () => { window = new Window({ - settings: { disableErrorCapturing: true } + settings: { + disableErrorCapturing: true, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -734,12 +935,16 @@ describe('HTMLScriptElement', () => { expect(() => { document.body.appendChild(element); - }).toThrow(new TypeError('Invalid regular expression: missing /')); + }).toThrow(new SyntaxError('Invalid regular expression: missing /')); }); it('Throws an exception when appending an element that contains invalid Javascript and the Happy DOM setting "errorCapture" is set to "disabled".', () => { window = new Window({ - settings: { errorCapture: BrowserErrorCaptureEnum.disabled } + settings: { + errorCapture: BrowserErrorCaptureEnum.disabled, + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } }); document = window.document; @@ -749,14 +954,16 @@ describe('HTMLScriptElement', () => { expect(() => { document.body.appendChild(element); - }).toThrow(new TypeError('Invalid regular expression: missing /')); + }).toThrow(new SyntaxError('Invalid regular expression: missing /')); }); - it('Handles loading of a modules.', async () => { + it('Handles loading of a modules with "src" attribute.', async () => { const requests: string[] = []; const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { interceptor: { beforeAsyncRequest: async ({ request }) => { @@ -780,7 +987,7 @@ describe('HTMLScriptElement', () => { script.src = 'https://localhost:8080/base/js/TestModuleElement.js'; script.type = 'module'; script.addEventListener('load', () => { - modulesLoadedAfterLoadEvent = window['moduleLoadOrder'].slice(); + modulesLoadedAfterLoadEvent = (<any>window)['moduleLoadOrder'].slice(); }); document.body.appendChild(script); @@ -803,7 +1010,7 @@ describe('HTMLScriptElement', () => { 'https://localhost:8080/base/js/utilities/lazyload.js' ]); - expect(window['moduleLoadOrder']).toEqual([ + expect((<any>window)['moduleLoadOrder']).toEqual([ 'apostrophWrapper.js', 'StringUtilityClass.js', 'stringUtility.js', @@ -836,11 +1043,88 @@ describe('HTMLScriptElement', () => { ).toBe('red'); }); + it('Handles loading of a modules by code.', async () => { + const requests: string[] = []; + const window = new Window({ + url: 'https://localhost:8080/base/', + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, + fetch: { + interceptor: { + beforeAsyncRequest: async ({ request }) => { + requests.push(request.url); + } + }, + virtualServers: [ + { + url: 'https://localhost:8080/base/js/', + directory: './test/nodes/html-script-element/modules/' + } + ] + } + }, + console + }); + const document = window.document; + const script = document.createElement('script'); + + script.type = 'module'; + script.textContent = `import('./js/TestModuleElement.js');`; + + document.body.appendChild(script); + + await window.happyDOM?.waitUntilComplete(); + + const testModule = document.createElement('test-module'); + + document.body.appendChild(testModule); + + await window.happyDOM?.waitUntilComplete(); + + expect(requests).toEqual([ + 'https://localhost:8080/base/js/TestModuleElement.js', + 'https://localhost:8080/base/js/utilities/StringUtilityClass.js', + 'https://localhost:8080/base/js/utilities/stringUtility.js', + 'https://localhost:8080/base/js/json/data.json', + 'https://localhost:8080/base/js/css/style.css', + 'https://localhost:8080/base/js/utilities/apostrophWrapper.js', + 'https://localhost:8080/base/js/utilities/lazyload.js' + ]); + + expect((<any>window)['moduleLoadOrder']).toEqual([ + 'apostrophWrapper.js', + 'StringUtilityClass.js', + 'stringUtility.js', + 'TestModuleElement.js', + 'lazyload.js' + ]); + + expect(testModule.shadowRoot?.innerHTML).toBe(`<div> + Expect lower case: "value" + Expect upper case: "VALUE" + Expect lower case. "value" + Expect trimmed lower case: "value" + Import URL: https://localhost:8080/base/js/TestModuleElement.js + Resolved URL: https://localhost:8080/base/js/Resolved.js + </div><div>Lazy-loaded module: true</div>`); + + expect(testModule.shadowRoot?.adoptedStyleSheets[0].cssRules[0].cssText).toBe( + 'div { background: red; }' + ); + expect( + window.getComputedStyle(<HTMLElement>testModule.shadowRoot?.querySelector('div')) + .backgroundColor + ).toBe('red'); + }); + it('Handles modules using an import map.', async () => { const requests: string[] = []; const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { interceptor: { beforeAsyncRequest: async ({ request }) => { @@ -915,7 +1199,7 @@ describe('HTMLScriptElement', () => { 'https://localhost:8080/base/js/utilities/lazyload.js' ]); - expect(window['moduleLoadOrder']).toEqual([ + expect((<any>window)['moduleLoadOrder']).toEqual([ 'apostrophWrapper.js', 'StringUtilityClass.js', 'stringUtility.js', @@ -944,6 +1228,8 @@ describe('HTMLScriptElement', () => { const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ { @@ -983,6 +1269,8 @@ DOMException: Failed to perform request to "https://localhost:8080/base/js/utili const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ { @@ -1024,6 +1312,8 @@ DOMException: Failed to perform request to "https://localhost:8080/base/js/utili const window = new Window({ url: 'https://localhost:8080/base/', settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true, fetch: { virtualServers: [ { @@ -1053,7 +1343,7 @@ DOMException: Failed to perform request to "https://localhost:8080/base/js/utili expect((<ErrorEvent>(<unknown>errorEvent)).type).toBe('error'); expect((<ErrorEvent>(<unknown>errorEvent)).bubbles).toBe(false); expect( - /^ReferenceError: notFound is not defined\n at eval \((.+?)\/nodes\/html-script-element\/modules-with-evaluation-error\/utilities\/stringUtility.js:10:14\)/.test( + /^ReferenceError: notFound is not defined\n at (.+?)\/nodes\/html-script-element\/modules-with-evaluation-error\/utilities\/stringUtility.js:10:14/.test( window.happyDOM?.virtualConsolePrinter.readAsString() ) ).toBe(true);
packages/happy-dom/test/nodes/html-select-element/HTMLSelectElement.test.ts+18 −16 modified@@ -14,7 +14,9 @@ describe('HTMLSelectElement', () => { let element: HTMLSelectElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLSelectElement>document.createElement('select'); }); @@ -37,21 +39,21 @@ describe('HTMLSelectElement', () => { it('Returns the event listener.', () => { const element = document.createElement('script'); element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { const element = document.createElement('script'); - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -147,15 +149,15 @@ describe('HTMLSelectElement', () => { for (const property of ['disabled', 'autofocus', 'required', 'multiple']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(false); + expect((<any>element)[property]).toBe(false); element.setAttribute(property, ''); - expect(element[property]).toBe(true); + expect((<any>element)[property]).toBe(true); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = true; + (<any>element)[property] = true; expect(element.getAttribute(property)).toBe(''); }); }); @@ -268,14 +270,14 @@ describe('HTMLSelectElement', () => { describe('get symbol()', () => { it('returns existing symbol properties', () => { const symbol = Symbol('test'); - element[symbol] = 'test'; - expect(element[symbol]).toBe('test'); + (<any>element)[symbol] = 'test'; + expect((<any>element)[symbol]).toBe('test'); }); it('ignores missing symbol properties', () => { const symbol = Symbol('other-test'); - expect(element[symbol]).toBe(undefined); + expect((<any>element)[symbol]).toBe(undefined); // https://github.com/capricorn86/happy-dom/issues/1526 expect(symbol in element).toBe(false); @@ -799,7 +801,7 @@ describe('HTMLSelectElement', () => { element.required = true; element.disabled = true; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "false" if invalid.', () => { @@ -809,7 +811,7 @@ describe('HTMLSelectElement', () => { element.required = true; - expect(element[method]()).toBe(false); + expect((<any>element)[method]()).toBe(false); }); it('Triggers an "invalid" event when invalid.', () => { @@ -822,7 +824,7 @@ describe('HTMLSelectElement', () => { let dispatchedEvent: Event | null = null; element.addEventListener('invalid', (event: Event) => (dispatchedEvent = event)); - element[method](); + (<any>element)[method](); expect((<Event>(<unknown>dispatchedEvent)).type).toBe('invalid'); });
packages/happy-dom/test/nodes/html-slot-element/HTMLSlotElement.test.ts+9 −7 modified@@ -15,7 +15,9 @@ describe('HTMLSlotElement', () => { let customElementWithSlot: CustomElementWithSlot; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; window.customElements.define('custom-element-with-named-slots', CustomElementWithNamedSlots); @@ -37,21 +39,21 @@ describe('HTMLSlotElement', () => { it('Returns the event listener.', () => { const element = <HTMLSlotElement>customElementWithSlot.shadowRoot?.querySelector('slot'); element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { const element = <HTMLSlotElement>customElementWithSlot.shadowRoot?.querySelector('slot'); - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/html-text-area-element/HTMLTextAreaElement.test.ts+23 −21 modified@@ -14,7 +14,9 @@ describe('HTMLTextAreaElement', () => { let element: HTMLTextAreaElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <HTMLTextAreaElement>document.createElement('textarea'); }); @@ -29,20 +31,20 @@ describe('HTMLTextAreaElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -169,15 +171,15 @@ describe('HTMLTextAreaElement', () => { for (const property of ['disabled', 'autofocus', 'required', 'readOnly']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(false); + expect((<any>element)[property]).toBe(false); element.setAttribute(property, ''); - expect(element[property]).toBe(true); + expect((<any>element)[property]).toBe(true); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = true; + (<any>element)[property] = true; expect(element.getAttribute(property)).toBe(''); }); }); @@ -186,15 +188,15 @@ describe('HTMLTextAreaElement', () => { for (const property of ['name', 'autocomplete', 'cols', 'rows', 'placeholder', 'inputMode']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(''); + expect((<any>element)[property]).toBe(''); element.setAttribute(property, 'value'); - expect(element[property]).toBe('value'); + expect((<any>element)[property]).toBe('value'); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = 'value'; + (<any>element)[property] = 'value'; expect(element.getAttribute(property)).toBe('value'); }); }); @@ -203,16 +205,16 @@ describe('HTMLTextAreaElement', () => { for (const property of ['minLength', 'maxLength']) { describe(`get ${property}()`, () => { it('Returns attribute value.', () => { - expect(element[property]).toBe(-1); + expect((<any>element)[property]).toBe(-1); element.setAttribute(property, '50'); - expect(element[property]).toBe(50); + expect((<any>element)[property]).toBe(50); }); }); describe(`set ${property}()`, () => { it('Sets attribute value.', () => { - element[property] = 50; - expect(element[property]).toBe(50); + (<any>element)[property] = 50; + expect((<any>element)[property]).toBe(50); expect(element.getAttribute(property)).toBe('50'); }); }); @@ -285,25 +287,25 @@ describe('HTMLTextAreaElement', () => { it('Returns "true" if the field is "disabled".', () => { element.required = true; element.disabled = true; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "true" if the field is "readOnly".', () => { element.required = true; element.readOnly = true; - expect(element[method]()).toBe(true); + expect((<any>element)[method]()).toBe(true); }); it('Returns "false" if invalid.', () => { element.required = true; - expect(element[method]()).toBe(false); + expect((<any>element)[method]()).toBe(false); }); it('Triggers an "invalid" event when invalid.', () => { element.required = true; let triggeredEvent: Event | null = null; element.addEventListener('invalid', (event: Event) => (triggeredEvent = event)); - element[method](); + (<any>element)[method](); expect((<Event>(<unknown>triggeredEvent)).type).toBe('invalid'); }); });
packages/happy-dom/test/nodes/html-track-element/HTMLTrackElement.test.ts+9 −7 modified@@ -11,7 +11,9 @@ describe('HTMLTrackElement', () => { let element: HTMLTrackElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElement('track'); }); @@ -26,20 +28,20 @@ describe('HTMLTrackElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/svg-animation-element/SVGAnimationElement.test.ts+9 −7 modified@@ -11,7 +11,9 @@ describe('SVGAnimationElement', () => { let element: SVGAnimationElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElementNS('http://www.w3.org/2000/svg', 'animate'); }); @@ -30,20 +32,20 @@ describe('SVGAnimationElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/svg-element/SVGElement.test.ts+9 −9 modified@@ -1,7 +1,5 @@ import Window from '../../../src/window/Window.js'; import Document from '../../../src/nodes/document/Document.js'; -import SVGSVGElement from '../../../src/nodes/svg-svg-element/SVGSVGElement.js'; -import NamespaceURI from '../../../src/config/NamespaceURI.js'; import SVGElement from '../../../src/nodes/svg-element/SVGElement.js'; import HTMLElementUtility from '../../../src/nodes/html-element/HTMLElementUtility.js'; import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest'; @@ -15,7 +13,9 @@ describe('SVGElement', () => { let element: SVGElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = <SVGElement>document.createElementNS('http://www.w3.org/2000/svg', 'unknown'); }); @@ -126,20 +126,20 @@ describe('SVGElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/nodes/svg-graphics-element/SVGGraphicsElement.test.ts+39 −37 modified@@ -13,7 +13,9 @@ describe('SVGGraphicsElement', () => { let element: SVGGraphicsElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElementNS('http://www.w3.org/2000/svg', 'g'); }); @@ -32,20 +34,20 @@ describe('SVGGraphicsElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); } @@ -129,38 +131,38 @@ describe('SVGGraphicsElement', () => { element.setAttribute('transform', 'matrix(1 2 3 4 5 6) translate(10 20)'); expect(element.transform.baseVal.numberOfItems).toBe(2); - expect(element.transform.baseVal.getItem(0).type).toBe(SVGTransformTypeEnum.matrix); - expect(element.transform.baseVal.getItem(0).matrix.a).toBe(1); - expect(element.transform.baseVal.getItem(0).matrix.b).toBe(2); - expect(element.transform.baseVal.getItem(0).matrix.c).toBe(3); - expect(element.transform.baseVal.getItem(0).matrix.d).toBe(4); - expect(element.transform.baseVal.getItem(0).matrix.e).toBe(5); - expect(element.transform.baseVal.getItem(0).matrix.f).toBe(6); - - expect(element.transform.baseVal.getItem(1).type).toBe(SVGTransformTypeEnum.translate); - expect(element.transform.baseVal.getItem(1).matrix.a).toBe(1); - expect(element.transform.baseVal.getItem(1).matrix.b).toBe(0); - expect(element.transform.baseVal.getItem(1).matrix.c).toBe(0); - expect(element.transform.baseVal.getItem(1).matrix.d).toBe(1); - expect(element.transform.baseVal.getItem(1).matrix.e).toBe(10); - expect(element.transform.baseVal.getItem(1).matrix.f).toBe(20); + expect(element.transform.baseVal.getItem(0)!.type).toBe(SVGTransformTypeEnum.matrix); + expect(element.transform.baseVal.getItem(0)!.matrix.a).toBe(1); + expect(element.transform.baseVal.getItem(0)!.matrix.b).toBe(2); + expect(element.transform.baseVal.getItem(0)!.matrix.c).toBe(3); + expect(element.transform.baseVal.getItem(0)!.matrix.d).toBe(4); + expect(element.transform.baseVal.getItem(0)!.matrix.e).toBe(5); + expect(element.transform.baseVal.getItem(0)!.matrix.f).toBe(6); + + expect(element.transform.baseVal.getItem(1)!.type).toBe(SVGTransformTypeEnum.translate); + expect(element.transform.baseVal.getItem(1)!.matrix.a).toBe(1); + expect(element.transform.baseVal.getItem(1)!.matrix.b).toBe(0); + expect(element.transform.baseVal.getItem(1)!.matrix.c).toBe(0); + expect(element.transform.baseVal.getItem(1)!.matrix.d).toBe(1); + expect(element.transform.baseVal.getItem(1)!.matrix.e).toBe(10); + expect(element.transform.baseVal.getItem(1)!.matrix.f).toBe(20); expect(element.transform.animVal.numberOfItems).toBe(2); - expect(element.transform.animVal.getItem(0).type).toBe(SVGTransformTypeEnum.matrix); - expect(element.transform.animVal.getItem(0).matrix.a).toBe(1); - expect(element.transform.animVal.getItem(0).matrix.b).toBe(2); - expect(element.transform.animVal.getItem(0).matrix.c).toBe(3); - expect(element.transform.animVal.getItem(0).matrix.d).toBe(4); - expect(element.transform.animVal.getItem(0).matrix.e).toBe(5); - expect(element.transform.animVal.getItem(0).matrix.f).toBe(6); - - expect(element.transform.animVal.getItem(1).type).toBe(SVGTransformTypeEnum.translate); - expect(element.transform.animVal.getItem(1).matrix.a).toBe(1); - expect(element.transform.animVal.getItem(1).matrix.b).toBe(0); - expect(element.transform.animVal.getItem(1).matrix.c).toBe(0); - expect(element.transform.animVal.getItem(1).matrix.d).toBe(1); - expect(element.transform.animVal.getItem(1).matrix.e).toBe(10); - expect(element.transform.animVal.getItem(1).matrix.f).toBe(20); + expect(element.transform.animVal.getItem(0)!.type).toBe(SVGTransformTypeEnum.matrix); + expect(element.transform.animVal.getItem(0)!.matrix.a).toBe(1); + expect(element.transform.animVal.getItem(0)!.matrix.b).toBe(2); + expect(element.transform.animVal.getItem(0)!.matrix.c).toBe(3); + expect(element.transform.animVal.getItem(0)!.matrix.d).toBe(4); + expect(element.transform.animVal.getItem(0)!.matrix.e).toBe(5); + expect(element.transform.animVal.getItem(0)!.matrix.f).toBe(6); + + expect(element.transform.animVal.getItem(1)!.type).toBe(SVGTransformTypeEnum.translate); + expect(element.transform.animVal.getItem(1)!.matrix.a).toBe(1); + expect(element.transform.animVal.getItem(1)!.matrix.b).toBe(0); + expect(element.transform.animVal.getItem(1)!.matrix.c).toBe(0); + expect(element.transform.animVal.getItem(1)!.matrix.d).toBe(1); + expect(element.transform.animVal.getItem(1)!.matrix.e).toBe(10); + expect(element.transform.animVal.getItem(1)!.matrix.f).toBe(20); const transform = new window.SVGTransform(PropertySymbol.illegalConstructor, window);
packages/happy-dom/test/nodes/svg-svg-element/SVGSVGElement.test.ts+9 −7 modified@@ -25,7 +25,9 @@ describe('SVGSVGElement', () => { let element: SVGSVGElement; beforeEach(() => { - window = new Window(); + window = new Window({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); document = window.document; element = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); }); @@ -53,20 +55,20 @@ describe('SVGSVGElement', () => { describe(`get on${event}()`, () => { it('Returns the event listener.', () => { element.setAttribute(`on${event}`, 'window.test = 1'); - expect(element[`on${event}`]).toBeTypeOf('function'); - element[`on${event}`](new Event(event)); - expect(window['test']).toBe(1); + expect((<any>element)[`on${event}`]).toBeTypeOf('function'); + (<any>element)[`on${event}`](new Event(event)); + expect((<any>window)['test']).toBe(1); }); }); describe(`set on${event}()`, () => { it('Sets the event listener.', () => { - element[`on${event}`] = () => { - window['test'] = 1; + (<any>element)[`on${event}`] = () => { + (<any>window)['test'] = 1; }; element.dispatchEvent(new Event(event)); expect(element.getAttribute(`on${event}`)).toBe(null); - expect(window['test']).toBe(1); + expect((<any>window)['test']).toBe(1); }); }); }
packages/happy-dom/test/window/BrowserWindow.test.ts+89 −27 modified@@ -58,7 +58,9 @@ describe('BrowserWindow', () => { let document: Document; beforeEach(() => { - browser = new Browser(); + browser = new Browser({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); browserPage = browser.newPage(); browserFrame = browserPage.mainFrame; window = browserFrame.window; @@ -75,9 +77,58 @@ describe('BrowserWindow', () => { vi.restoreAllMocks(); }); + describe('constructor()', () => { + it('Outputs a warning if "enableJavaScriptEvaluation" is enabled in an environment with code generation enabled.', () => { + const consoleWarn: string[] = []; + vi.spyOn(globalThis.console, 'warn').mockImplementation((...args: any[]) => + consoleWarn.push(args.join(' ')) + ); + const browser = new Browser({ settings: { enableJavaScriptEvaluation: true } }); + + browser.newPage(); + + expect(consoleWarn).toEqual([ + '\nWarning! Happy DOM has JavaScript evaluation enabled and is running in an environment with code generation from strings (eval) enabled at process level.' + + '\n\nA VM Context is not an isolated environment, and if you run untrusted code you are at risk of RCE (Remote Code Execution) attacks. The attacker can use code generation to escape the VM and run code at process level.' + + '\n\nIt is recommended to disable code generation at process level by running node with the "--disallow-code-generation-from-strings" flag enabled when Javascript evaluation is enabled in Happy DOM.' + + ' You can suppress this warning by setting "suppressCodeGenerationFromStringsWarning" to "true" at your own risk.' + + '\n\nFor more information, see https://github.com/capricorn86/happy-dom/wiki/Code-Generation-From-Strings-Warning\n\n' + ]); + }); + + it('Does not output a warning if "enableJavaScriptEvaluation" is disabled.', () => { + const consoleWarn: string[] = []; + vi.spyOn(globalThis.console, 'warn').mockImplementation((...args: any[]) => + consoleWarn.push(args.join(' ')) + ); + const browser = new Browser(); + + new BrowserWindow(browser.newPage().mainFrame); + + expect(consoleWarn.length).toBe(0); + }); + + it('Does not output a warning if "suppressCodeGenerationFromStringsWarning" is enabled.', () => { + const consoleWarn: string[] = []; + vi.spyOn(globalThis.console, 'warn').mockImplementation((...args: any[]) => + consoleWarn.push(args.join(' ')) + ); + const browser = new Browser({ + settings: { + enableJavaScriptEvaluation: true, + suppressCodeGenerationFromStringsWarning: true + } + }); + + new BrowserWindow(browser.newPage().mainFrame); + + expect(consoleWarn.length).toBe(0); + }); + }); + describe('get happyDOM()', () => { it('Returns "undefined" for an attached browser.', () => { - expect(browserFrame.window['happyDOM']).toBeUndefined(); + expect((<any>browserFrame.window)['happyDOM']).toBeUndefined(); }); }); @@ -114,18 +165,22 @@ describe('BrowserWindow', () => { }); describe('get Function()', () => { - it('Is not the same as (() => {}).constructorr when inside the VM.', () => { + it('Is not the same as (() => {}).constructor when inside the VM.', () => { expect(typeof window.Function).toBe('function'); expect((() => {}).constructor).not.toBe(window.Function); }); it('Is the same as (() => {}).constructor when using eval().', () => { - expect(window.eval('(() => {}).constructor === window.Function')).toBe(true); + expect(window.Function('return (() => {}).constructor === window.Function')()).toBe(true); + }); + + it('Does not execute unsafe code using import', () => { + expect(() => window.Function('return import("process")')()).rejects.toThrow(); }); }); describe('get Array()', () => { - it('Is not the same as [].constructorr when inside the VM.', () => { + it('Is not the same as [].constructor when inside the VM.', () => { expect(typeof window.Array).toBe('function'); expect([].constructor).not.toBe(window.Array); }); @@ -182,15 +237,15 @@ describe('BrowserWindow', () => { describe('get {ElementClass}()', () => { for (const tagName of Object.keys(HTMLElementConfig)) { it(`Exposes the HTML element class "${HTMLElementConfig[tagName].className}" for tag name "${tagName}"`, () => { - expect(window[HTMLElementConfig[tagName].className].name).toBe( + expect((<any>window)[HTMLElementConfig[tagName].className].name).toBe( HTMLElementConfig[tagName].className ); }); } for (const tagName of Object.keys(SVGElementConfig)) { it(`Exposes the SVG element class "${SVGElementConfig[tagName]}" for tag name "${tagName}"`, () => { - expect(window[SVGElementConfig[tagName].className].name).toBe( + expect((<any>window)[SVGElementConfig[tagName].className].name).toBe( SVGElementConfig[tagName].className ); }); @@ -205,7 +260,7 @@ describe('BrowserWindow', () => { describe('get process()', () => { it('Returns undefined.', () => { - expect(window['process']).toBeUndefined(); + expect((<any>window)['process']).toBeUndefined(); }); }); @@ -283,7 +338,7 @@ describe('BrowserWindow', () => { }; for (const propertyKey in referenceValues) { - expect(window.navigator[propertyKey]).toEqual(referenceValues[propertyKey]); + expect((<any>window.navigator)[propertyKey]).toEqual((<any>referenceValues)[propertyKey]); } }); }); @@ -408,7 +463,7 @@ describe('BrowserWindow', () => { return eval('variable'); })()`); expect(result).toBe('locally defined'); - expect(window['variable']).toBe('globally defined'); + expect((<any>window)['variable']).toBe('globally defined'); }); it('Respects indirect eval.', () => { @@ -419,12 +474,12 @@ describe('BrowserWindow', () => { return (0,eval)('variable'); })()`); expect(result).toBe('globally defined'); - expect(window['variable']).toBe('globally defined'); + expect((<any>window)['variable']).toBe('globally defined'); }); it('Has access to the window and document.', () => { window.eval(`window.variable = document.characterSet;`); - expect(window['variable']).toBe('UTF-8'); + expect((<any>window)['variable']).toBe('UTF-8'); }); }); @@ -1751,8 +1806,10 @@ describe('BrowserWindow', () => { }; let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = <Request>this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = <Request>(<any>this).request; return Promise.resolve(expectedResponse); }); @@ -1767,7 +1824,7 @@ describe('BrowserWindow', () => { for (const functionName of ['scroll', 'scrollTo']) { describe(`${functionName}()`, () => { it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset', () => { - window[functionName](50, 60); + (<any>window)[functionName](50, 60); expect(window.document.documentElement.scrollLeft).toBe(50); expect(window.document.documentElement.scrollTop).toBe(60); expect(window.pageXOffset).toBe(50); @@ -1777,7 +1834,7 @@ describe('BrowserWindow', () => { }); it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset using object.', () => { - window[functionName]({ left: 50, top: 60 }); + (<any>window)[functionName]({ left: 50, top: 60 }); expect(window.document.documentElement.scrollLeft).toBe(50); expect(window.document.documentElement.scrollTop).toBe(60); expect(window.pageXOffset).toBe(50); @@ -1787,7 +1844,7 @@ describe('BrowserWindow', () => { }); it('Sets only the property scrollTop, pageYOffset, and scrollY', () => { - window[functionName]({ top: 60 }); + (<any>window)[functionName]({ top: 60 }); expect(window.document.documentElement.scrollLeft).toBe(0); expect(window.document.documentElement.scrollTop).toBe(60); expect(window.pageXOffset).toBe(0); @@ -1797,7 +1854,7 @@ describe('BrowserWindow', () => { }); it('Sets only the property scrollLeft, pageXOffset, and scrollX', () => { - window[functionName]({ left: 60 }); + (<any>window)[functionName]({ left: 60 }); expect(window.document.documentElement.scrollLeft).toBe(60); expect(window.document.documentElement.scrollTop).toBe(0); expect(window.document.documentElement.scrollLeft).toBe(60); @@ -1809,7 +1866,7 @@ describe('BrowserWindow', () => { }); it('Sets the properties scrollTop, scrollLeft, scrollY, scrollX, pageXOffset and pageYOffset with animation.', async () => { - window[functionName]({ left: 50, top: 60, behavior: 'smooth' }); + (<any>window)[functionName]({ left: 50, top: 60, behavior: 'smooth' }); expect(window.document.documentElement.scrollLeft).toBe(0); expect(window.document.documentElement.scrollTop).toBe(0); expect(window.pageXOffset).toBe(0); @@ -1826,7 +1883,7 @@ describe('BrowserWindow', () => { }); it('Throws an exception if the there is only one argument and it is not an object.', () => { - expect(() => window[functionName](10)).toThrow( + expect(() => (<any>window)[functionName](10)).toThrow( new TypeError( `Failed to execute '${functionName}' on 'Window': The provided value is not of type 'ScrollToOptions'.` ) @@ -1930,15 +1987,16 @@ describe('BrowserWindow', () => { let loadEventCurrentTarget: EventTarget | null = null; vi.spyOn(ResourceFetch.prototype, 'fetch').mockImplementation(async function ( + this: ResourceFetch, url: string | URL ) { if ((<string>url).endsWith('.css')) { - resourceFetchCSSWindow = this.window; + resourceFetchCSSWindow = (<any>this).window; resourceFetchCSSURL = <string>url; return { content: cssResponse, virtualServerFile: null }; } - resourceFetchJSWindow = this.window; + resourceFetchJSWindow = (<any>this).window; resourceFetchJSURL = <string>url; return { content: jsResponse, virtualServerFile: null }; }); @@ -1973,7 +2031,7 @@ describe('BrowserWindow', () => { expect(document.styleSheets.length).toBe(1); expect(document.styleSheets[0].cssRules[0].cssText).toBe(cssResponse); - expect(window['test']).toBe('test'); + expect((<any>window)['test']).toBe('test'); resolve(null); }, 20); @@ -2185,8 +2243,10 @@ describe('BrowserWindow', () => { const html = '<html><body>Test</body></html>'; let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = <Request>this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = <Request>(<any>this).request; return Promise.resolve(<Response>{ text: () => new Promise((resolve) => setTimeout(() => resolve(html))) }); @@ -2267,8 +2327,10 @@ describe('BrowserWindow', () => { const html = '<html><body>Test</body></html>'; let request: Request | null = null; - vi.spyOn(Fetch.prototype, 'send').mockImplementation(function (): Promise<Response> { - request = <Request>this.request; + vi.spyOn(Fetch.prototype, 'send').mockImplementation(function ( + this: Fetch + ): Promise<Response> { + request = <Request>(<any>this).request; return Promise.resolve(<Response>{ text: () => new Promise((resolve) => setTimeout(() => resolve(html))) });
packages/happy-dom/test/window/DetachedWindowAPI.test.ts+4 −4 modified@@ -21,16 +21,16 @@ describe('DetachedWindowAPI', () => { describe('get settings()', () => { it('Returns browser settings.', () => { - const window = new Window({ settings: { disableJavaScriptEvaluation: true } }); + const window = new Window({ settings: { enableJavaScriptEvaluation: true } }); expect(window.happyDOM?.settings).toEqual({ ...DefaultBrowserSettings, - disableJavaScriptEvaluation: true + enableJavaScriptEvaluation: true }); }); it('Supports editing setting properties.', () => { - const window = new Window({ settings: { disableJavaScriptEvaluation: true } }); - (<DetachedWindowAPI>window.happyDOM).settings.disableJavaScriptEvaluation = false; + const window = new Window({ settings: { enableJavaScriptEvaluation: true } }); + (<DetachedWindowAPI>window.happyDOM).settings.enableJavaScriptEvaluation = false; expect(window.happyDOM?.settings).toEqual(DefaultBrowserSettings); }); });
packages/happy-dom/test/window/GlobalWindow.test.ts+35 −14 modified@@ -1,12 +1,14 @@ import GlobalWindow from '../../src/window/GlobalWindow.js'; -import Window from '../../src/window/Window.js'; +import * as PropertySymbol from '../../src/PropertySymbol.js'; import { beforeEach, describe, it, expect } from 'vitest'; describe('GlobalWindow', () => { - let window: Window; + let window: GlobalWindow; beforeEach(() => { - window = new GlobalWindow(); + window = new GlobalWindow({ + settings: { enableJavaScriptEvaluation: true, suppressCodeGenerationFromStringsWarning: true } + }); }); describe('get Object()', () => { @@ -15,9 +17,9 @@ describe('GlobalWindow', () => { }); it('Is the same as {}.constructor when using eval().', () => { - global['globalWindow'] = window; + (<any>global)['globalWindow'] = window; expect(window.eval('({}).constructor === globalWindow.Object')).toBe(true); - delete global['globalWindow']; + delete (<any>global)['globalWindow']; }); }); @@ -27,9 +29,11 @@ describe('GlobalWindow', () => { }); it('Is the same as (() => {}).constructor when using eval().', () => { - global['globalWindow'] = window; - expect(window.eval('(() => {}).constructor === globalWindow.Function')).toBe(true); - delete global['globalWindow']; + expect(window.Function('return (() => {}).constructor === globalThis.Function')()).toBe(true); + }); + + it('Does not execute unsafe code using import', () => { + expect(() => window.Function('return import("process")')()).rejects.toThrow(); }); }); @@ -39,9 +43,9 @@ describe('GlobalWindow', () => { }); it('Is the same as [].constructor when using eval().', () => { - global['globalWindow'] = window; + (<any>global)['globalWindow'] = window; expect(window.eval('[].constructor === globalWindow.Array')).toBe(true); - delete global['globalWindow']; + delete (<any>global)['globalWindow']; }); }); @@ -55,9 +59,9 @@ describe('GlobalWindow', () => { })()`); expect(result).toBe('locally defined'); - expect(globalThis['variable']).toBe('globally defined'); + expect((<any>globalThis)['variable']).toBe('globally defined'); - delete globalThis['variable']; + delete (<any>globalThis)['variable']; }); it('Respects indirect eval.', () => { @@ -69,9 +73,26 @@ describe('GlobalWindow', () => { })()`); expect(result).toBe('globally defined'); - expect(globalThis['variable']).toBe('globally defined'); + expect((<any>globalThis)['variable']).toBe('globally defined'); + + delete (<any>globalThis)['variable']; + }); + }); + + describe('[PropertySymbol.evaluateScript]()', () => { + it('Evaluates script.', () => { + const result = window[PropertySymbol.evaluateScript]('1 + 1;', { + filename: 'filename.js' + }); + expect(result).toBe(2); + }); - delete globalThis['variable']; + it('Evaluates code from script elements', () => { + const script = window.document.createElement('script'); + script.textContent = 'globalThis["$scriptResult"] = 1 + 1;'; + window.document.body.appendChild(script); + expect((<any>globalThis)['$scriptResult']).toBe(2); + delete (<any>globalThis)['$scriptResult']; }); });
packages/happy-dom/test/window/Window.test.ts+3 −0 modified@@ -149,6 +149,7 @@ describe('Window', () => { console: globalThis.console, settings: { disableJavaScriptEvaluation: true, + enableJavaScriptEvaluation: true, navigator: { userAgent: 'test' }, @@ -169,6 +170,7 @@ describe('Window', () => { VirtualConsolePrinter ); expect(windowWithOptions.happyDOM?.settings.disableJavaScriptEvaluation).toBe(true); + expect(windowWithOptions.happyDOM?.settings.enableJavaScriptEvaluation).toBe(true); expect(windowWithOptions.happyDOM?.settings.disableJavaScriptFileLoading).toBe(false); expect(windowWithOptions.happyDOM?.settings.disableCSSFileLoading).toBe(false); expect(windowWithOptions.happyDOM?.settings.disableIframePageLoading).toBe(false); @@ -191,6 +193,7 @@ describe('Window', () => { VirtualConsolePrinter ); expect(windowWithoutOptions.happyDOM?.settings.disableJavaScriptEvaluation).toBe(false); + expect(windowWithoutOptions.happyDOM?.settings.enableJavaScriptEvaluation).toBe(false); expect(windowWithoutOptions.happyDOM?.settings.disableJavaScriptFileLoading).toBe(false); expect(windowWithoutOptions.happyDOM?.settings.disableCSSFileLoading).toBe(false); expect(windowWithoutOptions.happyDOM?.settings.disableIframePageLoading).toBe(false);
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-37j7-fg3j-429fghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-61927ghsaADVISORY
- github.com/capricorn86/happy-dom/commit/819d15ba289495439eda8be360d92a614ce22405nvdWEB
- github.com/capricorn86/happy-dom/commit/de438ad72921c69793584aa657b48d3655dfac97ghsaWEB
- github.com/capricorn86/happy-dom/releases/tag/v20.0.0ghsaWEB
- github.com/capricorn86/happy-dom/security/advisories/GHSA-37j7-fg3j-429fnvdWEB
News mentions
0No linked articles in our index yet.