import { makeDeferred, makeTestEnv, makeTestFixture, nextTick } from "./helpers"; import { Component } from "../src/component/component"; import { Context, useContext } from "../src/context"; import { xml } from "../src/tags"; import { useState } from "../src/hooks"; //------------------------------------------------------------------------------ // Setup and helpers //------------------------------------------------------------------------------ // We create before each test: // - fixture: a div, appended to the DOM, intended to be the target of dom // manipulations. Note that it is removed after each test. // - a test env, necessary to create components, that is set as env let fixture: HTMLElement; beforeEach(() => { fixture = makeTestFixture(); Component.env = makeTestEnv(); }); afterEach(() => { fixture.remove(); }); //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ describe("Context", () => { test("very simple use, with initial value", async () => { const testContext = new Context({ value: 123 }); class Test extends Component { static template = xml`
`; contextObj = useContext(testContext); } const test = new Test(); await test.mount(fixture); expect(fixture.innerHTML).toBe("
123
"); }); test("useContext hook is reactive, for one component", async () => { const testContext = new Context({ value: 123 }); class Test extends Component { static template = xml`
`; contextObj = useContext(testContext); } const test = new Test(); await test.mount(fixture); expect(fixture.innerHTML).toBe("
123
"); test.contextObj.value = 321; await nextTick(); expect(fixture.innerHTML).toBe("
321
"); }); test("two components can subscribe to same context", async () => { const testContext = new Context({ value: 123 }); class Child extends Component { static template = xml``; contextObj = useContext(testContext); } class Parent extends Component { static template = xml`
`; static components = { Child }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
123123
"); testContext.state.value = 321; await nextTick(); expect(fixture.innerHTML).toBe("
321321
"); }); test("two async components are updated in parallel", async () => { const testContext = new Context({ value: 123 }); const def = makeDeferred(); const steps: string[] = []; class Child extends Component { static template = xml``; contextObj = useContext(testContext); async render() { steps.push("render"); await def; return super.render(); } } class Parent extends Component { static template = xml`
`; static components = { Child }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
123123
"); testContext.state.value = 321; await nextTick(); expect(steps).toEqual(["render", "render"]); expect(fixture.innerHTML).toBe("
123123
"); def.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("
321321
"); }); test("two async components on two levels are updated (mostly) in parallel", async () => { const testContext = new Context({ value: 123 }); const def = makeDeferred(); const steps: string[] = []; class SlowComp extends Component { static template = xml`

