UNPKG

@platform/state

Version:

A small, simple, strongly typed, [rx/observable] state-machine.

238 lines (237 loc) 11 kB
import { expect, time } from '../test'; import { Subject } from 'rxjs'; import { Store } from '.'; const initial = { count: 0, foo: { list: [] } }; describe('Store', () => { describe('lifecycle', () => { it('constructs', () => { const store = Store.create({ initial }); expect(store.isDisposed).to.eql(false); expect(store.state).to.not.equal(initial); expect(store.state).to.eql(initial); }); it('disposes', () => { const store = Store.create({ initial }); let count = 0; store.dispose$.subscribe(() => count++); store.dispose(); store.dispose(); expect(store.isDisposed).to.eql(true); expect(count).to.eql(1); }); it('takes event$ at creation', () => { const event$ = new Subject(); const store = Store.create({ initial, event$ }); expect(store._event$).to.equal(event$); }); }); describe('state', () => { it('returns new immutable object from [state] property', () => { const store = Store.create({ initial }); const state1 = store.state; const state2 = store.state; expect(store.state).to.eql(initial); expect(store.state).to.not.equal(initial); expect(state1).to.eql(store.state); expect(state1).to.eql(initial); expect(state1).to.not.equal(state2); expect(store.state).to.not.equal(initial); }); }); describe('dispatch', () => { it('returns the state object', () => { const state = Store.create({ initial }); const res = state.dispatch({ type: 'TEST/increment', payload: { by: 1 } }); expect(res).to.equal(res); }); it('fires dispatch event', () => { const store = Store.create({ initial }); const events = []; store.event$.subscribe((e) => events.push(e)); store.dispatch({ type: 'TEST/increment', payload: { by: 1 } }); store.dispatch({ type: 'TEST/decrement', payload: { by: 2 } }); expect(events.length).to.eql(2); expect(events[0].type).to.eql('TEST/increment'); expect(events[1].type).to.eql('TEST/decrement'); }); it('fires (via injected event$)', () => { const event$ = new Subject(); const store = Store.create({ initial, event$ }); const events = []; store.event$.subscribe((e) => events.push(e)); event$.next({ type: 'TEST/increment', payload: { by: 1 } }); event$.next({ type: 'TEST/decrement', payload: { by: 2 } }); expect(events.length).to.eql(2); expect(events[0].type).to.eql('TEST/increment'); expect(events[1].type).to.eql('TEST/decrement'); }); it('returns copy of the current state object on event', () => { const store = Store.create({ initial }); const states = []; store.on('TEST/increment').subscribe((e) => { states.push(e.state); states.push(e.state); }); store.dispatch({ type: 'TEST/increment', payload: { by: 1 } }); expect(states.length).to.eql(2); expect(states[0]).to.eql(store.state); expect(states[1]).to.eql(store.state); expect(states[0]).to.not.equal(store.state); expect(states[1]).to.not.equal(store.state); expect(states[0]).to.not.equal(states[1]); }); it('changes the current state (via {...object})', () => { const store = Store.create({ initial }); expect(store.state.count).to.eql(0); store.on('TEST/increment').subscribe((e) => { const count = e.state.count + e.payload.by; const next = Object.assign(Object.assign({}, e.state), { count }); e.change(next); }); store.on('TEST/decrement').subscribe((e) => { const count = e.state.count - e.payload.by; const next = Object.assign(Object.assign({}, e.state), { count }); e.change(next); }); store.dispatch({ type: 'TEST/increment', payload: { by: 1 } }); expect(store.state.count).to.eql(1); store.dispatch({ type: 'TEST/decrement', payload: { by: 2 } }); expect(store.state.count).to.eql(-1); }); it('changes the current state (via immutable function)', () => { const store = Store.create({ initial: Object.assign(Object.assign({}, initial), { bar: { msg: 'hello' } }), }); const before = store.state; expect(before.count).to.eql(0); store.on('TEST/increment').subscribe((e) => { e.change((draft) => { draft.count += e.payload.by; }); }); store.on('TEST/changeFoo').subscribe((e) => { e.change((draft) => { draft.foo.list.push(123); const foo = draft.foo; foo.msg = 'hello'; }); }); store.dispatch({ type: 'TEST/increment', payload: { by: 1 } }); const after1 = store.state; expect(before).to.not.equal(after1); expect(after1.count).to.eql(1); expect(after1.foo).to.equal(before.foo); expect(after1.bar).to.equal(before.bar); store.dispatch({ type: 'TEST/changeFoo', payload: {} }); const after2 = store.state; expect(after2.foo.list).to.eql([123]); expect(after2.foo.msg).to.eql('hello'); expect(after2).to.not.equal(after1); expect(after2.foo).to.not.equal(after1.foo); expect(after2.foo.list).to.not.equal(after1.foo.list); }); it('fires [changing] event', () => { const store = Store.create({ initial }); const events = []; store.changing$.subscribe((e) => events.push(e)); store.on('TEST/increment').subscribe((e) => e.change(e.state)); store .on('TEST/changeFoo') .subscribe((e) => e.change((draft) => (draft.foo.list = [1, 2, 3]))); store.dispatch({ type: 'TEST/increment', payload: { by: 1 } }); expect(events.length).to.eql(1); expect(events[0].isCancelled).to.eql(false); expect(events[0].change.type).to.eql('TEST/increment'); store.dispatch({ type: 'TEST/changeFoo', payload: {} }); expect(events.length).to.eql(2); expect(events[1].isCancelled).to.eql(false); expect(events[1].change.type).to.eql('TEST/changeFoo'); }); it('cancels change', () => { const store = Store.create({ initial }); let cancel = false; store.changing$.subscribe((e) => { if (cancel) { e.cancel(); } }); store.on('TEST/increment').subscribe((e) => { if (e.payload.by > 0) { const count = e.state.count + e.payload.by; const next = Object.assign(Object.assign({}, e.state), { count }); e.change(next); } }); store .on('TEST/changeFoo') .subscribe((e) => e.change((draft) => (draft.foo.list = [1, 2, 3]))); expect(store.state.count).to.eql(0); store.dispatch({ type: 'TEST/increment', payload: { by: 1 } }); expect(store.state.count).to.eql(1); cancel = true; store.dispatch({ type: 'TEST/increment', payload: { by: 99 } }); expect(store.state.count).to.eql(1); store.dispatch({ type: 'TEST/changeFoo', payload: {} }); expect(store.state.foo.list).to.eql([]); cancel = false; store.dispatch({ type: 'TEST/changeFoo', payload: {} }); expect(store.state.foo.list).to.eql([1, 2, 3]); }); it('fires [changed] event', () => { const store = Store.create({ initial }); const events = []; store.changed$.subscribe((e) => events.push(e)); store.on('TEST/increment').subscribe((e) => { if (e.payload.by > 0) { const count = e.state.count + e.payload.by; const next = Object.assign(Object.assign({}, e.state), { count }); e.change(next); } }); store.dispatch({ type: 'TEST/increment', payload: { by: 90 } }); store.dispatch({ type: 'TEST/increment', payload: { by: 0 } }); store.dispatch({ type: 'TEST/increment', payload: { by: 2 } }); expect(events.length).to.eql(2); const change1 = events[0]; const change2 = events[1]; expect(change1.type).to.eql('TEST/increment'); expect(change1.event.type).to.eql('TEST/increment'); expect(change1.event.payload.by).to.eql(90); expect(change2.type).to.eql('TEST/increment'); expect(change2.event.type).to.eql('TEST/increment'); expect(change2.event.payload.by).to.eql(2); expect(change1.from.count).to.eql(0); expect(change1.to.count).to.eql(90); expect(change2.from.count).to.eql(90); expect(change2.to.count).to.eql(92); }); }); describe('epics', () => { it('dispatches a follow-on event (sync)', () => { const store = Store.create({ initial }); const events = []; store.event$.subscribe((e) => events.push(e)); store.on('TEST/increment').subscribe((e) => { e.dispatch({ type: 'TEST/decrement', payload: { by: 2 } }); }); store.dispatch({ type: 'TEST/increment', payload: { by: 1 } }); expect(events.length).to.eql(2); expect(events[0].type).to.eql('TEST/increment'); expect(events[1].type).to.eql('TEST/decrement'); }); it('dispatches a follow-on event (async)', async () => { const store = Store.create({ initial }); const events = []; store.event$.subscribe((e) => events.push(e)); store.on('TEST/increment').subscribe(async (e) => { await time.wait(3); e.dispatch({ type: 'TEST/decrement', payload: { by: 2 } }); }); expect(events.length).to.eql(0); store.dispatch({ type: 'TEST/increment', payload: { by: 1 } }); expect(events.length).to.eql(1); await time.wait(10); expect(events.length).to.eql(2); }); }); });