import { Component, Env } from "../../src/component/component";
import { useState } from "../../src/hooks";
import { xml } from "../../src/tags";
import { makeDeferred, makeTestEnv, makeTestFixture, nextMicroTick, nextTick } from "../helpers";
//------------------------------------------------------------------------------
// 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.
// - env: a WEnv, necessary to create new components
let fixture: HTMLElement;
let env: Env;
beforeEach(() => {
fixture = makeTestFixture();
env = makeTestEnv();
Component.env = env;
});
afterEach(() => {
fixture.remove();
});
function children(w: Component): Component[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map(id => childrenMap[id]);
}
describe("async rendering", () => {
test("destroying a widget before start is over", async () => {
let def = makeDeferred();
class W extends Component {
static template = xml`
`;
willStart(): Promise {
return def;
}
}
const w = new W();
w.mount(fixture);
expect(w.__owl__.isDestroyed).toBe(false);
expect(w.__owl__.isMounted).toBe(false);
w.destroy();
def.resolve();
await nextTick();
expect(w.__owl__.isDestroyed).toBe(true);
expect(w.__owl__.isMounted).toBe(false);
});
test("destroying/recreating a subwidget with different props (if start is not over)", async () => {
let def = makeDeferred();
let n = 0;
class Child extends Component {
static template = xml`child:`;
constructor(parent, props) {
super(parent, props);
n++;
}
willStart(): Promise {
return def;
}
}
class W extends Component {
static template = xml`
");
parent.state.valA = 2;
await nextMicroTick();
expect(steps).toEqual(["render"]);
await nextMicroTick();
// For an unknown reason, this test fails on windows without the next microtick. It works
// in linux and osx, but fails on at least this machine.
// I do not see anything harmful in waiting an extra tick. But it is annoying to not
// know what is different.
await nextMicroTick();
expect(steps).toEqual(["render", "render"]);
expect(fixture.innerHTML).toBe("
1
");
parent.state.valA = 3;
await nextMicroTick();
expect(steps).toEqual(["render", "render"]);
await nextMicroTick();
// same as above
await nextMicroTick();
expect(steps).toEqual(["render", "render", "render"]);
expect(fixture.innerHTML).toBe("
");
// this change triggers a rendering of the parent. This rendering is delayed,
// because child is now waiting for def to be resolved
parent.state.val = "Framboise Girardin";
await nextTick();
expect(fixture.innerHTML).toBe("
");
// with this, we remove child, and subchild, even though it is not finished
// rendering from previous changes
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("");
// we now resolve def, so the child rendering is now complete.
(def).resolve();
await nextTick();
expect(fixture.innerHTML).toBe("");
});
test.skip("reuse widget if possible, in some async situation", async () => {
// this optimization has been temporarily deactivated
env.qweb.addTemplates(`
ab
`);
let destroyCount = 0;
class ChildA extends Component {
destroy() {
destroyCount++;
super.destroy();
}
}
class ChildB extends Component {
willStart(): any {
return new Promise(function() {});
}
}
class Parent extends Component {
static components = { ChildA, ChildB };
state = useState({ valA: 1, valB: 2, flag: false });
}
const parent = new Parent();
await parent.mount(fixture);
expect(destroyCount).toBe(0);
parent.state.flag = true;
await nextTick();
expect(destroyCount).toBe(0);
parent.state.valB = 3;
await nextTick();
expect(destroyCount).toBe(0);
});
test("rendering component again in next microtick", async () => {
class Child extends Component {
static template = xml`
");
});
test("concurrent renderings scenario 2", async () => {
// this test asserts that a rendering initiated before another one, and that
// ends after it, is re-mapped to that second rendering
const defs = [makeDeferred(), makeDeferred()];
let index = 0;
let stateB;
class ComponentC extends Component {
static template = xml``;
willUpdateProps() {
return defs[index++];
}
}
class ComponentB extends Component {
static template = xml`
");
defs[0].resolve(); // resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("
1b
"); // TODO: is this what we want?? 2b could be ok too
defs[1].resolve(); // resolve rendering initiated in B
await nextTick();
expect(fixture.innerHTML).toBe("
");
defB.resolve(); // resolve rendering initiated in A (still blocked in D)
await nextTick();
expect(fixture.innerHTML).toBe("
1c
");
defsD[0].resolve(); // resolve rendering initiated in C (should be ignored)
await nextTick();
expect(ComponentD.prototype.someValue).toBeCalledTimes(1);
expect(fixture.innerHTML).toBe("
1c
");
defsD[1].resolve(); // completely resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("
");
defB.resolve(); // resolve rendering initiated in A (still blocked in D)
await nextTick();
expect(fixture.innerHTML).toBe("
1c
");
defsD[1].resolve(); // completely resolve rendering initiated in A
await nextTick();
expect(fixture.innerHTML).toBe("
2d
");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
defsD[0].resolve(); // resolve rendering initiated in C (should be ignored)
await nextTick();
expect(fixture.innerHTML).toBe("
2d
");
expect(ComponentD.prototype.someValue).toBeCalledTimes(2);
});
test("concurrent renderings scenario 5", async () => {
const defsB = [makeDeferred(), makeDeferred()];
let index = 0;
class ComponentB extends Component {
static template = xml`
");
});
test("concurrent renderings scenario 9", async () => {
// Here is the global idea of this scenario:
// A
// / \
// B C
// |
// D
// A state is updated, triggering a whole re-rendering
// B is async, and blocked
// C (and D) are rendered
// C state is updated, producing a re-rendering of C and D
// this last re-rendering of C should be correctly re-mapped to the whole
// re-rendering
const def = makeDeferred();
let stateC;
class ComponentD extends Component {
static template = xml``;
}
class ComponentC extends Component {
static template = xml`
");
});
test("concurrent renderings scenario 10", async () => {
// Here is the global idea of this scenario:
// A
// |
// B <- async willUpdateProps
// ----- <- conditional (initialy false)
// |
// C <- async willStart
// Render A and B normally
// Change the condition on B to trigger a re-rendering with C (async willStart)
// Change the state on A to trigger a global re-rendering, which is blocked
// in B (async willUpdateProps)
// Resolve the willStart of C: the first re-rendering has been cancelled by
// the global re-rendering, but handlers waiting for the rendering promise to
// resolve might execute and we don't want them to crash/do anything
const defB = makeDeferred();
const defC = makeDeferred();
let stateB;
class ComponentC extends Component {
static template = xml``;
willStart() {
return defC;
}
}
ComponentC.prototype.__render = jest.fn(ComponentC.prototype.__render);
class ComponentB extends Component {
static template = xml`
");
expect(ComponentC.prototype.__render).toHaveBeenCalledTimes(1);
});
test("concurrent renderings scenario 11", async () => {
// This scenario is the following: we have a component being updated (by props),
// and then rendered (render method), but before the willUpdateProps resolves.
// We check that in that case, the return value of the render method is a promise
// that is resolved when the component is completely rendered (so, properly
// remapped to the promise of the ambient rendering)
const def = makeDeferred();
let child;
class Child extends Component {
static template = xml`|`;
val = 3;
willUpdateProps() {
child = this;
return def;
}
}
class Parent extends Component {
static template = xml`
");
});
test("concurrent renderings scenario 12", async () => {
// In this scenario, we have a parent component that will be re-rendered
// several times simultaneously:
// - once in a tick: it will create a new fiber, render it, but will have
// to wait for its child (blocking) to be completed
// - twice in the next tick: it will twice reuse the same fiber (as it is
// rendered but not completed yet)
const def = makeDeferred();
class Child extends Component {
static template = xml``;
willUpdateProps() {
return def;
}
}
class Parent extends Component {
static template = xml`
");
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("
1
");
parent.state.bool = true;
await nextTick(); // wait for this change to be applied
await nextTick(); // wait for changes triggered in mounted to be applied
expect(fixture.innerHTML).toBe("
01
");
});
test("change state and call manually render: no unnecessary rendering", async () => {
class Widget extends Component {
static template = xml`
`;
state = useState({ val: 1 });
}
Widget.prototype.__render = jest.fn(Widget.prototype.__render);
const widget = new Widget();
await widget.mount(fixture);
expect(fixture.innerHTML).toBe("