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`
`; contextObj = useState(testContext); } await mount(Comp, fixture); expect(fixture.innerHTML).toBe("
123
"); }); test("useContext=useState hook is reactive, for one component", async () => { const testContext = createAtom({ value: 123 }); class Comp extends Component { static template = xml`
`; contextObj = useState(testContext); } const comp = await mount(Comp, fixture); expect(fixture.innerHTML).toBe("
123
"); (comp as any).contextObj.value = 321; await nextTick(); expect(fixture.innerHTML).toBe("
321
"); }); test("two components can subscribe to same context", async () => { const testContext = createAtom({ value: 123 }); const steps: string[] = []; class Child extends Component { static template = xml``; contextObj = useState(testContext); setup() { onRender(() => { steps.push("child"); }); } } class Parent extends Component { static template = xml`
`; static components = { Child }; setup() { onRender(() => { steps.push("parent"); }); } } await mount(Parent, fixture); expect(steps).toEqual(["parent", "child", "child"]); expect(fixture.innerHTML).toBe("
123123
"); testContext.value = 321; await nextTick(); expect(steps).toEqual(["parent", "child", "child", "child", "child"]); expect(fixture.innerHTML).toBe("
321321
"); }); test("two components are updated in parallel", async () => { const testContext = createAtom({ value: 123 }); const steps: string[] = []; class Child extends Component { static template = xml``; contextObj = useState(testContext); setup() { onRender(async () => { steps.push("render"); }); } } class Parent extends Component { static template = xml`
`; static components = { Child }; } await mount(Parent, fixture); expect(steps).toEqual(["render", "render"]); expect(fixture.innerHTML).toBe("
123123
"); testContext.value = 321; await nextMicroTick(); await nextMicroTick(); expect(steps).toEqual(["render", "render", "render", "render"]); expect(fixture.innerHTML).toBe("
123123
"); await nextTick(); expect(fixture.innerHTML).toBe("
321321
"); }); 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``; static components = {}; contextObj = useState(testContext); setup() { onRender(() => { steps.push("render"); }); } } class Parent extends Component { static template = xml`
`; static components = { Child }; } class GrandFather extends Component { static template = xml`
`; static components = { Child, Parent }; } await mount(GrandFather, fixture); expect(fixture.innerHTML).toBe("
123
123
"); expect(steps).toEqual(["render", "render"]); testContext.value = 321; await nextMicroTick(); await nextMicroTick(); expect(steps).toEqual(["render", "render", "render", "render"]); expect(fixture.innerHTML).toBe("
123
123
"); await nextTick(); expect(fixture.innerHTML).toBe("
321
321
"); }); 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`
`; contextObj1 = useState(testContext); contextObj2 = useState(testContext); setup() { onRender(() => { steps.push("comp"); }); } } await mount(Comp, fixture); expect(fixture.innerHTML).toBe("
12
"); expect(steps).toEqual(["comp"]); testContext.a = 3; await nextTick(); expect(fixture.innerHTML).toBe("
32
"); 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``; contextObj = useState(testContext); setup() { onRender(() => { steps.push("child"); }); } } class Parent extends Component { static template = xml`
`; static components = { Child }; contextObj = useState(testContext); setup() { onRender(() => { steps.push("parent"); }); } } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
123321
"); expect(steps).toEqual(["parent", "child"]); (parent as any).contextObj.a = 124; await nextTick(); expect(fixture.innerHTML).toBe("
124321
"); 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 = new Set(); /** * Scheme: * L1A * / \ * L2A L2B * | * L3A */ class L3A extends Component { static template = xml`
`; contextObj = useState(testContext); setup() { onRender(() => { steps.add("L3A"); }); } } class L2B extends Component { static template = xml`
`; contextObj = useState(testContext); setup() { onRender(() => { steps.add("L2B"); }); } } class L2A extends Component { static template = xml`
`; static components = { L3A }; contextObj = useState(testContext); setup() { onRender(() => { steps.add("L2A"); }); } } class L1A extends Component { static template = xml`
`; static components = { L2A, L2B }; contextObj = useState(testContext); setup() { onRender(() => { steps.add("L1A"); }); } } await mount(L1A, fixture); expect(fixture.innerHTML).toBe("
123
123 456
456
"); 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("
321
321 456
456
"); 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("
321
321 654
654
"); 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("
777
777 777
777
"); 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``; contextObj = useState(testContext); setup() { onRender(() => { steps.push("child"); }); } } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ flag: true }); setup() { onRender(() => { steps.push("parent"); }); } } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
123
"); 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("
"); 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``; contextObj = useState(testContext); setup() { onWillStart(() => { return makeDeferred(); }); onRender(() => { steps.push("child"); }); } } let parent: any; class Parent extends Component { static template = xml`
`; 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("
"); 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 = new Set(); class Quantity extends Component { static template = xml`
`; state = useState(testContext[this.props.id]); setup() { onRender(() => { steps.add(`quantity${this.props.id}`); }); } } class ListOfQuantities extends Component { static template = xml`
Total: Count:
`; 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("
3
5
Total: 8 Count: 2
"); 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("
3
Total: 3 Count: 1
"); expect([...steps]).toEqual(["list", "quantity1"]); steps.clear(); secondQuantity.quantity = 2; await nextMicroTick(); await nextMicroTick(); expect(fixture.innerHTML).toBe("
3
Total: 3 Count: 1
"); 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``; context = useState(testContext); state = useState({ x: "a" }); setup() { stateC = this.state; } } class ComponentB extends Component { static components = { ComponentC }; static template = xml`

`; setup() { onWillUpdateProps(() => def); } } class ComponentA extends Component { static components = { ComponentB }; static template = xml`
`; context = useState(testContext); } await mount(ComponentA, fixture); expect(fixture.innerHTML).toBe("

1a

"); testContext.key = "y"; testContext.y = { n: 2 }; delete testContext.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

"); }); });