Skip to content

Commit

Permalink
#450@patch: Adds support for <template> elements to XMLSerializer.ser…
Browse files Browse the repository at this point in the history
…ializeToString(). Fixes issue with HTMLTemplateElement.outerHTML.
  • Loading branch information
capricorn86 committed Jun 28, 2022
1 parent 48d5ff2 commit 33b84ca
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 34 deletions.
@@ -1,11 +1,9 @@
import Node from '../node/Node';
import HTMLElement from '../html-element/HTMLElement';
import IDocumentFragment from '../document-fragment/IDocumentFragment';
import INode from '../node/INode';
import IHTMLTemplateElement from './IHTMLTemplateElement';
import XMLParser from '../../xml-parser/XMLParser';
import XMLSerializer from '../../xml-serializer/XMLSerializer';
import DOMException from '../../exception/DOMException';

/**
* HTML Template Element.
Expand Down Expand Up @@ -36,34 +34,18 @@ export default class HTMLTemplateElement extends HTMLElement implements IHTMLTem
}
}

/**
* @override
*/
public get outerHTML(): string {
return new XMLSerializer().serializeToString(this.content);
}

/**
* @override
*/
public set outerHTML(_html: string) {
throw new DOMException(
`Failed to set the 'outerHTML' property on 'Element': This element has no parent node.`
);
}

/**
* @override
*/
public get previousSibling(): INode {
return this.content.previousSibling;
return null;
}

/**
* @override
*/
public get nextSibling(): INode {
return this.content.nextSibling;
return null;
}

