import StateContainer, {lensTransformer} from '../src/state_container'; import sinon from 'sinon'; import * as R from 'ramda'; test("initial state setting works", () => { const container = new StateContainer({foo: 'bar'}); expect(container.get('foo')).toBe('bar')//, 'get works with initial data'); expect(container.getState().foo).toBe('bar')//, 'getState works with initial data'); }); test("set updates state", () => { const container = new StateContainer({foo: 'bar'}); container.set('foo', 'baz'); expect(container.get('foo')).toBe('baz')//, 'set updated the data'); }); test("setState updates state", () => { const container = new StateContainer({foo: 'bar', moo: 'cow'}); container.setState({foo: 'baz'}); expect(container.get('foo')).toBe('baz')//, 'setState updated the data'); expect(container.get('moo')).toBe('cow')//, 'setState did not lose data'); }); test("returned state cannot modify internal state", () => { const container = new StateContainer({foo: 'bar'}); const outputState = container.getState(); expect(container.get('foo')).toBe(outputState.foo)//, "output state matches container initially"); outputState.foo = 'moo'; expect(container.get('foo')).toBe('bar')//, "container state was not modified by output state"); }); test("onUpdate listeners are fired when set is called", () => { let container, listener; container = new StateContainer({foo: 'bar'}); listener = sinon.spy(); container.onUpdate(listener); container.set('foo', 'hi'); expect(JSON.stringify(listener.args[0])).toBe(JSON.stringify([{foo: 'bar'}, {foo: 'hi'}])); container = new StateContainer({foo: 'bar'}); const recorder = container.getRecorder(); listener = sinon.spy(); recorder.onUpdate(listener); recorder.set('foo', 'hi'); expect(JSON.stringify(listener.args[0])).toBe(JSON.stringify([{foo: 'bar'}, {foo: 'hi'}])); }); test("onUpdate listeners are fired when setState is called", () => { let container = new StateContainer({foo: 'bar'}); let listener1 = sinon.spy(); let listener2 = sinon.spy(); let rfn1 = container.onUpdate(listener1); let rfn2 = container.onUpdate(listener2); container.setState({'foo': 'hi'}); rfn1(); container.setState({'foo': 'unfoo'}); expect(listener1.args.length).toBe(1); expect(JSON.stringify(listener1.args[0])) .toEqual(JSON.stringify( [{ foo: 'bar' }, { foo: 'hi' }] )); expect(listener2.args.length).toBe(2); expect(JSON.stringify(listener2.args[0])) .toEqual(JSON.stringify( [{ foo: 'bar' }, { foo: 'hi' }] )); expect(JSON.stringify(listener2.args[1])) .toEqual(JSON.stringify( [{ foo: 'hi' }, { foo: 'unfoo' }] )); container = new StateContainer({foo: 'bar'}); const recorder = container.getRecorder(); listener1 = sinon.spy(); listener2 = sinon.spy(); rfn1 = recorder.onUpdate(listener1); rfn2 = recorder.onUpdate(listener2); recorder.setState({'foo': 'hi'}); rfn1(); recorder.setState({'foo': 'unfoo'}); expect(listener1.args.length).toBe(1); expect(JSON.stringify(listener1.args[0])) .toEqual(JSON.stringify( [{ foo: 'bar' }, { foo: 'hi' }] )); expect(listener2.args.length).toBe(2); expect(JSON.stringify(listener2.args[0])) .toEqual(JSON.stringify( [{ foo: 'bar' }, { foo: 'hi' }] )); expect(JSON.stringify(listener2.args[1])) .toEqual(JSON.stringify( [{ foo: 'hi' }, { foo: 'unfoo' }] )); }); test("recording works", () => { const container = new StateContainer({foo: 'bar'}); const recorder = container.getRecorder(); // recorder mirrors container container.set('foo', 3); expect(recorder.get('foo')).toBe(3); // copy on write - reads from local recorder.set('foo', 4); expect(recorder.get('foo')).toBe(4); expect(container.get('foo')).toBe(3); // copy on write - does not read from parent container.set('foo', 5); expect(recorder.get('foo')).toBe(4); container.commit(recorder); expect(container.get('foo')).toBe(4)//, 'changes commited affect parent'); // after committing, reflects parent again container.set('foo', 5); expect(recorder.get('foo')).toBe(5); // getState works? let theState = recorder.getState(); expect(theState).toEqual({foo: 5}); recorder.setState({bar: 7}); expect(recorder.getState()).toEqual({foo: 5, bar: 7}); expect(container.getState()).toEqual({foo: 5}); container.commit(recorder); expect(container.getState()).toEqual({foo: 5, bar: 7}); // cannot commit a recorder into wrong container const otherContainer = new StateContainer({foo: 'bar'}); let err = 42; try { otherContainer.commit(recorder); } catch (e) { err = e; } expect(err instanceof Error).toBe(true)//, 'throws an error on invalid commit'); }); test("recorder getState works as expected", () => { let container = new StateContainer({foo: {bar: 1, baz: 1}}); let recorder = container.getRecorder(); let lens = recorder.lensFor('foo'); lens.set({bar: 2}); expect(recorder.getState()).toEqual({foo: {bar: 2}}, 'replaces when set with complex value'); container = new StateContainer({foo: {bar: 1, baz: 1}}); recorder = container.getRecorder(); lens = recorder.lensFor(['foo', 'bar']); lens.set(2); expect(recorder.getState()).toEqual({foo: {bar: 2, baz: 1}}, 'updates when set with simple value'); }); test("recorder playback works correctly", () => { let container = new StateContainer({foo: {bar: 1, baz: 1}}); let recorder = container.getRecorder(); let lens = recorder.lensFor('foo'); lens.set({bar: 2}); container.commit(recorder); expect(container.getState()).toEqual({foo: {bar: 2}}, 'replaces when set with complex value'); container = new StateContainer({foo: {bar: 1, baz: 1}}); recorder = container.getRecorder(); lens = recorder.lensFor(['foo', 'bar']); lens.set(2); container.commit(recorder); expect(container.getState()).toEqual({foo: {bar: 2, baz: 1}}, 'updates when set with simple value'); }); test("recorder from recorder also records - infinite turtles", () => { let container = new StateContainer({foo: {bar: 1, baz: 1}}); let recorder = container.getRecorder(); let subRecorder = recorder.getRecorder(); subRecorder.set(['foo', 'bar'], 2); recorder.commit(subRecorder); expect(subRecorder.get(['foo', 'bar'])).toBe(2); recorder.set(['foo', 'bar'], 3); expect(subRecorder.get(['foo', 'bar'])).toBe(3); }); test("recorder from recorder also records - infinite turtles", () => { let container = new StateContainer({foo: {bar: 1, baz: 1}}); let recorder = container.getRecorder(); let subRecorder = recorder.getRecorder(); expect(subRecorder.get(['foo', 'bar'])).toBe(1); subRecorder.set(['foo', 'bar'], 2); expect(subRecorder.get(['foo', 'bar'])).toBe(2); expect(recorder.get(['foo', 'bar'])).toBe(1); expect(container.get(['foo', 'bar'])).toBe(1); recorder.commit(subRecorder); expect(subRecorder.get(['foo', 'bar'])).toBe(2); expect(recorder.get(['foo', 'bar'])).toBe(2); expect(container.get(['foo', 'bar'])).toBe(1); container.commit(recorder); expect(subRecorder.get(['foo', 'bar'])).toBe(2); expect(recorder.get(['foo', 'bar'])).toBe(2); expect(container.get(['foo', 'bar'])).toBe(2); container.set(['foo', 'bar'], 3); expect(subRecorder.get(['foo', 'bar'])).toBe(3); }); test("lenses work", () => { let container, lens, sublens; container = new StateContainer({ foo: { bar: 1, baz: 2 } }); lens = container.lensFor('foo'); expect(lens.get()).toEqual({bar:1, baz: 2}); sublens = lens.lensFor('bar'); expect(sublens.get()).toBe(1); sublens.set(2); expect(sublens.get()).toBe(2, 'changing a nested lens updates its value'); expect(lens.get()).toEqual({bar:2, baz: 2}, 'changing a nested lens updates its parent\'s value'); expect(container.getState()).toEqual({foo: {bar:2, baz: 2}}, 'changing a nested lens updates container state'); lens.set({bar: 3}); expect(sublens.get()).toBe(3, 'changing parent lens to complex value updates nested lens value'); expect(lens.get()).toEqual({bar:3}, 'changing parent lens to complex value updates itself'); expect(container.getState()).toEqual({foo: {bar:3}}, 'changing parent lens to complex value updates container'); lens.set(3); expect(lens.get()).toBe(3); expect(lens.withValue(R.identity)).toBe(3); expect(lens.withValue((value,prop) => R.assoc(prop, value, {b: 2, a:1}), 'a')).toEqual({a:3, b:2}); expect(container.getState()).toEqual({foo: 3}); expect(lens.swap(R.assoc('a', R.__, {b: 2, a:1}))).toEqual({a:3, b:2}); expect(lens.get()).toEqual({a:3, b:2}); container = new StateContainer({ foo: { bar: 1, baz: 2 } }); const recorder = container.getRecorder(); lens = recorder.lensFor('foo'); expect(lens.get()).toEqual({bar:1, baz: 2}); sublens = lens.lensFor('bar'); expect(sublens.get()).toBe(1); sublens.set(2); expect(sublens.get()).toBe(2, 'changing a nested lens updates its value'); expect(lens.get()).toEqual({bar:2, baz: 2}, 'changing a nested lens updates its parent\'s value'); expect(recorder.getState()).toEqual({foo: {bar:2, baz: 2}}, 'changing a nested lens updates recorder state'); lens.set({bar: 3}); expect(sublens.get()).toBe(3, 'changing parent lens to complex value updates nested lens value'); expect(lens.get()).toEqual({bar:3}, 'changing parent lens to complex value updates itself'); expect(recorder.getState()).toEqual({foo: {bar:3}}, 'changing parent lens to complex value updates recorder'); lens.set(3); expect(lens.get()).toBe(3); expect(lens.withValue(R.identity)).toBe(3); expect(lens.withValue((value,prop) => R.assoc(prop, value, {b: 2, a:1}), 'a')).toEqual({a:3, b:2}); expect(recorder.getState()).toEqual({foo: 3}); expect(lens.swap(R.assoc('a', R.__, {b: 2, a:1}))).toEqual({a:3, b:2}); expect(lens.get()).toEqual({a:3, b:2}); }); test("lenses treat undefined properly", () => { let container = new StateContainer({foo: undefined}); let lens, sublens, recorder; lens = container.lensFor('foo'); expect(lens.get()).toEqual(undefined); sublens = lens.lensFor('bar'); expect(sublens.get()).toBe(undefined); try { sublens.set(2); } catch (e) { // nothing } expect(sublens.get()).toBe(2, 'changing a nested lens updates its value'); expect(lens.get()).toEqual({bar:2}, 'changing a nested lens updates its parent\'s value'); expect(container.getState()).toEqual({foo: {bar:2}}, 'changing a nested lens updates container state'); container = new StateContainer({foo: undefined}); recorder = container.getRecorder(); lens = recorder.lensFor(['foo', 'bar']); try { lens.set(2); } catch (e) { // nothing } expect(lens.get()).toBe(2, 'changing a lens updates its value'); container = new StateContainer({foo: {bar: undefined, qwerty: 2}}); recorder = container.getRecorder(); lens = recorder.lensFor(['foo', 'bar', 'baz']); try { lens.set(2); } catch (e) { // nothing } expect(lens.get()).toBe(2, 'changing a lens updates its value'); expect(recorder.getState()).toEqual({foo: {bar: {baz: 2}, qwerty: 2}}); container.commit(recorder); expect(container.getState()).toEqual({foo: {bar: {baz: 2}, qwerty: 2}}); }); test('returned values are just Javascript objects', () => { const container = new StateContainer({foo: {bar: {baz: 1}}}); const recorder = container.getRecorder(); expect(container.get('foo').bar).toEqual({baz: 1}); expect(recorder.get('foo').bar).toEqual({baz: 1}); }); test("lensFor accepts array paths", () => { const container = new StateContainer({foo: {bar: 1, baz: {'qwerty': 2}}}); let lens, sublens; lens = container.lensFor(['foo', 'bar']); expect(lens.get()).toBe(1); lens.set(2); expect(lens.get()).toBe(2); lens = container.lensFor('foo').lensFor(['baz', 'qwerty']); expect(lens.get()).toBe(2); }); test("lensFor accepts objects nested in array paths", () => { const container = new StateContainer({foo: {bar: 1, baz: {}}}); let lens; lens = container.lensFor('foo').lensFor(['baz', 'qwerty', 0, 'asdf']); lens.set(2); expect(lens.get()).toBe(2); }); test("lensFor indexes into arrays", () => { const container = new StateContainer({foo: {bar: [1], baz: {'qwerty': 2}}}); let lens, sublens; lens = container.lensFor(['foo', 'bar']).lensFor(0); expect(lens.get()).toBe(1); lens.set(2); expect(lens.get()).toBe(2); lens = container.lensFor(['foo', 'bar']).lensFor(2); lens.set(3); expect(lens.get()).toBe(3); }); test("lens transformers work", () => { const container = new StateContainer({foo: {bar: 1, baz: {'qwerty': 2}}}); let lens, transformer; lens = container.lensFor(['foo', 'bar']); transformer = lensTransformer( lens, v => v * 2, v => v / 2 ); expect(transformer.get()).toBe(2); transformer.set(4); expect(transformer.get()).toBe(4); });