zliq
Version:
slim and quick framework in low loc
596 lines (548 loc) • 15.6 kB
JavaScript
import { render, zx, stream, if$, isStream, join$, Component } from "../src";
import { testRender, test$ } from "./helpers/test-component";
// TODO groom tests
describe("Components", () => {
it("should show a component", done => {
testRender(zx`<p>HELLO WORLD</p>`, ["<p>HELLO WORLD</p>"], done);
});
it("should return a constructor function", () => {
let constructor = zx`<p>HELLO WORLD</p>`;
expect(constructor).toBeInstanceOf(Component);
});
it("should return a virtual dom stream when constructed", () => {
let component = zx`<p>HELLO WORLD</p>`;
let vdom$ = component.build({});
expect(isStream(vdom$)).toBe(true);
expect(vdom$.value.tag).toBe("p");
expect(vdom$.value.props).toEqual({});
expect(vdom$.value.children).toEqual(["HELLO WORLD"]);
});
it("should render into a parentElement provided", done => {
let container = document.createElement("div");
test$(
render(zx`<p>HELLO WORLD</p>`, container),
[
({ element }) =>
expect(container.innerHTML).toEqual("<p>HELLO WORLD</p>")
],
done
);
});
it("should render without a parentElement provided", done => {
test$(
render(zx`<p>HELLO WORLD</p>`, null),
[({ element }) => expect(element.outerHTML).toBe("<p>HELLO WORLD</p>")],
done
);
});
let DoubleClicks = ({ clicks$ }) => zx`
<p>Clicks times 2: ${clicks$.map(clicks => 2 * clicks)}</p>
`;
it("should react to inputs", done => {
let clicks$ = stream(3);
testRender(
DoubleClicks({ clicks$ }),
[
({ element }) =>
expect(element.outerHTML).toBe("<p>Clicks times 2: 6</p>")
],
done
);
});
it("CleverComponent should update on input stream update", done => {
let clicks$ = stream(3);
testRender(
DoubleClicks({ clicks$ }),
[
({ element }) => {
expect(element.outerHTML).toBe("<p>Clicks times 2: 6</p>");
clicks$(6);
},
({ element }) =>
expect(element.outerHTML).toBe("<p>Clicks times 2: 12</p>")
],
done
);
});
it("Components should have access to the provided globals", done => {
const ShowGlobals = new Component(globals => {
return zx`<p>${globals.value}</p>`;
});
let app = zx`<div>
${ShowGlobals}
</div>`;
testRender(app, ["<div><p>GLOBAL TEXT</p></div>"], done, {
globals: {
value: "GLOBAL TEXT"
}
});
});
it("should update elements with textnodes", done => {
let trigger$ = stream(true);
testRender(
zx`<p>${if$(trigger$, zx`<div />`, "HELLO WORLD")}</p>`,
["<p><div></div></p>", "<p>HELLO WORLD</p>"],
done
).schedule([() => trigger$(false), null]);
});
it("should set the class", done => {
let class$ = stream("x");
let app = zx`<div class=${class$} />`;
testRender(
app,
[
({ element }) => {
expect(element.classList).toContain("x");
class$(null);
},
({ element }) => {
expect(element.classList).not.toContain("x");
}
],
done
);
});
it("should resolve nested streams in props", done => {
let trigger$ = stream(false);
let style = {
display: if$(trigger$, "block", "none")
};
let app = zx`<div>
<div style=${style} />
</div>`;
testRender(
app,
[
'<div><div style="display: none;"></div></div>',
'<div><div style="display: block;"></div></div>'
],
done
);
setTimeout(() => {
trigger$(true);
}, 10);
});
it("should allow returning streams from components", done => {
let Component = () => stream(zx`<p>Hello World</p>`);
let app = zx`
<div>
${Component()}
</div>
`;
testRender(app, ["<div><p>Hello World</p></div>"], done);
});
it("should allow returning arrays of subcomponents from components", done => {
let Component = () => [zx`<p>Hello</p>`, zx`<p>World</p>`];
let app = zx`
<div>
${Component()}
</div>
`;
testRender(app, ["<div><p>Hello</p><p>World</p></div>"], done);
});
it("should allow simple components that just receive resolved props", done => {
let component = props =>
new Component(
globals => zx`
<div>
${props.hello} ${globals.bye}
</div>
`
);
let app = zx`
<div>
${component({ hello: "world" })}
</div>
`;
testRender(app, ["<div><div>world cu</div></div>"], done, {
globals: {
bye: "cu"
}
});
});
it("should set style in different ways", done => {
let style$ = stream("width: 100px;");
let app = zx`<div style=${style$} />`;
testRender(
app,
[
({ element }) => {
expect(element.style.width).toBe("100px");
style$({ height: "200px" });
},
({ element }) => {
expect(element.style.width).toBe("");
expect(element.style.height).toBe("200px");
style$(null);
},
({ element }) => {
expect(element.style.width).toBe("");
expect(element.style.height).toBe("");
}
],
done
);
});
it("should react to attached events", done => {
// input streams are scoped to be able to remove the listener if the element gets removed
// this means you can not manipulate the stream from the inside to the outside but need to use a callback function
let DumbComponent = ({ clicks$, onclick }) => zx`
<div>
<button onclick=${() => onclick(clicks$.value + 1)}>
Click to emit event
</button>
</div>
`;
let clicks$ = stream(0);
testRender(
// this component fires a action on the store when clicked
DumbComponent({ clicks$, onclick: x => clicks$(x) }),
[
// perform the actions on the element
({ element }) => {
element.querySelector("button").click();
expect(clicks$.value).toBe(1);
}
],
done
);
});
it("should update lists correctly", done => {
var arr = [];
var length = 3;
for (let i = 0; i < length; i++) {
arr.push({ name: i });
}
let list$ = stream(arr);
let listElems$ = list$.map(arr => arr.map(x => zx`<li>${x.name}</li>`));
let component = zx`<ul>${listElems$}</ul>`;
testRender(
component,
[
({ element }) => {
expect(element.querySelectorAll("li").length).toBe(3);
expect(element.querySelectorAll("li")[2].innerHTML).toBe("2");
let newArr = arr.slice(1);
list$(newArr);
},
({ element }) => {
expect(element.querySelectorAll("li").length).toBe(2);
expect(element.querySelectorAll("li")[1].innerHTML).toBe("2");
}
],
done
);
});
it("should remove attributes on null value", done => {
let value$ = stream(true);
let component = zx`<div disabled=${value$} />`;
testRender(
component,
[
({ element }) => {
expect(element.disabled).toBe(true);
value$(null);
},
({ element }) => {
expect(element.disabled).toBe(undefined);
}
],
done
);
});
it("should remove attributes on updates if not available anymore", done => {
let trigger$ = stream(true);
let app = zx`
<div>
${if$(
trigger$,
zx`<img src="img_girl.jpg" width="500" height="600" />`,
zx`<img src="img_girl.jpg" height="600" />`
)}
</div>
`;
testRender(
app,
[
({ element }) => {
expect(element.querySelector("img").getAttribute("width")).toBe(
"500"
);
trigger$(false);
},
({ element }) => {
expect(element.querySelector("img").getAttribute("width")).toBe(null);
}
],
done
);
});
it("should trigger lifecycle events on nested components", done => {
const mountedMock = jest.fn();
const createdMock = jest.fn();
const removedMock = jest.fn();
let trigger$ = stream(true);
let cycle = {
mounted: mountedMock,
created: createdMock,
removed: removedMock
};
const component = zx`<div id="test" cycle=${cycle} />`;
let app = zx`<div>${if$(trigger$, component)}</div>`;
testRender(
app,
[
() => trigger$(false),
() => trigger$(true),
() => {
expect(mountedMock.mock.calls.length).toBe(2);
expect(createdMock.mock.calls.length).toBe(1);
expect(removedMock.mock.calls.length).toBe(1);
}
],
done
);
});
it("should isolate children from updates", done => {
let trigger$ = stream(true);
let app = zx`<div isolated>${if$(
trigger$,
"HALLO WORLD",
"BYE WORLD"
)}</div>`;
testRender(
app,
["<div>HALLO WORLD</div>", "<div>HALLO WORLD</div>"],
done
).schedule([
() => {
trigger$(false);
},
null
]);
});
it("should increment versions up to the root", done => {
let content$ = stream("");
let app = zx`
<div>
<div>${content$}</div>
</div>
`;
testRender(
app,
[
({ element, version }) => {
expect(version).toBe(0);
content$("text");
},
({ element, version }) => {
expect(version).toBe(1);
}
],
done
);
});
it("should save id elements to reuse them", done => {
let content$ = stream("");
let app = zx`
<div>
<div id="test">${content$}</div>
</div>
`;
let i;
testRender(
app,
[
({ keyContainer }) => {
expect(keyContainer["test"].element.outerHTML).toMatchSnapshot();
expect(keyContainer["test"].version).toBe(0);
},
({ keyContainer }) => {
expect(keyContainer["test"].element.outerHTML).toMatchSnapshot();
expect(keyContainer["test"].version).toBe(1);
}
],
done
).schedule([() => content$("text"), null]);
});
it("should reuse id elements on rerenderings", done => {
let content$ = stream("");
let app = zx`
<div>
${content$}
<div id="test" />
</div>
`;
testRender(
app,
[
({ element, keyContainer }) => {
// manipulating the dom to prove update
element.replaceChild(
document.createElement("div"),
keyContainer["test"].element
);
// manipulating the stored element
keyContainer["test"].element.setAttribute("id", "updated");
content$("text");
},
({ element, keyContainer }) => {
expect(element.querySelector("#updated")).not.toBe(null);
}
],
done
);
});
it("should debounce renderings", done => {
let content$ = stream("");
let app = zx`<div>${content$}</div>`;
const myMock = jest.fn();
testRender(
app,
[
() => {
setImmediate(() => content$("text"));
setImmediate(() => content$("text2"));
},
() => {
// render only ran twice for 3 values
}
],
done,
{
debounce: 50
}
);
});
it("should replace idd elements again", done => {
let trigger$ = stream(false);
let app = zx`
<div>
<img />
${if$(
trigger$,
zx`<i id="x" />`,
zx`<div>
<div />
</div>`
)}
</div>
`;
testRender(
app,
[
({ element }) => {
expect(element.querySelector("#x")).toBeNull();
},
({ element }) => {
expect(element.querySelector("#x")).not.toBeNull();
},
({ element }) => {
expect(element.querySelector("#x")).toBeNull();
}
],
done
).schedule([() => trigger$(true), () => trigger$(false), null]);
});
it("should print a warning if the child nodes have been removed/added outside of zliq", done => {
let trigger$ = stream(false);
let app = zx`
<div>
<div id="remove"></div>
<div id="stays"></div>
${if$(trigger$, zx`<div id="added"></div>`)}
</div>
`;
let spy = jest.spyOn(global.console, "warn");
global.console.warn.mockImplementation(() => {});
testRender(
app,
[
({ element }) => {
element.querySelector("#remove").remove();
},
({ element }) => {
expect(spy).toHaveBeenCalled();
global.console.warn.mockReset();
element.appendChild(document.createElement("div"));
},
() => {}
],
done
).schedule([() => trigger$(true), () => trigger$(false), null]);
});
it("should resolve in streams nested elements with streams", done => {
let trigger$ = stream(false);
let trigger2$ = stream(false);
let app = zx`
<div>${if$(
trigger$,
zx`<div class=${join$(if$(trigger2$, "bold"))} />`
)}</div>
`;
testRender(
app,
[
"<div></div>",
'<div><div class=""></div></div>',
'<div><div class="bold"></div></div>'
],
done
).schedule([() => trigger$(true), () => trigger2$(true), null]);
});
it("should allow a component as root element", done => {
let component = new Component(globals => zx`<div>TESTING A STRING</div>`);
testRender(component, [`<div>TESTING A STRING</div>`], done);
});
it("should allow a component to return a component", done => {
let component = new Component(
globals => new Component(globals => zx`<div>TESTING A STRING</div>`)
);
testRender(component, [`<div>TESTING A STRING</div>`], done);
});
it("should allow a list of elements to be returned from a component", done => {
let component = new Component(globals => [
"TESTING A STRING",
zx`<div>HALLO WORLD</div>`,
"ANOTHER STRING"
]);
testRender(
zx`<div>${component}</div>`,
[`<div>TESTING A STRING<div>HALLO WORLD</div>ANOTHER STRING</div>`],
done
);
});
it("should allow a list of elements to be returned from a component", done => {
let component = zx`TESTING A STRING<div>HALLO WORLD</div>ANOTHER STRING`;
testRender(
zx`<div>${component}</div>`,
[`<div>TESTING A STRING<div>HALLO WORLD</div>ANOTHER STRING</div>`],
done
);
});
it("should trim line breaks in array returns of the template function", done => {
let component = zx`TESTING A STRING
<div>HALLO WORLD</div>
ANOTHER STRING`;
testRender(
zx`<div>${component}</div>`,
[`<div>TESTING A STRING<div>HALLO WORLD</div>ANOTHER STRING</div>`],
done
);
});
it("should allow returning streams of Component constructors", done => {
let component$ = stream(zx`<div></div>`);
testRender(component$, [`<div></div>`, `<p></p>`], done).schedule([
() => component$(new Component(globals => zx`<p></p>`)),
null
]);
});
it("should allow nesting Component constructors", done => {
let component = new Component(x => new Component(y => zx`<div></div>`));
testRender(component, [`<div></div>`], done);
});
it("should allow custom attributes", done => {
let component = zx`<div *custom="hallo_world"></div>`;
testRender(component, [`<div custom="hallo_world"></div>`], done);
});
});