UNPKG

@traxjs/trax

Version:

Reactive state management

986 lines (862 loc) 44.2 kB
import { beforeEach, describe, expect, it } from 'vitest'; import { Store, Trax, TraxObjectType, trax as trx } from '../index'; import { createTraxEnv } from '../core'; import { pause, Person, printEvents, SimpleFamilyStore } from './utils'; describe('Trax Core', () => { let trax: Trax; beforeEach(() => { trax = createTraxEnv(); }); function printLogs(minCycleId = 0, ignoreCycleEvents = true): string[] { return printEvents(trax.log, ignoreCycleEvents, minCycleId); } describe('Basics', () => { it('should support cycleComplete', async () => { expect(trax.pendingChanges).toBe(false); trax.log.info("A"); expect(trax.pendingChanges).toBe(false); await trax.reconciliation(); expect(trax.pendingChanges).toBe(false); expect(printLogs(0, false)).toMatchObject([ '0:0 !CS - 0', '0:1 !LOG - A', '0:2 !CC - 0', ]); // no changes await trax.reconciliation(); expect(trax.pendingChanges).toBe(false); expect(printLogs(0, false)).toMatchObject([ '0:0 !CS - 0', '0:1 !LOG - A', '0:2 !CC - 0', ]); trax.log.info("B"); await trax.reconciliation(); expect(trax.pendingChanges).toBe(false); expect(printLogs(0, false)).toMatchObject([ '0:0 !CS - 0', '0:1 !LOG - A', '0:2 !CC - 0', '1:0 !CS - 0', '1:1 !LOG - B', '1:2 !CC - 0', ]); }); it('should discard non trax objects', async () => { expect(trax.isTraxObject(undefined)).toBe(false); expect(trax.isTraxObject(42)).toBe(false); expect(trax.isTraxObject({})).toBe(false); expect(trax.isTraxObject(true)).toBe(false); expect(trax.getTraxId(undefined)).toBe(""); expect(trax.getTraxId(42)).toBe(""); expect(trax.getTraxId({})).toBe(""); expect(trax.getTraxId(true)).toBe(""); expect(trax.getTraxObjectType(undefined)).toBe(TraxObjectType.NotATraxObject); expect(trax.getTraxObjectType(42)).toBe(TraxObjectType.NotATraxObject); expect(trax.getTraxObjectType({})).toBe(TraxObjectType.NotATraxObject); expect(trax.getTraxObjectType(true)).toBe(TraxObjectType.NotATraxObject); }); }); describe('Stores', () => { it('should be created wih a unique id', async () => { const initFn = (store: Store<any>) => { const root = store.init({ msg: "Hello" }); return { msg: root } } const s1 = trax.createStore("MyStore", initFn); expect(s1.id).toBe("MyStore"); expect(trax.getTraxId(s1)).toBe(""); // the store wrapper is not a trax object const s2 = trax.createStore("MyStore", initFn); expect(s2.id).toBe("MyStore1"); const s3 = trax.createStore("MyStore", initFn); expect(s3.id).toBe("MyStore2"); s1.dispose(); s2.dispose(); s3.dispose(); const s4 = trax.createStore("MyStore", initFn); expect(s4.id).toBe("MyStore"); // no need for suffix const s5 = trax.createStore("MyStore", initFn); expect(s5.id).toBe("MyStore1"); s4.dispose(); const s6 = trax.createStore("MyStore", initFn); expect(s6.id).toBe("MyStore"); // MyStore can be reused const s7 = trax.createStore(["MyStore", "A", 42], initFn); expect(s7.id).toBe("MyStore:A:42"); // id from array }); it('should support creation with no init function', async () => { const ps = trax.createStore("PStore", { firstName: "Homer", lastName: "Simpson" }); expect(ps.data.firstName).toBe("Homer"); }); it('should be able to define a custom dispose behaviour', async () => { let traces = ""; const initFn = (store: Store<any>) => { const root = store.init({ msg: "Hello" }); return { msg: root, dispose() { traces += store.id + ";"; } } } const s1 = trax.createStore("MyStore", initFn); expect(s1.id).toBe("MyStore"); const s2 = trax.createStore("MyStore", initFn); expect(s2.id).toBe("MyStore1"); s1.dispose(); expect(traces).toBe("MyStore;") s2.dispose(); expect(traces).toBe("MyStore;MyStore1;") const s3 = trax.createStore("MyStore", initFn); expect(s3.id).toBe("MyStore"); s3.dispose(); expect(traces).toBe("MyStore;MyStore1;MyStore;") }); it('should be identified as trax objects', async () => { const st = trax.createStore("MyStore", (store: Store<any>) => { store.init({ msg: "Hello" }); }); expect(trax.isTraxObject(st)).toBe(true); }); it('should support init functions that don\'t return any object', async () => { const st = trax.createStore("MyStore", (store: Store<any>) => { store.init({ msg: "Hello" }); }); expect(st.id).toBe("MyStore"); expect(typeof st.add).toBe("function"); expect(trax.isTraxObject(st)).toBe(true); expect(trax.getTraxId(st)).toBe("MyStore"); }); it('should properly dispose all sub-objects', async () => { const ps = trax.createStore("PStore", { firstName: "Homer", lastName: "Simpson" }); const o1 = ps.add("O1", { value: "o1" }); const o2 = ps.add("O2", { value: "o2" }); const rootId = trax.getTraxId(ps.data); const id1 = trax.getTraxId(o1); const id2 = trax.getTraxId(o2); expect(trax.getData(rootId)).toBe(ps.data); expect(trax.getData(id1)).toBe(o1); expect(trax.getData(id2)).toBe(o2); ps.dispose(); expect(trax.getData(rootId)).toBe(undefined); expect(trax.getData(id1)).toBe(undefined); expect(trax.getData(id2)).toBe(undefined); }); describe('Sub Stores', () => { it('should support create / dispose', async () => { const ps = trax.createStore("PStore", { firstName: "Homer", lastName: "Simpson" }); const pss = ps.createStore("SubStore", (store: Store<{ msg: string }>) => { const root = store.init({ msg: "" }); store.compute("Msg", () => { root.msg = ps.data.firstName + "!"; }); }); const data = pss.data; const pssId = pss.id; expect(pssId).toBe("PStore>SubStore"); expect(trax.getTraxId(pss)).toBe("PStore>SubStore"); expect(pss.disposed).toBe(false); expect(data.msg).toBe("Homer!"); await trax.reconciliation(); ps.data.firstName = "Bart"; await trax.reconciliation(); expect(data.msg).toBe("Bart!"); expect(trax.getStore(pssId)).toBe(pss); expect(pss.dispose()).toBe(true); expect(trax.getStore(pssId)).toBe(undefined); expect(pss.dispose()).toBe(false); // already disposed await trax.reconciliation(); ps.data.firstName = "Lisa"; await trax.reconciliation(); expect(data.msg).toBe("Bart!"); // no changes }); it('should be disposed when parent is disposed', async () => { const ps = trax.createStore("PStore", { firstName: "Homer", lastName: "Simpson" }); const pss = ps.createStore("SubStore", (store: Store<{ msg: string }>) => { const root = store.init({ msg: "" }); store.compute("Msg", () => { root.msg = ps.data.firstName + "!"; }); }); const data1 = ps.data; const data1Id = trax.getTraxId(data1); expect(data1Id).toBe("PStore/data"); const data2 = pss.data; const data2Id = trax.getTraxId(data2); expect(data1Id).toBe("PStore/data"); expect(data2Id).toBe("PStore>SubStore/data"); expect(trax.getData(data1Id)).toBe(data1); expect(trax.getData(data2Id)).toBe(data2); const psId = ps.id; const pssId = pss.id; const pr = pss.getProcessor("Msg")!; expect(pr.id).toBe("PStore>SubStore#Msg"); expect(pr.disposed).toBe(false); expect(trax.getStore(psId)).toBe(ps); expect(trax.getStore(pssId)).toBe(pss); ps.dispose(); expect(trax.getStore(psId)).toBe(undefined); expect(trax.getStore(pssId)).toBe(undefined); expect(trax.getData(data1Id)).toBe(undefined); expect(trax.getData(data2Id)).toBe(undefined); expect(ps.disposed).toBe(true); expect(pss.disposed).toBe(true); expect(pr.disposed).toBe(true); }); it('should support substores in substores', async () => { let output = ""; const ps = trax.createStore("PStore", { firstName: "Homer", lastName: "Simpson" }); const pss = ps.createStore("SubStore", (store: Store<{ msg: string }>) => { const root = store.init({ msg: "" }); store.compute("Msg", () => { root.msg = ps.data.firstName + "!"; }); store.createStore("SubSubStore", (sst: Store<{ info: string }>) => { const root2 = sst.init({ info: "" }); sst.compute("Info", () => { output = root2.info = "<" + root.msg + ">"; }); }) }); expect(output).toBe("<Homer!>"); await trax.reconciliation(); ps.data.firstName = "Bart"; await trax.reconciliation(); expect(output).toBe("<Bart!>"); const sst = pss.getStore("SubSubStore")!; expect(sst.id).toBe("PStore>SubStore>SubSubStore"); expect(sst.disposed).toBe(false); expect(trax.getStore("PStore>SubStore>SubSubStore")).toBe(sst); sst.dispose(); expect(sst.disposed).toBe(true); expect(trax.getStore("PStore>SubStore>SubSubStore")).toBe(undefined); expect(pss.getStore("SubSubStore")).toBe(undefined); await trax.reconciliation(); ps.data.firstName = "Lisa"; await trax.reconciliation(); expect(output).toBe("<Bart!>"); // unchanged expect(pss.data.msg).toBe("Lisa!"); // not disposed }); }); describe('Wrappers', () => { function createPStore(throwError = false) { return trax.createStore("PStore", (store: Store<Person>) => { const root = store.init({ firstName: "Homer", lastName: "Simpson" }); store.compute("PrettyName", () => { root.prettyName = root.firstName + " " + root.lastName; }); return { person: root, updateNameSync(value1: string, value2: string) { root.firstName += value1; if (throwError) { throw "Something bad happened"; } const r = root.lastName + value2 + value2; root.lastName = r; return r; }, async updateNameAsync(value1: string, value2: string) { root.firstName += value1; await pause(1); trax.log.event("@traxjs/trax/test/updateNameAsyncDone"); if (throwError) { throw "Something bad happened"; } root.lastName += value2 + value2; }, updateNameAsync2: store.async(function* (value1: string, value2: string) { root.firstName += value1; yield pause(1); trax.log.event("@traxjs/trax/test/updateNameAsync2Done"); if (throwError) { throw "Something bad happened"; } const r = root.lastName + value2 + value2; root.lastName = r; return r; }) } }); } it('should wrap sync functions and log calls', async () => { const ps = createPStore(); expect(ps.person.firstName).toBe("Homer"); await trax.reconciliation(); trax.log.info("A") const r = ps.updateNameSync("A", "B"); expect(r).toBe("SimpsonBB"); trax.processChanges(); expect(ps.person.prettyName).toBe("HomerA SimpsonBB"); expect(printLogs(1)).toMatchObject([ "1:1 !LOG - A", "1:2 !PCS - PStore.updateNameSync()", "1:3 !GET - PStore/data.firstName -> 'Homer'", "1:4 !SET - PStore/data.firstName = 'HomerA' (prev: 'Homer')", "1:5 !DRT - PStore#PrettyName <- PStore/data.firstName", "1:6 !GET - PStore/data.lastName -> 'Simpson'", "1:7 !SET - PStore/data.lastName = 'SimpsonBB' (prev: 'Simpson')", "1:8 !PCE - 1:2", "1:9 !PCS - !Reconciliation #1 - 1 processor", "1:10 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:9", "1:11 !GET - PStore/data.firstName -> 'HomerA'", "1:12 !GET - PStore/data.lastName -> 'SimpsonBB'", "1:13 !SET - PStore/data.prettyName = 'HomerA SimpsonBB' (prev: 'Homer Simpson')", "1:14 !PCE - 1:10", "1:15 !PCE - 1:9", "1:16 !GET - PStore/data.prettyName -> 'HomerA SimpsonBB'", ]); }); it('should log errors on sync function calls', async () => { const ps = createPStore(true); expect(ps.person.firstName).toBe("Homer"); await trax.reconciliation(); trax.log.info("A") ps.updateNameSync("A", "B"); trax.processChanges(); expect(ps.person.prettyName).toBe("HomerA Simpson"); // half processed expect(printLogs(1)).toMatchObject([ "1:1 !LOG - A", "1:2 !PCS - PStore.updateNameSync()", "1:3 !GET - PStore/data.firstName -> 'Homer'", "1:4 !SET - PStore/data.firstName = 'HomerA' (prev: 'Homer')", "1:5 !DRT - PStore#PrettyName <- PStore/data.firstName", "1:6 !ERR - [TRAX] (PStore.updateNameSync) error: Something bad happened", "1:7 !PCE - 1:2", "1:8 !PCS - !Reconciliation #1 - 1 processor", "1:9 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:8", "1:10 !GET - PStore/data.firstName -> 'HomerA'", "1:11 !GET - PStore/data.lastName -> 'Simpson'", "1:12 !SET - PStore/data.prettyName = 'HomerA Simpson' (prev: 'Homer Simpson')", "1:13 !PCE - 1:9", "1:14 !PCE - 1:8", "1:15 !GET - PStore/data.prettyName -> 'HomerA Simpson'", ]); }); it('should wrap async functions and log calls', async () => { const ps = createPStore(); expect(ps.person.firstName).toBe("Homer"); await trax.reconciliation(); trax.log.info("A") ps.updateNameAsync("A", "B"); await trax.log.awaitEvent("@traxjs/trax/test/updateNameAsyncDone"); await trax.reconciliation(); expect(ps.person.prettyName).toBe("HomerA SimpsonBB"); expect(printLogs(1)).toMatchObject([ "1:1 !LOG - A", "1:2 !PCS - PStore.updateNameAsync()", "1:3 !GET - PStore/data.firstName -> 'Homer'", "1:4 !SET - PStore/data.firstName = 'HomerA' (prev: 'Homer')", "1:5 !DRT - PStore#PrettyName <- PStore/data.firstName", "1:6 !PCE - 1:2", "1:7 !PCS - !Reconciliation #1 - 1 processor", "1:8 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:7", "1:9 !GET - PStore/data.firstName -> 'HomerA'", "1:10 !GET - PStore/data.lastName -> 'Simpson'", "1:11 !SET - PStore/data.prettyName = 'HomerA Simpson' (prev: 'Homer Simpson')", "1:12 !PCE - 1:8", "1:13 !PCE - 1:7", "2:1 @traxjs/trax/test/updateNameAsyncDone - NO-DATA", "2:2 !GET - PStore/data.lastName -> 'Simpson'", "2:3 !SET - PStore/data.lastName = 'SimpsonBB' (prev: 'Simpson')", "2:4 !DRT - PStore#PrettyName <- PStore/data.lastName", "2:5 !PCS - !Reconciliation #2 - 1 processor", "2:6 !PCS - !Compute #3 (PStore#PrettyName) P1 Reconciliation - parentId=2:5", "2:7 !GET - PStore/data.firstName -> 'HomerA'", "2:8 !GET - PStore/data.lastName -> 'SimpsonBB'", "2:9 !SET - PStore/data.prettyName = 'HomerA SimpsonBB' (prev: 'HomerA Simpson')", "2:10 !PCE - 2:6", "2:11 !PCE - 2:5", "3:1 !GET - PStore/data.prettyName -> 'HomerA SimpsonBB'", ]); }); it('should log errors on async function calls', async () => { const ps = createPStore(true); expect(ps.person.firstName).toBe("Homer"); await trax.reconciliation(); trax.log.info("A") ps.updateNameAsync("A", "B"); await trax.log.awaitEvent("@traxjs/trax/test/updateNameAsyncDone"); await trax.reconciliation(); expect(ps.person.prettyName).toBe("HomerA Simpson"); // half processed expect(printLogs(1)).toMatchObject([ "1:1 !LOG - A", "1:2 !PCS - PStore.updateNameAsync()", "1:3 !GET - PStore/data.firstName -> 'Homer'", "1:4 !SET - PStore/data.firstName = 'HomerA' (prev: 'Homer')", "1:5 !DRT - PStore#PrettyName <- PStore/data.firstName", "1:6 !PCE - 1:2", "1:7 !PCS - !Reconciliation #1 - 1 processor", "1:8 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:7", "1:9 !GET - PStore/data.firstName -> 'HomerA'", "1:10 !GET - PStore/data.lastName -> 'Simpson'", "1:11 !SET - PStore/data.prettyName = 'HomerA Simpson' (prev: 'Homer Simpson')", "1:12 !PCE - 1:8", "1:13 !PCE - 1:7", "2:1 @traxjs/trax/test/updateNameAsyncDone - NO-DATA", "3:1 !ERR - [TRAX] (PStore.updateNameAsync) error: Something bad happened", "4:1 !GET - PStore/data.prettyName -> 'HomerA Simpson'", ]); }); it('should wrap generator functions and return async functions', async () => { const ps = createPStore(); expect(ps.person.firstName).toBe("Homer"); await trax.reconciliation(); trax.log.info("A") const r = await ps.updateNameAsync2("A", "B"); expect(r).toBe("SimpsonBB"); expect(ps.person.prettyName).toBe("HomerA SimpsonBB"); expect(printLogs(1)).toMatchObject([ "1:1 !LOG - A", "1:2 !PCS - PStore.updateNameAsync2()", "1:3 !GET - PStore/data.firstName -> 'Homer'", "1:4 !SET - PStore/data.firstName = 'HomerA' (prev: 'Homer')", "1:5 !DRT - PStore#PrettyName <- PStore/data.firstName", "1:6 !PCP - 1:2", "1:7 !PCS - !Reconciliation #1 - 1 processor", "1:8 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:7", "1:9 !GET - PStore/data.firstName -> 'HomerA'", "1:10 !GET - PStore/data.lastName -> 'Simpson'", "1:11 !SET - PStore/data.prettyName = 'HomerA Simpson' (prev: 'Homer Simpson')", "1:12 !PCE - 1:8", "1:13 !PCE - 1:7", "2:1 !PCR - 1:2", "2:2 @traxjs/trax/test/updateNameAsync2Done - NO-DATA", "2:3 !GET - PStore/data.lastName -> 'Simpson'", "2:4 !SET - PStore/data.lastName = 'SimpsonBB' (prev: 'Simpson')", "2:5 !DRT - PStore#PrettyName <- PStore/data.lastName", "2:6 !PCE - 1:2", "2:7 !PCS - !Reconciliation #2 - 1 processor", "2:8 !PCS - !Compute #3 (PStore#PrettyName) P1 Reconciliation - parentId=2:7", "2:9 !GET - PStore/data.firstName -> 'HomerA'", "2:10 !GET - PStore/data.lastName -> 'SimpsonBB'", "2:11 !SET - PStore/data.prettyName = 'HomerA SimpsonBB' (prev: 'HomerA Simpson')", "2:12 !PCE - 2:8", "2:13 !PCE - 2:7", "3:1 !GET - PStore/data.prettyName -> 'HomerA SimpsonBB'", ]); }); it('should provide means to support async init', async () => { const ps = trax.createStore("PStore", (store: Store<Person>) => { const root = store.init({ firstName: "Homer", lastName: "Simpson" }); store.compute("PrettyName", () => { root.prettyName = root.firstName + " " + root.lastName; }); let initialized = false; store.async("Init", function* () { // allows to perform asyn operation to initialize the store // e.g. call a server / read from local storage / etc. yield pause(1); initialized = true; root.avatar = "AVATAR"; root.lastName += "!"; // will trigger PrettyName update trax.log.event("@traxjs/trax/test/core/asyncInitDone"); })(); return { person: root, get initialized() { return initialized; }, updateNameSync(value1: string, value2: string) { root.firstName += value1; const r = root.lastName + value2 + value2; root.lastName = r; return r; } } }); expect(ps.person.prettyName).toBe("Homer Simpson"); expect(ps.person.avatar).toBe(undefined); expect(ps.initialized).toBe(false); await trax.log.awaitEvent("@traxjs/trax/test/core/asyncInitDone"); await trax.reconciliation(); expect(ps.person.prettyName).toBe("Homer Simpson!"); expect(ps.person.avatar).toBe("AVATAR"); expect(ps.initialized).toBe(true); expect(printLogs(0)).toMatchObject([ "0:1 !PCS - !StoreInit (PStore)", "0:2 !NEW - S: PStore", "0:3 !NEW - O: PStore/data", "0:4 !NEW - P: PStore#PrettyName", "0:5 !PCS - !Compute #1 (PStore#PrettyName) P1 Init - parentId=0:1", "0:6 !GET - PStore/data.firstName -> 'Homer'", "0:7 !GET - PStore/data.lastName -> 'Simpson'", "0:8 !SET - PStore/data.prettyName = 'Homer Simpson' (prev: undefined)", "0:9 !PCE - 0:5", "0:10 !PCS - PStore.Init() - parentId=0:1", "0:11 !PCP - 0:10", "0:12 !PCE - 0:1", "0:13 !GET - PStore/data.prettyName -> 'Homer Simpson'", "0:14 !GET - PStore/data.avatar -> undefined", "1:1 !PCR - 0:10", "1:2 !SET - PStore/data.avatar = 'AVATAR' (prev: undefined)", "1:3 !GET - PStore/data.lastName -> 'Simpson'", "1:4 !SET - PStore/data.lastName = 'Simpson!' (prev: 'Simpson')", "1:5 !DRT - PStore#PrettyName <- PStore/data.lastName", "1:6 @traxjs/trax/test/core/asyncInitDone - NO-DATA", "1:7 !PCE - 0:10", "1:8 !PCS - !Reconciliation #1 - 1 processor", "1:9 !PCS - !Compute #2 (PStore#PrettyName) P1 Reconciliation - parentId=1:8", "1:10 !GET - PStore/data.firstName -> 'Homer'", "1:11 !GET - PStore/data.lastName -> 'Simpson!'", "1:12 !SET - PStore/data.prettyName = 'Homer Simpson!' (prev: 'Homer Simpson')", "1:13 !PCE - 1:9", "1:14 !PCE - 1:8", "2:1 !GET - PStore/data.prettyName -> 'Homer Simpson!'", "2:2 !GET - PStore/data.avatar -> 'AVATAR'", ]); }); }); describe('Errors', () => { it('must be raised when init is not called during the store initialization', async () => { const st = trax.createStore("MyStore", (store: Store<any>) => { store.add("foo", { msg: "Hello" }); }); expect(printLogs()).toMatchObject([ '0:1 !PCS - !StoreInit (MyStore)', "0:2 !NEW - S: MyStore", '0:3 !NEW - O: MyStore/foo', '0:4 !ERR - [TRAX] (MyStore) createStore init must define a root data object - see also: init()', '0:5 !NEW - O: MyStore/data', '0:6 !PCE - 0:1', ]); }); it('must be raised if we try to remove the root object', async () => { const st = trax.createStore("MyStore", (store: Store<any>) => { store.init({ msg: "Hello Ford" }); }); st.remove(st.data); expect(trax.isTraxObject(st.data)).toBe(true); expect(printLogs()).toMatchObject([ "0:1 !PCS - !StoreInit (MyStore)", "0:2 !NEW - S: MyStore", "0:3 !NEW - O: MyStore/data", "0:4 !PCE - 0:1", "0:5 !ERR - [TRAX] (MyStore/data) Root objects cannot be disposed through store.remove()", ]); }); it('must be raised when init is called outside the init function', async () => { const st = trax.createStore("MyStore", (store: Store<any>) => { store.init({ msg: "Hello" }); }); expect(printLogs()).toMatchObject([ '0:1 !PCS - !StoreInit (MyStore)', "0:2 !NEW - S: MyStore", '0:3 !NEW - O: MyStore/data', '0:4 !PCE - 0:1', ]); st.init({ msg: "abc" }); expect(printLogs()).toMatchObject([ '0:1 !PCS - !StoreInit (MyStore)', "0:2 !NEW - S: MyStore", '0:3 !NEW - O: MyStore/data', '0:4 !PCE - 0:1', '0:5 !ERR - [TRAX] (MyStore) Store.init can only be called during the store init phase', ]); }); it('must be raised when init functions dont return an object', async () => { const st = trax.createStore("MyStore", (store: Store<any>) => { store.init({ msg: "Hello" }); return 42; }); expect(printLogs()).toMatchObject([ '0:1 !PCS - !StoreInit (MyStore)', "0:2 !NEW - S: MyStore", '0:3 !NEW - O: MyStore/data', '0:4 !ERR - [TRAX] createStore init function must return a valid object (MyStore)', '0:5 !PCE - 0:1', ]); }); it('must be raised when init function throws an error', async () => { const st = trax.createStore("MyStore", (store: Store<any>) => { store.init({ msg: "Hello" }); throw Error("Unexpected error"); }); expect(printLogs()).toMatchObject([ '0:1 !PCS - !StoreInit (MyStore)', "0:2 !NEW - S: MyStore", '0:3 !NEW - O: MyStore/data', '0:4 !ERR - [TRAX] createStore init error (MyStore): Error: Unexpected error', '0:5 !PCE - 0:1', ]); }); it('must be raised when the store dispose throws an error', async () => { const st = trax.createStore("MyStore", (store: Store<any>) => { store.init({ msg: "Hello" }); return { dispose() { throw Error("Unexpected dispose error"); } } }); st.dispose(); expect(printLogs()).toMatchObject([ '0:1 !PCS - !StoreInit (MyStore)', "0:2 !NEW - S: MyStore", '0:3 !NEW - O: MyStore/data', '0:4 !PCE - 0:1', "0:5 !PCS - MyStore.dispose()", "0:6 !ERR - [TRAX] (MyStore.dispose) error: Error: Unexpected dispose error", "0:7 !PCE - 0:5", "0:8 !DEL - MyStore/data", "0:9 !DEL - MyStore", ]); }); it('must be raised if store id is provided by the init function', async () => { const st = trax.createStore("MyStore", (store: Store<any>) => { store.init({ msg: "Hello" }); return { id: "abcd" } }); expect(printLogs()).toMatchObject([ '0:1 !PCS - !StoreInit (MyStore)', "0:2 !NEW - S: MyStore", '0:3 !NEW - O: MyStore/data', '0:4 !ERR - [TRAX] Store id will be overridden and must not be provided by init function (MyStore)', '0:5 !PCE - 0:1' ]); }); it('must be raised in case of invalid id', async () => { const st = trax.createStore("My/Store/ABC", (store: Store<any>) => { store.init({ msg: "Hello" }); }); expect(st.id).toBe("MyStoreABC"); expect(printLogs()).toMatchObject([ '0:1 !ERR - [TRAX] Invalid trax id: My/Store/ABC (changed into MyStoreABC)', '0:2 !PCS - !StoreInit (MyStoreABC)', "0:3 !NEW - S: MyStoreABC", '0:4 !NEW - O: MyStoreABC/data', '0:5 !PCE - 0:2' ]); await trax.reconciliation(); st.add("AB>CD", { foo: "bar" }); expect(printLogs(1)).toMatchObject([ "1:1 !ERR - [TRAX] Invalid trax id: AB>CD (changed into ABCD)", "1:2 !NEW - O: MyStoreABC/ABCD", ]); await trax.reconciliation(); st.add("AB#C#D", { foo: "bar" }); expect(printLogs(2)).toMatchObject([ "2:1 !ERR - [TRAX] Invalid trax id: AB#C#D (changed into ABCD)", ]); }); it('must be raised in case of invalid add parameter', async () => { const st = trax.createStore("MyStore", (store: Store<any>) => { store.init({ msg: "Hello" }); store.add("abc", 42); }); expect(printLogs()).toMatchObject([ '0:1 !PCS - !StoreInit (MyStore)', "0:2 !NEW - S: MyStore", '0:3 !NEW - O: MyStore/data', '0:4 !ERR - [TRAX] (MyStore) Store.add(abc): Invalid init object parameter: [number]', '0:5 !NEW - O: MyStore/abc', '0:6 !PCE - 0:1' ]); }); it('must be raised if "data" is used as an id for a new object', async () => { const st = trax.createStore("MyStore", (store: Store<any>) => { store.init({ msg: "Hello" }); }); const o = st.add("data", { msg: "abc" }); const id = trax.getTraxId(o); // e.g. MyStore/87524 expect(id.match(/^MyStore\/\d+$/)).not.toBe(null); expect(printLogs()).toMatchObject([ "0:1 !PCS - !StoreInit (MyStore)", "0:2 !NEW - S: MyStore", "0:3 !NEW - O: MyStore/data", "0:4 !PCE - 0:1", "0:5 !ERR - [TRAX] Store.add: Invalid id 'data' (reserved)", "0:6 !NEW - O: " + id, ]); }); it('must be raised if stores are used after being disposed', async () => { const ps = trax.createStore("PStore", { firstName: "Homer", lastName: "Simpson" }); await trax.reconciliation(); ps.dispose(); const o = ps.add("Foo", { bar: 123 }); expect(printLogs(1)).toMatchObject([ "1:1 !DEL - PStore/data", "1:2 !DEL - PStore", "1:3 !ERR - [TRAX] (PStore) Stores cannot be used after being disposed", ]); }); it('must be raise if delete is called for SubStores', async () => { const ps = trax.createStore("PStore", { firstName: "Homer", lastName: "Simpson" }); const pss = ps.createStore("SubStore", (store: Store<{ msg: string }>) => { const root = store.init({ msg: "" }); store.compute("Msg", () => { root.msg = ps.data.firstName + "!"; }); }); await trax.reconciliation(); ps.remove(pss); expect(printLogs(1)).toMatchObject([ "1:1 !ERR - [TRAX] (PStore>SubStore) Stores cannot be disposed through store.remove()", ]); }); it('must be raise if delete is called for Processors', async () => { const ps = trax.createStore("PStore", { firstName: "Homer", lastName: "Simpson", misc: "" }); const pr = ps.compute("Misc", () => { const root = ps.data; root.misc = root.firstName + " " + root.lastName; }) await trax.reconciliation(); ps.remove(pr); expect(printLogs(1)).toMatchObject([ "1:1 !ERR - [TRAX] (PStore#Misc) Processors cannot be disposed through store.remove()", ]); }); }); }); describe('Reconciliation', () => { it('should support manual trigger (sync)', async () => { const st = trax.createStore("MyStore", (store: Store<Person>) => { const p = store.init({ firstName: "Homer", lastName: "Simpson" }); store.compute("PrettyName", () => { const nm = p.firstName + " " + p.lastName; p.prettyName = nm; p.prettyNameLength = nm.length; }); }); trax.log.info("A"); expect(trax.pendingChanges).toBe(false); trax.processChanges(); // no effect (no changes) trax.log.info("B"); st.data.lastName = "SIMPSON"; trax.log.info("C"); expect(trax.pendingChanges).toBe(true); trax.processChanges(); expect(trax.pendingChanges).toBe(false); trax.log.info("D"); trax.processChanges(); // no effect (no changes) await trax.reconciliation(); expect(printLogs(0, false)).toMatchObject([ "0:0 !CS - 0", "0:1 !PCS - !StoreInit (MyStore)", "0:2 !NEW - S: MyStore", "0:3 !NEW - O: MyStore/data", "0:4 !NEW - P: MyStore#PrettyName", "0:5 !PCS - !Compute #1 (MyStore#PrettyName) P1 Init - parentId=0:1", "0:6 !GET - MyStore/data.firstName -> 'Homer'", "0:7 !GET - MyStore/data.lastName -> 'Simpson'", "0:8 !SET - MyStore/data.prettyName = 'Homer Simpson' (prev: undefined)", "0:9 !SET - MyStore/data.prettyNameLength = 13 (prev: undefined)", "0:10 !PCE - 0:5", "0:11 !PCE - 0:1", "0:12 !LOG - A", "0:13 !LOG - B", "0:14 !SET - MyStore/data.lastName = 'SIMPSON' (prev: 'Simpson')", "0:15 !DRT - MyStore#PrettyName <- MyStore/data.lastName", "0:16 !LOG - C", "0:17 !PCS - !Reconciliation #1 - 1 processor", "0:18 !PCS - !Compute #2 (MyStore#PrettyName) P1 Reconciliation - parentId=0:17", "0:19 !GET - MyStore/data.firstName -> 'Homer'", "0:20 !GET - MyStore/data.lastName -> 'SIMPSON'", "0:21 !SET - MyStore/data.prettyName = 'Homer SIMPSON' (prev: 'Homer Simpson')", "0:22 !PCE - 0:18", "0:23 !PCE - 0:17", "0:24 !LOG - D", "0:25 !CC - 0", ]); }); it('should support automatic trigger (async)', async () => { const st = trax.createStore("MyStore", (store: Store<Person>) => { const p = store.init({ firstName: "Homer", lastName: "Simpson" }); store.compute("PrettyName", () => { const nm = p.firstName + " " + p.lastName; p.prettyName = nm; p.prettyNameLength = nm.length; }); }); const p = st.data; trax.log.info("A"); expect(trax.pendingChanges).toBe(false); p.lastName = "SIMPSON"; expect(trax.pendingChanges).toBe(true); trax.log.info("B"); await trax.reconciliation(); trax.log.info("C"); expect(trax.pendingChanges).toBe(false); p.firstName = "Bart"; expect(trax.pendingChanges).toBe(true); p.lastName = "Simpson"; await trax.reconciliation(); expect(p.prettyName).toBe("Bart Simpson"); expect(printLogs(0, false)).toMatchObject([ "0:0 !CS - 0", "0:1 !PCS - !StoreInit (MyStore)", "0:2 !NEW - S: MyStore", "0:3 !NEW - O: MyStore/data", "0:4 !NEW - P: MyStore#PrettyName", "0:5 !PCS - !Compute #1 (MyStore#PrettyName) P1 Init - parentId=0:1", "0:6 !GET - MyStore/data.firstName -> 'Homer'", "0:7 !GET - MyStore/data.lastName -> 'Simpson'", "0:8 !SET - MyStore/data.prettyName = 'Homer Simpson' (prev: undefined)", "0:9 !SET - MyStore/data.prettyNameLength = 13 (prev: undefined)", "0:10 !PCE - 0:5", "0:11 !PCE - 0:1", "0:12 !LOG - A", "0:13 !SET - MyStore/data.lastName = 'SIMPSON' (prev: 'Simpson')", "0:14 !DRT - MyStore#PrettyName <- MyStore/data.lastName", "0:15 !LOG - B", "0:16 !PCS - !Reconciliation #1 - 1 processor", "0:17 !PCS - !Compute #2 (MyStore#PrettyName) P1 Reconciliation - parentId=0:16", "0:18 !GET - MyStore/data.firstName -> 'Homer'", "0:19 !GET - MyStore/data.lastName -> 'SIMPSON'", "0:20 !SET - MyStore/data.prettyName = 'Homer SIMPSON' (prev: 'Homer Simpson')", "0:21 !PCE - 0:17", "0:22 !PCE - 0:16", "0:23 !CC - 0", "1:0 !CS - 0", "1:1 !LOG - C", "1:2 !SET - MyStore/data.firstName = 'Bart' (prev: 'Homer')", "1:3 !DRT - MyStore#PrettyName <- MyStore/data.firstName", "1:4 !SET - MyStore/data.lastName = 'Simpson' (prev: 'SIMPSON')", "1:5 !PCS - !Reconciliation #2 - 1 processor", "1:6 !PCS - !Compute #3 (MyStore#PrettyName) P1 Reconciliation - parentId=1:5", "1:7 !GET - MyStore/data.firstName -> 'Bart'", "1:8 !GET - MyStore/data.lastName -> 'Simpson'", "1:9 !SET - MyStore/data.prettyName = 'Bart Simpson' (prev: 'Homer SIMPSON')", "1:10 !SET - MyStore/data.prettyNameLength = 12 (prev: 13)", "1:11 !PCE - 1:6", "1:12 !PCE - 1:5", "1:13 !CC - 0", "2:0 !CS - 0", "2:1 !GET - MyStore/data.prettyName -> 'Bart Simpson'", ]); }); }); describe('Main trax', () => { it('should be accessible from index.ts', async () => { expect(typeof trx.createStore).toBe("function"); }); }); });