mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
2b90e3a688
The deletion of a key in an observed object did clear the observers of that key but did not clear the atoms created (if any) when the key existed (e.g. on the key value if it was trackable). This can lead to a growing set of atoms that are useless but kept in memory if a lot of keys are added/deleted. The same thing can happen if a key value is changed many times and the values are trackable. Here we fix the problem by - keeping tracks of the objects that have been observed by an external call to the method "atom" (we call them seeds). - remove all observer atoms that are not seeds for observers of at least one key deletion or key value change. Note the fix is in some sense partial: a user could use the "atom" primitive making the new system uncapable of avoiding a leak (see test "atom on an object with a getter 3" where a getter is used in a weird way in an observed object).
1560 lines
41 KiB
TypeScript
1560 lines
41 KiB
TypeScript
import { Component, mount, onRender, onWillStart, onWillUpdateProps, useState } from "../src";
|
|
import { atom, registerObserver } from "../src/reactivity";
|
|
import { xml } from "../src/tags";
|
|
import {
|
|
makeDeferred,
|
|
makeTestFixture,
|
|
nextMicroTick,
|
|
nextTick,
|
|
snapshotEverything,
|
|
} from "./helpers";
|
|
|
|
function createAtom(value: any, observer?: any) {
|
|
observer = observer || (() => {});
|
|
registerObserver(observer);
|
|
return atom(value, observer);
|
|
}
|
|
|
|
describe("Reactivity: atom", () => {
|
|
test("can read", async () => {
|
|
const atom1 = createAtom({ a: 1 });
|
|
expect(atom1.a).toBe(1);
|
|
});
|
|
|
|
test("can write", () => {
|
|
const atom1 = createAtom({});
|
|
atom1.a = 1;
|
|
expect(atom1.a).toBe(1);
|
|
});
|
|
|
|
test("can update", () => {
|
|
const atom1 = createAtom({ a: 1 });
|
|
atom1.a = 2;
|
|
expect(atom1.a).toBe(2);
|
|
});
|
|
|
|
test("can delete", () => {
|
|
const atom1 = createAtom({ a: 1 });
|
|
delete atom1.a;
|
|
expect(atom1.a).toBeUndefined();
|
|
});
|
|
|
|
test("act like an object", () => {
|
|
const atom1 = createAtom({ a: 1 });
|
|
expect(Object.keys(atom1)).toEqual(["a"]);
|
|
expect(Object.values(atom1)).toEqual([1]);
|
|
expect(typeof atom1).toBe("object");
|
|
});
|
|
|
|
test("act like an array", () => {
|
|
const atom1 = createAtom(["a", "b"]);
|
|
expect(atom1.length).toBe(2);
|
|
expect(atom1).toEqual(["a", "b"]);
|
|
expect(typeof atom1).toBe("object");
|
|
expect(Array.isArray(atom1)).toBe(true);
|
|
});
|
|
|
|
test("return value if not proxifiable", () => {
|
|
const atom1 = createAtom(1);
|
|
expect(atom1).toBe(1);
|
|
});
|
|
|
|
test("return value if observer is not registered", () => {
|
|
const obj = { a: 1 };
|
|
const atom1 = atom(obj, () => {});
|
|
expect(atom1).toBe(obj);
|
|
});
|
|
|
|
test("atom observer is called properly", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: 1 }, () => n++);
|
|
atom1.a = 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(0); // key has not be read yet
|
|
atom1.a = atom1.a + 5; // key is read and then modified
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
});
|
|
|
|
test("atom on an object with a getter 1", async () => {
|
|
let n = 0;
|
|
let value = 1;
|
|
const atom = createAtom(
|
|
{
|
|
get a() {
|
|
return value;
|
|
},
|
|
set a(val) {
|
|
value = val;
|
|
},
|
|
},
|
|
() => n++
|
|
);
|
|
atom.a = atom.a + 4;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
});
|
|
|
|
test("atom on an object with a getter 2", async () => {
|
|
let n = 0;
|
|
let value = { b: 1 };
|
|
const atom = createAtom(
|
|
{
|
|
get a() {
|
|
return value;
|
|
},
|
|
},
|
|
() => n++
|
|
);
|
|
expect(atom.a.b).toBe(1);
|
|
atom.a.b = 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
});
|
|
|
|
test("atom on an object with a getter 3", async () => {
|
|
let n = 0;
|
|
const values: { b: number }[] = createAtom([]);
|
|
function createValue() {
|
|
const o = { b: values.length };
|
|
values.push(o);
|
|
return o;
|
|
}
|
|
const atom = createAtom(
|
|
{
|
|
get a() {
|
|
return createValue();
|
|
},
|
|
},
|
|
() => n++
|
|
);
|
|
for (let i = 0; i < 10; i++) {
|
|
expect(atom.a.b).toEqual(i);
|
|
}
|
|
expect(n).toBe(0);
|
|
values[0].b = 3;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1); // !!! atoms for each object in values are still there !!!
|
|
});
|
|
|
|
test("atom observer is called after batch of operation", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: 1, b: 2 }, () => n++);
|
|
atom1.a = 2;
|
|
expect(n).toBe(0);
|
|
await nextMicroTick();
|
|
expect(n).toBe(0); // key has not be read yet
|
|
atom1.a = atom1.a + 5; // key is read and then modified
|
|
expect(n).toBe(0);
|
|
atom1.b = atom1.b + 5; // key is read and then modified
|
|
expect(n).toBe(0);
|
|
await nextMicroTick();
|
|
expect(n).toBe(1); // two operations but only one notification
|
|
});
|
|
|
|
test("setting property to same value does not trigger callback", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: 1 }, () => n++);
|
|
atom1.a = atom1.a + 5; // read and modifies property a to have value 6
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
atom1.a = 6; // same value
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
});
|
|
|
|
test("observe cycles", async () => {
|
|
const a = { a: {} };
|
|
a.a = a;
|
|
|
|
let n = 0;
|
|
const atom1 = createAtom(a, () => n++);
|
|
|
|
atom1.k = 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
delete atom1.l;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
delete atom1.k;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
atom1.a = 1;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
atom1.a = atom1.a + 5;
|
|
await nextMicroTick();
|
|
expect(n).toBe(3);
|
|
});
|
|
|
|
test("two observers for same source", async () => {
|
|
let m = 0;
|
|
let n = 0;
|
|
const obj = { a: 1 } as any;
|
|
const atom1 = createAtom(obj, () => m++);
|
|
const atom2 = createAtom(obj, () => n++);
|
|
|
|
obj.new = 2;
|
|
await nextMicroTick();
|
|
expect(m).toBe(0);
|
|
expect(n).toBe(0);
|
|
|
|
atom1.new = 2; // already exists!
|
|
await nextMicroTick();
|
|
expect(m).toBe(0);
|
|
expect(n).toBe(0);
|
|
|
|
atom1.veryNew = 2;
|
|
await nextMicroTick();
|
|
expect(m).toBe(1);
|
|
expect(n).toBe(1);
|
|
|
|
atom1.a = atom1.a + 5;
|
|
await nextMicroTick();
|
|
expect(m).toBe(2);
|
|
expect(n).toBe(1);
|
|
|
|
atom2.a = atom2.a + 5;
|
|
await nextMicroTick();
|
|
expect(m).toBe(3);
|
|
expect(n).toBe(2);
|
|
|
|
delete atom2.veryNew;
|
|
await nextMicroTick();
|
|
expect(m).toBe(4);
|
|
expect(n).toBe(3);
|
|
});
|
|
|
|
test("create atom from another atom", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: 1 });
|
|
const atom2 = createAtom(atom1, () => n++);
|
|
atom2.a = atom2.a + 5;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
atom1.a = 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
});
|
|
|
|
test("create atom from another atom 2", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: 1 });
|
|
const atom2 = createAtom(atom1, () => n++);
|
|
atom1.a = atom2.a + 5;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
atom2.a = atom2.a + 5;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
});
|
|
|
|
test("create atom from another atom 3", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: 1 });
|
|
const atom2 = createAtom(atom1, () => n++);
|
|
atom1.a = atom1.a + 5;
|
|
await nextMicroTick();
|
|
expect(n).toBe(0); // atom2.a was not yet read
|
|
atom2.a = atom2.a + 5;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1); // atom2.a has been read and is now observed
|
|
atom1.a = atom1.a + 5;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
});
|
|
|
|
test("immediately returns primitive values", () => {
|
|
expect(createAtom(1)).toBe(1);
|
|
expect(createAtom("asf")).toBe("asf");
|
|
expect(createAtom(true)).toBe(true);
|
|
expect(createAtom(null)).toBe(null);
|
|
expect(createAtom(undefined)).toBe(undefined);
|
|
});
|
|
|
|
test("immediately returns dates", () => {
|
|
const date = new Date();
|
|
expect(createAtom(date)).toBe(date);
|
|
});
|
|
|
|
test("can observe object with some key set to null", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: { b: null } } as any, () => n++);
|
|
expect(n).toBe(0);
|
|
atom1.a.b = Boolean(atom1.a.b);
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
});
|
|
|
|
test("can reobserve object with some key set to null", async () => {
|
|
let n = 0;
|
|
const fn = () => n++;
|
|
const unregisterObserver = registerObserver(fn);
|
|
const atom1 = createAtom({ a: { b: null } } as any, fn);
|
|
const atom2 = createAtom(atom1, fn);
|
|
expect(atom2).toBe(atom1);
|
|
expect(atom2).toEqual(atom1);
|
|
await nextMicroTick();
|
|
expect(n).toBe(0);
|
|
atom1.a.b = Boolean(atom1.a.b);
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
unregisterObserver();
|
|
atom1.a.b = !atom1.a.b;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
});
|
|
|
|
test("contains initial values", () => {
|
|
const atom1 = createAtom({ a: 1, b: 2 });
|
|
expect(atom1.a).toBe(1);
|
|
expect(atom1.b).toBe(2);
|
|
expect((atom1 as any).c).toBeUndefined();
|
|
});
|
|
|
|
test("detect object value changes", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: 1 }, () => n++) as any;
|
|
expect(n).toBe(0);
|
|
|
|
atom1.a = atom1.a + 5;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
atom1.b = atom1.b + 5;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
atom1.a = null;
|
|
atom1.b = undefined;
|
|
await nextMicroTick();
|
|
expect(n).toBe(3);
|
|
expect(atom1).toEqual({ a: null, b: undefined });
|
|
});
|
|
|
|
test("properly handle dates", async () => {
|
|
const date = new Date();
|
|
let n = 0;
|
|
const atom1 = createAtom({ date }, () => n++);
|
|
|
|
await nextMicroTick();
|
|
expect(typeof atom1.date.getFullYear()).toBe("number");
|
|
expect(atom1.date).toBe(date);
|
|
|
|
atom1.date = new Date();
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
expect(atom1.date).not.toBe(date);
|
|
});
|
|
|
|
test("properly handle promise", async () => {
|
|
let resolved = false;
|
|
const prom = new Promise((r) => r());
|
|
let n = 0;
|
|
const atom1 = createAtom({ prom }, () => n++);
|
|
|
|
expect(atom1.prom).toBeInstanceOf(Promise);
|
|
atom1.prom.then(() => (resolved = true));
|
|
expect(n).toBe(0);
|
|
expect(resolved).toBe(false);
|
|
await Promise.resolve();
|
|
expect(resolved).toBe(true);
|
|
expect(n).toBe(0);
|
|
});
|
|
|
|
test("can observe value change in array in an object", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ arr: [1, 2] }, () => n++) as any;
|
|
|
|
expect(Array.isArray(atom1.arr)).toBe(true);
|
|
expect(n).toBe(0);
|
|
|
|
atom1.arr[0] = atom1.arr[0] + "nope";
|
|
await nextMicroTick();
|
|
|
|
expect(n).toBe(1);
|
|
expect(atom1.arr[0]).toBe("1nope");
|
|
expect(atom1.arr).toEqual(["1nope", 2]);
|
|
});
|
|
|
|
test("can observe: changing array in object to another array", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ arr: [1, 2] }, () => n++) as any;
|
|
|
|
expect(Array.isArray(atom1.arr)).toBe(true);
|
|
expect(n).toBe(0);
|
|
|
|
atom1.arr = [2, 1];
|
|
await nextMicroTick();
|
|
|
|
expect(n).toBe(1);
|
|
expect(atom1.arr[0]).toBe(2);
|
|
expect(atom1.arr).toEqual([2, 1]);
|
|
});
|
|
|
|
test("getting twice an object properties return same object", () => {
|
|
const atom1 = createAtom({ a: { b: 1 } });
|
|
const a1 = atom1.a;
|
|
const a2 = atom1.a;
|
|
expect(a1).toBe(a2);
|
|
});
|
|
|
|
test("various object property changes", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: 1 }, () => n++) as any;
|
|
expect(n).toBe(0);
|
|
|
|
atom1.a = atom1.a + 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
// same value again
|
|
atom1.a = 3;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
atom1.a = 4;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
});
|
|
|
|
test("properly observe arrays", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom([], () => n++) as any;
|
|
|
|
expect(Array.isArray(atom1)).toBe(true);
|
|
expect(atom1.length).toBe(0);
|
|
expect(n).toBe(0);
|
|
|
|
atom1.push(1);
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
expect(atom1.length).toBe(1);
|
|
expect(atom1).toEqual([1]);
|
|
|
|
atom1.splice(1, 0, "hey");
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
expect(atom1).toEqual([1, "hey"]);
|
|
expect(atom1.length).toBe(2);
|
|
|
|
atom1.unshift("lindemans");
|
|
await nextMicroTick();
|
|
//it generates 3 primitive operations
|
|
expect(n).toBe(3);
|
|
expect(atom1).toEqual(["lindemans", 1, "hey"]);
|
|
expect(atom1.length).toBe(3);
|
|
|
|
atom1.reverse();
|
|
await nextMicroTick();
|
|
//it generates 2 primitive operations
|
|
expect(n).toBe(4);
|
|
expect(atom1).toEqual(["hey", 1, "lindemans"]);
|
|
expect(atom1.length).toBe(3);
|
|
|
|
atom1.pop(); // one set, one delete
|
|
await nextMicroTick();
|
|
expect(n).toBe(5);
|
|
expect(atom1).toEqual(["hey", 1]);
|
|
expect(atom1.length).toBe(2);
|
|
|
|
atom1.shift(); // 2 sets, 1 delete
|
|
await nextMicroTick();
|
|
expect(n).toBe(6);
|
|
expect(atom1).toEqual([1]);
|
|
expect(atom1.length).toBe(1);
|
|
});
|
|
|
|
test("object pushed into arrays are observed", async () => {
|
|
let n = 0;
|
|
const arr: any = createAtom([], () => n++);
|
|
|
|
arr.push({ kriek: 5 });
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
arr[0].kriek = 6;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
arr[0].kriek = arr[0].kriek + 6;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
});
|
|
|
|
test("set new property on observed object", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({}, () => n++) as any;
|
|
expect(n).toBe(0);
|
|
|
|
atom1.b = 8;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
expect(atom1.b).toBe(8);
|
|
});
|
|
|
|
test("delete property from observed object", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: 1, b: 8 }, () => n++) as any;
|
|
expect(n).toBe(0);
|
|
|
|
delete atom1.b;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
expect(atom1).toEqual({ a: 1 });
|
|
});
|
|
|
|
test("delete property from observed object 2", async () => {
|
|
let n = 0;
|
|
const observer = () => n++;
|
|
const obj = { a: { b: 1 } };
|
|
const atom1 = createAtom(obj.a, observer) as any;
|
|
const atom2 = createAtom(obj, observer) as any;
|
|
expect(n).toBe(0);
|
|
|
|
delete atom2.a;
|
|
await nextMicroTick();
|
|
// key "a" is no longer observed
|
|
expect(n).toBe(1);
|
|
|
|
atom1.new = 2; // but { b: 1 } is still observed!
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
});
|
|
|
|
test("set element in observed array", async () => {
|
|
let n = 0;
|
|
const arr = createAtom(["a"], () => n++);
|
|
|
|
arr[1] = "b";
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
expect(arr).toEqual(["a", "b"]);
|
|
});
|
|
|
|
test("properly observe arrays in object", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ arr: [] }, () => n++) as any;
|
|
|
|
expect(atom1.arr.length).toBe(0);
|
|
expect(n).toBe(0);
|
|
|
|
atom1.arr.push(1);
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
expect(atom1.arr.length).toBe(1);
|
|
});
|
|
|
|
test("properly observe objects in array", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ arr: [{ something: 1 }] }, () => n++) as any;
|
|
expect(n).toBe(0);
|
|
|
|
atom1.arr[0].something = atom1.arr[0].something + 1;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
expect(atom1.arr[0].something).toBe(2);
|
|
});
|
|
|
|
test("properly observe objects in object", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: { b: 1 } }, () => n++) as any;
|
|
expect(n).toBe(0);
|
|
|
|
atom1.a.b = atom1.a.b + 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
});
|
|
|
|
test("reobserve new object values", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: 1 }, () => n++) as any;
|
|
expect(n).toBe(0);
|
|
|
|
atom1.a++;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
atom1.a = { b: 2 };
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
atom1.a.b = atom1.a.b + 3;
|
|
await nextMicroTick();
|
|
expect(n).toBe(3);
|
|
});
|
|
|
|
test("deep observe misc changes", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ o: { a: 1 }, arr: [1], n: 13 }, () => n++) as any;
|
|
expect(n).toBe(0);
|
|
|
|
atom1.o.a = atom1.o.a + 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
atom1.arr.push(2);
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
atom1.n = 155;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
atom1.n = atom1.n + 1;
|
|
await nextMicroTick();
|
|
expect(n).toBe(3);
|
|
});
|
|
|
|
test("properly handle already observed atom", async () => {
|
|
let n1 = 0;
|
|
let n2 = 0;
|
|
|
|
const obj1 = createAtom({ a: 1 }, () => n1++) as any;
|
|
const obj2 = createAtom({ b: 1 }, () => n2++) as any;
|
|
|
|
obj1.a = obj1.a + 2;
|
|
obj2.b = obj2.b + 3;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(1);
|
|
expect(n2).toBe(1);
|
|
|
|
obj2.b = obj1;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(1);
|
|
expect(n2).toBe(2);
|
|
|
|
obj1.a = 33;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(2);
|
|
expect(n2).toBe(2);
|
|
|
|
obj2.b.a = obj2.b.a + 2;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(3);
|
|
expect(n2).toBe(3);
|
|
});
|
|
|
|
test("properly handle already observed atom in observed atom", async () => {
|
|
let n1 = 0;
|
|
let n2 = 0;
|
|
const obj1 = createAtom({ a: { c: 2 } }, () => n1++) as any;
|
|
const obj2 = createAtom({ b: 1 }, () => n2++) as any;
|
|
|
|
obj2.c = obj1;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(0);
|
|
expect(n2).toBe(1);
|
|
|
|
obj1.a.c = obj1.a.c + 33;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(1);
|
|
expect(n2).toBe(1);
|
|
|
|
obj2.c.a.c = obj2.c.a.c + 3;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(2);
|
|
expect(n2).toBe(2);
|
|
});
|
|
|
|
test("can reobserve object", async () => {
|
|
let n1 = 0;
|
|
let n2 = 0;
|
|
const atom1 = createAtom({ a: 0 }, () => n1++) as any;
|
|
|
|
atom1.a = atom1.a + 1;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(1);
|
|
expect(n2).toBe(0);
|
|
|
|
const atom2 = createAtom(atom1, () => n2++) as any;
|
|
expect(atom1).toEqual(atom2);
|
|
|
|
atom2.a = 2;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(2);
|
|
expect(n2).toBe(1);
|
|
});
|
|
|
|
test("can reobserve nested properties in object", async () => {
|
|
let n1 = 0;
|
|
let n2 = 0;
|
|
const atom1 = createAtom({ a: [{ b: 1 }] }, () => n1++) as any;
|
|
|
|
const atom2 = createAtom(atom1, () => n2++) as any;
|
|
|
|
atom1.a[0].b = atom1.a[0].b + 2;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(1);
|
|
expect(n2).toBe(0);
|
|
|
|
atom2.c = 2;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(2);
|
|
expect(n2).toBe(1);
|
|
});
|
|
|
|
test("rereading some property again give exactly same result", () => {
|
|
const atom1 = createAtom({ a: { b: 1 } });
|
|
const obj1 = atom1.a;
|
|
const obj2 = atom1.a;
|
|
expect(obj1).toBe(obj2);
|
|
});
|
|
|
|
test("can reobserve new properties in object", async () => {
|
|
let n1 = 0;
|
|
let n2 = 0;
|
|
const atom1 = createAtom({ a: [{ b: 1 }] }, () => n1++) as any;
|
|
|
|
createAtom(atom1, () => n2++) as any;
|
|
|
|
atom1.a[0].b = { c: 1 };
|
|
await nextMicroTick();
|
|
expect(n1).toBe(0);
|
|
expect(n2).toBe(0);
|
|
|
|
atom1.a[0].b.c = atom1.a[0].b.c + 2;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(1);
|
|
expect(n2).toBe(0);
|
|
});
|
|
|
|
test("can observe sub property of observed object", async () => {
|
|
let n1 = 0;
|
|
let n2 = 0;
|
|
const atom1 = createAtom({ a: { b: 1 }, c: 1 }, () => n1++) as any;
|
|
|
|
const atom2 = createAtom(atom1.a, () => n2++) as any;
|
|
|
|
atom1.a.b = atom1.a.b + 2;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(1);
|
|
expect(n2).toBe(0);
|
|
|
|
atom1.l = 2;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(2);
|
|
expect(n2).toBe(0);
|
|
|
|
atom1.a.k = 3;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(3);
|
|
expect(n2).toBe(1);
|
|
|
|
atom1.c = 14;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(3);
|
|
expect(n2).toBe(1);
|
|
|
|
atom2.b = atom2.b + 3;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(4);
|
|
expect(n2).toBe(2);
|
|
});
|
|
|
|
test("can set a property more than once", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({}, () => n++) as any;
|
|
|
|
atom1.aky = atom1.aku;
|
|
expect(n).toBe(0);
|
|
atom1.aku = "always finds annoying problems";
|
|
expect(n).toBe(0);
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
atom1.aku = "always finds good problems";
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
});
|
|
|
|
test("properly handle swapping elements", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: { arr: [] }, b: 1 }, () => n++) as any;
|
|
|
|
// swap a and b
|
|
const b = atom1.b;
|
|
atom1.b = atom1.a;
|
|
atom1.a = b;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
// push something into array to make sure it works
|
|
atom1.b.arr.push("blanche");
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
});
|
|
|
|
test("properly handle assigning observed atom containing array", async () => {
|
|
let n = 0;
|
|
const atom1 = createAtom({ a: { arr: [], val: "test" } }, () => n++) as any;
|
|
expect(n).toBe(0);
|
|
|
|
atom1.a = { ...atom1.a, val: "test2" };
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
// push something into array to make sure it works
|
|
atom1.a.arr.push("blanche");
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
});
|
|
|
|
test("accept cycles in observed atom", async () => {
|
|
let n = 0;
|
|
let obj1: any = {};
|
|
let obj2: any = { b: obj1, key: 1 };
|
|
obj1.a = obj2;
|
|
obj1 = createAtom(obj1, () => n++) as any;
|
|
obj2 = obj1.a;
|
|
await nextMicroTick();
|
|
expect(n).toBe(0);
|
|
|
|
obj1.key = 3;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
});
|
|
|
|
test("call callback when atom is changed", async () => {
|
|
let n = 0;
|
|
const atom1: any = createAtom({ a: 1, b: { c: 2 }, d: [{ e: 3 }], f: 4 }, () => n++);
|
|
expect(n).toBe(0);
|
|
|
|
atom1.a = atom1.a + 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
atom1.b.c = atom1.b.c + 3;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
atom1.d[0].e = atom1.d[0].e + 5;
|
|
await nextMicroTick();
|
|
expect(n).toBe(3);
|
|
|
|
atom1.a = 111;
|
|
atom1.f = 222;
|
|
await nextMicroTick();
|
|
expect(n).toBe(4);
|
|
});
|
|
|
|
test("can unobserve a value", async () => {
|
|
let n = 0;
|
|
const cb = () => n++;
|
|
const unregisterObserver = registerObserver(cb);
|
|
|
|
const atom1 = createAtom({ a: 1 }, cb);
|
|
|
|
atom1.a = atom1.a + 3;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
unregisterObserver();
|
|
|
|
atom1.a = 4;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
});
|
|
|
|
test("observing some observed atom", async () => {
|
|
let n1 = 0;
|
|
let n2 = 0;
|
|
const inner = createAtom({ a: 1 }, () => n1++);
|
|
const outer = createAtom({ b: inner }, () => n2++);
|
|
expect(n1).toBe(0);
|
|
expect(n2).toBe(0);
|
|
|
|
outer.b.a = outer.b.a + 2;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(0);
|
|
expect(n2).toBe(1);
|
|
});
|
|
|
|
test("observing some observed atom, variant", async () => {
|
|
let n1 = 0;
|
|
let n2 = 0;
|
|
const inner = createAtom({ a: 1 }, () => n1++);
|
|
const outer = createAtom({ b: inner, c: 0 }, () => n2++);
|
|
expect(n1).toBe(0);
|
|
expect(n2).toBe(0);
|
|
|
|
inner.a = inner.a + 2;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(1);
|
|
expect(n2).toBe(0);
|
|
|
|
outer.c = outer.c + 3;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(1);
|
|
expect(n2).toBe(1);
|
|
});
|
|
|
|
test("observing some observed atom, variant 2", async () => {
|
|
let n1 = 0;
|
|
let n2 = 0;
|
|
let n3 = 0;
|
|
const obj1 = createAtom({ a: 1 }, () => n1++);
|
|
const obj2 = createAtom({ b: {} }, () => n2++);
|
|
const obj3 = createAtom({ c: {} }, () => n3++);
|
|
|
|
obj2.b = obj2.b;
|
|
obj3.c = obj3.c;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(0);
|
|
expect(n2).toBe(1);
|
|
expect(n3).toBe(1);
|
|
|
|
obj2.b = obj1;
|
|
obj3.c = obj1;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(0);
|
|
expect(n2).toBe(2);
|
|
expect(n3).toBe(2);
|
|
|
|
obj1.a = obj1.a + 2;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(1);
|
|
expect(n2).toBe(2);
|
|
expect(n3).toBe(2);
|
|
|
|
obj2.b.a = obj2.b.a + 1;
|
|
await nextMicroTick();
|
|
expect(n1).toBe(2);
|
|
expect(n2).toBe(3);
|
|
expect(n3).toBe(2);
|
|
});
|
|
|
|
test("notification is not done after unregistration", async () => {
|
|
let n = 0;
|
|
const observer = () => n++;
|
|
const unregisterObserver = registerObserver(observer);
|
|
const state = atom({ a: 1 } as any, observer);
|
|
|
|
state.a = state.a;
|
|
await nextMicroTick();
|
|
expect(n).toBe(0);
|
|
|
|
unregisterObserver();
|
|
|
|
state.a = { b: 2 };
|
|
await nextMicroTick();
|
|
expect(n).toBe(0);
|
|
|
|
state.a.b = state.a.b + 3;
|
|
await nextMicroTick();
|
|
expect(n).toBe(0);
|
|
});
|
|
|
|
test("atom cleaning after key deleted 1", async () => {
|
|
let n = 0;
|
|
const a = { k: {} } as any;
|
|
const atom = createAtom(a, () => n++);
|
|
|
|
atom.k.l = 1;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
const kVal = atom.k;
|
|
|
|
delete atom.k;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
kVal.l = 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2); // kVal must no longer be observed
|
|
});
|
|
|
|
test("atom cleaning after key deleted 2", async () => {
|
|
let n = 0;
|
|
const b = {} as any;
|
|
const a = { k: b } as any;
|
|
const observer = () => n++;
|
|
const atom2 = createAtom(b, observer);
|
|
const atom1 = createAtom(a, observer); // atom2 should be still a seed
|
|
|
|
atom1.c = 1;
|
|
atom1.k.d = 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
delete atom1.k;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
atom2.e = 3;
|
|
await nextMicroTick();
|
|
expect(n).toBe(3); // b must still be observed
|
|
});
|
|
|
|
test("atom cleaning after key deleted 3", async () => {
|
|
let n = 0;
|
|
const b = {} as any;
|
|
const a = { k: b } as any;
|
|
const observer = () => n++;
|
|
const atom1 = createAtom(a, observer);
|
|
const atom2 = createAtom(b, observer); // atom2 should be a seed now
|
|
|
|
atom1.c = 1;
|
|
atom1.k.d = 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
delete atom1.k;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
atom2.e = 3;
|
|
await nextMicroTick();
|
|
expect(n).toBe(3); // b must still be observed
|
|
});
|
|
|
|
test("atom cleaning after key deleted 4", async () => {
|
|
let n = 0;
|
|
const a = { k: {} } as any;
|
|
a.k = a;
|
|
const atom = createAtom(a, () => n++);
|
|
|
|
atom.b = 1;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
|
|
delete atom.k;
|
|
await nextMicroTick();
|
|
expect(n).toBe(2);
|
|
|
|
atom.c = 2;
|
|
await nextMicroTick();
|
|
expect(n).toBe(3); // a must still be observed
|
|
});
|
|
|
|
test("atom cleaning after trackable key value changed", async () => {
|
|
let n = 0;
|
|
const a = { k: { n: 1 } } as any;
|
|
const atom = createAtom(a, () => n++);
|
|
const kVal = atom.k; // read k
|
|
|
|
atom.k = { n: atom.k.n + 1 };
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
expect(atom.k).toEqual({ n: 2 });
|
|
|
|
kVal.n = 3;
|
|
await nextMicroTick();
|
|
expect(n).toBe(1);
|
|
expect(atom.k).toEqual({ n: 2 });
|
|
});
|
|
});
|
|
|
|
describe("Reactivity: useState", () => {
|
|
let fixture: HTMLElement;
|
|
|
|
snapshotEverything();
|
|
|
|
beforeEach(() => {
|
|
fixture = makeTestFixture();
|
|
});
|
|
|
|
/**
|
|
* A context can be defined as an atom with a default observer.
|
|
* It can be exposed and share by multiple components or other objects
|
|
* (via useState for instance)
|
|
*/
|
|
|
|
test("very simple use, with initial value", async () => {
|
|
const testContext = createAtom({ value: 123 });
|
|
|
|
class Comp extends Component {
|
|
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
|
|
contextObj = useState(testContext);
|
|
}
|
|
await mount(Comp, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>123</div>");
|
|
});
|
|
|
|
test("useContext=useState hook is reactive, for one component", async () => {
|
|
const testContext = createAtom({ value: 123 });
|
|
|
|
class Comp extends Component {
|
|
static template = xml`<div><t t-esc="contextObj.value"/></div>`;
|
|
contextObj = useState(testContext);
|
|
}
|
|
const comp = await mount(Comp, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>123</div>");
|
|
(comp as any).contextObj.value = 321;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>321</div>");
|
|
});
|
|
|
|
test("two components can subscribe to same context", async () => {
|
|
const testContext = createAtom({ value: 123 });
|
|
const steps: string[] = [];
|
|
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
|
|
contextObj = useState(testContext);
|
|
setup() {
|
|
onRender(() => {
|
|
steps.push("child");
|
|
});
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child /><Child /></div>`;
|
|
static components = { Child };
|
|
setup() {
|
|
onRender(() => {
|
|
steps.push("parent");
|
|
});
|
|
}
|
|
}
|
|
await mount(Parent, fixture);
|
|
expect(steps).toEqual(["parent", "child", "child"]);
|
|
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
|
|
testContext.value = 321;
|
|
await nextTick();
|
|
expect(steps).toEqual(["parent", "child", "child", "child", "child"]);
|
|
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
|
|
});
|
|
|
|
test("two components are updated in parallel", async () => {
|
|
const testContext = createAtom({ value: 123 });
|
|
const steps: string[] = [];
|
|
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
|
|
contextObj = useState(testContext);
|
|
setup() {
|
|
onRender(async () => {
|
|
steps.push("render");
|
|
});
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child /><Child /></div>`;
|
|
static components = { Child };
|
|
}
|
|
await mount(Parent, fixture);
|
|
expect(steps).toEqual(["render", "render"]);
|
|
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
|
|
testContext.value = 321;
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect(steps).toEqual(["render", "render", "render", "render"]);
|
|
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
|
|
});
|
|
|
|
test("two independent components on different levels are updated in parallel", async () => {
|
|
const testContext = createAtom({ value: 123 });
|
|
const steps: string[] = [];
|
|
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="contextObj.value"/></span>`;
|
|
static components = {};
|
|
contextObj = useState(testContext);
|
|
setup() {
|
|
onRender(() => {
|
|
steps.push("render");
|
|
});
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child /></div>`;
|
|
static components = { Child };
|
|
}
|
|
|
|
class GrandFather extends Component {
|
|
static template = xml`<div><Child /><Parent /></div>`;
|
|
static components = { Child, Parent };
|
|
}
|
|
|
|
await mount(GrandFather, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><span>123</span><div><span>123</span></div></div>");
|
|
expect(steps).toEqual(["render", "render"]);
|
|
|
|
testContext.value = 321;
|
|
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect(steps).toEqual(["render", "render", "render", "render"]);
|
|
expect(fixture.innerHTML).toBe("<div><span>123</span><div><span>123</span></div></div>");
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>321</span><div><span>321</span></div></div>");
|
|
});
|
|
|
|
test("one components can subscribe twice to same context", async () => {
|
|
const testContext = createAtom({ a: 1, b: 2 });
|
|
const steps: string[] = [];
|
|
|
|
class Comp extends Component {
|
|
static template = xml`<div><t t-esc="contextObj1.a"/><t t-esc="contextObj2.b"/></div>`;
|
|
contextObj1 = useState(testContext);
|
|
contextObj2 = useState(testContext);
|
|
setup() {
|
|
onRender(() => {
|
|
steps.push("comp");
|
|
});
|
|
}
|
|
}
|
|
await mount(Comp, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>12</div>");
|
|
expect(steps).toEqual(["comp"]);
|
|
testContext.a = 3;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>32</div>");
|
|
expect(steps).toEqual(["comp", "comp"]);
|
|
});
|
|
|
|
test("parent and children subscribed to same context", async () => {
|
|
const testContext = createAtom({ a: 123, b: 321 });
|
|
const steps: string[] = [];
|
|
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
|
|
contextObj = useState(testContext);
|
|
setup() {
|
|
onRender(() => {
|
|
steps.push("child");
|
|
});
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child /><t t-esc="contextObj.b"/></div>`;
|
|
static components = { Child };
|
|
contextObj = useState(testContext);
|
|
setup() {
|
|
onRender(() => {
|
|
steps.push("parent");
|
|
});
|
|
}
|
|
}
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>123</span>321</div>");
|
|
expect(steps).toEqual(["parent", "child"]);
|
|
|
|
(parent as any).contextObj.a = 124;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>124</span>321</div>");
|
|
|
|
expect(steps).toEqual(["parent", "child", "child"]);
|
|
});
|
|
|
|
test("several nodes on different level use same context", async () => {
|
|
const testContext = createAtom({ a: 123, b: 456 });
|
|
const steps: Set<string> = new Set();
|
|
|
|
/**
|
|
* Scheme:
|
|
* L1A
|
|
* / \
|
|
* L2A L2B
|
|
* |
|
|
* L3A
|
|
*/
|
|
|
|
class L3A extends Component {
|
|
static template = xml`<div><t t-esc="contextObj.a"/> <t t-esc="contextObj.b"/></div>`;
|
|
contextObj = useState(testContext);
|
|
setup() {
|
|
onRender(() => {
|
|
steps.add("L3A");
|
|
});
|
|
}
|
|
}
|
|
|
|
class L2B extends Component {
|
|
static template = xml`<div><t t-esc="contextObj.b"/></div>`;
|
|
contextObj = useState(testContext);
|
|
setup() {
|
|
onRender(() => {
|
|
steps.add("L2B");
|
|
});
|
|
}
|
|
}
|
|
|
|
class L2A extends Component {
|
|
static template = xml`<div><t t-esc="contextObj.a"/><L3A /></div>`;
|
|
static components = { L3A };
|
|
contextObj = useState(testContext);
|
|
setup() {
|
|
onRender(() => {
|
|
steps.add("L2A");
|
|
});
|
|
}
|
|
}
|
|
|
|
class L1A extends Component {
|
|
static template = xml`<div><L2A /><L2B /></div>`;
|
|
static components = { L2A, L2B };
|
|
contextObj = useState(testContext);
|
|
setup() {
|
|
onRender(() => {
|
|
steps.add("L1A");
|
|
});
|
|
}
|
|
}
|
|
|
|
await mount(L1A, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div>123<div>123 456</div></div><div>456</div></div>");
|
|
expect([...steps]).toEqual(["L1A", "L2A", "L2B", "L3A"]);
|
|
steps.clear();
|
|
|
|
testContext.a = 321;
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect([...steps]).toEqual(["L2A"]);
|
|
await nextTick();
|
|
expect([...steps]).toEqual(["L2A", "L3A"]);
|
|
expect(fixture.innerHTML).toBe("<div><div>321<div>321 456</div></div><div>456</div></div>");
|
|
steps.clear();
|
|
|
|
testContext.b = 654;
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect([...steps]).toEqual(["L2B", "L3A"]);
|
|
await nextTick();
|
|
expect([...steps]).toEqual(["L2B", "L3A"]);
|
|
expect(fixture.innerHTML).toBe("<div><div>321<div>321 654</div></div><div>654</div></div>");
|
|
steps.clear();
|
|
|
|
testContext.a = 777;
|
|
testContext.b = 777;
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect([...steps]).toEqual(["L2A", "L2B"]);
|
|
await nextTick();
|
|
expect([...steps]).toEqual(["L2A", "L2B", "L3A"]);
|
|
expect(fixture.innerHTML).toBe("<div><div>777<div>777 777</div></div><div>777</div></div>");
|
|
steps.clear();
|
|
|
|
testContext.c = 444;
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect([...steps]).toEqual(["L1A"]);
|
|
await nextTick();
|
|
expect([...steps]).toEqual(["L1A", "L2A", "L2B", "L3A"]);
|
|
});
|
|
|
|
test("destroyed component is inactive", async () => {
|
|
const testContext = createAtom({ a: 123 });
|
|
const steps: string[] = [];
|
|
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
|
|
contextObj = useState(testContext);
|
|
setup() {
|
|
onRender(() => {
|
|
steps.push("child");
|
|
});
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child t-if="state.flag"/></div>`;
|
|
static components = { Child };
|
|
state = useState({ flag: true });
|
|
setup() {
|
|
onRender(() => {
|
|
steps.push("parent");
|
|
});
|
|
}
|
|
}
|
|
const parent = await mount(Parent, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
|
|
expect(steps).toEqual(["parent", "child"]);
|
|
|
|
testContext.a = 321;
|
|
await nextTick();
|
|
expect(steps).toEqual(["parent", "child", "child"]);
|
|
|
|
parent.state.flag = false;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div></div>");
|
|
expect(steps).toEqual(["parent", "child", "child", "parent"]);
|
|
|
|
testContext.a = 456;
|
|
await nextTick();
|
|
expect(steps).toEqual(["parent", "child", "child", "parent"]);
|
|
});
|
|
|
|
test("destroyed component before being mounted is inactive", async () => {
|
|
const testContext = createAtom({ a: 123 });
|
|
const steps: string[] = [];
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="contextObj.a"/></span>`;
|
|
contextObj = useState(testContext);
|
|
setup() {
|
|
onWillStart(() => {
|
|
return makeDeferred();
|
|
});
|
|
onRender(() => {
|
|
steps.push("child");
|
|
});
|
|
}
|
|
}
|
|
let parent: any;
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child t-if="state.flag"/></div>`;
|
|
static components = { Child };
|
|
state = useState({ flag: true });
|
|
setup() {
|
|
parent = this;
|
|
}
|
|
}
|
|
|
|
const prom = mount(Parent, fixture); // cannot work
|
|
await nextTick(); // wait for Child to be instantiated
|
|
testContext.a = 321;
|
|
await nextMicroTick();
|
|
parent.state.flag = false;
|
|
await prom;
|
|
expect(fixture.innerHTML).toBe("<div></div>");
|
|
testContext.a = 456;
|
|
await nextMicroTick();
|
|
expect(steps).toEqual([]);
|
|
});
|
|
|
|
test("useless atoms should be deleted", async () => {
|
|
const testContext = createAtom({
|
|
1: { id: 1, quantity: 3, description: "First quantity" },
|
|
2: { id: 2, quantity: 5, description: "Second quantity" },
|
|
});
|
|
|
|
const secondQuantity = testContext[2];
|
|
|
|
const steps: Set<string> = new Set();
|
|
|
|
class Quantity extends Component {
|
|
static template = xml`<div><t t-esc="state.quantity"/></div>`;
|
|
state = useState(testContext[this.props.id]);
|
|
|
|
setup() {
|
|
onRender(() => {
|
|
steps.add(`quantity${this.props.id}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
class ListOfQuantities extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<t t-foreach="Object.keys(state)" t-as="id" t-key="id">
|
|
<Quantity id="id"/>
|
|
</t>
|
|
Total: <t t-esc="total"/>
|
|
Count: <t t-esc="Object.keys(state).length"/>
|
|
</div>`;
|
|
static components = { Quantity };
|
|
state = useState(testContext);
|
|
|
|
setup() {
|
|
onRender(() => {
|
|
steps.add("list");
|
|
});
|
|
}
|
|
|
|
get total() {
|
|
let total = 0;
|
|
for (const { quantity } of Object.values(this.state) as any) {
|
|
total += quantity;
|
|
}
|
|
return total;
|
|
}
|
|
}
|
|
|
|
await mount(ListOfQuantities, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div>3</div><div>5</div> Total: 8 Count: 2</div>");
|
|
expect([...steps]).toEqual(["list", "quantity1", "quantity2"]);
|
|
steps.clear();
|
|
|
|
delete testContext[2];
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect([...steps]).toEqual(["list"]);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><div>3</div> Total: 3 Count: 1</div>");
|
|
expect([...steps]).toEqual(["list", "quantity1"]);
|
|
steps.clear();
|
|
|
|
secondQuantity.quantity = 2;
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect(fixture.innerHTML).toBe("<div><div>3</div> Total: 3 Count: 1</div>");
|
|
expect([...steps]).toEqual([]);
|
|
steps.clear();
|
|
});
|
|
|
|
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 = createAtom({ x: { n: 1 }, key: "x" });
|
|
const def = makeDeferred();
|
|
let stateC: any;
|
|
class ComponentC extends Component {
|
|
static template = xml`<span><t t-esc="context[props.key].n"/><t t-esc="state.x"/></span>`;
|
|
context = useState(testContext);
|
|
state = useState({ x: "a" });
|
|
setup() {
|
|
stateC = this.state;
|
|
}
|
|
}
|
|
class ComponentB extends Component {
|
|
static components = { ComponentC };
|
|
static template = xml`<p><ComponentC key="props.key"/></p>`;
|
|
setup() {
|
|
onWillUpdateProps(() => def);
|
|
}
|
|
}
|
|
class ComponentA extends Component {
|
|
static components = { ComponentB };
|
|
static template = xml`<div><ComponentB key="context.key"/></div>`;
|
|
context = useState(testContext);
|
|
}
|
|
|
|
await mount(ComponentA, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
|
|
testContext.key = "y";
|
|
testContext.y = { n: 2 };
|
|
delete testContext.x;
|
|
await nextTick();
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
|
|
stateC.x = "b";
|
|
await nextTick();
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p><span>1a</span></p></div>");
|
|
def.resolve();
|
|
await nextTick();
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p><span>2b</span></p></div>");
|
|
});
|
|
});
|