import { observe, unobserve } from "../src/reactivity"; describe("observe", () => { test("basic properties", () => { 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); 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; expect(n).toBe(1); }); 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); }); test("immediately returns dates", () => { const d = new Date(); expect(observe(d, () => {})).toBe(d); }); test("can observe object with some key set to null", () => { let n = 0; const obj = observe({ a: { b: null } } as any, () => n++); expect(n).toBe(0); obj.a.b = 3; 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); expect(n).toBe(0); obj.a.b = 3; expect(n).toBe(1); unobserve(obj, fn); obj.a.b = 5; 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); }); test("detect object value changes", () => { let n = 0; const obj = observe({ a: 1 }, () => n++) as any; expect(n).toBe(0); obj.a = 3; expect(n).toBe(1); obj.b = 5; expect(n).toBe(2); obj.a = null; obj.b = undefined; expect(n).toBe(4); expect(obj).toEqual({ a: null, b: undefined }); }); test("properly handle dates", () => { const date = new Date(); let n = 0; const obj = observe({ date }, () => n++); expect(typeof obj.date.getFullYear()).toBe("number"); expect(obj.date).toBe(date); obj.date = new Date(); expect(n).toBe(1); expect(obj.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++); expect(obj.prom).toBeInstanceOf(Promise); obj.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", () => { let n = 0; const obj = observe({ arr: [1, 2] }, () => n++) as any; expect(Array.isArray(obj.arr)).toBe(true); expect(n).toBe(0); obj.arr[0] = "nope"; expect(n).toBe(1); expect(obj.arr[0]).toBe("nope"); expect(obj.arr).toEqual(["nope", 2]); }); test("can observe: changing array in object to another array", () => { let n = 0; const obj = observe({ arr: [1, 2] }, () => n++) as any; expect(Array.isArray(obj.arr)).toBe(true); expect(n).toBe(0); obj.arr = [2, 1]; expect(n).toBe(1); expect(obj.arr[0]).toBe(2); expect(obj.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; expect(a1).toBe(a2); }); test("various object property changes", () => { let n = 0; const obj = observe({ a: 1 }, () => n++) as any; expect(n).toBe(0); obj.a = 2; expect(n).toBe(1); // same value again obj.a = 2; expect(n).toBe(1); obj.a = 3; expect(n).toBe(2); }); test("properly observe arrays", () => { let n = 0; const arr = observe([], () => n++) as any; expect(Array.isArray(arr)).toBe(true); expect(arr.length).toBe(0); expect(n).toBe(0); arr.push(1); expect(n).toBe(1); expect(arr.length).toBe(1); expect(arr).toEqual([1]); arr.splice(1, 0, "hey"); expect(n).toBe(2); expect(arr).toEqual([1, "hey"]); expect(arr.length).toBe(2); arr.unshift("lindemans"); //it generates 3 primitive operations expect(n).toBe(5); expect(arr).toEqual(["lindemans", 1, "hey"]); expect(arr.length).toBe(3); arr.reverse(); //it generates 2 primitive operations expect(n).toBe(7); expect(arr).toEqual(["hey", 1, "lindemans"]); expect(arr.length).toBe(3); arr.pop(); // one set, one delete expect(n).toBe(9); expect(arr).toEqual(["hey", 1]); expect(arr.length).toBe(2); arr.shift(); // 2 sets, 1 delete expect(n).toBe(12); expect(arr).toEqual([1]); expect(arr.length).toBe(1); }); test("object pushed into arrays are observed", () => { let n = 0; const arr: any = observe([], () => n++); arr.push({ kriek: 5 }); expect(n).toBe(1); 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; expect(n).toBe(0); state.b = 8; expect(n).toBe(1); expect(state.b).toBe(8); }); test("delete property from observed object", async () => { let n = 0; const obj = observe({ a: 1, b: 8 }, () => n++) as any; expect(n).toBe(0); delete obj.b; expect(n).toBe(1); expect(obj).toEqual({ a: 1 }); }); test("set element in observed array", async () => { let n = 0; const arr = observe(["a"], () => n++); arr[1] = "b"; expect(n).toBe(1); expect(arr).toEqual(["a", "b"]); }); test("properly observe arrays in object", () => { let n = 0; const obj = observe({ arr: [] }, () => n++) as any; expect(obj.arr.length).toBe(0); obj.arr.push(1); expect(n).toBe(1); expect(obj.arr.length).toBe(1); }); test("properly observe objects in array", () => { let n = 0; const obj = observe({ arr: [{ something: 1 }] }, () => n++) as any; expect(n).toBe(0); obj.arr[0].something = 2; expect(n).toBe(1); expect(obj.arr[0].something).toBe(2); }); test("properly observe objects in object", () => { let n = 0; const obj = observe({ a: { b: 1 } }, () => n++) as any; expect(n).toBe(0); obj.a.b = 2; expect(n).toBe(1); }); test("reobserve new object values", () => { let n = 0; const obj = observe({ a: 1 }, () => n++) as any; expect(n).toBe(0); obj.a = { b: 2 }; expect(n).toBe(1); obj.a.b = 3; expect(n).toBe(2); }); test("deep observe misc changes", () => { let n = 0; const obj = observe({ o: { a: 1 }, arr: [1], n: 13 }, () => n++) as any; expect(n).toBe(0); obj.o.a = 2; expect(n).toBe(1); obj.arr.push(2); expect(n).toBe(2); obj.n = 155; expect(n).toBe(3); }); test("properly handle already observed state", () => { let n1 = 0; let n2 = 0; const obj1 = observe({ a: 1 }, () => n1++) as any; const obj2 = observe({ b: 1 }, () => n2++) as any; obj1.a = 2; obj2.b = 3; expect(n1).toBe(1); expect(n2).toBe(1); obj2.b = obj1; expect(n1).toBe(1); expect(n2).toBe(2); obj1.a = 33; expect(n1).toBe(2); expect(n2).toBe(3); }); test("properly handle already observed state in observed state", () => { let n1 = 0; let n2 = 0; const obj1 = observe({ a: { c: 2 } }, () => n1++) as any; const obj2 = observe({ b: 1 }, () => n2++) as any; obj2.c = obj1; expect(n1).toBe(0); expect(n2).toBe(1); obj1.a.c = 33; expect(n1).toBe(1); 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; expect(n1).toBe(1); expect(n2).toBe(0); const obj2 = observe(obj, () => n2++) as any; expect(obj).toBe(obj2); obj.a = 2; expect(n1).toBe(2); expect(n2).toBe(1); }); test("can reobserve nested properties in object", () => { let n1 = 0; let n2 = 0; const obj = observe({ a: [{ b: 1 }] }, () => n1++) as any; observe(obj, () => n2++) as any; obj.a[0].b = 2; expect(n1).toBe(1); expect(n2).toBe(1); }); test("can reobserve new properties in object", () => { let n1 = 0; let n2 = 0; const obj = observe({ a: [{ b: 1 }] }, () => n1++) as any; observe(obj, () => n2++) as any; obj.a[0].b = { c: 1 }; expect(n1).toBe(1); expect(n2).toBe(1); obj.a[0].b.c = 2; expect(n1).toBe(2); expect(n2).toBe(2); }); 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; observe(obj.a, () => n2++) as any; obj.a.b = 2; expect(n1).toBe(1); expect(n2).toBe(1); obj.c = 14; expect(n1).toBe(2); expect(n2).toBe(1); }); test("can set a property more than once", () => { let n = 0; const obj = observe({}, () => n++) as any; obj.aku = "always finds annoying problems"; expect(n).toBe(1); obj.aku = "always finds good problems"; expect(n).toBe(2); }); test("properly handle swapping elements", () => { let n = 0; const obj = observe({ a: { arr: [] }, b: 1 }, () => n++) as any; // swap a and b const b = obj.b; obj.b = obj.a; obj.a = b; expect(n).toBe(2); // push something into array to make sure it works obj.b.arr.push("blanche"); expect(n).toBe(3); }); test("properly handle assigning observed obj containing array", () => { let n = 0; const obj = observe({ a: { arr: [], val: "test" } }, () => n++) as any; expect(n).toBe(0); obj.a = { ...obj.a, val: "test2" }; expect(n).toBe(1); // push something into array to make sure it works obj.a.arr.push("blanche"); expect(n).toBe(2); }); test("accept cycles in observed state", () => { let n = 0; let obj1: any = {}; let obj2: any = { b: obj1, key: 1 }; obj1.a = obj2; obj1 = observe(obj1, () => n++) as any; obj2 = obj1.a; expect(n).toBe(0); obj2.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.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); obj.a = 3; expect(n).toBe(1); unobserve(obj, cb); obj.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", () => { let n1 = 0; let n2 = 0; const inner = observe({ a: 1 }, () => n1++); const outer = observe({ b: inner }, () => n2++); expect(n1).toBe(0); expect(n2).toBe(0); outer.b.a = 2; expect(n1).toBe(1); expect(n2).toBe(1); }); test.skip("observing some observed state, variant", () => { 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); outer.c = 3; expect(n1).toBe(1); expect(n2).toBe(2); }); }); // 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