v0.0.13: Refactored the render-loop to solve hard-to-track bugs and added more (elaborate) unit-tests to make sure it all works

This commit is contained in:
2020-04-15 16:33:32 +02:00
parent b95e5506d2
commit 0da07549e7
28 changed files with 1113 additions and 235 deletions

View File

@@ -0,0 +1,169 @@
import { render, CustomElement, Host, ShadowDOM, defineElement, state, prop } from "@cerxes/csx";
import { testContainer } from "../utils/test-container";
import { nextAnimationFrame } from "../utils/next-animation-frame";
describe("Shadow-DOM tests", () => {
/**
* Assert that shadow dom behaves as expected
*/
test("Simple shadow-component", async () => {
@defineElement('shadow-component')
class ShadowComponent extends CustomElement{
@prop()
title = 'Content here';
render(){
return (
<Host>
<ShadowDOM>
<div>
<h1>{this.title}</h1>
<slot />
</div>
</ShadowDOM>
</Host>
)
}
}
let initialVSpec = <ShadowComponent />;
let rendered = render(initialVSpec);
let container = testContainer(rendered);
document.body.appendChild(container);// Components need to be added to the DOM or their connectecCallback will not be called
// Initial render
expect(
container.innerHTML
).toBe([
`<shadow-component>`,
`</shadow-component>`,
].join(''));
expect(
rendered.shadowRoot.innerHTML
).toBe([
`<div>`,
`<h1>Content here</h1>`,
`<slot></slot>`,
`</div>`,
].join(''));
// Update behaves as it should
let updatedVSpec = (
<ShadowComponent title={"New content here"}>
<li><ul>contents</ul></li>
</ShadowComponent>
);
render(updatedVSpec, {host: rendered, old: initialVSpec});
// Wait for it to update
await nextAnimationFrame();
expect(
container.innerHTML
).toBe([
`<shadow-component>`,
`<li><ul>contents</ul></li>`,
`</shadow-component>`,
].join(''));
expect(
rendered.shadowRoot.innerHTML
).toBe([
`<div>`,
`<h1>New content here</h1>`,
`<slot></slot>`,
`</div>`,
].join(''));
document.body.removeChild(container);
});
test("Nested shadow-component", async () => {
@defineElement('todo-item')
class TodoItem extends CustomElement {
@prop()
get model(){ return this.#model; }
set model(value){ this.#model = value; }
#model;
render(){
return (
<Host>
<ShadowDOM>
<input type="checkbox" checked={ this.model.checked }/>
<label>
<slot />
</label>
</ShadowDOM>
</Host>
)
}
}
@defineElement('my-todo')
class MyTodo extends CustomElement {
@state()
todos = [
{ text: "todo 1", checked: true },
{ text: "todo 2", checked: false },
];
rendered = [];
render(){
return (
<Host>
<h1>Todos</h1>
<ul>
{this.todos.map((todo,index)=>(
<TodoItem model={todo}
ref={(el)=>this.rendered[index]=el}
>
{todo.text}
</TodoItem>
))}
</ul>
</Host>
)
}
}
let initialVSpec = <MyTodo />;
let rendered = render(initialVSpec);
let container = testContainer(rendered);
document.body.appendChild(container);// Components need to be added to the DOM or their connectecCallback will not be called
// Initial render
expect(
container.innerHTML
).toBe([
`<my-todo>`,
`<h1>Todos</h1>`,
`<ul>`,
...rendered.todos.map(todo=>(
`<todo-item>${todo.text}</todo-item>`
)),
`</ul>`,
`</my-todo>`,
].join(''));
for(let i = 0; i < rendered.todos.length; ++i){
let todo = rendered.todos[i];
let el = rendered.rendered[i];
expect(el).not.toBeUndefined();
expect(
el.shadowRoot.innerHTML
).toBe([
`<input type="checkbox"${todo.checked? ' checked=""': ''}>`,
`<label>`,
`<slot></slot>`,
`</label>`
].join(''));
}
document.body.removeChild(container);
});
});

159
jest/components/svg.test.js Normal file
View File

@@ -0,0 +1,159 @@
import { render, CustomElement, Host, ShadowDOM, defineElement, state, prop } from "@cerxes/csx";
import { testContainer } from "../utils/test-container";
import { nextAnimationFrame } from "../utils/next-animation-frame";
const svgs = {
"/assets/icons/checkbox-checked.svg": [
`<svg fill="currentcolor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">`,
`<title>checkbox-checked</title>`,
`<g><path d="M401.7,411.8c-38,0-76.1,0-114.1,0c-55.6,0-111.2,0-166.8,0c-6.3,0-12.6,0-19,0c-1.1,0-2.3,0-3.4,0 c-0.5,0-1.1,0-1.5,0c-4.5,0.1,4.2,1.3,0,0c-0.9-0.3-2.2-0.8-3.2-0.9c-2.3-0.2,4,2.4,0.6,0.2c-0.2-0.1-3.4-1.9-3.3-2 c0.1-0.2,3.5,3.2,0.1-0.2c-0.6-0.6-1.2-1.2-1.7-1.9c1.8,2.4,2.1,2.7,0.8,0.9c-0.1-0.2-2-3.3-1.9-3.4c0.7-0.4,1.2,4.6,0.4,0.5 c-0.1-0.3-0.9-3.2-0.8-3.2c0.1,0.9,0.2,1.8,0.4,2.6c-0.1-0.9-0.1-1.8-0.1-2.7c0-0.2,0-0.5,0-0.7c0-1.5,0-3.1,0-4.6 c0-7.2,0-14.5,0-21.7c0-56.3,0-112.6,0-168.9c0-25.9,0-51.9,0-77.8c0-7.6,0-15.3,0-22.9c0-1.8,0-3.6,0-5.4c0-0.4,0-0.7,0-1.1 c0-0.5,0-1.1,0-1.5c0-3.3,0.5,1.4-0.3,1.3c0,0,1.1-5.2,1.4-5.1c0.1,0-2.2,4.2,0.2,0.1c2.4-4.1,0,0-0.2-0.2 c-0.1-0.1,2.5-2.6,2.7-2.8c2.9-3.1-3.7,2,0,0c0.7-0.4,1.5-0.9,2.2-1.3c3.6-2.3-4.2,1,0,0c0.3-0.1,3.8-1.1,3.9-1 c-3.1,0.4-3.5,0.4-1.3,0.3c0.5,0,0.9,0,1.4,0c0.9,0,1.8,0,2.6,0c5.9,0,11.8,0,17.7,0c24,0,48,0,72,0c58.1,0,116.3,0,174.4,0 c12.2,0,24.4-0.2,36.6,0c0.5,0,1.2,0,1.4,0c4.5-0.1-4.2-1.3,0,0c0.3,0.1,3.8,0.9,3.8,1.1c0,0.1-4.2-2.2-0.1,0.2 c4.1,2.4,0,0,0.2-0.2c0.1-0.1,2.1,2,2.3,2.2c3.5,3.1-1.2-2.7,0.5,0.5c0.1,0.2,2,3.3,1.9,3.4c-0.3,0.1-1.5-4.5-0.2,0.1 c0.1,0.4,0.2,0.9,0.3,1.3c0.4,2.2,0.4,1.7-0.1-1.3c0.1,0,0.1,2.7,0.1,2.7c0,0.2,0,0.5,0,0.7c0,4.5,0,9.1,0,13.7 c0,53.1,0,106.2,0,159.2c0,42.9,0.2,85.8,0,128.7c0,0.3,0,0.7,0,1c0,0.5,0,1.1,0,1.5c0.1,4.5,1.3-4.2,0,0 c-0.1,0.3-0.9,3.8-1.1,3.8c-0.1,0,2.2-4.2-0.2-0.1c-0.2,0.3-1.3,2.2-1.4,2.2c-0.2-0.1,3.2-3.5-0.2-0.1c-3.3,3.3,0,0,0.1,0.2 c0.1,0.2-3.1,1.9-3.3,2c-3.6,2.3,4.2-1,0,0c-0.3,0.1-3.8,1.1-3.9,1c0.9-0.1,1.8-0.2,2.6-0.4C403.5,411.7,402.6,411.8,401.7,411.8 c-10.5,0.2-20.5,9-20,20c0.5,10.6,8.8,20.3,20,20c27.8-0.6,49.6-22.6,50.1-50.3c0.1-4.1,0-8.3,0-12.4c0-22.3,0-44.6,0-67 c0-60,0-120.1,0-180.1c0-14.4,0.2-28.8,0-43.2c-0.3-23.1-16.1-44.8-39.4-49.4c-6.3-1.3-12.3-1.2-18.7-1.2 c-51.3,0-102.5,0-153.8,0c-46.5,0-93,0-139.5,0c-17.6,0-33,7.3-43.3,21.6c-7,9.7-9,21.1-9,32.8c0,18.3,0,36.6,0,54.8 c0,60.9,0,121.8,0,182.8c0,18.8,0,37.6,0,56.5c0,10.2,1.3,20,6.2,29.2c8.9,16.8,27,25.9,45.6,25.9c45.4,0.1,90.7,0,136.1,0 c52.2,0,104.4,0,156.7,0c3,0,6,0,9,0c10.5,0,20.5-9.2,20-20C421.2,420.9,412.9,411.8,401.7,411.8z"></path></g>`,
`<g><path d="M136.9,276.6c20,18,40,36,60.1,54.1c8.1,7.3,20.2,7.8,28.3,0c15.6-15,31.1-30,46.7-45.1 c24.7-23.8,49.4-47.7,74.1-71.5c5.7-5.5,11.4-11,17.2-16.6c7.5-7.3,7.9-21.1,0-28.3c-8.1-7.4-20.2-7.8-28.3,0 c-15.6,15-31.1,30-46.7,45.1c-24.7,23.8-49.4,47.7-74.1,71.5c-5.7,5.5-11.4,11-17.2,16.6c9.4,0,18.9,0,28.3,0 c-20-18-40-36-60.1-54.1c-7.8-7-20.7-8.2-28.3,0C129.9,256,128.6,269.2,136.9,276.6L136.9,276.6z"></path></g>`,
`</svg>`
].join(''),
"/assets/icons/checkbox.svg": [
`<svg fill="currentcolor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">`,
`<title>checkbox</title>`,
`<g><path d="M401.7,411.8c-38,0-76.1,0-114.1,0c-55.6,0-111.2,0-166.8,0c-6.3,0-12.6,0-19,0c-1.1,0-2.3,0-3.4,0 c-0.5,0-1.1,0-1.5,0c-4.5,0.1,4.2,1.3,0,0c-0.9-0.3-2.2-0.8-3.2-0.9c-2.3-0.2,4,2.4,0.6,0.2c-0.2-0.1-3.4-1.9-3.3-2 c0.1-0.2,3.5,3.2,0.1-0.2c-0.6-0.6-1.2-1.2-1.7-1.9c1.8,2.4,2.1,2.7,0.8,0.9c-0.1-0.2-2-3.3-1.9-3.4c0.7-0.4,1.2,4.6,0.4,0.5 c-0.1-0.3-0.9-3.2-0.8-3.2c0.1,0.9,0.2,1.8,0.4,2.6c-0.1-0.9-0.1-1.8-0.1-2.7c0-0.2,0-0.5,0-0.7c0-1.5,0-3.1,0-4.6 c0-7.2,0-14.5,0-21.7c0-56.3,0-112.6,0-168.9c0-25.9,0-51.9,0-77.8c0-7.6,0-15.3,0-22.9c0-1.8,0-3.6,0-5.4c0-0.4,0-0.7,0-1.1 c0-0.5,0-1.1,0-1.5c0-3.3,0.5,1.4-0.3,1.3c0,0,1.1-5.2,1.4-5.1c0.1,0-2.2,4.2,0.2,0.1c2.4-4.1,0,0-0.2-0.2 c-0.1-0.1,2.5-2.6,2.7-2.8c2.9-3.1-3.7,2,0,0c0.7-0.4,1.5-0.9,2.2-1.3c3.6-2.3-4.2,1,0,0c0.3-0.1,3.8-1.1,3.9-1 c-3.1,0.4-3.5,0.4-1.3,0.3c0.5,0,0.9,0,1.4,0c0.9,0,1.8,0,2.6,0c5.9,0,11.8,0,17.7,0c24,0,48,0,72,0c58.1,0,116.3,0,174.4,0 c12.2,0,24.4-0.2,36.6,0c0.5,0,1.2,0,1.4,0c4.5-0.1-4.2-1.3,0,0c0.3,0.1,3.8,0.9,3.8,1.1c0,0.1-4.2-2.2-0.1,0.2 c4.1,2.4,0,0,0.2-0.2c0.1-0.1,2.1,2,2.3,2.2c3.5,3.1-1.2-2.7,0.5,0.5c0.1,0.2,2,3.3,1.9,3.4c-0.3,0.1-1.5-4.5-0.2,0.1 c0.1,0.4,0.2,0.9,0.3,1.3c0.4,2.2,0.4,1.7-0.1-1.3c0.1,0,0.1,2.7,0.1,2.7c0,0.2,0,0.5,0,0.7c0,4.5,0,9.1,0,13.7 c0,53.1,0,106.2,0,159.2c0,42.9,0.2,85.8,0,128.7c0,0.3,0,0.7,0,1c0,0.5,0,1.1,0,1.5c0.1,4.5,1.3-4.2,0,0 c-0.1,0.3-0.9,3.8-1.1,3.8c-0.1,0,2.2-4.2-0.2-0.1c-0.2,0.3-1.3,2.2-1.4,2.2c-0.2-0.1,3.2-3.5-0.2-0.1c-3.3,3.3,0,0,0.1,0.2 c0.1,0.2-3.1,1.9-3.3,2c-3.6,2.3,4.2-1,0,0c-0.3,0.1-3.8,1.1-3.9,1c0.9-0.1,1.8-0.2,2.6-0.4C403.5,411.7,402.6,411.8,401.7,411.8 c-10.5,0.2-20.5,9-20,20c0.5,10.6,8.8,20.3,20,20c27.8-0.6,49.6-22.6,50.1-50.3c0.1-4.1,0-8.3,0-12.4c0-22.3,0-44.6,0-67 c0-60,0-120.1,0-180.1c0-14.4,0.2-28.8,0-43.2c-0.3-23.1-16.1-44.8-39.4-49.4c-6.3-1.3-12.3-1.2-18.7-1.2c-51.3,0-102.5,0-153.8,0 c-46.5,0-93,0-139.5,0c-17.6,0-33,7.3-43.3,21.6c-7,9.7-9,21.1-9,32.8c0,18.3,0,36.6,0,54.8c0,60.9,0,121.8,0,182.8 c0,18.8,0,37.6,0,56.5c0,10.2,1.3,20,6.2,29.2c8.9,16.8,27,25.9,45.6,25.9c45.4,0.1,90.7,0,136.1,0c52.2,0,104.4,0,156.7,0 c3,0,6,0,9,0c10.5,0,20.5-9.2,20-20C421.2,420.9,412.9,411.8,401.7,411.8z"></path></g>`,
`</svg>`
].join('')
};
const iconStyle = `:host { display: inline-block; }`;// The rest is left out
describe("SVG Component tests", () => {
/**
* Assert that a basic component renders as expected
*/
test("Simple example-component", async () => {
let svgCache = new Map();
async function fetchSvg(svgUrl){
// Fake timer
await new Promise((resolve,reject)=>setTimeout(()=>resolve(), 1));
let svg = svgs[svgUrl];
if(svg){
return {
text: ()=>svg
}
}else{
throw new Error("Not found");
}
}
/**
* @param {string} svgUrl
* @returns {Promise<Element>}
*/
async function loadSvg(svgUrl) {
const response = await fetchSvg(svgUrl);
const svgResource = await response.text();
const parser = new DOMParser();
const svgDocument = parser.parseFromString(svgResource, "image/svg+xml");
let svgElement = svgDocument.firstElementChild;
if (svgElement.hasAttribute("fill")) {
svgElement.setAttribute("fill", "currentcolor")
}
svgCache.set(svgUrl, svgElement);
return svgElement;
}
@defineElement("test-icon")
class Icon extends CustomElement {
// Again JEST fucks this up in that these magically become read-only (probably not using our CSX-version of babel!)
@state()
set svgElement(value){ this.#svgElement = value};
get svgElement(){ return this.#svgElement};
#svgElement;
/**
* @param {string} icon
*/
@prop() set icon(icon) {
if(icon !== this.#icon) {
this.#icon = icon;
this.updateIcon();
}
};
get icon(){
return this.#icon;
}
#icon;
updateIcon(){
let icon = this.#icon;
const svgUrl = `/assets/icons/${icon}.svg`;
let cached = svgCache.get(svgUrl);
if(cached){
// Use from cache (without passing by async functions, to optimize rendering loop!)
this.svgElement = cached.cloneNode(true);
}else{
loadSvg(svgUrl).then(svgEl=> {
if(icon===this.#icon) {
// If this is still the desired icon, load it
this.svgElement = svgEl.cloneNode(true);
}
});
}
}
render() {
return (
<Host>
<ShadowDOM>
<style>{iconStyle}</style>
<div className="icon">
{this.svgElement}
</div>
</ShadowDOM>
</Host>
);
}
}
let initialVSpec = <Icon icon={'checkbox'}/>
let rendered = render(initialVSpec);
let container = testContainer(rendered);
document.body.appendChild(container);// Components need to be added to the DOM or their connectecCallback will not be called
expect(
container.innerHTML
).toBe([
`<test-icon>`,
`</test-icon>`,
].join(''));
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),10));
await nextAnimationFrame();
expect(
rendered.shadowRoot.innerHTML
).toBe([
`<style>${iconStyle}</style>`,
`<div class="icon">`,
svgs["/assets/icons/checkbox.svg"],
`</div>`
].join(''));
let updatedVSpec = <Icon icon={'checkbox-checked'}/>;
render(updatedVSpec, {host: rendered, old: initialVSpec});
await new Promise((resolve,reject)=>setTimeout(()=>resolve(),10));
await nextAnimationFrame();
expect(
rendered.shadowRoot.innerHTML
).toBe([
`<style>${iconStyle}</style>`,
`<div class="icon">`,
svgs["/assets/icons/checkbox-checked.svg"],
`</div>`
].join(''));
document.body.removeChild(container);
});
});

View File

@@ -194,13 +194,14 @@ describe("Children", () => {
].join(''));
});
test("Nulls are ignored", async () => {
test("Nulls and undefined are ignored", async () => {
expect(
testContainer(
render(
<div class="container">
<h1>Title</h1>
{null}
{undefined}
<button>Save</button>
</div>
)
@@ -212,4 +213,50 @@ describe("Children", () => {
`</div>`
].join(''));
});
test("Update maintains the same elements where possible", async () => {
let initialVSpec = (
<div class="container">
<h1>Title</h1>
<button>Save</button>
</div>
);
let rendered = render(initialVSpec);
let container = testContainer(rendered);
let children = Array.from(rendered.childNodes);// Capture current child-nodes
expect(
container.innerHTML
).toBe([
`<div class="container">`,
`<h1>Title</h1>`,
`<button>Save</button>`,
`</div>`
].join(''));
// Update it
let updatedVSpec = (
<div class="container">
<h1>Update</h1>
<button>Dismiss</button>
</div>
);
render(updatedVSpec, {host: rendered, old: initialVSpec});
expect(
container.innerHTML
).toBe([
`<div class="container">`,
`<h1>Update</h1>`,
`<button>Dismiss</button>`,
`</div>`
].join(''));
let updatedChildren = Array.from(rendered.childNodes);// Capture current child-nodes
expect(children.length).toBe(updatedChildren.length);
for(let i = 0; i < children.length; ++i){
expect(children[i] === updatedChildren[i]).toBe(true);// Expect the element to be the same by ref
}
});
});

View File

@@ -3,14 +3,14 @@ import { testContainer } from "../utils/test-container";
describe("Key-property tests", () => {
test("Keyed list", async () => {
let renderedIndexes = [];
let initIndexes = [0, 1, 2, 3];
let initialRendered = new Map();
let initIndexes = [1, 2, 3, 4];
let makeSpec = (targetList, indexes)=>(
<ul>
{
indexes.map(index => (
<li id={`li_${index}`} key={index} ref={(el) => targetList[ index ] = el}>
<li id={`li_${index}`} key={index} ref={(el) => targetList.set(index, el)}>
{index}
</li>
))
@@ -18,7 +18,7 @@ describe("Key-property tests", () => {
</ul>
);
let initialVSpec = makeSpec(renderedIndexes, initIndexes);
let initialVSpec = makeSpec(initialRendered, initIndexes);
let rendered = render(initialVSpec);
let container = testContainer(rendered);
@@ -31,17 +31,18 @@ describe("Key-property tests", () => {
].join('')
);
expect(renderedIndexes.length).toBe(4);
for(let rendered of renderedIndexes){
expect(initialRendered.size).toBe(4);
for(let rendered of initialRendered){
expect(rendered).not.toBeUndefined();
}
let reorderedIndexes = [3,2,1,0];
let rerenderedIndexes = renderedIndexes.slice();
// Reverse order
let reorderedIndexes = [4,3,2,1];
let rerenderedIndexes = new Map(initialRendered);
let updatedVSpec = makeSpec(rerenderedIndexes, reorderedIndexes);
render(updatedVSpec, {host: rendered, old: initialVSpec});// Is this host right? it seems inconsistent and the source of our bug (as it is probably also misused in custom-elements)
render(updatedVSpec, {host: rendered, old: initialVSpec});
// Updated
// Updated (reverse order)
expect(container.innerHTML).toBe(
[
`<ul>`,
@@ -51,11 +52,46 @@ describe("Key-property tests", () => {
);
// Validate that items were merely re-arranged and not re-created
expect(rerenderedIndexes.length).toBe(4);
for(let i=0; i<4; ++i){
let initRendered = renderedIndexes[i];
let reorderedRendered = rerenderedIndexes[i];
expect(rerenderedIndexes.size).toBe(4);
for(let i of initIndexes){
let initRendered = initialRendered.get(i);
let reorderedRendered = rerenderedIndexes.get(i);
expect(initRendered === reorderedRendered).toBe(true); // These should've remained the same
}
// Add items and change order
let additionalIndexes = [0, 1, 2.5, 2, 3, 4, 5.5];
let additionalRerenderedIndexes = new Map(initialRendered);
let secondUpdatedVSpec = makeSpec(additionalRerenderedIndexes, additionalIndexes);
render(secondUpdatedVSpec, {host: rendered, old: updatedVSpec});// Is this host right? it seems inconsistent and the source of our bug (as it is probably also misused in custom-elements)
// Validate add items and changed order
expect(container.innerHTML).toBe(
[
`<ul>`,
...additionalIndexes.map(index=>`<li id="${`li_${index}`}">${index}</li>`),
`</ul>`
].join('')
);
// Validate that items were merely re-arranged and not re-created
expect(additionalRerenderedIndexes.size).toBe(additionalIndexes.length);
for(let i of initIndexes){
let initRendered = initialRendered.get(i);
let additionalRendered = additionalRerenderedIndexes.get(i);
expect(initRendered === additionalRendered).toBe(true); // These should've still remained the same
}
// Revert back to the original
render(initialVSpec, {host: rendered, old: secondUpdatedVSpec});// Is this host right? it seems inconsistent and the source of our bug (as it is probably also misused in custom-elements)
// Validate reverting back to the original
expect(container.innerHTML).toBe(
[
`<ul>`,
...initIndexes.map(index=>`<li id="${`li_${index}`}">${index}</li>`),
`</ul>`
].join('')
);
});
});

View File

@@ -0,0 +1,62 @@
import { render } from "@cerxes/csx";
import { testContainer } from "../utils/test-container";
describe("SVG-rendering test", () => {
test("Simple", async () => {
let makeSpec = (stroke, strokeWidth) => (
<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg"
stroke={stroke}>
<g fill="none" fill-rule="evenodd">
<g transform="translate(1 1)" stroke-width={strokeWidth}>
<circle stroke-opacity=".5" cx="18" cy="18" r="18"/>
<path d="M36 18c0-9.94-8.06-18-18-18">
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="1s"
repeatCount="indefinite"/>
</path>
</g>
</g>
</svg>
)
let initialVSpec = makeSpec("#000", 2);
let rendered = render(initialVSpec);
let container = testContainer(rendered);
expect(container.innerHTML).toBe([
`<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#000">`,
`<g fill="none" fill-rule="evenodd">`,
`<g transform="translate(1 1)" stroke-width="2">`,
`<circle stroke-opacity=".5" cx="18" cy="18" r="18">`,
`</circle>`,
`<path d="M36 18c0-9.94-8.06-18-18-18">`,
`<animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="1s" repeatCount="indefinite">`,
`</animateTransform>`,
`</path>`,
`</g>`,
`</g>`,
`</svg>`
].join(''));
let updatedVSpec = makeSpec("#FFF", 4);
render(updatedVSpec, { host: rendered, old: initialVSpec });
expect(container.innerHTML).toBe([
`<svg width="38" height="38" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#FFF">`,
`<g fill="none" fill-rule="evenodd">`,
`<g transform="translate(1 1)" stroke-width="4">`,
`<circle stroke-opacity=".5" cx="18" cy="18" r="18">`,
`</circle>`,
`<path d="M36 18c0-9.94-8.06-18-18-18">`,
`<animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="1s" repeatCount="indefinite">`,
`</animateTransform>`,
`</path>`,
`</g>`,
`</g>`,
`</svg>`
].join(''));
});
});