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`
`;
state = useState({ text: "" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("
");
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"]);
});
});