mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[IMP] reactivity: new primitives for reactivity
fine grained reactivity: existing key in source changes --> only observer having read the key are notified add/delete key in source changes --> all source observers are notified Co-authored-by: Aaron Bohy <aab@odoo.com> Co-authored-by: Géry Debongnie <ged@odoo.com> Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
This commit is contained in:
committed by
Géry Debongnie
parent
6479631983
commit
4e4b85e2a9
+430
-242
@@ -1,107 +1,248 @@
|
||||
import { observe, unobserve } from "../src/reactivity";
|
||||
import { atom, registerObserver } from "../src/reactivity";
|
||||
|
||||
describe("observe", () => {
|
||||
test("basic properties", () => {
|
||||
function createAtom(atom1: any, observer?: any) {
|
||||
observer = observer || (() => {});
|
||||
registerObserver(observer);
|
||||
return atom(atom1, observer);
|
||||
}
|
||||
|
||||
describe("getAtom", () => {
|
||||
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", () => {
|
||||
let n = 0;
|
||||
const obj = observe({} as any, () => n++);
|
||||
expect(typeof obj).toBe("object");
|
||||
expect(n).toBe(0);
|
||||
|
||||
obj.a = 3;
|
||||
expect(n).toBe(1);
|
||||
expect(obj.a).toBe(3);
|
||||
const atom1 = createAtom({ a: 1 }, () => n++);
|
||||
atom1.a = 2;
|
||||
expect(n).toBe(0); // key has not be read yet.
|
||||
atom1.a = atom1.a + 5; // key is read and then modified
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("setting property to same value does not trigger callback", () => {
|
||||
let n = 0;
|
||||
const obj = observe({} as any, () => n++);
|
||||
|
||||
obj.a = 3;
|
||||
obj.a = 3;
|
||||
const atom1 = createAtom({ a: 1 }, () => n++);
|
||||
atom1.a = atom1.a + 5; // read and modifies property a to have value 6
|
||||
expect(n).toBe(1);
|
||||
atom1.a = 6; // same value
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("observe cycles", () => {
|
||||
const a = { b: {} };
|
||||
a.b = a;
|
||||
|
||||
let n = 0;
|
||||
const atom1 = createAtom(a, () => n++);
|
||||
|
||||
atom1.k = 2;
|
||||
expect(n).toBe(1);
|
||||
|
||||
delete atom1.l;
|
||||
expect(n).toBe(1);
|
||||
|
||||
delete atom1.k;
|
||||
expect(n).toBe(2);
|
||||
|
||||
atom1.b = 1;
|
||||
expect(n).toBe(2);
|
||||
|
||||
atom1.b = atom1.b + 5;
|
||||
expect(n).toBe(3);
|
||||
});
|
||||
|
||||
test("two observers for same source", () => {
|
||||
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;
|
||||
expect(m).toBe(0);
|
||||
expect(n).toBe(0);
|
||||
|
||||
atom1.new = 2; // already exists!
|
||||
expect(m).toBe(0);
|
||||
expect(n).toBe(0);
|
||||
|
||||
atom1.veryNew = 2;
|
||||
expect(m).toBe(1);
|
||||
expect(n).toBe(1);
|
||||
|
||||
atom1.a = atom1.a + 5;
|
||||
expect(m).toBe(2);
|
||||
expect(n).toBe(1);
|
||||
|
||||
atom2.a = atom2.a + 5;
|
||||
expect(m).toBe(3);
|
||||
expect(n).toBe(2);
|
||||
|
||||
delete atom2.veryNew;
|
||||
expect(m).toBe(4);
|
||||
expect(n).toBe(3);
|
||||
});
|
||||
|
||||
test("create atom from another atom", () => {
|
||||
let n = 0;
|
||||
const atom1 = createAtom({ a: 1 });
|
||||
const atom2 = createAtom(atom1, () => n++);
|
||||
atom2.a = atom2.a + 5;
|
||||
expect(n).toBe(1);
|
||||
atom1.a = 2;
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
test("create atom from another atom 2", () => {
|
||||
let n = 0;
|
||||
const atom1 = createAtom({ a: 1 });
|
||||
const atom2 = createAtom(atom1, () => n++);
|
||||
atom1.a = atom2.a + 5;
|
||||
expect(n).toBe(1);
|
||||
|
||||
atom2.a = atom2.a + 5;
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
test("create atom from another atom 3", () => {
|
||||
let n = 0;
|
||||
const atom1 = createAtom({ a: 1 });
|
||||
const atom2 = createAtom(atom1, () => n++);
|
||||
atom1.a = atom1.a + 5;
|
||||
expect(n).toBe(0); // atom2.a was not yet read
|
||||
atom2.a = atom2.a + 5;
|
||||
expect(n).toBe(1); // atom2.a has been read and is now observed
|
||||
atom1.a = atom1.a + 5;
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
test("immediately returns primitive values", () => {
|
||||
expect(observe(1, () => {})).toBe(1);
|
||||
expect(observe("asf", () => {})).toBe("asf");
|
||||
expect(observe(true, () => {})).toBe(true);
|
||||
expect(observe(null, () => {})).toBe(null);
|
||||
expect(observe(undefined, () => {})).toBe(undefined);
|
||||
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 d = new Date();
|
||||
expect(observe(d, () => {})).toBe(d);
|
||||
const date = new Date();
|
||||
expect(createAtom(date)).toBe(date);
|
||||
});
|
||||
|
||||
test("can observe object with some key set to null", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ a: { b: null } } as any, () => n++);
|
||||
|
||||
const atom1 = createAtom({ a: { b: null } } as any, () => n++);
|
||||
expect(n).toBe(0);
|
||||
obj.a.b = 3;
|
||||
atom1.a.b = Boolean(atom1.a.b);
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("can reobserve object with some key set to null", () => {
|
||||
let n = 0;
|
||||
const fn = () => n++;
|
||||
const obj = observe({ a: { b: null } } as any, fn);
|
||||
const obj2 = observe(obj, fn);
|
||||
expect(obj2).toBe(obj);
|
||||
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);
|
||||
expect(n).toBe(0);
|
||||
obj.a.b = 3;
|
||||
atom1.a.b = Boolean(atom1.a.b);
|
||||
expect(n).toBe(1);
|
||||
unobserve(obj, fn);
|
||||
obj.a.b = 5;
|
||||
unregisterObserver();
|
||||
atom1.a.b = !atom1.a.b;
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("contains initial values", () => {
|
||||
const obj = observe({ a: 1, b: 2 }, () => {});
|
||||
expect(obj.a).toBe(1);
|
||||
expect(obj.b).toBe(2);
|
||||
expect((obj as any).c).toBe(undefined);
|
||||
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", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ a: 1 }, () => n++) as any;
|
||||
const atom1 = createAtom({ a: 1 }, () => n++) as any;
|
||||
|
||||
expect(n).toBe(0);
|
||||
obj.a = 3;
|
||||
atom1.a = atom1.a + 5;
|
||||
expect(n).toBe(1);
|
||||
|
||||
obj.b = 5;
|
||||
atom1.b = atom1.b + 5;
|
||||
expect(n).toBe(2);
|
||||
|
||||
obj.a = null;
|
||||
obj.b = undefined;
|
||||
atom1.a = null;
|
||||
atom1.b = undefined;
|
||||
expect(n).toBe(4);
|
||||
expect(obj).toEqual({ a: null, b: undefined });
|
||||
expect(atom1).toEqual({ a: null, b: undefined });
|
||||
});
|
||||
|
||||
test("properly handle dates", () => {
|
||||
const date = new Date();
|
||||
let n = 0;
|
||||
const obj = observe({ date }, () => n++);
|
||||
const atom1 = createAtom({ date }, () => n++);
|
||||
|
||||
expect(typeof obj.date.getFullYear()).toBe("number");
|
||||
expect(obj.date).toBe(date);
|
||||
obj.date = new Date();
|
||||
expect(typeof atom1.date.getFullYear()).toBe("number");
|
||||
expect(atom1.date).toBe(date);
|
||||
atom1.date = new Date();
|
||||
expect(n).toBe(1);
|
||||
expect(obj.date).not.toBe(date);
|
||||
expect(atom1.date).not.toBe(date);
|
||||
});
|
||||
|
||||
test("properly handle promise", async () => {
|
||||
let resolved = false;
|
||||
const prom = new Promise((r) => r());
|
||||
let n = 0;
|
||||
const obj = observe({ prom }, () => n++);
|
||||
const atom1 = createAtom({ prom }, () => n++);
|
||||
|
||||
expect(obj.prom).toBeInstanceOf(Promise);
|
||||
obj.prom.then(() => (resolved = true));
|
||||
expect(atom1.prom).toBeInstanceOf(Promise);
|
||||
atom1.prom.then(() => (resolved = true));
|
||||
expect(n).toBe(0);
|
||||
expect(resolved).toBe(false);
|
||||
await Promise.resolve();
|
||||
@@ -111,133 +252,150 @@ describe("observe", () => {
|
||||
|
||||
test("can observe value change in array in an object", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ arr: [1, 2] }, () => n++) as any;
|
||||
const atom1 = createAtom({ arr: [1, 2] }, () => n++) as any;
|
||||
|
||||
expect(Array.isArray(obj.arr)).toBe(true);
|
||||
expect(Array.isArray(atom1.arr)).toBe(true);
|
||||
expect(n).toBe(0);
|
||||
|
||||
obj.arr[0] = "nope";
|
||||
atom1.arr[0] = atom1.arr[0] + "nope";
|
||||
|
||||
expect(n).toBe(1);
|
||||
expect(obj.arr[0]).toBe("nope");
|
||||
expect(obj.arr).toEqual(["nope", 2]);
|
||||
expect(atom1.arr[0]).toBe("1nope");
|
||||
expect(atom1.arr).toEqual(["1nope", 2]);
|
||||
});
|
||||
|
||||
test("can observe: changing array in object to another array", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ arr: [1, 2] }, () => n++) as any;
|
||||
const atom1 = createAtom({ arr: [1, 2] }, () => n++) as any;
|
||||
|
||||
expect(Array.isArray(obj.arr)).toBe(true);
|
||||
expect(Array.isArray(atom1.arr)).toBe(true);
|
||||
expect(n).toBe(0);
|
||||
|
||||
obj.arr = [2, 1];
|
||||
atom1.arr = [2, 1];
|
||||
|
||||
expect(n).toBe(1);
|
||||
expect(obj.arr[0]).toBe(2);
|
||||
expect(obj.arr).toEqual([2, 1]);
|
||||
expect(atom1.arr[0]).toBe(2);
|
||||
expect(atom1.arr).toEqual([2, 1]);
|
||||
});
|
||||
|
||||
test("getting twice an object properties return same object", () => {
|
||||
const obj = observe({ a: { b: 1 } }, () => {});
|
||||
|
||||
const a1 = obj.a;
|
||||
const a2 = obj.a;
|
||||
const atom1 = createAtom({ a: { b: 1 } });
|
||||
const a1 = atom1.a;
|
||||
const a2 = atom1.a;
|
||||
expect(a1).toBe(a2);
|
||||
});
|
||||
|
||||
test("various object property changes", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ a: 1 }, () => n++) as any;
|
||||
const atom1 = createAtom({ a: 1 }, () => n++) as any;
|
||||
expect(n).toBe(0);
|
||||
|
||||
obj.a = 2;
|
||||
atom1.a = atom1.a + 2;
|
||||
expect(n).toBe(1);
|
||||
|
||||
// same value again
|
||||
obj.a = 2;
|
||||
atom1.a = 3;
|
||||
expect(n).toBe(1);
|
||||
|
||||
obj.a = 3;
|
||||
atom1.a = 4;
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
test("properly observe arrays", () => {
|
||||
let n = 0;
|
||||
const arr = observe([], () => n++) as any;
|
||||
const atom1 = createAtom([], () => n++) as any;
|
||||
|
||||
expect(Array.isArray(arr)).toBe(true);
|
||||
expect(arr.length).toBe(0);
|
||||
expect(Array.isArray(atom1)).toBe(true);
|
||||
expect(atom1.length).toBe(0);
|
||||
expect(n).toBe(0);
|
||||
|
||||
arr.push(1);
|
||||
atom1.push(1);
|
||||
expect(n).toBe(1);
|
||||
expect(arr.length).toBe(1);
|
||||
expect(arr).toEqual([1]);
|
||||
expect(atom1.length).toBe(1);
|
||||
expect(atom1).toEqual([1]);
|
||||
|
||||
arr.splice(1, 0, "hey");
|
||||
atom1.splice(1, 0, "hey");
|
||||
expect(n).toBe(2);
|
||||
expect(arr).toEqual([1, "hey"]);
|
||||
expect(arr.length).toBe(2);
|
||||
expect(atom1).toEqual([1, "hey"]);
|
||||
expect(atom1.length).toBe(2);
|
||||
|
||||
arr.unshift("lindemans");
|
||||
atom1.unshift("lindemans");
|
||||
//it generates 3 primitive operations
|
||||
expect(n).toBe(5);
|
||||
expect(arr).toEqual(["lindemans", 1, "hey"]);
|
||||
expect(arr.length).toBe(3);
|
||||
expect(atom1).toEqual(["lindemans", 1, "hey"]);
|
||||
expect(atom1.length).toBe(3);
|
||||
|
||||
arr.reverse();
|
||||
atom1.reverse();
|
||||
//it generates 2 primitive operations
|
||||
expect(n).toBe(7);
|
||||
expect(arr).toEqual(["hey", 1, "lindemans"]);
|
||||
expect(arr.length).toBe(3);
|
||||
expect(atom1).toEqual(["hey", 1, "lindemans"]);
|
||||
expect(atom1.length).toBe(3);
|
||||
|
||||
arr.pop(); // one set, one delete
|
||||
atom1.pop(); // one set, one delete
|
||||
expect(n).toBe(9);
|
||||
expect(arr).toEqual(["hey", 1]);
|
||||
expect(arr.length).toBe(2);
|
||||
expect(atom1).toEqual(["hey", 1]);
|
||||
expect(atom1.length).toBe(2);
|
||||
|
||||
arr.shift(); // 2 sets, 1 delete
|
||||
atom1.shift(); // 2 sets, 1 delete
|
||||
expect(n).toBe(12);
|
||||
expect(arr).toEqual([1]);
|
||||
expect(arr.length).toBe(1);
|
||||
expect(atom1).toEqual([1]);
|
||||
expect(atom1.length).toBe(1);
|
||||
});
|
||||
|
||||
test("object pushed into arrays are observed", () => {
|
||||
let n = 0;
|
||||
const arr: any = observe([], () => n++);
|
||||
const arr: any = createAtom([], () => n++);
|
||||
|
||||
arr.push({ kriek: 5 });
|
||||
expect(n).toBe(1);
|
||||
|
||||
arr[0].kriek = 6;
|
||||
expect(n).toBe(1);
|
||||
|
||||
arr[0].kriek = arr[0].kriek + 6;
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
test("set new property on observed object", async () => {
|
||||
let n = 0;
|
||||
const state = observe({}, () => n++) as any;
|
||||
const atom1 = createAtom({}, () => n++) as any;
|
||||
|
||||
expect(n).toBe(0);
|
||||
state.b = 8;
|
||||
atom1.b = 8;
|
||||
|
||||
expect(n).toBe(1);
|
||||
expect(state.b).toBe(8);
|
||||
expect(atom1.b).toBe(8);
|
||||
});
|
||||
|
||||
test("delete property from observed object", async () => {
|
||||
let n = 0;
|
||||
const obj = observe({ a: 1, b: 8 }, () => n++) as any;
|
||||
const atom1 = createAtom({ a: 1, b: 8 }, () => n++) as any;
|
||||
expect(n).toBe(0);
|
||||
|
||||
delete obj.b;
|
||||
delete atom1.b;
|
||||
expect(n).toBe(1);
|
||||
expect(obj).toEqual({ a: 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;
|
||||
// key "a" is no longer observed
|
||||
expect(n).toBe(1);
|
||||
|
||||
atom1.new = 2; // but { b: 1 } is still observed!
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
test("set element in observed array", async () => {
|
||||
let n = 0;
|
||||
const arr = observe(["a"], () => n++);
|
||||
const arr = createAtom(["a"], () => n++);
|
||||
|
||||
arr[1] = "b";
|
||||
expect(n).toBe(1);
|
||||
@@ -246,69 +404,74 @@ describe("observe", () => {
|
||||
|
||||
test("properly observe arrays in object", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ arr: [] }, () => n++) as any;
|
||||
const atom1 = createAtom({ arr: [] }, () => n++) as any;
|
||||
|
||||
expect(obj.arr.length).toBe(0);
|
||||
expect(atom1.arr.length).toBe(0);
|
||||
expect(n).toBe(0);
|
||||
|
||||
obj.arr.push(1);
|
||||
atom1.arr.push(1);
|
||||
expect(n).toBe(1);
|
||||
expect(obj.arr.length).toBe(1);
|
||||
expect(atom1.arr.length).toBe(1);
|
||||
});
|
||||
|
||||
test("properly observe objects in array", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ arr: [{ something: 1 }] }, () => n++) as any;
|
||||
const atom1 = createAtom({ arr: [{ something: 1 }] }, () => n++) as any;
|
||||
expect(n).toBe(0);
|
||||
|
||||
obj.arr[0].something = 2;
|
||||
atom1.arr[0].something = atom1.arr[0].something + 1;
|
||||
expect(n).toBe(1);
|
||||
expect(obj.arr[0].something).toBe(2);
|
||||
expect(atom1.arr[0].something).toBe(2);
|
||||
});
|
||||
|
||||
test("properly observe objects in object", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ a: { b: 1 } }, () => n++) as any;
|
||||
const atom1 = createAtom({ a: { b: 1 } }, () => n++) as any;
|
||||
expect(n).toBe(0);
|
||||
|
||||
obj.a.b = 2;
|
||||
atom1.a.b = atom1.a.b + 2;
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("reobserve new object values", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ a: 1 }, () => n++) as any;
|
||||
const atom1 = createAtom({ a: 1 }, () => n++) as any;
|
||||
expect(n).toBe(0);
|
||||
|
||||
obj.a = { b: 2 };
|
||||
atom1.a++;
|
||||
expect(n).toBe(1);
|
||||
|
||||
obj.a.b = 3;
|
||||
atom1.a = { b: 2 };
|
||||
expect(n).toBe(2);
|
||||
|
||||
atom1.a.b = atom1.a.b + 3;
|
||||
expect(n).toBe(3);
|
||||
});
|
||||
|
||||
test("deep observe misc changes", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ o: { a: 1 }, arr: [1], n: 13 }, () => n++) as any;
|
||||
const atom1 = createAtom({ o: { a: 1 }, arr: [1], n: 13 }, () => n++) as any;
|
||||
expect(n).toBe(0);
|
||||
|
||||
obj.o.a = 2;
|
||||
atom1.o.a = atom1.o.a + 2;
|
||||
expect(n).toBe(1);
|
||||
|
||||
obj.arr.push(2);
|
||||
atom1.arr.push(2);
|
||||
expect(n).toBe(2);
|
||||
|
||||
obj.n = 155;
|
||||
atom1.n = 155;
|
||||
expect(n).toBe(2);
|
||||
atom1.n = atom1.n + 1;
|
||||
expect(n).toBe(3);
|
||||
});
|
||||
|
||||
test("properly handle already observed state", () => {
|
||||
test("properly handle already observed atom", () => {
|
||||
let n1 = 0;
|
||||
let n2 = 0;
|
||||
const obj1 = observe({ a: 1 }, () => n1++) as any;
|
||||
const obj2 = observe({ b: 1 }, () => n2++) as any;
|
||||
const obj1 = createAtom({ a: 1 }, () => n1++) as any;
|
||||
const obj2 = createAtom({ b: 1 }, () => n2++) as any;
|
||||
|
||||
obj1.a = 2;
|
||||
obj2.b = 3;
|
||||
obj1.a = obj1.a + 2;
|
||||
obj2.b = obj2.b + 3;
|
||||
expect(n1).toBe(1);
|
||||
expect(n2).toBe(1);
|
||||
|
||||
@@ -318,36 +481,42 @@ describe("observe", () => {
|
||||
|
||||
obj1.a = 33;
|
||||
expect(n1).toBe(2);
|
||||
expect(n2).toBe(2);
|
||||
obj2.b.a = obj2.b.a + 2;
|
||||
expect(n1).toBe(3);
|
||||
expect(n2).toBe(3);
|
||||
});
|
||||
|
||||
test("properly handle already observed state in observed state", () => {
|
||||
test("properly handle already observed atom in observed atom", () => {
|
||||
let n1 = 0;
|
||||
let n2 = 0;
|
||||
const obj1 = observe({ a: { c: 2 } }, () => n1++) as any;
|
||||
const obj2 = observe({ b: 1 }, () => n2++) as any;
|
||||
const obj1 = createAtom({ a: { c: 2 } }, () => n1++) as any;
|
||||
const obj2 = createAtom({ b: 1 }, () => n2++) as any;
|
||||
|
||||
obj2.c = obj1;
|
||||
expect(n1).toBe(0);
|
||||
expect(n2).toBe(1);
|
||||
|
||||
obj1.a.c = 33;
|
||||
obj1.a.c = obj1.a.c + 33;
|
||||
expect(n1).toBe(1);
|
||||
expect(n2).toBe(1);
|
||||
obj2.c.a.c = obj2.c.a.c + 3;
|
||||
expect(n1).toBe(2);
|
||||
expect(n2).toBe(2);
|
||||
});
|
||||
|
||||
test("can reobserve object", () => {
|
||||
let n1 = 0;
|
||||
let n2 = 0;
|
||||
const obj = observe({ a: 0 }, () => n1++) as any;
|
||||
obj.a = 1;
|
||||
const atom1 = createAtom({ a: 0 }, () => n1++) as any;
|
||||
atom1.a = atom1.a + 1;
|
||||
expect(n1).toBe(1);
|
||||
expect(n2).toBe(0);
|
||||
|
||||
const obj2 = observe(obj, () => n2++) as any;
|
||||
expect(obj).toBe(obj2);
|
||||
const atom2 = createAtom(atom1, () => n2++) as any;
|
||||
expect(atom1).toEqual(atom2);
|
||||
|
||||
obj.a = 2;
|
||||
atom2.a = 2;
|
||||
expect(n1).toBe(2);
|
||||
expect(n2).toBe(1);
|
||||
});
|
||||
@@ -355,205 +524,224 @@ describe("observe", () => {
|
||||
test("can reobserve nested properties in object", () => {
|
||||
let n1 = 0;
|
||||
let n2 = 0;
|
||||
const obj = observe({ a: [{ b: 1 }] }, () => n1++) as any;
|
||||
const atom1 = createAtom({ a: [{ b: 1 }] }, () => n1++) as any;
|
||||
|
||||
observe(obj, () => n2++) as any;
|
||||
const atom2 = createAtom(atom1, () => n2++) as any;
|
||||
|
||||
obj.a[0].b = 2;
|
||||
atom1.a[0].b = atom1.a[0].b + 2;
|
||||
expect(n1).toBe(1);
|
||||
expect(n2).toBe(0);
|
||||
atom2.c = 2;
|
||||
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", () => {
|
||||
let n1 = 0;
|
||||
let n2 = 0;
|
||||
const obj = observe({ a: [{ b: 1 }] }, () => n1++) as any;
|
||||
const atom1 = createAtom({ a: [{ b: 1 }] }, () => n1++) as any;
|
||||
|
||||
observe(obj, () => n2++) as any;
|
||||
createAtom(atom1, () => n2++) as any;
|
||||
|
||||
obj.a[0].b = { c: 1 };
|
||||
atom1.a[0].b = { c: 1 };
|
||||
expect(n1).toBe(0);
|
||||
expect(n2).toBe(0);
|
||||
|
||||
atom1.a[0].b.c = atom1.a[0].b.c + 2;
|
||||
expect(n1).toBe(1);
|
||||
expect(n2).toBe(1);
|
||||
|
||||
obj.a[0].b.c = 2;
|
||||
expect(n1).toBe(2);
|
||||
expect(n2).toBe(2);
|
||||
expect(n2).toBe(0);
|
||||
});
|
||||
|
||||
test("can observe sub property of observed object", () => {
|
||||
let n1 = 0;
|
||||
let n2 = 0;
|
||||
const obj = observe({ a: { b: 1 }, c: 1 }, () => n1++) as any;
|
||||
const atom1 = createAtom({ a: { b: 1 }, c: 1 }, () => n1++) as any;
|
||||
|
||||
observe(obj.a, () => n2++) as any;
|
||||
const atom2 = createAtom(atom1.a, () => n2++) as any;
|
||||
|
||||
obj.a.b = 2;
|
||||
atom1.a.b = atom1.a.b + 2;
|
||||
expect(n1).toBe(1);
|
||||
expect(n2).toBe(0);
|
||||
|
||||
atom1.l = 2;
|
||||
expect(n1).toBe(2);
|
||||
expect(n2).toBe(0);
|
||||
|
||||
atom1.a.k = 3;
|
||||
expect(n1).toBe(3);
|
||||
expect(n2).toBe(1);
|
||||
|
||||
obj.c = 14;
|
||||
expect(n1).toBe(2);
|
||||
atom1.c = 14;
|
||||
expect(n1).toBe(3);
|
||||
expect(n2).toBe(1);
|
||||
atom2.b = atom2.b + 3;
|
||||
expect(n1).toBe(4);
|
||||
expect(n2).toBe(2);
|
||||
});
|
||||
|
||||
test("can set a property more than once", () => {
|
||||
let n = 0;
|
||||
const obj = observe({}, () => n++) as any;
|
||||
const atom1 = createAtom({}, () => n++) as any;
|
||||
|
||||
obj.aku = "always finds annoying problems";
|
||||
expect(n).toBe(1);
|
||||
atom1.aky = atom1.aku;
|
||||
|
||||
obj.aku = "always finds good problems";
|
||||
atom1.aku = "always finds annoying problems";
|
||||
expect(n).toBe(2);
|
||||
|
||||
atom1.aku = "always finds good problems";
|
||||
expect(n).toBe(3);
|
||||
});
|
||||
|
||||
test("properly handle swapping elements", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ a: { arr: [] }, b: 1 }, () => n++) as any;
|
||||
const atom1 = createAtom({ a: { arr: [] }, b: 1 }, () => n++) as any;
|
||||
|
||||
// swap a and b
|
||||
const b = obj.b;
|
||||
obj.b = obj.a;
|
||||
obj.a = b;
|
||||
const b = atom1.b;
|
||||
atom1.b = atom1.a;
|
||||
atom1.a = b;
|
||||
expect(n).toBe(2);
|
||||
|
||||
// push something into array to make sure it works
|
||||
obj.b.arr.push("blanche");
|
||||
atom1.b.arr.push("blanche");
|
||||
expect(n).toBe(3);
|
||||
});
|
||||
|
||||
test("properly handle assigning observed obj containing array", () => {
|
||||
test("properly handle assigning observed atom containing array", () => {
|
||||
let n = 0;
|
||||
const obj = observe({ a: { arr: [], val: "test" } }, () => n++) as any;
|
||||
const atom1 = createAtom({ a: { arr: [], val: "test" } }, () => n++) as any;
|
||||
|
||||
expect(n).toBe(0);
|
||||
obj.a = { ...obj.a, val: "test2" };
|
||||
atom1.a = { ...atom1.a, val: "test2" };
|
||||
expect(n).toBe(1);
|
||||
|
||||
// push something into array to make sure it works
|
||||
obj.a.arr.push("blanche");
|
||||
atom1.a.arr.push("blanche");
|
||||
expect(n).toBe(2);
|
||||
});
|
||||
|
||||
test("accept cycles in observed state", () => {
|
||||
test("accept cycles in observed atom", () => {
|
||||
let n = 0;
|
||||
let obj1: any = {};
|
||||
let obj2: any = { b: obj1, key: 1 };
|
||||
obj1.a = obj2;
|
||||
obj1 = observe(obj1, () => n++) as any;
|
||||
obj1 = createAtom(obj1, () => n++) as any;
|
||||
obj2 = obj1.a;
|
||||
expect(n).toBe(0);
|
||||
|
||||
obj2.key = 3;
|
||||
obj1.key = 3;
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test.skip("call callback when state is changed", async () => {
|
||||
/* const observer = new Observer();
|
||||
observer.notifyCB = jest.fn();
|
||||
const obj: any = observer.observe({ a: 1, b: { c: 2 }, d: [{ e: 3 }], f: 4 });
|
||||
|
||||
expect(observer.notifyCB).toBeCalledTimes(0);
|
||||
|
||||
obj.a = 2;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(1);
|
||||
|
||||
obj.b.c = 3;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(2);
|
||||
|
||||
obj.d[0].e = 5;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(3);
|
||||
|
||||
obj.a = 111;
|
||||
obj.f = 222;
|
||||
await nextMicroTick();
|
||||
expect(observer.notifyCB).toBeCalledTimes(5);*/
|
||||
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;
|
||||
expect(n).toBe(1);
|
||||
atom1.b.c = atom1.b.c + 3;
|
||||
expect(n).toBe(2);
|
||||
atom1.d[0].e = atom1.d[0].e + 5;
|
||||
expect(n).toBe(3);
|
||||
atom1.a = 111;
|
||||
atom1.f = 222;
|
||||
expect(n).toBe(4);
|
||||
});
|
||||
|
||||
test.skip("throw error when state is mutated in object if allowMutation=false", async () => {
|
||||
/*const observer = new Observer();
|
||||
observer.allowMutations = false;
|
||||
const obj: any = observer.observe({ a: 1 });
|
||||
expect(() => {
|
||||
obj.a = 2;
|
||||
}).toThrow('Observed state cannot be changed here! (key: "a", val: "2")');*/
|
||||
});
|
||||
|
||||
test.skip("throw error when state is mutated in array if allowMutation=false", async () => {
|
||||
/* const observer = new Observer();
|
||||
observer.allowMutations = false;
|
||||
const obj: any = observer.observe({ a: [1] });
|
||||
|
||||
expect(() => {
|
||||
obj.a.push(2);
|
||||
}).toThrow('Observed state cannot be changed here! (key: "1", val: "2")');*/
|
||||
});
|
||||
});
|
||||
|
||||
describe("unobserve", () => {
|
||||
test("can unobserve a value", () => {
|
||||
let n = 0;
|
||||
const cb = () => n++;
|
||||
const obj = observe({ a: 1 }, cb);
|
||||
const unregisterObserver = registerObserver(cb);
|
||||
|
||||
obj.a = 3;
|
||||
const atom1 = createAtom({ a: 1 }, cb);
|
||||
|
||||
atom1.a = atom1.a + 3;
|
||||
expect(n).toBe(1);
|
||||
unobserve(obj, cb);
|
||||
obj.a = 4;
|
||||
|
||||
unregisterObserver();
|
||||
|
||||
atom1.a = 4;
|
||||
expect(n).toBe(1);
|
||||
});
|
||||
|
||||
test("can 'unobserve' primitive values, and dates", () => {
|
||||
unobserve(null, () => {});
|
||||
unobserve(undefined, () => {});
|
||||
unobserve(false, () => {});
|
||||
unobserve("iroh", () => {});
|
||||
unobserve(32, () => {});
|
||||
unobserve(new Date(), () => {});
|
||||
});
|
||||
|
||||
test("rereading some property again give exactly same result", () => {
|
||||
const obj = observe({ a: { b: 1 } }, () => {});
|
||||
const obj1 = obj.a;
|
||||
const obj2 = obj.a;
|
||||
expect(obj1).toBe(obj2);
|
||||
});
|
||||
|
||||
test("observing some observed state", () => {
|
||||
test("observing some observed atom", () => {
|
||||
let n1 = 0;
|
||||
let n2 = 0;
|
||||
const inner = observe({ a: 1 }, () => n1++);
|
||||
const outer = observe({ b: inner }, () => n2++);
|
||||
const inner = createAtom({ a: 1 }, () => n1++);
|
||||
const outer = createAtom({ b: inner }, () => n2++);
|
||||
expect(n1).toBe(0);
|
||||
expect(n2).toBe(0);
|
||||
outer.b.a = 2;
|
||||
outer.b.a = outer.b.a + 2;
|
||||
expect(n1).toBe(0);
|
||||
expect(n2).toBe(1);
|
||||
});
|
||||
|
||||
test("observing some observed atom, variant", () => {
|
||||
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;
|
||||
expect(n1).toBe(1);
|
||||
expect(n2).toBe(0);
|
||||
|
||||
outer.c = outer.c + 3;
|
||||
expect(n1).toBe(1);
|
||||
expect(n2).toBe(1);
|
||||
});
|
||||
|
||||
test.skip("observing some observed state, variant", () => {
|
||||
test("observing some observed atom, variant 2", () => {
|
||||
let n1 = 0;
|
||||
let n2 = 0;
|
||||
const inner = observe({ a: 1 }, () => n1++);
|
||||
const outer = observe({ b: inner, c: 0 }, () => n2++);
|
||||
expect(n1).toBe(0);
|
||||
expect(n2).toBe(0);
|
||||
inner.a = 2;
|
||||
expect(n1).toBe(1);
|
||||
expect(n2).toBe(1);
|
||||
let n3 = 0;
|
||||
const obj1 = createAtom({ a: 1 }, () => n1++);
|
||||
const obj2 = createAtom({ b: {} }, () => n2++);
|
||||
const obj3 = createAtom({ c: {} }, () => n3++);
|
||||
|
||||
outer.c = 3;
|
||||
obj2.b = obj2.b;
|
||||
obj3.c = obj3.c;
|
||||
expect(n1).toBe(0);
|
||||
expect(n2).toBe(1);
|
||||
expect(n3).toBe(1);
|
||||
|
||||
obj2.b = obj1;
|
||||
obj3.c = obj1;
|
||||
|
||||
expect(n1).toBe(0);
|
||||
expect(n2).toBe(2);
|
||||
expect(n3).toBe(2);
|
||||
|
||||
obj1.a = obj1.a + 2;
|
||||
expect(n1).toBe(1);
|
||||
expect(n2).toBe(2);
|
||||
expect(n3).toBe(2);
|
||||
|
||||
obj2.b.a = obj2.b.a + 1;
|
||||
expect(n1).toBe(2);
|
||||
expect(n2).toBe(3);
|
||||
expect(n3).toBe(2);
|
||||
});
|
||||
|
||||
test("notification is not done after unregistration", () => {
|
||||
let n = 0;
|
||||
const observer = () => n++;
|
||||
const unregisterObserver = registerObserver(observer);
|
||||
const state = atom({ a: 1 }, observer);
|
||||
state.a = state.a;
|
||||
expect(n).toBe(0);
|
||||
unregisterObserver();
|
||||
state.a = { b: 2 };
|
||||
expect(n).toBe(0);
|
||||
state.a.b = state.a.b + 3;
|
||||
expect(n).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// note: does not work!!!
|
||||
// if we have
|
||||
// obj1 = {a:1}
|
||||
// obj2 = {b: obj1}
|
||||
// obj3 = {c: obj1}
|
||||
// modifying obj1 => should call cb for obj2 and obj3
|
||||
|
||||
Reference in New Issue
Block a user