diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts index 96d3d0526..14e02a390 100644 --- a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElement.ts @@ -1,14 +1,13 @@ import IAttr from '../attr/IAttr'; import CSSStyleSheet from '../../css/CSSStyleSheet'; -import ResourceFetch from '../../fetch/ResourceFetch'; import HTMLElement from '../html-element/HTMLElement'; -import Document from '../document/Document'; import IHTMLLinkElement from './IHTMLLinkElement'; import Event from '../../event/Event'; import ErrorEvent from '../../event/events/ErrorEvent'; import INode from '../../nodes/node/INode'; import DOMTokenList from '../../dom-token-list/DOMTokenList'; import IDOMTokenList from '../../dom-token-list/IDOMTokenList'; +import HTMLLinkElementUtility from './HTMLLinkElementUtility'; /** * HTML Link Element. @@ -184,51 +183,13 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle */ public override setAttributeNode(attribute: IAttr): IAttr { const replacedAttribute = super.setAttributeNode(attribute); - const rel = this.getAttribute('rel'); - const href = this.getAttribute('href'); if (attribute.name === 'rel' && this._relList) { this._relList._updateIndices(); } - if ( - (attribute.name === 'rel' || attribute.name === 'href') && - href !== null && - rel && - rel.toLowerCase() === 'stylesheet' && - this.isConnected && - !this.ownerDocument.defaultView.happyDOM.settings.disableCSSFileLoading - ) { - (this.ownerDocument)._readyStateManager.startTask(); - ResourceFetch.fetch(this.ownerDocument, href) - .then((code) => { - const styleSheet = new CSSStyleSheet(); - styleSheet.replaceSync(code); - (this.sheet) = styleSheet; - this.dispatchEvent(new Event('load')); - (this.ownerDocument)._readyStateManager.endTask(); - }) - .catch((error) => { - this.dispatchEvent( - new ErrorEvent('error', { - message: error.message, - error - }) - ); - this.ownerDocument.defaultView.dispatchEvent( - new ErrorEvent('error', { - message: error.message, - error - }) - ); - (this.ownerDocument)._readyStateManager.endTask(); - if ( - !this['_listeners']['error'] && - !this.ownerDocument.defaultView['_listeners']['error'] - ) { - this.ownerDocument.defaultView.console.error(error); - } - }); + if (attribute.name === 'rel' || attribute.name === 'href') { + HTMLLinkElementUtility.loadExternalStylesheet(this); } return replacedAttribute; @@ -256,47 +217,8 @@ export default class HTMLLinkElement extends HTMLElement implements IHTMLLinkEle super._connectToNode(parentNode); - if ( - isParentConnected && - isConnected !== isParentConnected && - this._evaluateCSS && - !this.ownerDocument.defaultView.happyDOM.settings.disableCSSFileLoading - ) { - const href = this.getAttribute('href'); - const rel = this.getAttribute('rel'); - - if (href !== null && rel && rel.toLowerCase() === 'stylesheet') { - (this.ownerDocument)._readyStateManager.startTask(); - ResourceFetch.fetch(this.ownerDocument, href) - .then((code) => { - const styleSheet = new CSSStyleSheet(); - styleSheet.replaceSync(code); - (this.sheet) = styleSheet; - this.dispatchEvent(new Event('load')); - (this.ownerDocument)._readyStateManager.endTask(); - }) - .catch((error) => { - this.dispatchEvent( - new ErrorEvent('error', { - message: error.message, - error - }) - ); - this.ownerDocument.defaultView.dispatchEvent( - new ErrorEvent('error', { - message: error.message, - error - }) - ); - (this.ownerDocument)._readyStateManager.endTask(); - if ( - !this['_listeners']['error'] && - !this.ownerDocument.defaultView['_listeners']['error'] - ) { - this.ownerDocument.defaultView.console.error(error); - } - }); - } + if (isParentConnected && isConnected !== isParentConnected && this._evaluateCSS) { + HTMLLinkElementUtility.loadExternalStylesheet(this); } } } diff --git a/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts new file mode 100644 index 000000000..a8dc437a9 --- /dev/null +++ b/packages/happy-dom/src/nodes/html-link-element/HTMLLinkElementUtility.ts @@ -0,0 +1,82 @@ +import Document from '../document/Document'; +import Event from '../../event/Event'; +import ErrorEvent from '../../event/events/ErrorEvent'; +import ResourceFetch from '../../fetch/ResourceFetch'; +import HTMLLinkElement from './HTMLLinkElement'; +import CSSStyleSheet from '../../css/CSSStyleSheet'; +import DOMException from '../../exception/DOMException'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum'; + +/** + * Helper class for getting the URL relative to a Location object. + */ +export default class HTMLLinkElementUtility { + /** + * Returns a URL relative to the given Location object. + * + * @param options Options. + * @param options.element Element. + * @param element + */ + public static async loadExternalStylesheet(element: HTMLLinkElement): Promise { + const href = element.getAttribute('href'); + const rel = element.getAttribute('rel'); + + if (href !== null && rel && rel.toLowerCase() === 'stylesheet' && element.isConnected) { + if (element.ownerDocument.defaultView.happyDOM.settings.disableCSSFileLoading) { + this.onError( + element, + new DOMException( + `Failed to load external stylesheet "${href}". CSS file loading is disabled.`, + DOMExceptionNameEnum.notSupportedError + ) + ); + return; + } + + (element.ownerDocument)._readyStateManager.startTask(); + + let code: string; + try { + code = await ResourceFetch.fetch(element.ownerDocument, href); + } catch (error) { + this.onError(element, error); + return; + } + + const styleSheet = new CSSStyleSheet(); + styleSheet.replaceSync(code); + (element.sheet) = styleSheet; + element.dispatchEvent(new Event('load')); + (element.ownerDocument)._readyStateManager.endTask(); + } + } + + /** + * Triggered when an error occurs. + * + * @param element Element. + * @param error Error. + */ + private static onError(element: HTMLLinkElement, error: Error): void { + element.dispatchEvent( + new ErrorEvent('error', { + message: error.message, + error + }) + ); + element.ownerDocument.defaultView.dispatchEvent( + new ErrorEvent('error', { + message: error.message, + error + }) + ); + (element.ownerDocument)._readyStateManager.endTask(); + if ( + !element['_listeners']['error'] && + !element.ownerDocument.defaultView['_listeners']['error'] + ) { + element.ownerDocument.defaultView.console.error(error); + } + } +} diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts index 46eae69ca..f76db5961 100644 --- a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElement.ts @@ -1,7 +1,7 @@ import IAttr from '../attr/IAttr'; import HTMLElement from '../html-element/HTMLElement'; import IHTMLScriptElement from './IHTMLScriptElement'; -import ScriptUtility from './ScriptUtility'; +import HTMLScriptElementUtility from './HTMLScriptElementUtility'; import Event from '../../event/Event'; import ErrorEvent from '../../event/events/ErrorEvent'; import INode from '../../nodes/node/INode'; @@ -162,7 +162,7 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip const replacedAttribute = super.setAttributeNode(attribute); if (attribute.name === 'src' && attribute.value !== null && this.isConnected) { - ScriptUtility.loadExternalScript(this); + HTMLScriptElementUtility.loadExternalScript(this); } return replacedAttribute; @@ -192,7 +192,7 @@ export default class HTMLScriptElement extends HTMLElement implements IHTMLScrip const src = this.getAttribute('src'); if (src !== null) { - ScriptUtility.loadExternalScript(this); + HTMLScriptElementUtility.loadExternalScript(this); } else if (!this.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptEvaluation) { const textContent = this.textContent; const type = this.getAttribute('type'); diff --git a/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts new file mode 100644 index 000000000..1f1afa8cd --- /dev/null +++ b/packages/happy-dom/src/nodes/html-script-element/HTMLScriptElementUtility.ts @@ -0,0 +1,94 @@ +import Document from '../document/Document'; +import Event from '../../event/Event'; +import ErrorEvent from '../../event/events/ErrorEvent'; +import DOMException from '../../exception/DOMException'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum'; +import ResourceFetch from '../../fetch/ResourceFetch'; +import HTMLScriptElement from './HTMLScriptElement'; + +/** + * Helper class for getting the URL relative to a Location object. + */ +export default class HTMLScriptElementUtility { + /** + * Returns a URL relative to the given Location object. + * + * @param options Options. + * @param options.element Element. + * @param element + */ + public static async loadExternalScript(element: HTMLScriptElement): Promise { + const src = element.getAttribute('src'); + const async = element.getAttribute('async') !== null; + + if ( + element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptFileLoading || + element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptEvaluation + ) { + this.onError( + element, + new DOMException( + `Failed to load external script "${src}". JavaScript file loading is disabled.`, + DOMExceptionNameEnum.notSupportedError + ) + ); + return; + } + + if (async) { + (element.ownerDocument)._readyStateManager.startTask(); + + let code = null; + + try { + code = await ResourceFetch.fetch(element.ownerDocument, src); + } catch (error) { + this.onError(element, error); + return; + } + + element.ownerDocument.defaultView.eval(code); + element.dispatchEvent(new Event('load')); + (element.ownerDocument)._readyStateManager.endTask(); + } else { + let code = null; + + try { + code = ResourceFetch.fetchSync(element.ownerDocument, src); + } catch (error) { + this.onError(element, error); + return; + } + + element.ownerDocument.defaultView.eval(code); + element.dispatchEvent(new Event('load')); + } + } + + /** + * Triggered when an error occurs. + * + * @param element Element. + * @param error Error. + */ + private static onError(element: HTMLScriptElement, error: Error): void { + element.dispatchEvent( + new ErrorEvent('error', { + message: error.message, + error + }) + ); + element.ownerDocument.defaultView.dispatchEvent( + new ErrorEvent('error', { + message: error.message, + error + }) + ); + if ( + !element['_listeners']['error'] && + !element.ownerDocument.defaultView['_listeners']['error'] + ) { + element.ownerDocument.defaultView.console.error(error); + } + } +} diff --git a/packages/happy-dom/src/nodes/html-script-element/ScriptUtility.ts b/packages/happy-dom/src/nodes/html-script-element/ScriptUtility.ts deleted file mode 100644 index aef329a4c..000000000 --- a/packages/happy-dom/src/nodes/html-script-element/ScriptUtility.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { Document } from '../..'; -import Event from '../../event/Event'; -import ErrorEvent from '../../event/events/ErrorEvent'; -import ResourceFetch from '../../fetch/ResourceFetch'; -import HTMLScriptElement from './HTMLScriptElement'; - -/** - * Helper class for getting the URL relative to a Location object. - */ -export default class ScriptUtility { - /** - * Returns a URL relative to the given Location object. - * - * @param options Options. - * @param options.element Element. - * @param element - */ - public static async loadExternalScript(element: HTMLScriptElement): Promise { - const src = element.getAttribute('src'); - const async = element.getAttribute('async') !== null; - - if ( - element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptFileLoading || - element.ownerDocument.defaultView.happyDOM.settings.disableJavaScriptEvaluation - ) { - return; - } - - if (async) { - let code = null; - (element.ownerDocument)._readyStateManager.startTask(); - try { - code = await ResourceFetch.fetch(element.ownerDocument, src); - } catch (error) { - element.dispatchEvent( - new ErrorEvent('error', { - message: error.message, - error - }) - ); - element.ownerDocument.defaultView.dispatchEvent( - new ErrorEvent('error', { - message: error.message, - error - }) - ); - if ( - !element['_listeners']['error'] && - !element.ownerDocument.defaultView['_listeners']['error'] - ) { - element.ownerDocument.defaultView.console.error(error); - } - } - if (code) { - element.ownerDocument.defaultView.eval(code); - element.dispatchEvent(new Event('load')); - } - (element.ownerDocument)._readyStateManager.endTask(); - } else { - let code = null; - try { - code = ResourceFetch.fetchSync(element.ownerDocument, src); - } catch (error) { - element.dispatchEvent( - new ErrorEvent('error', { - message: error.message, - error - }) - ); - element.ownerDocument.defaultView.dispatchEvent( - new ErrorEvent('error', { - message: error.message, - error - }) - ); - if ( - !element['_listeners']['error'] && - !element.ownerDocument.defaultView['_listeners']['error'] - ) { - element.ownerDocument.defaultView.console.error(error); - } - } - if (code) { - element.ownerDocument.defaultView.eval(code); - element.dispatchEvent(new Event('load')); - } - } - } -} diff --git a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts index bfff27e70..ca5b2ddb8 100644 --- a/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts +++ b/packages/happy-dom/test/nodes/html-link-element/HTMLLinkElement.test.ts @@ -3,6 +3,7 @@ import IWindow from '../../../src/window/IWindow'; import IDocument from '../../../src/nodes/document/IDocument'; import IHTMLLinkElement from '../../../src/nodes/html-link-element/IHTMLLinkElement'; import ResourceFetch from '../../../src/fetch/ResourceFetch'; +import ErrorEvent from '../../../src/event/events/ErrorEvent'; describe('HTMLLinkElement', () => { let window: IWindow; @@ -74,7 +75,7 @@ describe('HTMLLinkElement', () => { expect(element.getAttribute('href')).toBe('test'); }); - it('Loads and evaluates an external CSS file when the attribute "href" and "rel" is set and the element is connected to DOM.', (done) => { + it('Loads and evaluates an external CSS file when the attribute "href" and "rel" is set and the element is connected to DOM.', async () => { const element = document.createElement('link'); const css = 'div { background: red; }'; let loadedDocument = null; @@ -98,17 +99,16 @@ describe('HTMLLinkElement', () => { element.rel = 'stylesheet'; element.href = 'test'; - setTimeout(() => { - expect(loadedDocument).toBe(document); - expect(loadedURL).toBe('test'); - expect(element.sheet.cssRules.length).toBe(1); - expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); - expect(loadEvent.target).toBe(element); - done(); - }, 0); + await window.happyDOM.whenAsyncComplete(); + + expect(loadedDocument).toBe(document); + expect(loadedURL).toBe('test'); + expect(element.sheet.cssRules.length).toBe(1); + expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); + expect(loadEvent.target).toBe(element); }); - it('Triggers error event when fetching a CSS file fails during setting the "href" and "rel" attributes.', (done) => { + it('Triggers error event when fetching a CSS file fails during setting the "href" and "rel" attributes.', async () => { const element = document.createElement('link'); const thrownError = new Error('error'); let errorEvent = null; @@ -126,11 +126,10 @@ describe('HTMLLinkElement', () => { element.rel = 'stylesheet'; element.href = 'test'; - setTimeout(() => { - expect(errorEvent.error).toEqual(thrownError); - expect(errorEvent.message).toEqual('error'); - done(); - }, 0); + await window.happyDOM.whenAsyncComplete(); + + expect(errorEvent.error).toEqual(thrownError); + expect(errorEvent.message).toEqual('error'); }); it('Does not load and evaluate external CSS files if the element is not connected to DOM.', () => { @@ -156,7 +155,7 @@ describe('HTMLLinkElement', () => { }); describe('set isConnected()', () => { - it('Loads and evaluates an external script when "href" attribute has been set, but does not evaluate text content.', (done) => { + it('Loads and evaluates an external CSS file when "href" attribute has been set, but does not evaluate text content.', async () => { const element = document.createElement('link'); const css = 'div { background: red; }'; let loadEvent = null; @@ -179,17 +178,16 @@ describe('HTMLLinkElement', () => { document.body.appendChild(element); - setTimeout(() => { - expect(loadedDocument).toBe(document); - expect(loadedURL).toBe('test'); - expect(element.sheet.cssRules.length).toBe(1); - expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); - expect(loadEvent.target).toBe(element); - done(); - }, 0); + await window.happyDOM.whenAsyncComplete(); + + expect(loadedDocument).toBe(document); + expect(loadedURL).toBe('test'); + expect(element.sheet.cssRules.length).toBe(1); + expect(element.sheet.cssRules[0].cssText).toBe('div { background: red; }'); + expect(loadEvent.target).toBe(element); }); - it('Triggers error event when fetching a CSS file fails while appending the element to the document.', (done) => { + it('Triggers error event when fetching a CSS file fails while appending the element to the document.', async () => { const element = document.createElement('link'); const thrownError = new Error('error'); let errorEvent = null; @@ -206,14 +204,13 @@ describe('HTMLLinkElement', () => { document.body.appendChild(element); - setTimeout(() => { - expect(errorEvent.error).toEqual(thrownError); - expect(errorEvent.message).toEqual('error'); - done(); - }, 0); + await window.happyDOM.whenAsyncComplete(); + + expect(errorEvent.error).toEqual(thrownError); + expect(errorEvent.message).toEqual('error'); }); - it('Does not load external scripts when "href" attribute has been set if the element is not connected to DOM.', () => { + it('Does not load external CSS file when "href" attribute has been set if the element is not connected to DOM.', () => { const element = document.createElement('link'); const css = 'div { background: red; }'; let loadedDocument = null; @@ -234,5 +231,23 @@ describe('HTMLLinkElement', () => { expect(loadedURL).toBe(null); expect(element.sheet).toBe(null); }); + + it('Triggers an error event when "window.happyDOM.settings.disableCSSFileLoading" is set to "true".', async () => { + const element = document.createElement('link'); + let errorEvent: ErrorEvent = null; + + element.rel = 'stylesheet'; + element.href = '/test/path/file.css'; + element.addEventListener('error', (event: ErrorEvent) => (errorEvent = event)); + + window.happyDOM.settings.disableCSSFileLoading = true; + + document.body.appendChild(element); + + expect(element.sheet).toBe(null); + expect(errorEvent.message).toBe( + 'Failed to load external stylesheet "/test/path/file.css". CSS file loading is disabled.' + ); + }); }); }); diff --git a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts index 68d746b84..c0ffd68c8 100644 --- a/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts +++ b/packages/happy-dom/test/nodes/html-script-element/HTMLScriptElement.test.ts @@ -1,7 +1,9 @@ import Window from '../../../src/window/Window'; import Document from '../../../src/nodes/document/Document'; import HTMLScriptElement from '../../../src/nodes/html-script-element/HTMLScriptElement'; -import ScriptUtility from '../../../src/nodes/html-script-element/ScriptUtility'; +import IDocument from '../../../src/nodes/document/IDocument'; +import IResponse from '../../../src/fetch/types/IResponse'; +import ResourceFetch from '../../../src/fetch/ResourceFetch'; describe('HTMLScriptElement', () => { let window: Window; @@ -74,12 +76,15 @@ describe('HTMLScriptElement', () => { 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.', () => { + 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'); - let loadedElement = null; - jest.spyOn(ScriptUtility, 'loadExternalScript').mockImplementation(async (element) => { - loadedElement = element; + jest.spyOn(window, 'fetch').mockImplementation(() => { + return Promise.resolve({ + text: async () => 'globalThis.test = "test";', + ok: true, + status: 200 + }); }); document.body.appendChild(element); @@ -87,21 +92,28 @@ describe('HTMLScriptElement', () => { element.async = true; element.src = 'test'; - expect(loadedElement).toBe(element); + await window.happyDOM.whenAsyncComplete(); + + expect(window['test']).toBe('test'); }); - it('Does not evaluate script if the element is not connected to DOM.', () => { + it('Does not evaluate script if the element is not connected to DOM.', async () => { const element = document.createElement('script'); - let loadedElement = null; - jest.spyOn(ScriptUtility, 'loadExternalScript').mockImplementation(async (element) => { - loadedElement = element; + jest.spyOn(window, 'fetch').mockImplementation(() => { + return Promise.resolve({ + text: async () => 'globalThis.test = "test";', + ok: true, + status: 200 + }); }); element.async = true; element.src = 'test'; - expect(loadedElement).toBe(null); + await window.happyDOM.whenAsyncComplete(); + + expect(window['test']).toBe(undefined); }); }); @@ -146,6 +158,112 @@ describe('HTMLScriptElement', () => { expect(window['test']).toBe('test'); }); + it('Loads external script asynchronously.', async () => { + let fetchedURL = null; + let loadEvent = null; + + jest.spyOn(window, 'fetch').mockImplementation((url: string) => { + fetchedURL = url; + return Promise.resolve({ + text: async () => 'globalThis.test = "test";', + ok: true + }); + }); + + const script = window.document.createElement('script'); + script.src = 'path/to/script/'; + script.async = true; + script.addEventListener('load', (event) => { + loadEvent = event; + }); + + document.body.appendChild(script); + + await window.happyDOM.whenAsyncComplete(); + + expect(loadEvent.target).toBe(script); + expect(fetchedURL).toBe('path/to/script/'); + expect(window['test']).toBe('test'); + }); + + it('Triggers error event when loading external script asynchronously.', async () => { + let errorEvent = null; + + jest.spyOn(window, 'fetch').mockImplementation(() => { + return Promise.resolve({ + text: () => null, + ok: false, + status: 404 + }); + }); + + const script = window.document.createElement('script'); + script.src = 'path/to/script/'; + script.async = true; + script.addEventListener('error', (event) => { + errorEvent = event; + }); + + document.body.appendChild(script); + + await window.happyDOM.whenAsyncComplete(); + + expect(errorEvent.message).toBe( + 'Failed to perform request to "path/to/script/". Status code: 404' + ); + }); + + it('Loads external script synchronously with relative URL.', async () => { + let fetchedDocument = null; + let fetchedURL = null; + let loadEvent = null; + + window.location.href = 'https://localhost:8080/base/'; + + jest + .spyOn(ResourceFetch, 'fetchSync') + .mockImplementation((document: IDocument, url: string) => { + fetchedDocument = document; + fetchedURL = url; + return 'globalThis.test = "test";'; + }); + + const script = window.document.createElement('script'); + script.src = 'path/to/script/'; + script.addEventListener('load', (event) => { + loadEvent = event; + }); + + document.body.appendChild(script); + + expect(loadEvent.target).toBe(script); + expect(fetchedDocument).toBe(document); + expect(fetchedURL).toBe('path/to/script/'); + expect(window['test']).toBe('test'); + }); + + it('Triggers error event when loading external script synchronously with relative URL.', () => { + const thrownError = new Error('error'); + let errorEvent = null; + + window.location.href = 'https://localhost:8080/base/'; + + jest.spyOn(ResourceFetch, 'fetchSync').mockImplementation(() => { + throw thrownError; + }); + + const script = window.document.createElement('script'); + script.src = 'path/to/script/'; + script.addEventListener('error', (event) => { + errorEvent = event; + }); + + document.body.appendChild(script); + + expect(errorEvent.message).toBe('error'); + expect(errorEvent.error).toBe(thrownError); + }); + it('Does not evaluate types that are not supported.', () => { const div = document.createElement('div'); const element = document.createElement('script'); @@ -190,34 +308,106 @@ describe('HTMLScriptElement', () => { it('Loads and evaluates an external script when "src" attribute has been set, but does not evaluate text content.', () => { const element = document.createElement('script'); - let loadedElement = null; - jest.spyOn(ScriptUtility, 'loadExternalScript').mockImplementation(async (element) => { - loadedElement = element; - }); + jest + .spyOn(ResourceFetch, 'fetchSync') + .mockImplementation(() => 'globalThis.testFetch = "test";'); element.src = 'test'; - element.text = 'globalThis.test = "test";'; + element.text = 'globalThis.testContent = "test";'; document.body.appendChild(element); - expect(window['test']).toBe(undefined); - expect(loadedElement).toBe(element); + expect(window['testFetch']).toBe('test'); + expect(window['testContent']).toBe(undefined); }); it('Does not load external scripts when "src" attribute has been set if the element is not connected to DOM.', () => { const element = document.createElement('script'); - let loadedElement = null; - jest.spyOn(ScriptUtility, 'loadExternalScript').mockImplementation(async (element) => { - loadedElement = element; - }); + jest + .spyOn(ResourceFetch, 'fetchSync') + .mockImplementation(() => 'globalThis.testFetch = "test";'); element.src = 'test'; element.text = 'globalThis.test = "test";'; - expect(window['test']).toBe(undefined); - expect(loadedElement).toBe(null); + expect(window['testFetch']).toBe(undefined); + expect(window['testContent']).toBe(undefined); + }); + + it('Triggers an error event when attempting to perform an asynchrounous request and "window.happyDOM.settings.disableJavaScriptFileLoading" is set to "true".', () => { + let errorEvent = null; + + const script = window.document.createElement('script'); + script.src = 'path/to/script/'; + script.async = true; + script.addEventListener('error', (event) => { + errorEvent = event; + }); + + window.happyDOM.settings.disableJavaScriptFileLoading = true; + + document.body.appendChild(script); + + expect(errorEvent.message).toBe( + 'Failed to load external script "path/to/script/". JavaScript file loading is disabled.' + ); + }); + + it('Triggers an error event when attempting to perform a synchrounous request and "window.happyDOM.settings.disableJavaScriptFileLoading" is set to "true".', () => { + let errorEvent = null; + + const script = window.document.createElement('script'); + script.src = 'path/to/script/'; + script.addEventListener('error', (event) => { + errorEvent = event; + }); + + window.happyDOM.settings.disableJavaScriptFileLoading = true; + + document.body.appendChild(script); + + expect(errorEvent.message).toBe( + 'Failed to load external script "path/to/script/". JavaScript file loading is disabled.' + ); + }); + + it('Triggers an error event when attempting to perform an asynchrounous request and "window.happyDOM.settings.disableJavaScriptEvaluation" is set to "true".', () => { + let errorEvent = null; + + const script = window.document.createElement('script'); + script.src = 'path/to/script/'; + script.async = true; + script.addEventListener('error', (event) => { + errorEvent = event; + }); + + window.happyDOM.settings.disableJavaScriptEvaluation = true; + + document.body.appendChild(script); + + expect(errorEvent.message).toBe( + 'Failed to load external script "path/to/script/". JavaScript file loading is disabled.' + ); + }); + + it('Triggers an error event when attempting to perform a synchrounous request and "window.happyDOM.settings.disableJavaScriptEvaluation" is set to "true".', () => { + let errorEvent = null; + + const script = window.document.createElement('script'); + script.src = 'path/to/script/'; + script.addEventListener('error', (event) => { + errorEvent = event; + }); + + window.happyDOM.settings.disableJavaScriptEvaluation = true; + + document.body.appendChild(script); + + expect(errorEvent.message).toBe( + 'Failed to load external script "path/to/script/". JavaScript file loading is disabled.' + ); }); }); }); diff --git a/packages/happy-dom/test/nodes/html-script-element/ScriptUtility.test.ts b/packages/happy-dom/test/nodes/html-script-element/ScriptUtility.test.ts deleted file mode 100644 index 5377acc6d..000000000 --- a/packages/happy-dom/test/nodes/html-script-element/ScriptUtility.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import Window from '../../../src/window/Window'; -import IWindow from '../../../src/window/IWindow'; -import IDocument from '../../../src/nodes/document/IDocument'; -import ScriptUtility from '../../../src/nodes/html-script-element/ScriptUtility'; -import IResponse from '../../../src/fetch/types/IResponse'; -import HTMLScriptElement from '../../../src/nodes/html-script-element/HTMLScriptElement'; -import ResourceFetch from '../../../src/fetch/ResourceFetch'; - -describe('ScriptUtility', () => { - let window: IWindow; - let document: IDocument; - - beforeEach(() => { - window = new Window(); - document = window.document; - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - describe('loadExternalScript()', () => { - it('Loads external script asynchronously.', async () => { - let fetchedURL = null; - let loadEvent = null; - - jest.spyOn(window, 'fetch').mockImplementation((url: string) => { - fetchedURL = url; - return Promise.resolve({ - text: () => Promise.resolve('globalThis.test = "test";'), - ok: true - }); - }); - - const script = window.document.createElement('script'); - script.src = 'path/to/script/'; - script.async = true; - script.addEventListener('load', (event) => { - loadEvent = event; - }); - - await ScriptUtility.loadExternalScript(script); - - expect(loadEvent.target).toBe(script); - expect(fetchedURL).toBe('path/to/script/'); - expect(window['test']).toBe('test'); - }); - - it('Triggers error event when loading external script asynchronously.', async () => { - let errorEvent = null; - - jest.spyOn(window, 'fetch').mockImplementation(() => { - return Promise.resolve({ - text: () => null, - ok: false, - status: 404 - }); - }); - - const script = window.document.createElement('script'); - script.src = 'path/to/script/'; - script.async = true; - script.addEventListener('error', (event) => { - errorEvent = event; - }); - - await ScriptUtility.loadExternalScript(script); - - expect(errorEvent.message).toBe( - 'Failed to perform request to "path/to/script/". Status code: 404' - ); - }); - - it('Loads external script synchronously with relative URL.', async () => { - let fetchedDocument = null; - let fetchedURL = null; - let loadEvent = null; - - window.location.href = 'https://localhost:8080/base/'; - - jest - .spyOn(ResourceFetch, 'fetchSync') - .mockImplementation((document: IDocument, url: string) => { - fetchedDocument = document; - fetchedURL = url; - return 'globalThis.test = "test";'; - }); - - const script = window.document.createElement('script'); - script.src = 'path/to/script/'; - script.addEventListener('load', (event) => { - loadEvent = event; - }); - - await ScriptUtility.loadExternalScript(script); - - expect(loadEvent.target).toBe(script); - expect(fetchedDocument).toBe(document); - expect(fetchedURL).toBe('path/to/script/'); - expect(window['test']).toBe('test'); - }); - - it('Triggers error event when loading external script synchronously with relative URL.', () => { - const thrownError = new Error('error'); - let errorEvent = null; - - window.location.href = 'https://localhost:8080/base/'; - - jest.spyOn(ResourceFetch, 'fetchSync').mockImplementation(() => { - throw thrownError; - }); - - const script = window.document.createElement('script'); - script.src = 'path/to/script/'; - script.addEventListener('error', (event) => { - errorEvent = event; - }); - - ScriptUtility.loadExternalScript(script); - - expect(errorEvent.message).toBe('error'); - expect(errorEvent.error).toBe(thrownError); - }); - }); -});