import { App, Component, mount, onMounted, useState, xml } from "../../src/index";
import {
makeTestFixture,
nextAppError,
nextTick,
snapshotEverything,
steps,
useLogLifecycle,
} from "../helpers";
snapshotEverything();
let originalconsoleWarn = console.warn;
let mockConsoleWarn: any;
let fixture: HTMLElement;
beforeEach(() => {
fixture = makeTestFixture();
mockConsoleWarn = jest.fn(() => {});
console.warn = mockConsoleWarn;
});
afterEach(() => {
console.warn = originalconsoleWarn;
});
describe("list of components", () => {
test("simple list", async () => {
class Child extends Component {
static template = xml``;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({
elems: [
{ id: 1, value: "a" },
{ id: 2, value: "b" },
],
});
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("ab");
parent.state.elems.push({ id: 4, value: "d" });
await nextTick();
expect(fixture.innerHTML).toBe("abd");
parent.state.elems.pop();
await nextTick();
expect(fixture.innerHTML).toBe("ab");
});
test("components in a node in a t-foreach ", async () => {
class Child extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
setup() {
useLogLifecycle();
}
get items() {
return [1, 2];
}
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(
""
);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Child:mounted",
"Parent:mounted",
]
`);
});
test("reconciliation alg works for t-foreach in t-foreach", async () => {
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = { s: [{ blips: ["a1", "a2"] }, { blips: ["b1"] }] };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("");
});
test("reconciliation alg works for t-foreach in t-foreach, 2", async () => {
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ rows: [1, 2], cols: ["a", "b"] });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(
""
);
parent.state.rows = [2, 1];
await nextTick();
expect(fixture.innerHTML).toBe(
""
);
});
test("sub components rendered in a loop", async () => {
class Child extends Component {
static template = xml`
`;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ numbers: [1, 2, 3] });
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(``);
});
test("sub components with some state rendered in a loop", async () => {
let n = 1;
class Child extends Component {
static template = xml`
`;
state: any;
setup() {
this.state = useState({ n });
n++;
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({
numbers: [1, 2, 3],
});
}
const parent = await mount(Parent, fixture);
parent.state.numbers = [1, 3];
await nextTick();
expect(fixture.innerHTML).toBe(``);
});
test("list of sub components inside other nodes", async () => {
// this confuses the patching algorithm...
class SubComponent extends Component {
static template = xml`asdf`;
}
class Parent extends Component {
static template = xml`
`;
static components = { SubComponent };
state = useState({
blips: [
{ a: "a", id: 1 },
{ b: "b", id: 2 },
{ c: "c", id: 4 },
],
});
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(
""
);
parent.state.blips.splice(0, 1);
await nextTick();
expect(fixture.innerHTML).toBe(
""
);
});
test("t-foreach with t-component, and update", async () => {
class Child extends Component {
static template = xml`
`;
state = useState({ val: "A" });
setup() {
onMounted(() => {
this.state.val = "B";
});
}
}
class Parent extends Component {
static components = { Child };
static template = xml`
`;
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("A0A1
");
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("B0B1
");
});
test("switch component position", async () => {
const childInstances = [];
class Child extends Component {
static template = xml``;
setup() {
childInstances.push(this);
}
}
class Parent extends Component {
static components = { Child };
static template = xml`
`;
clist = [1, 2];
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1
2
");
parent.clist = [2, 1];
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe("2
1
");
expect(childInstances.length).toBe(2);
});
test("crash on duplicate key in dev mode", async () => {
const consoleInfo = console.info;
console.info = jest.fn();
class Child extends Component {
static template = xml``;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
}
const app = new App(Parent, { test: true });
const mountProm = expect(app.mount(fixture)).rejects.toThrow(
"Got duplicate key in t-foreach: child"
);
await expect(nextAppError(app)).resolves.toThrow("Got duplicate key in t-foreach: child");
await mountProm;
console.info = consoleInfo;
expect(mockConsoleWarn).toBeCalledTimes(1);
});
test("crash when using object as keys that serialize to the same string", async () => {
const consoleInfo = console.info;
console.info = jest.fn();
class Child extends Component {
static template = xml``;
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
}
const app = new App(Parent, { test: true });
const mountProm = expect(app.mount(fixture)).rejects.toThrow(
"Got duplicate key in t-foreach: [object Object]"
);
await expect(nextAppError(app)).resolves.toThrow(
"Got duplicate key in t-foreach: [object Object]"
);
await mountProm;
console.info = consoleInfo;
expect(mockConsoleWarn).toBeCalledTimes(1);
});
test("order is correct when slots are not of same type", async () => {
class Child extends Component {
static template = xml`
`;
get slotNames() {
return Object.entries(this.props.slots)
.filter((entry: any) => entry[1].active)
.map((entry) => entry[0]);
}
}
class Parent extends Component {
static template = xml`
A
B
C
`;
static components = { Child };
state = useState({ active: false });
}
const parent = await mount(Parent, fixture);
expect(fixture.textContent).toBe("AB");
parent.state.active = true;
await nextTick();
expect(fixture.textContent).toBe("BC");
});
});