diff --git a/packages/happy-dom/src/PropertySymbol.ts b/packages/happy-dom/src/PropertySymbol.ts index c20c208b..32fbe3e4 100644 --- a/packages/happy-dom/src/PropertySymbol.ts +++ b/packages/happy-dom/src/PropertySymbol.ts @@ -374,7 +374,7 @@ export const getLength = Symbol('getLength'); export const currentScale = Symbol('currentScale'); export const rotate = Symbol('rotate'); export const bindMethods = Symbol('bindMethods'); -export const hasXMLProcessingInstruction = Symbol('hasXMLProcessingInstruction'); +export const xmlProcessingInstruction = Symbol('xmlProcessingInstruction'); export const root = Symbol('root'); export const filterNode = Symbol('filterNode'); export const customElementReactionStack = Symbol('customElementReactionStack'); diff --git a/packages/happy-dom/src/nodes/document/Document.ts b/packages/happy-dom/src/nodes/document/Document.ts index 4717fa2f..b77ba907 100644 --- a/packages/happy-dom/src/nodes/document/Document.ts +++ b/packages/happy-dom/src/nodes/document/Document.ts @@ -79,7 +79,7 @@ export default class Document extends Node { { htmlCollection: HTMLCollection | null; elements: Element[] } > = new Map(); public [PropertySymbol.contentType]: string = 'text/html'; - public [PropertySymbol.hasXMLProcessingInstruction] = false; + public [PropertySymbol.xmlProcessingInstruction]: ProcessingInstruction | null = null; public declare cloneNode: (deep?: boolean) => Document; // Private properties diff --git a/packages/happy-dom/src/xml-parser/XMLParser.ts b/packages/happy-dom/src/xml-parser/XMLParser.ts index f14c8ba5..ea051a38 100755 --- a/packages/happy-dom/src/xml-parser/XMLParser.ts +++ b/packages/happy-dom/src/xml-parser/XMLParser.ts @@ -325,8 +325,12 @@ export default class XMLParser { } else { // When the processing instruction has "xml" as target, we should not add it as a child node. // Instead we will store the state on the root node, so that it is added when serializing the document with XMLSerializer. - // TODO: We need to handle validation of version and encoding. - this.rootNode[PropertySymbol.hasXMLProcessingInstruction] = true; + // TODO: We need to handle validation of encoding. + const name = parts[0]; + // We need to remove the ending "?". + const content = parts.slice(1).join(' ').slice(0, -1); + this.rootNode[PropertySymbol.xmlProcessingInstruction] = + this.rootNode.createProcessingInstruction(name, content); this.readState = MarkupReadStateEnum.any; } } else { diff --git a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts index e593966d..f568a5b9 100644 --- a/packages/happy-dom/src/xml-serializer/XMLSerializer.ts +++ b/packages/happy-dom/src/xml-serializer/XMLSerializer.ts @@ -88,9 +88,14 @@ export default class XMLSerializer { return `<${tagName}${attributes}>${innerHTML}`; case Node.DOCUMENT_FRAGMENT_NODE: case Node.DOCUMENT_NODE: - let html = root[PropertySymbol.hasXMLProcessingInstruction] - ? '' - : ''; + let html = ''; + if (root[PropertySymbol.xmlProcessingInstruction]) { + html += this.#serializeToString( + root[PropertySymbol.xmlProcessingInstruction], + inheritedDefaultNamespace, + new Map(inheritedNamespacePrefixes) + ); + } for (const node of (root)[PropertySymbol.nodeArray]) { html += this.#serializeToString( node, 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 0b947792..85c9c957 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 @@ -175,69 +175,75 @@ describe('HTMLScriptElement', () => { expect(window['currentScript']).toBe(element); }); - it('Loads external script asynchronously.', async () => { - let fetchedURL: string | null = null; - let loadEvent: Event | null = null; - let loadEventTarget: EventTarget | null = null; - let loadEventCurrentTarget: EventTarget | null = null; - - vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function () { - fetchedURL = this.request.url; - return { - text: async () => - 'globalThis.test = "test";globalThis.currentScript = document.currentScript;', - ok: true - }; + for (const attribute of [ + { name: 'async', value: '' }, + { name: 'defer', value: '' }, + { name: 'type', value: 'module' } + ]) { + it(`Loads external script asynchronously when the attribute "${attribute.name}" is set to "${attribute.value}".`, async () => { + let fetchedURL: string | null = null; + let loadEvent: Event | null = null; + let loadEventTarget: EventTarget | null = null; + let loadEventCurrentTarget: EventTarget | null = null; + + vi.spyOn(Fetch.prototype, 'send').mockImplementation(async function () { + fetchedURL = this.request.url; + return { + text: async () => + 'globalThis.test = "test";globalThis.currentScript = document.currentScript;', + ok: true + }; + }); + + const script = window.document.createElement('script'); + script.src = 'https://localhost:8080/path/to/script.js'; + script.setAttribute(attribute.name, attribute.value); + script.addEventListener('load', (event) => { + loadEvent = event; + loadEventTarget = event.target; + loadEventCurrentTarget = event.currentTarget; + }); + + document.body.appendChild(script); + + await window.happyDOM?.waitUntilComplete(); + + expect(((loadEvent)).target).toBe(null); + 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); }); - const script = window.document.createElement('script'); - script.src = 'https://localhost:8080/path/to/script.js'; - script.async = true; - script.addEventListener('load', (event) => { - loadEvent = event; - loadEventTarget = event.target; - loadEventCurrentTarget = event.currentTarget; - }); + it(`Triggers error event when loading external script asynchronously when the attribute "${attribute.name}" is set to "${attribute.value}".`, async () => { + let errorEvent: ErrorEvent | null = null; - document.body.appendChild(script); + vi.spyOn(Fetch.prototype, 'send').mockImplementation( + async () => ({ + text: () => null, + ok: false, + status: 404, + statusText: 'Not Found' + }) + ); - await window.happyDOM?.waitUntilComplete(); + const script = window.document.createElement('script'); + script.src = 'https://localhost:8080/path/to/script.js'; + script.setAttribute(attribute.name, attribute.value); + script.addEventListener('error', (event) => { + errorEvent = event; + }); - expect(((loadEvent)).target).toBe(null); - 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); - }); + document.body.appendChild(script); - it('Triggers error event when loading external script asynchronously.', async () => { - let errorEvent: ErrorEvent | null = null; - - vi.spyOn(Fetch.prototype, 'send').mockImplementation( - async () => ({ - text: () => null, - ok: false, - status: 404, - statusText: 'Not Found' - }) - ); + await window.happyDOM?.waitUntilComplete(); - const script = window.document.createElement('script'); - script.src = 'https://localhost:8080/path/to/script.js'; - script.async = true; - script.addEventListener('error', (event) => { - errorEvent = event; + expect(((errorEvent)).message).toBe( + 'Failed to perform request to "https://localhost:8080/path/to/script.js". Status 404 Not Found.' + ); }); - - document.body.appendChild(script); - - await window.happyDOM?.waitUntilComplete(); - - expect(((errorEvent)).message).toBe( - 'Failed to perform request to "https://localhost:8080/path/to/script.js". Status 404 Not Found.' - ); - }); + } it('Loads external script synchronously with relative URL.', async () => { const window = new Window({ url: 'https://localhost:8080/base/' });