import { Component } from "../../src/runtime/component"; import { mount, useState, xml } from "../../src/index"; import { editInput, makeTestFixture, nextTick, snapshotEverything } from "../helpers"; snapshotEverything(); let fixture: HTMLElement; beforeEach(() => { fixture = makeTestFixture(); }); describe("t-model directive", () => { test("basic use, on an input", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "" }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe("
"); const input = fixture.querySelector("input")!; await editInput(input, "test"); expect(comp.state.text).toBe("test"); expect(fixture.innerHTML).toBe("
test
"); }); test("t-model on an input with an undefined value", async () => { class SomeComponent extends Component { static template = xml``; state = useState({ text: undefined }); } await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe(""); const input = fixture.querySelector("input")!; expect(input.value).toBe(""); }); test("basic use, on an input with bracket expression", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "" }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe("
"); const input = fixture.querySelector("input")!; await editInput(input, "test"); expect(comp.state.text).toBe("test"); expect(fixture.innerHTML).toBe("
test
"); }); test("throws if invalid expression", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "" }); } let error: Error; try { await mount(SomeComponent, fixture); } catch (e) { error = e as Error; } expect(error!).toBeDefined(); expect(error!.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`); }); test("basic use, on another key in component", async () => { class SomeComponent extends Component { static template = xml`
`; some = useState({ text: "" }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe("
"); const input = fixture.querySelector("input")!; await editInput(input, "test"); expect(comp.some.text).toBe("test"); expect(fixture.innerHTML).toBe("
test
"); }); test("on an input, type=checkbox", async () => { class SomeComponent extends Component { static template = xml`
yes no
`; state = useState({ flag: false }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe('
no
'); let input = fixture.querySelector("input")!; input.click(); await nextTick(); expect(fixture.innerHTML).toBe('
yes
'); expect(comp.state.flag).toBe(true); input.click(); await nextTick(); expect(comp.state.flag).toBe(false); }); test("on an textarea", async () => { class SomeComponent extends Component { static template = xml`
"); const textarea = fixture.querySelector("textarea")!; await editInput(textarea, "test"); expect(comp.state.text).toBe("test"); expect(fixture.innerHTML).toBe("
test
"); }); test("on an input type=radio", async () => { class SomeComponent extends Component { static template = xml`
Choice:
`; state = useState({ choice: "" }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe( '
Choice:
' ); const firstInput = fixture.querySelector("input")!; firstInput.click(); await nextTick(); expect(comp.state.choice).toBe("One"); expect(fixture.innerHTML).toBe( '
Choice: One
' ); const secondInput = fixture.querySelectorAll("input")[1]; secondInput.click(); await nextTick(); expect(comp.state.choice).toBe("Two"); expect(fixture.innerHTML).toBe( '
Choice: Two
' ); }); test("on an input type=radio, with initial value", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ choice: "Two" }); } await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe( '
' ); const secondInput = fixture.querySelectorAll("input")[1]; expect(secondInput.checked).toBe(true); }); test("on a select", async () => { class SomeComponent extends Component { static template = xml`
Choice:
`; state = useState({ color: "" }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe( '
Choice:
' ); const select = fixture.querySelector("select")!; select.value = "red"; select.dispatchEvent(new Event("change")); await nextTick(); expect(comp.state.color).toBe("red"); expect(fixture.innerHTML).toBe( '
Choice: red
' ); }); test("on a select, initial state", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ color: "red" }); } await mount(SomeComponent, fixture); const select = fixture.querySelector("select")!; expect(select.value).toBe("red"); }); test("on a sub state key", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ something: { text: "" } }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe("
"); const input = fixture.querySelector("input")!; await editInput(input, "test"); expect(comp.state.something.text).toBe("test"); expect(fixture.innerHTML).toBe("
test
"); }); test("with expression having a changing key", async () => { class SomeComponent extends Component { static template = xml`
`; state: { something: { [key: string]: string } } = useState({ something: {} }); text = useState({ key: "foo" }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe("
"); let input = fixture.querySelector("input")!; await editInput(input, "footest"); expect(comp.state.something[comp.text.key]).toBe("footest"); expect(fixture.innerHTML).toBe("
footest
"); comp.text.key = "bar"; await nextTick(); input = fixture.querySelector("input")!; await editInput(input, "test bar"); expect(comp.state.something[comp.text.key]).toBe("test bar"); expect(fixture.innerHTML).toBe("
test bar
"); }); test(".lazy modifier", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "" }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe("
"); const input = fixture.querySelector("input")!; input.value = "test"; input.dispatchEvent(new Event("input")); await nextTick(); expect(comp.state.text).toBe(""); expect(fixture.innerHTML).toBe("
"); input.dispatchEvent(new Event("change")); await nextTick(); expect(comp.state.text).toBe("test"); expect(fixture.innerHTML).toBe("
test
"); }); test(".trim modifier", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "" }); } const comp = await mount(SomeComponent, fixture); const input = fixture.querySelector("input")!; await editInput(input, " test "); expect(comp.state.text).toBe("test"); expect(fixture.innerHTML).toBe("
test
"); }); test(".trim modifier implies .lazy modifier", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "" }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe("
"); const input = fixture.querySelector("input")!; input.value = "test "; input.dispatchEvent(new Event("input")); await nextTick(); expect(comp.state.text).toBe(""); expect(fixture.innerHTML).toBe("
"); input.dispatchEvent(new Event("change")); await nextTick(); expect(comp.state.text).toBe("test"); expect(fixture.innerHTML).toBe("
test
"); }); test(".number modifier", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ number: 0 }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe("
0
"); const input = fixture.querySelector("input")!; await editInput(input, "13"); expect(comp.state.number).toBe(13); expect(fixture.innerHTML).toBe("
13
"); await editInput(input, "invalid"); expect(comp.state.number).toBe("invalid"); expect(fixture.innerHTML).toBe("
invalid
"); }); test("in a t-foreach", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState([ { f: false, id: 1 }, { f: false, id: 2 }, { f: false, id: 3 }, ]); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe( '
' ); const input = fixture.querySelectorAll("input")[1]!; input.click(); expect(comp.state[1].f).toBe(true); expect(comp.state[0].f).toBe(false); expect(comp.state[2].f).toBe(false); }); test("in a t-foreach, part 2", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState(["zuko", "iroh"]); } const comp = await mount(SomeComponent, fixture); expect(comp.state).toEqual(["zuko", "iroh"]); const input = fixture.querySelectorAll("input")[1]!; await editInput(input, "uncle iroh"); expect(comp.state).toEqual(["zuko", "uncle iroh"]); }); test("in a t-foreach, part 3", async () => { class SomeComponent extends Component { static template = xml`
`; names = ["Crusher", "Data", "Riker", "Worf"]; state = useState({ values: {} }); } const comp = await mount(SomeComponent, fixture); expect(comp.state).toEqual({ values: {} }); const input = fixture.querySelectorAll("input")[1]!; await editInput(input, "Commander"); expect(comp.state).toEqual({ values: { Data: "Commander" } }); }); test("two inputs in a div alternating with a t-if", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ flag: true, text1: "", text2: "" }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe('
'); let input = fixture.querySelector("input")!; expect(input.value).toBe(""); await editInput(input, "Jean-Luc"); expect(comp.state.text1).toBe("Jean-Luc"); comp.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe('
'); input = fixture.querySelector("input")!; expect(input.value).toBe(""); await editInput(input, "Picard"); expect(comp.state.text2).toBe("Picard"); }); test("following a scope protecting directive (e.g. t-set)", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "Jean-Luc Picard" }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe("
"); const input = fixture.querySelector("input")!; expect(input.value).toBe("Jean-Luc Picard"); await editInput(input, "Commander Data"); expect(comp.state.text).toBe("Commander Data"); }); test("can also define t-on directive on same event, part 1", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "", other: "" }); onInput(ev: InputEvent) { this.state.other = (ev.target as HTMLInputElement).value; } } const comp = await mount(SomeComponent, fixture); expect(comp.state.text).toBe(""); expect(comp.state.other).toBe(""); const input = fixture.querySelector("input")!; await editInput(input, "Beam me up, Scotty"); expect(comp.state.text).toBe("Beam me up, Scotty"); expect(comp.state.other).toBe("Beam me up, Scotty"); }); test("can also define t-on directive on same event, part 2", async () => { class SomeComponent extends Component { static template = xml`
`; state = useState({ choice: "", lastClicked: "" }); onClick(ev: MouseEvent) { this.state.lastClicked = (ev.target as HTMLInputElement).value; } } const comp = await mount(SomeComponent, fixture); expect(comp.state.choice).toBe(""); expect(comp.state.lastClicked).toBe(""); const lastInput = fixture.querySelectorAll("input")[2]; lastInput.click(); await nextTick(); expect(comp.state.choice).toBe("Three"); expect(comp.state.lastClicked).toBe("Three"); }); test("t-model on select with static options", async () => { class Test extends Component { static template = xml`
`; state: any; options: any; setup() { this.state = useState({ model: "b" }); this.options = ["a", "b", "c"]; } } await mount(Test, fixture); expect(fixture.querySelector("select")!.value).toEqual("b"); }); test("t-model with dynamic values on select options", async () => { class Test extends Component { static template = xml`
`; state: any; options: any; setup() { this.state = useState({ model: "b" }); this.options = ["a", "b"]; } } await mount(Test, fixture); expect(fixture.querySelector("select")!.value).toEqual("b"); }); test("t-model with dynamic values on select options -- 2", async () => { class Test extends Component { static template = xml`
`; state: any; options: any; setup() { this.state = useState({ model: "b" }); this.options = ["a", "b"]; } } await mount(Test, fixture); expect(fixture.querySelector("select")!.value).toEqual("b"); }); test("t-model with dynamic values on select options -- 3", async () => { class Test extends Component { static template = xml`
`; state: any; options: any; setup() { this.state = useState({ model: "b" }); this.options = ["a", "b"]; } } await mount(Test, fixture); expect(fixture.querySelector("select")!.value).toEqual("b"); }); test("t-model with dynamic values on select options in foreach", async () => { class Test extends Component { static template = xml`
`; state: any; options: any; setup() { this.state = useState({ model: "b" }); this.options = ["a", "b", "c"]; } } await mount(Test, fixture); expect(fixture.querySelector("select")!.value).toEqual("b"); }); test("t-model with dynamic number values on select options in foreach", async () => { class Test extends Component { static template = xml` `; state: any; setup() { this.state = useState({ value: 2, options: [{ value: 1 }, { value: 2 }, { value: 3 }], }); } } const comp = await mount(Test, fixture); // check that we have a value of 2 selected expect(fixture.querySelector("select")!.value).toEqual("2"); expect(comp.state.value).toBe(2); // emulate a click on the option=3 element fixture.querySelectorAll("option")[2].selected = true; fixture.querySelector("select")!.dispatchEvent(new Event("change")); await nextTick(); // check that we have now selected the number 3 (and not the string) expect(fixture.querySelector("select")!.value).toEqual("3"); expect(comp.state.value).toBe(3); }); test("t-model is applied before t-on-input", async () => { expect.assertions(3); class SomeComponent extends Component { static template = xml`
`; state = useState({ text: "", other: "" }); onInput(ev: InputEvent) { expect(this.state.text).toBe("Beam me up, Scotty"); expect((ev.target as HTMLInputElement).value).toBe("Beam me up, Scotty"); } } await mount(SomeComponent, fixture); const input = fixture.querySelector("input")!; await editInput(input, "Beam me up, Scotty"); }); test("t-model with radio button group in t-foreach", async () => { expect.assertions(6); const steps: string[] = []; class SomeComponent extends Component { static template = xml`
`; state = useState({ group: "scotty" }); options = ["beam", "scotty"]; getData() { steps.push(`group: ${this.state.group}`); } } await mount(SomeComponent, fixture); const divEl = fixture.querySelector("#get_data") as HTMLElement; expect(fixture.querySelector("input:checked")!.getAttribute("id")).toBe("scotty"); divEl.click(); expect(steps).toEqual(["group: scotty"]); fixture.querySelector("input")!.click(); expect(steps).toEqual(["group: scotty", "group: beam"]); await nextTick(); expect(fixture.querySelector("input:checked")!.getAttribute("id")).toBe("beam"); divEl.click(); expect(steps).toEqual(["group: scotty", "group: beam", "group: beam"]); }); });