`; willUpdateProps() { return def; } } class Child extends Component { static template = xml``; static components = { SlowComp }; contextObj = useContext(testContext); render() { steps.push("render"); return super.render(); } } class Parent extends Component { static template = xml`
`; static components = { Child }; } class App extends Component { static template = xml`
`; static components = { Child, Parent }; } const app = new App(); await app.mount(fixture); expect(fixture.innerHTML).toBe( "

123

123

123

" ); testContext.state.value = 321; await nextTick(); expect(steps).toEqual(["render"]); expect(fixture.innerHTML).toBe( "

123

123

123

" ); def.resolve(); await nextTick(); // we need to wait for an extra tick because it could happen (even though it // is rare) that the second batch of renderings is not done yet, because // the initial promise has been given to the macrotask queue, so a small // delay happens. await nextTick(); expect(steps).toEqual(["render", "render", "render"]); expect(fixture.innerHTML).toBe( "

321

321

321

" ); }); test("one components can subscribe twice to same context", async () => { const testContext = new Context({ a: 1, b: 2 }); const steps: string[] = []; class Child extends Component { static template = xml``; contextObj1 = useContext(testContext); contextObj2 = useContext(testContext); __render(fiber) { steps.push("child"); return super.__render(fiber); } } class Parent extends Component { static template = xml`
`; static components = { Child }; } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
12
"); expect(steps).toEqual(["child"]); testContext.state.a = 3; await nextTick(); expect(fixture.innerHTML).toBe("
32
"); expect(steps).toEqual(["child", "child"]); }); test("parent and children subscribed to same context", async () => { const testContext = new Context({ a: 123, b: 321 }); const steps: string[] = []; class Child extends Component { static template = xml``; contextObj = useContext(testContext); __render(fiber) { steps.push("child"); return super.__render(fiber); } } class Parent extends Component { static template = xml`
`; static components = { Child }; contextObj = useContext(testContext); __render(fiber) { steps.push("parent"); return super.__render(fiber); } } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
123321
"); expect(steps).toEqual(["parent", "child"]); parent.contextObj.a = 124; await nextTick(); expect(fixture.innerHTML).toBe("
124321
"); // we only want one render from the child here, not two expect(steps).toEqual(["parent", "child", "parent", "child"]); }); test("destroyed component is inactive", async () => { const testContext = new Context({ a: 123 }); const steps: string[] = []; class Child extends Component { static template = xml``; contextObj = useContext(testContext); __render(fiber) { steps.push("child"); return super.__render(fiber); } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ flag: true }); __render(fiber) { steps.push("parent"); return super.__render(fiber); } } const parent = new Parent(); await parent.mount(fixture); expect(fixture.innerHTML).toBe("
123
"); expect(steps).toEqual(["parent", "child"]); expect(testContext.subscriptions.update.length).toBe(1); parent.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
"); expect(steps).toEqual(["parent", "child", "parent"]); // kind of whitebox... // we make sure we do not have any pending subscriptions to the 'update' // event expect(testContext.subscriptions.update.length).toBe(0); }); test("destroyed component before being mounted is inactive", async () => { const testContext = new Context({ a: 123 }); class Child extends Component { static template = xml``; contextObj = useContext(testContext); willStart() { return makeDeferred(); } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ flag: true }); } const parent = new Parent(); const prom = parent.mount(fixture); await nextTick(); // wait for Child to be instantiated expect(testContext.subscriptions.update.length).toBe(1); parent.state.flag = false; await prom; expect(fixture.innerHTML).toBe("
"); // kind of whitebox... // we make sure we do not have any pending subscriptions to the 'update' // event expect(testContext.subscriptions.update.length).toBe(0); }); test.skip("concurrent renderings", async () => { /** * Note: this test is interesting, but sadly just an incomplete attempt at * protecting users against themselves. With the context API, it is not * possible for the framework to protect completely against crashes. Maybe * like in this case, when a component is in a simple hierarchy where all * renderings come from the context changes, but in a real case, where some * code can trigger a rendering independently, it is insufficient. * * The main problem is that the sub component depends on some external state, * which may be modified, and then incompatible with the component actual * state (for example, if the sub component has an id key related to some * object that has been removed from the context). * * For now, sadly, the only solution is that components that depends on external * state should guarantee their own integrity themselves. Then maybe this * could be solved at the level of a state management solution that has a * more advanced API, to let components determine if they should be updated * or not (so, something slightly more advanced that the useStore hook). */ const testContext = new Context({ x: { n: 1 }, key: "x" }); const def = makeDeferred(); let stateC; class ComponentC extends Component { static template = xml``; context = useContext(testContext); state = useState({ x: "a" }); constructor(parent, props) { super(parent, props); stateC = this.state; } } class ComponentB extends Component { static components = { ComponentC }; static template = xml`

`; willUpdateProps() { return def; } } class ComponentA extends Component { static components = { ComponentB }; static template = xml`
`; context = useContext(testContext); } const component = new ComponentA(); await component.mount(fixture); expect(fixture.innerHTML).toBe("

1a

"); testContext.state.key = "y"; testContext.state.y = { n: 2 }; delete testContext.state.x; await nextTick(); expect(fixture.innerHTML).toBe("

1a

"); stateC.x = "b"; await nextTick(); expect(fixture.innerHTML).toBe("

1a

"); def.resolve(); await nextTick(); expect(fixture.innerHTML).toBe("

2b

"); }); });