Skip to content

Commit

Permalink
chore: [#1615] Continues on implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
capricorn86 committed Dec 27, 2024
1 parent e9e2063 commit d981746
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 62 deletions.
2 changes: 1 addition & 1 deletion packages/happy-dom/src/PropertySymbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
2 changes: 1 addition & 1 deletion packages/happy-dom/src/nodes/document/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default class Document extends Node {
{ htmlCollection: HTMLCollection<Element> | 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
Expand Down
8 changes: 6 additions & 2 deletions packages/happy-dom/src/xml-parser/XMLParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 8 additions & 3 deletions packages/happy-dom/src/xml-serializer/XMLSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,14 @@ export default class XMLSerializer {
return `<${tagName}${attributes}>${innerHTML}</${tagName}>`;
case Node.DOCUMENT_FRAGMENT_NODE:
case Node.DOCUMENT_NODE:
let html = root[PropertySymbol.hasXMLProcessingInstruction]
? '<?xml version="1.0" encoding="UTF-8"?>'
: '';
let html = '';
if (root[PropertySymbol.xmlProcessingInstruction]) {
html += this.#serializeToString(
root[PropertySymbol.xmlProcessingInstruction],
inheritedDefaultNamespace,
new Map(inheritedNamespacePrefixes)
);
}
for (const node of (<Node>root)[PropertySymbol.nodeArray]) {
html += this.#serializeToString(
node,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Response>{
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 <Response>{
text: async () =>
'globalThis.test = "test";globalThis.currentScript = document.currentScript;',
ok: true
};
});

const script = <HTMLScriptElement>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((<Event>(<unknown>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 = <HTMLScriptElement>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 () => <Response>(<unknown>{
text: () => null,
ok: false,
status: 404,
statusText: 'Not Found'
})
);

await window.happyDOM?.waitUntilComplete();
const script = <HTMLScriptElement>window.document.createElement('script');
script.src = 'https://localhost:8080/path/to/script.js';
script.setAttribute(attribute.name, attribute.value);
script.addEventListener('error', (event) => {
errorEvent = <ErrorEvent>event;
});

expect((<Event>(<unknown>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 () => <Response>(<unknown>{
text: () => null,
ok: false,
status: 404,
statusText: 'Not Found'
})
);
await window.happyDOM?.waitUntilComplete();

const script = <HTMLScriptElement>window.document.createElement('script');
script.src = 'https://localhost:8080/path/to/script.js';
script.async = true;
script.addEventListener('error', (event) => {
errorEvent = <ErrorEvent>event;
expect((<ErrorEvent>(<unknown>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>(<unknown>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/' });
Expand Down

0 comments on commit d981746

Please sign in to comment.