/// import {init} from './index'; const getState = () => init({ n: 1, s: 'test', b: true, a: [1, 2, 4], c: [{a: 1, b: 2, c: 3}], o: {a: 1, b: 2, c: 3}, test: 789 }); test('store initializes with public API', () => { const state = init({ n: 1, s: 'test', b: true, a: [1, 2, 4], c: [{a: 1, b: 2, c: 3}], o: {a: 1, b: 2, c: 3}, }); expect(Object.keys(state)).toContain('get'); expect(Object.keys(state)).toContain('set'); expect(Object.keys(state)).toContain('exclude'); expect(Object.keys(state)).toContain('trigger'); expect(Object.keys(state)).toContain('connect'); expect(Object.keys(state)).toContain('disconnect'); expect(Object.keys(state)).toContain('destroy'); expect(state.n).toBe(1); expect(state.s).toBe('test'); expect(state.b).toBe(true); expect(state.a).toEqual([1, 2, 4]); expect(state.c).toEqual([{a: 1, b: 2, c: 3}]); expect(state.o).toEqual({a: 1, b: 2, c: 3}); }); test('store can retrieve a value', () => { const state: State.Data = getState(); expect(state.get('n')).toBe(1); }); test('store can retrieve state object', () => { const state = getState(); expect(state.get('*')).toEqual({ n: 1, s: 'test', b: true, a: [1, 2, 4], c: [{a: 1, b: 2, c: 3}], o: {a: 1, b: 2, c: 3}, test: 789 }); }); test('store can retrieve state object by path', () => { const state = getState(); expect(state.get('o.a')).toEqual(1); }); test('passing an invalid path to get() returns null', () => { const state = getState(); expect(state.get('a.b.c')).toBe(null); }); test('store can retrieve filtered state object', () => { const state = getState(); expect(state.exclude(['a', 'c', 'o', 'test'])).toEqual({ n: 1, s: 'test', b: true, }); }); test('store can update a value', () => { const state = getState(); state.set({test: 123}); expect(state.test).toBe(123); }); test('store rejects setting of properties not declared initially', () => { const state = init({ n: 1, s: 'test', b: true, a: [1, 2, 4], c: [{a: 1, b: 2, c: 3}], o: {a: 1, b: 2, c: 3}, }); state.set({test: 123}) // Warning: [store -> set -> test] Property not found. expect(state.test).toBe(undefined); }); test('store can react to state change', (done) => { const state = getState(); state.connect({ test: ({test}: Partial) => { expect(test).toBe(123); done(); } }); state.set({test: 123}); }); test('context can be passed to connect', (done) => { const state = getState(); let obj = { finish: function() { done(); }, callback: function({test}: Partial) { expect(test).toBe(123); expect(typeof this.finish).toBe('function'); this.finish(); } } state.connect('test', obj.callback, obj); state.set({test: 123}); }); test('store can handle actions in response to triggers', () => { const state = getState(); state.connect({ testTrigger: (foo, bar) => { expect(foo).toBe('fooValue'); expect(bar).toBe('barValue'); state.set({test: 123}); }, test: ({test}: Partial) => { expect(test).toBe(123); } }); state.trigger('nonExistent'); state.trigger('testTrigger', 'fooValue', 'barValue'); }); test('trigger can return the callback\'s value', () => { const state = getState(); state.connect({ testTrigger: (foo, bar) => { expect(foo).toBe('fooValue'); expect(bar).toBe('barValue'); state.set({test: 123}); return 'finalValue'; }, test: ({test}: Partial) => { expect(test).toBe(123); } }); let result = state.trigger('testTrigger', 'fooValue', 'barValue'); expect(result).toBe('finalValue'); }); test('connect can accept a string', () => { const state = getState(); state.connect('test', ({test}: Partial) => { expect(test).toBe(123); }); state.set({test: 123}); }); test('connect can accept an array', () => { const state = getState(); const testFn = jest.fn(); state.connect(['test', 'n'], testFn); state.set({test: 123}); state.set({n: 96}); state.set({b: false}); expect(testFn).toHaveBeenCalledTimes(2); }); test('connect can add multiple state listeners', () => { const state = getState(); state.connect({ n: ({n}: Partial) => { expect(n).toBe(48); }, test: ({test}: Partial) => { expect(test).toBe(123); } }); state.set({ test: 123, n: 48 }); }); test('connect can add state listeners for all keys', () => { const state = getState(); const testFn = jest.fn(); state.connect('*', testFn); state.set({test: 123, n: 48}); state.set({s: 'test2'}); expect(testFn).toHaveBeenCalledTimes(2); }); test('listeners can be disconnected by number ID', () => { const state = getState(); const testFn = jest.fn(); let id = state.connect('*', testFn); expect(typeof id).toBe('number'); state.set({test: 123, n: 48}); state.set({s: 'test2'}); state.disconnect(id); state.set({test: 532, n: 543}); expect(testFn).toHaveBeenCalledTimes(2); }); test('listeners can be disconnected by key', () => { const state = getState(); const testFn = jest.fn(); state.connect('s', testFn); state.set({s: 'test2'}); state.set({s: 'test3'}); state.set({s: 'test4'}); state.disconnect('s'); state.disconnect('n'); state.set({s: 'test2'}); expect(testFn).toHaveBeenCalledTimes(3); }); test('duplicate listeners are concatenated', () => { const state = getState(); const testFn = jest.fn(); state.connect('s', testFn); state.connect({s: testFn}) state.set({s: 'test2'}); state.set({s: 'test3'}); state.set({s: 'test4'}); state.disconnect('s'); state.set({s: 'test2'}); expect(testFn).toHaveBeenCalledTimes(3); }); test('duplicate listener keys with unique callbacks are not concatenated', () => { const state = getState(); const testFn1 = jest.fn(); const testFn2 = jest.fn(); state.connect('s', testFn1); state.connect({s: testFn2}) state.set({s: 'test2'}); state.set({s: 'test3'}); state.set({s: 'test4'}); state.disconnect('s'); state.set({s: 'test2'}); expect(testFn1).toHaveBeenCalledTimes(3); expect(testFn2).toHaveBeenCalledTimes(3); }); test('listeners can be disconnected by a list of keys', () => { const state = getState(); const testFn1 = jest.fn(); const testFn2 = jest.fn(); state.connect({ n: testFn1, test: testFn2 }); state.set({ test: 123, n: 48 }); state.disconnect(['n', 'test']); state.set({ test: 256, n: 512 }); expect(testFn1).toHaveBeenCalledTimes(1); expect(testFn2).toHaveBeenCalledTimes(1); }); test('all listeners can be removed', () => { const state = getState(); const testFn1 = jest.fn(); const testFn2 = jest.fn(); state.connect({ n: testFn1, test: testFn2 }); state.set({ test: 123, n: 48 }); state.destroy(); state.set({ test: 256, n: 512 }); expect(testFn1).toHaveBeenCalledTimes(1); expect(testFn2).toHaveBeenCalledTimes(1); }); test('store can detect value change', () => { const state = getState(); const testFn1 = jest.fn(); const testFn2 = jest.fn(); const testFn3 = jest.fn(); const testFn4 = jest.fn(); const testFn5 = jest.fn(); state.connect({ n: testFn1, test: testFn2, o: testFn3, a: testFn4, c: testFn5 }); state.set({ test: 123, n: 48, o: {a: 2, b: 3, c: 4}, a: [9, 2, 4], c: [{a: 8, b: 2, c: 3}], }); state.set({ test: 123, n: 48, o: {a: 2, b: 3, c: 4}, a: [9, 2, 4], c: [{a: 8, b: 2, c: 3}], }); expect(testFn1).toHaveBeenCalledTimes(1); expect(testFn2).toHaveBeenCalledTimes(1); expect(testFn3).toHaveBeenCalledTimes(1); expect(testFn4).toHaveBeenCalledTimes(1); expect(testFn5).toHaveBeenCalledTimes(1); state.set({ test: 124, n: 49 }); expect(testFn1).toHaveBeenCalledTimes(2); expect(testFn2).toHaveBeenCalledTimes(2); expect(testFn3).toHaveBeenCalledTimes(1); expect(testFn4).toHaveBeenCalledTimes(1); expect(testFn5).toHaveBeenCalledTimes(1); state.set({ test: 124, n: 49, o: {a: 2, q: 3, c: 4}, a: null, c: [{a: 8, d: 4, b: 2, c: 3}, {a: 2, e: 5, b: 3, c: 4}], }); expect(testFn1).toHaveBeenCalledTimes(2); expect(testFn2).toHaveBeenCalledTimes(2); expect(testFn3).toHaveBeenCalledTimes(2); expect(testFn4).toHaveBeenCalledTimes(2); expect(testFn5).toHaveBeenCalledTimes(2); }); test('store\'s value change heuristic can be overridden', (done) => { const state = getState(); const testFn1 = jest.fn(); const testFn2 = jest.fn(); const testFn3 = jest.fn(); const testFn4 = jest.fn(); const testFn5 = jest.fn(); const callback = () => { expect(testFn1).toHaveBeenCalledTimes(2); expect(testFn2).toHaveBeenCalledTimes(2); expect(testFn3).toHaveBeenCalledTimes(2); expect(testFn4).toHaveBeenCalledTimes(2); expect(testFn5).toHaveBeenCalledTimes(2); done(); }; state.connect({ n: testFn1, test: testFn2, o: testFn3, a: testFn4, c: testFn5 }); state.set({ n: 1, s: 'test', b: true, a: [1, 2, 4], c: [{a: 1, b: 2, c: 3}], o: {a: 1, b: 2, c: 3}, test: 789 }); expect(testFn1).toHaveBeenCalledTimes(0); expect(testFn2).toHaveBeenCalledTimes(0); expect(testFn3).toHaveBeenCalledTimes(0); expect(testFn4).toHaveBeenCalledTimes(0); expect(testFn5).toHaveBeenCalledTimes(0); state.set({ n: 1, s: 'test', b: true, a: [1, 2, 4], c: [{a: 1, b: 2, c: 3}], o: {a: 1, b: 2, c: 3}, test: 789 }, true); expect(testFn1).toHaveBeenCalledTimes(1); expect(testFn2).toHaveBeenCalledTimes(1); expect(testFn3).toHaveBeenCalledTimes(1); expect(testFn4).toHaveBeenCalledTimes(1); expect(testFn5).toHaveBeenCalledTimes(1); state.set({ n: 1, s: 'test', b: true, a: [1, 2, 4], c: [{a: 1, b: 2, c: 3}], o: {a: 1, b: 2, c: 3}, test: 789 }, callback, true); }); test('ignores duplicate falsy values', () => { const state = init({ n: null }); const testFn1 = jest.fn(); state.connect({ n: testFn1, }); state.set({ n: null }); expect(testFn1).toHaveBeenCalledTimes(0); }); test('isEqual doesn\'t throw if null is passed to it', () => { const state = init({ n: null }); const testFn1 = jest.fn(); state.connect({ n: testFn1, }); state.set({ n: {a: 1, b: 2, c: 3} }); expect(testFn1).toHaveBeenCalledTimes(1); }); test('store can optionally accept partial object values and mutate child objects', () => { // Without setMergeKeys called let state = init({ obj: { a: 1, b: 2, c: 3 } }); state.set({ obj: {c: 4} }); // The object's value was copied over instead of mutated. expect(state.get('obj').a != null).toBe(false); // With setMergeKeys called state = init({ obj: { a: 1, b: 2, c: 3 } }) .setMergeKeys(['obj']); state.set({ obj: {c: 4} }); expect(state.get('obj').a != null).toBe(true); expect(state.get('obj')).toEqual({ a: 1, b: 2, c: 4 }); }); test('store dispatches state change callback for keys included in mergeKeys', () => { let state = init({ obj: { a: 1, b: 2, c: 3 } }) .setMergeKeys(['obj']); const testFn1 = jest.fn(({obj}) => { expect(obj.a != null).toBe(true); }); state.connect({ obj: testFn1 }); state.set({obj: {b: 9}}); expect(testFn1).toHaveBeenCalledTimes(1); });