import { App, Component, mount, useRef, useState, useComponent, onMounted, onPatched, onWillStart, onWillUpdateProps, onWillPatch, xml, onWillUnmount, } from "../../src"; import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; let fixture: HTMLElement; snapshotEverything(); beforeEach(() => { fixture = makeTestFixture(); }); describe("hooks", () => { test("useRef hook: basic use", async () => { let counter: Counter; class Counter extends Component { static template = xml`
`; button = useRef("button"); value = 0; setup() { counter = this; } increment() { this.value++; this.button.el!.innerHTML = String(this.value); } } const mounted = mount(Counter, fixture); expect(counter!.button.el).toBe(null); await mounted; expect(fixture.innerHTML).toBe("
"); expect(counter!.button.el).not.toBe(null); expect(counter!.button.el).toBe(fixture.querySelector("button")); counter!.increment(); expect(fixture.innerHTML).toBe("
"); }); // TODO: rename like next test (ensures willPatch/patched calls are symmetrical) test("two different call to willPatch/patched should work", async () => { const steps: string[] = []; function useMyHook(i: number) { onPatched(() => { steps.push("hook:patched" + i); }); onWillPatch(() => { steps.push("hook:willPatch" + i); }); } class Test extends Component { static template = xml`
hey
`; state = useState({ value: 1 }); setup() { useMyHook(1); useMyHook(2); } } const component = await mount(Test, fixture); expect(fixture.innerHTML).toBe("
hey1
"); component.state.value++; await nextTick(); expect(fixture.innerHTML).toBe("
hey2
"); expect(steps).toEqual(["hook:willPatch2", "hook:willPatch1", "hook:patched1", "hook:patched2"]); }); test("mounted callbacks should be called in reverse order from willUnmount callbacks", async () => { const steps: string[] = []; function useMyHook(i: number) { onMounted(() => { steps.push("hook:mounted" + i); }); onWillUnmount(() => { steps.push("hook:willUnmount" + i); }); } class Test extends Component { static template = xml`
hey
`; state = useState({ value: 1 }); setup() { useMyHook(1); useMyHook(2); } } const app = new App(Test); await app.mount(fixture); app.destroy(); expect(steps).toEqual([ "hook:mounted2", "hook:mounted1", "hook:willUnmount1", "hook:willUnmount2", ]); }); describe("autofocus hook", () => { function useAutofocus(name: string) { let ref = useRef(name); let isInDom = false; function updateFocus() { if (!isInDom && ref.el) { isInDom = true; ref.el.focus(); } else if (isInDom && !ref.el) { isInDom = false; } } onPatched(updateFocus); onMounted(updateFocus); } test("simple input", async () => { class Test extends Component { static template = xml`
`; setup() { useAutofocus("input2"); } } await mount(Test, fixture); expect(fixture.innerHTML).toBe("
"); const input2 = fixture.querySelectorAll("input")[1]; expect(input2).toBe(document.activeElement); }); test("input in a t-if", async () => { class Test extends Component { static template = xml`
`; state = useState({ flag: false }); setup() { useAutofocus("input2"); } } const component = await mount(Test, fixture); expect(fixture.innerHTML).toBe("
"); expect(document.activeElement).toBe(document.body); component.state.flag = true; await nextTick(); const input2 = fixture.querySelectorAll("input")[1]; expect(input2).toBe(document.activeElement); }); }); test.skip("can use useEnv", async () => { expect.assertions(2); class Test extends Component { static template = xml`
`; setup() { //expect(useEnv()).toBe(this.env); } } await mount(Test, fixture); }); test.skip("can use sub env", async () => { class Test extends Component { static template = xml`
`; setup() { //useSubEnv({ val: 3 }); } } const component = await mount(Test, fixture); expect(fixture.innerHTML).toBe("
3
"); expect(component.env).not.toHaveProperty("val"); expect(component.env).toHaveProperty("val"); }); test("can use useComponent", async () => { expect.assertions(2); class Test extends Component { static template = xml`
`; setup() { expect(useComponent()).toBe(this); } } await mount(Test, fixture); }); test.skip("parent and child env", async () => { class Child extends Component { static template = xml`
`; super() { //useSubEnv({ val: 5 }); } } class Parent extends Component { static template = xml``; static components = { Child }; setup() { //useSubEnv({ val: 3 }); } } mount(Parent, fixture); expect(fixture.innerHTML).toBe("3
5
"); }); test("can use onWillStart, onWillUpdateProps", async () => { const steps: string[] = []; async function slow(): Promise { return new Promise((resolve) => { setTimeout(() => { resolve("slow"); }, 0); }); } function useMyHook() { onWillStart(async () => { steps.push(await slow()); steps.push("onWillStart"); }); onWillUpdateProps(async (nextProps) => { expect(nextProps).toEqual({ value: 2 }); steps.push(await slow()); steps.push("onWillUpdateProps"); }); } function use2ndHook() { onWillStart(() => { steps.push("on2ndStart"); }); onWillUpdateProps((nextProps) => { expect(nextProps).toEqual({ value: 2 }); steps.push("on2ndUpdate"); }); } class MyComponent extends Component { static template = xml``; setup() { useMyHook(); use2ndHook(); } } class App extends Component { static template = xml``; static components = { MyComponent }; state = useState({ value: 1 }); } const app = await mount(App, fixture); expect(fixture.innerHTML).toBe("1"); // NOTE: 'on2ndStart' appears first in the list even though // the 'use2ndHook' is declared after 'useMyHook'. This is // because Promise.all is used to call the callbacks specified // in the hooks, which runs them simultaneously. // Additionally, 'slow' should be listed before 'onWillStart' // because call to `slow` is awaited. expect(steps).toEqual(["on2ndStart", "slow", "onWillStart"]); app.state.value = 2; await nextTick(); expect(fixture.innerHTML).toBe("2"); expect(steps).toEqual([ "on2ndStart", "slow", "onWillStart", "on2ndUpdate", "slow", "onWillUpdateProps", ]); }); test.skip("useExternalListener", async () => { let n = 0; class MyComponent extends Component { static template = xml``; setup() { //useExternalListener(window as any, "click", this.increment); } increment() { n++; } } class App extends Component { static template = xml``; static components = { MyComponent }; state = useState({ flag: false }); } const app = await mount(App, fixture); expect(n).toBe(0); window.dispatchEvent(new Event("click")); expect(n).toBe(0); app.state.flag = true; await nextTick(); window.dispatchEvent(new Event("click")); expect(n).toBe(1); app.state.flag = false; await nextTick(); window.dispatchEvent(new Event("click")); expect(n).toBe(1); }); });