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(
""
);
testContext.state.value = 321;
await nextTick();
expect(steps).toEqual(["render"]);
expect(fixture.innerHTML).toBe(
""
);
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(
""
);
});
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("");
testContext.state.key = "y";
testContext.state.y = { n: 2 };
delete testContext.state.x;
await nextTick();
expect(fixture.innerHTML).toBe("");
stateC.x = "b";
await nextTick();
expect(fixture.innerHTML).toBe("");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
});
});