import { Component, mount, onWillUpdateProps, useState, xml } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("basics", () => {
test("explicit object prop", async () => {
class Child extends Component {
static template = xml` `;
state: any;
setup() {
this.state = useState({ someval: this.props.value });
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ val: 42 });
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("42
");
});
test("prop names can contain -", async () => {
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("7
");
});
test("accept ES6-like syntax for props (with getters)", async () => {
class Child extends Component {
static template = xml` `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
get greetings() {
const name = "aaron";
return `hello ${name}`;
}
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("hello aaron
");
});
test("t-set works ", async () => {
class Child extends Component {
static template = xml` `;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("42
");
});
test("t-set with a body expression can be used as textual prop", async () => {
class Child extends Component {
static template = xml` `;
}
class Parent extends Component {
static components = { Child };
static template = xml`
42
`;
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("42
");
});
test("t-set with a body expression can be passed in props, and then t-out", async () => {
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
`;
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("");
});
test("arrow functions as prop correctly capture their scope", async () => {
class Child extends Component {
static template = xml` `;
}
let onClickArgs: [number, MouseEvent] | null = null;
class Parent extends Component {
static template = xml`
`;
static components = { Child };
items = [{ val: 1 }, { val: 2 }, { val: 3 }, { val: 4 }];
onClick(n: number, ev: MouseEvent) {
onClickArgs = [n, ev];
}
}
await mount(Parent, fixture);
expect(onClickArgs).toBeNull();
(fixture.querySelector("button")).click();
expect(onClickArgs![0]).toBe(1);
expect(onClickArgs![1]).toBeInstanceOf(MouseEvent);
});
test("support prop names that aren't valid bare object property names", async () => {
expect.assertions(3);
class Child extends Component {
static template = xml` `;
setup() {
expect(this.props["some-dashed-prop"]).toBe(5);
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
}
await mount(Parent, fixture);
});
test("template string in prop", async () => {
expect.assertions(3);
class Child extends Component {
static template = xml``;
setup() {
expect(this.props.propName).toBe("123");
}
}
class Parent extends Component {
static template = xml({ raw: [' '] });
static components = { Child };
someVal = 2;
}
await mount(Parent, fixture);
});
});
test("can bind function prop with bind suffix", async () => {
class Child extends Component {
static template = xml`child`;
setup() {
this.props.doSomething(123);
}
}
let boundedThing: any = null;
class Parent extends Component {
static template = xml` `;
static components = { Child };
doSomething(val: number) {
expect(val).toBe(123);
boundedThing = this;
}
}
const parent = await mount(Parent, fixture);
expect(boundedThing).toBe(parent);
expect(fixture.innerHTML).toBe("child");
});
test("do not crash when binding anonymous function prop with bind suffix", async () => {
class Child extends Component {
static template = xml`child`;
setup() {
this.props.doSomething(123);
}
}
let boundedThing: any = null;
class Parent extends Component {
static template = xml` `;
static components = { Child };
doSomething(val: number) {
expect(val).toBe(123);
boundedThing = this;
}
}
const parent = await mount(Parent, fixture);
expect(boundedThing).toBe(parent);
expect(fixture.innerHTML).toBe("child");
});
test("bound functions is not referentially equal after update", async () => {
let isEqual = false;
class Child extends Component {
static template = xml` `;
setup() {
onWillUpdateProps((nextProps: any) => {
isEqual = nextProps.fn === this.props.fn;
});
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ val: 1 });
someFunction() {}
}
const parent = await mount(Parent, fixture);
parent.state.val = 3;
await nextTick();
expect(fixture.innerHTML).toBe("3");
expect(isEqual).toBe(false);
});
test("bound functions are considered 'alike'", async () => {
class Child extends Component {
static template = xml`child`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
someFunction() {}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1child");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
parent.state.val = 3;
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("3child");
});
test("throw if prop uses an unknown suffix", async () => {
class Child extends Component {
static template = xml` `;
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
}
await expect(async () => {
await mount(Parent, fixture);
}).rejects.toThrowError("Invalid prop suffix");
});
test(".alike suffix in a simple case", async () => {
class Child extends Component {
static template = xml` `;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ counter: 0 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(fixture.innerHTML).toBe("01");
parent.state.counter++;
await nextTick();
expect(fixture.innerHTML).toBe("11");
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
});
test(".alike suffix in a list", async () => {
class Todo extends Component {
static template = xml`
V
`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Todo };
state = useState({
elems: [
{ id: 1, isChecked: false },
{ id: 2, isChecked: true },
],
});
setup() {
useLogLifecycle();
}
toggle(id: number) {
const todo = this.state.elems.find((el) => el.id === id)!;
todo.isChecked = !todo.isChecked;
}
}
await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Todo:setup",
"Todo:willStart",
"Todo:setup",
"Todo:willStart",
"Parent:rendered",
"Todo:willRender",
"Todo:rendered",
"Todo:willRender",
"Todo:rendered",
"Todo:mounted",
"Todo:mounted",
"Parent:mounted",
]).toBeLogged();
expect(fixture.innerHTML).toBe("1 2V ");
fixture.querySelector("button")?.click();
await nextTick();
expect(fixture.innerHTML).toBe("1V 2V ");
expect([
"Parent:willRender",
"Parent:rendered",
"Todo:willRender",
"Todo:rendered",
"Todo:willPatch",
"Todo:patched",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
});