/**
Expand Down Expand Up @@ -102,7 +84,7 @@ export default class HTMLTemplateElement extends HTMLElement implements IHTMLTem
/**
* @override
*/
public removeChild(node: Node): INode {
public removeChild(node: INode): INode {
return this.content.removeChild(node);
}

Expand Down
7 changes: 6 additions & 1 deletion packages/happy-dom/src/xml-serializer/XMLSerializer.ts
Expand Up @@ -5,6 +5,7 @@ import DocumentType from '../nodes/document-type/DocumentType';
import { escape } from 'he';
import INode from '../nodes/node/INode';
import IElement from '../nodes/element/IElement';
import IHTMLTemplateElement from '../nodes/html-template-element/IHTMLTemplateElement';

/**
* Utility for converting an element to string.
Expand All @@ -28,9 +29,13 @@ export default class XMLSerializer {
return `<${tagName}${this._getAttributes(element)}>`;
}

const childNodes =
element.tagName === 'TEMPLATE'
? (<IHTMLTemplateElement>root).content.childNodes
: root.childNodes;
let innerHTML = '';

for (const node of root.childNodes) {
for (const node of childNodes) {
innerHTML += this.serializeToString(node, options);
}

Expand Down
@@ -1,6 +1,7 @@
import Window from '../../../src/window/Window';
import Document from '../../../src/nodes/document/Document';
import HTMLTemplateElement from '../../../src/nodes/html-template-element/HTMLTemplateElement';
import XMLSerializer from '../../../src/xml-serializer/XMLSerializer';

describe('HTMLTemplateElement', () => {
let window: Window;
Expand All @@ -17,13 +18,215 @@ describe('HTMLTemplateElement', () => {
jest.restoreAllMocks();
});

it('InnerHTML', () => {
const div = '<div>happy-dom is cool!</div>';
expect(element.content.childNodes.length).toBe(0);
element.innerHTML = div;
expect(element.innerHTML).toBe(div);
expect(element.content.childNodes.length).toBe(1);
element.innerHTML = '';
expect(element.content.childNodes.length).toBe(0);
describe('get innerHTML()', () => {
it('Returns inner HTML of the "content" node.', () => {
const div = document.createElement('div');

div.innerHTML = 'Test';

expect(element.content.childNodes.length).toBe(0);
expect(element.innerHTML).toBe('');

element.appendChild(div);

expect(element.childNodes.length).toBe(0);
expect(element.innerHTML).toBe('<div>Test</div>');
expect(new XMLSerializer().serializeToString(element.content)).toBe('<div>Test</div>');

element.removeChild(div);

expect(element.content.childNodes.length).toBe(0);
expect(element.innerHTML).toBe('');
});
});

describe('set innerHTML()', () => {
it('Serializes the HTML into nodes and appends them to the "content" node.', () => {
expect(element.content.childNodes.length).toBe(0);
expect(element.innerHTML).toBe('');

element.innerHTML = '<div>Test</div>';

expect(element.childNodes.length).toBe(0);
expect(element.innerHTML).toBe('<div>Test</div>');
expect(new XMLSerializer().serializeToString(element.content)).toBe('<div>Test</div>');

element.innerHTML = '';

expect(element.content.childNodes.length).toBe(0);
expect(element.innerHTML).toBe('');
});
});

describe('get outerHTML()', () => {
it('Serializes the HTML into nodes and appends them to the "content" node.', () => {
expect(element.content.childNodes.length).toBe(0);
expect(element.innerHTML).toBe('');

element.innerHTML = '<div>Test</div>';

expect(element.childNodes.length).toBe(0);
expect(element.outerHTML).toBe('<template><div>Test</div></template>');

element.innerHTML = '';

expect(element.outerHTML).toBe('<template></template>');
});
});

describe('set outerHTML()', () => {
it('Replaces the template with a span.', () => {
element.innerHTML = '<div>Test</div>';

document.body.appendChild(element);

expect(document.body.innerHTML).toBe('<template><div>Test</div></template>');

element.outerHTML = '<span>Test</span>';

expect(document.body.innerHTML).toBe('<span>Test</span>');
});
});

describe('get previousSibling()', () => {
it('Returns null.', () => {
element.innerHTML = '<div>Test</div>';
expect(element.previousSibling).toBe(null);
});
});

describe('get nextSibling()', () => {
it('Returns null.', () => {
element.innerHTML = '<div>Test</div>';
expect(element.nextSibling).toBe(null);
});
});

describe('get firstChild()', () => {
it('Returns first child.', () => {
const div = document.createElement('div');
const span = document.createElement('span');
element.appendChild(div);
element.appendChild(span);
expect(element.firstChild).toBe(div);
});
});

describe('get lastChild()', () => {
it('Returns last child.', () => {
const div = document.createElement('div');
const span = document.createElement('span');
element.appendChild(div);
element.appendChild(span);
expect(element.lastChild).toBe(span);
});
});

describe('getInnerHTML()', () => {
it('Returns inner HTML of the "content" node.', () => {
const div = document.createElement('div');

div.innerHTML = 'Test';

expect(element.content.childNodes.length).toBe(0);
expect(element.getInnerHTML()).toBe('');

element.appendChild(div);

expect(element.childNodes.length).toBe(0);
expect(element.getInnerHTML()).toBe('<div>Test</div>');
expect(new XMLSerializer().serializeToString(element.content)).toBe('<div>Test</div>');

element.removeChild(div);

expect(element.content.childNodes.length).toBe(0);
expect(element.getInnerHTML()).toBe('');
});

it('Returns HTML of children and shadow roots of custom elements as a concatenated string.', () => {
const div = document.createElement('div');

element.appendChild(div);

jest
.spyOn(XMLSerializer.prototype, 'serializeToString')
.mockImplementation((rootElement, options) => {
expect(rootElement).toBe(div);
expect(options).toEqual({ includeShadowRoots: true });
return 'EXPECTED_HTML';
});

expect(element.getInnerHTML({ includeShadowRoots: true })).toBe('EXPECTED_HTML');
});
});

describe('appendChild()', () => {
it('Appends a node to the "content" node.', () => {
const div = document.createElement('div');

expect(element.childNodes.length).toBe(0);
expect(element.content.childNodes.length).toBe(0);

element.appendChild(div);

expect(element.childNodes.length).toBe(0);
expect(element.content.childNodes.length).toBe(1);
expect(element.content.childNodes[0] === div).toBe(true);

element.removeChild(div);

expect(element.childNodes.length).toBe(0);
expect(element.content.childNodes.length).toBe(0);
});
});

describe('removeChild()', () => {
it('Removes a node from the "content" node.', () => {
const div = document.createElement('div');

element.appendChild(div);

expect(element.childNodes.length).toBe(0);
expect(element.content.childNodes.length).toBe(1);

element.removeChild(div);

expect(element.childNodes.length).toBe(0);
expect(element.content.childNodes.length).toBe(0);
});
});

describe('insertBefore()', () => {
it('Inserts a node before another node in the "content" node.', () => {
const div = document.createElement('div');
const span = document.createElement('span');
const underline = document.createElement('u');
element.appendChild(div);
element.appendChild(span);
element.insertBefore(underline, span);
expect(element.innerHTML).toBe('<div></div><u></u><span></span>');
});
});

describe('replaceChild()', () => {
it('Removes a node from the "content" node.', () => {
const div = document.createElement('div');
const span = document.createElement('span');
const underline = document.createElement('u');
const bold = document.createElement('b');
element.appendChild(div);
element.appendChild(underline);
element.appendChild(span);
element.replaceChild(bold, underline);
expect(element.innerHTML).toBe('<div></div><b></b><span></span>');
});
});

describe('cloneNode()', () => {
it('Clones the nodes of the "content" node.', () => {
element.innerHTML = '<div></div><b></b><span></span>';
const clone = element.cloneNode(true);
expect(clone.innerHTML).toBe('<div></div><b></b><span></span>');
});
});
});
8 changes: 4 additions & 4 deletions packages/happy-dom/test/xml-parser/XMLParser.test.ts
Expand Up @@ -167,15 +167,15 @@ describe('XMLParser', () => {
const root = XMLParser.parse(
window.document,
`<div>
<script>if(1<Math['random']()){else if(Math['random']()>1){console.log("1")}</script>
<script>if(1<Math['random']()){}else if(Math['random']()>1){console.log("1")}</script>
<script><b></b></script>
<style><b></b></style>
<template><b></b></template>
</div>`
);

expect((<IHTMLElement>root.children[0].children[0]).innerText).toBe(
`if(1<Math['random']()){else if(Math['random']()>1){console.log("1")}`
`if(1<Math['random']()){}else if(Math['random']()>1){console.log("1")}`
);

expect((<IHTMLElement>root.children[0].children[1]).innerText).toBe('<b></b>');
Expand All @@ -186,10 +186,10 @@ describe('XMLParser', () => {

expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe(
`<div>
<script>if(1<Math['random']()){else if(Math['random']()>1){console.log("1")}</script>
<script>if(1<Math['random']()){}else if(Math['random']()>1){console.log("1")}</script>
<script><b></b></script>
<style><b></b></style>
<template></template>
<template><b></b></template>
</div>`.replace(/[\s]/gm, '')
);

Expand Down
8 changes: 8 additions & 0 deletions packages/happy-dom/test/xml-serializer/XMLSerializer.test.ts
Expand Up @@ -74,6 +74,14 @@ describe('XMLSerializer', () => {
expect(xmlSerializer.serializeToString(div)).toBe('<div>Text 1.Text 2.</div>');
});

it('Serializes a template node.', () => {
const template = document.createElement('template');
template.innerHTML = '<div>Test</div>';
expect(xmlSerializer.serializeToString(template)).toBe(
'<template><div>Test</div></template>'
);
});

it('Serializes a mix of nodes.', () => {
const div = document.createElement('div');
const comment1 = document.createComment('Comment 1.');
Expand Down

0 comments on commit 33b84ca

Please sign in to comment.