1 | import { expect, time } from '../test';
|
2 | import { Subject } from 'rxjs';
|
3 | import { Store } from '.';
|
4 | const initial = { count: 0, foo: { list: [] } };
|
5 | describe('Store', () => {
|
6 | describe('lifecycle', () => {
|
7 | it('constructs', () => {
|
8 | const store = Store.create({ initial });
|
9 | expect(store.isDisposed).to.eql(false);
|
10 | expect(store.state).to.not.equal(initial);
|
11 | expect(store.state).to.eql(initial);
|
12 | });
|
13 | it('disposes', () => {
|
14 | const store = Store.create({ initial });
|
15 | let count = 0;
|
16 | store.dispose$.subscribe(() => count++);
|
17 | store.dispose();
|
18 | store.dispose();
|
19 | expect(store.isDisposed).to.eql(true);
|
20 | expect(count).to.eql(1);
|
21 | });
|
22 | it('takes event$ at creation', () => {
|
23 | const event$ = new Subject();
|
24 | const store = Store.create({ initial, event$ });
|
25 | expect(store._event$).to.equal(event$);
|
26 | });
|
27 | });
|
28 | describe('state', () => {
|
29 | it('returns new immutable object from [state] property', () => {
|
30 | const store = Store.create({ initial });
|
31 | const state1 = store.state;
|
32 | const state2 = store.state;
|
33 | expect(store.state).to.eql(initial);
|
34 | expect(store.state).to.not.equal(initial);
|
35 | expect(state1).to.eql(store.state);
|
36 | expect(state1).to.eql(initial);
|
37 | expect(state1).to.not.equal(state2);
|
38 | expect(store.state).to.not.equal(initial);
|
39 | });
|
40 | });
|
41 | describe('dispatch', () => {
|
42 | it('returns the state object', () => {
|
43 | const state = Store.create({ initial });
|
44 | const res = state.dispatch({ type: 'TEST/increment', payload: { by: 1 } });
|
45 | expect(res).to.equal(res);
|
46 | });
|
47 | it('fires dispatch event', () => {
|
48 | const store = Store.create({ initial });
|
49 | const events = [];
|
50 | store.event$.subscribe((e) => events.push(e));
|
51 | store.dispatch({ type: 'TEST/increment', payload: { by: 1 } });
|
52 | store.dispatch({ type: 'TEST/decrement', payload: { by: 2 } });
|
53 | expect(events.length).to.eql(2);
|
54 | expect(events[0].type).to.eql('TEST/increment');
|
55 | expect(events[1].type).to.eql('TEST/decrement');
|
56 | });
|
57 | it('fires (via injected event$)', () => {
|
58 | const event$ = new Subject();
|
59 | const store = Store.create({ initial, event$ });
|
60 | const events = [];
|
61 | store.event$.subscribe((e) => events.push(e));
|
62 | event$.next({ type: 'TEST/increment', payload: { by: 1 } });
|
63 | event$.next({ type: 'TEST/decrement', payload: { by: 2 } });
|
64 | expect(events.length).to.eql(2);
|
65 | expect(events[0].type).to.eql('TEST/increment');
|
66 | expect(events[1].type).to.eql('TEST/decrement');
|
67 | });
|
68 | it('returns copy of the current state object on event', () => {
|
69 | const store = Store.create({ initial });
|
70 | const states = [];
|
71 | store.on('TEST/increment').subscribe((e) => {
|
72 | states.push(e.state);
|
73 | states.push(e.state);
|
74 | });
|
75 | store.dispatch({ type: 'TEST/increment', payload: { by: 1 } });
|
76 | expect(states.length).to.eql(2);
|
77 | expect(states[0]).to.eql(store.state);
|
78 | expect(states[1]).to.eql(store.state);
|
79 | expect(states[0]).to.not.equal(store.state);
|
80 | expect(states[1]).to.not.equal(store.state);
|
81 | expect(states[0]).to.not.equal(states[1]);
|
82 | });
|
83 | it('changes the current state (via {...object})', () => {
|
84 | const store = Store.create({ initial });
|
85 | expect(store.state.count).to.eql(0);
|
86 | store.on('TEST/increment').subscribe((e) => {
|
87 | const count = e.state.count + e.payload.by;
|
88 | const next = Object.assign(Object.assign({}, e.state), { count });
|
89 | e.change(next);
|
90 | });
|
91 | store.on('TEST/decrement').subscribe((e) => {
|
92 | const count = e.state.count - e.payload.by;
|
93 | const next = Object.assign(Object.assign({}, e.state), { count });
|
94 | e.change(next);
|
95 | });
|
96 | store.dispatch({ type: 'TEST/increment', payload: { by: 1 } });
|
97 | expect(store.state.count).to.eql(1);
|
98 | store.dispatch({ type: 'TEST/decrement', payload: { by: 2 } });
|
99 | expect(store.state.count).to.eql(-1);
|
100 | });
|
101 | it('changes the current state (via immutable function)', () => {
|
102 | const store = Store.create({
|
103 | initial: Object.assign(Object.assign({}, initial), { bar: { msg: 'hello' } }),
|
104 | });
|
105 | const before = store.state;
|
106 | expect(before.count).to.eql(0);
|
107 | store.on('TEST/increment').subscribe((e) => {
|
108 | e.change((draft) => {
|
109 | draft.count += e.payload.by;
|
110 | });
|
111 | });
|
112 | store.on('TEST/changeFoo').subscribe((e) => {
|
113 | e.change((draft) => {
|
114 | draft.foo.list.push(123);
|
115 | const foo = draft.foo;
|
116 | foo.msg = 'hello';
|
117 | });
|
118 | });
|
119 | store.dispatch({ type: 'TEST/increment', payload: { by: 1 } });
|
120 | const after1 = store.state;
|
121 | expect(before).to.not.equal(after1);
|
122 | expect(after1.count).to.eql(1);
|
123 | expect(after1.foo).to.equal(before.foo);
|
124 | expect(after1.bar).to.equal(before.bar);
|
125 | store.dispatch({ type: 'TEST/changeFoo', payload: {} });
|
126 | const after2 = store.state;
|
127 | expect(after2.foo.list).to.eql([123]);
|
128 | expect(after2.foo.msg).to.eql('hello');
|
129 | expect(after2).to.not.equal(after1);
|
130 | expect(after2.foo).to.not.equal(after1.foo);
|
131 | expect(after2.foo.list).to.not.equal(after1.foo.list);
|
132 | });
|
133 | it('fires [changing] event', () => {
|
134 | const store = Store.create({ initial });
|
135 | const events = [];
|
136 | store.changing$.subscribe((e) => events.push(e));
|
137 | store.on('TEST/increment').subscribe((e) => e.change(e.state));
|
138 | store
|
139 | .on('TEST/changeFoo')
|
140 | .subscribe((e) => e.change((draft) => (draft.foo.list = [1, 2, 3])));
|
141 | store.dispatch({ type: 'TEST/increment', payload: { by: 1 } });
|
142 | expect(events.length).to.eql(1);
|
143 | expect(events[0].isCancelled).to.eql(false);
|
144 | expect(events[0].change.type).to.eql('TEST/increment');
|
145 | store.dispatch({ type: 'TEST/changeFoo', payload: {} });
|
146 | expect(events.length).to.eql(2);
|
147 | expect(events[1].isCancelled).to.eql(false);
|
148 | expect(events[1].change.type).to.eql('TEST/changeFoo');
|
149 | });
|
150 | it('cancels change', () => {
|
151 | const store = Store.create({ initial });
|
152 | let cancel = false;
|
153 | store.changing$.subscribe((e) => {
|
154 | if (cancel) {
|
155 | e.cancel();
|
156 | }
|
157 | });
|
158 | store.on('TEST/increment').subscribe((e) => {
|
159 | if (e.payload.by > 0) {
|
160 | const count = e.state.count + e.payload.by;
|
161 | const next = Object.assign(Object.assign({}, e.state), { count });
|
162 | e.change(next);
|
163 | }
|
164 | });
|
165 | store
|
166 | .on('TEST/changeFoo')
|
167 | .subscribe((e) => e.change((draft) => (draft.foo.list = [1, 2, 3])));
|
168 | expect(store.state.count).to.eql(0);
|
169 | store.dispatch({ type: 'TEST/increment', payload: { by: 1 } });
|
170 | expect(store.state.count).to.eql(1);
|
171 | cancel = true;
|
172 | store.dispatch({ type: 'TEST/increment', payload: { by: 99 } });
|
173 | expect(store.state.count).to.eql(1);
|
174 | store.dispatch({ type: 'TEST/changeFoo', payload: {} });
|
175 | expect(store.state.foo.list).to.eql([]);
|
176 | cancel = false;
|
177 | store.dispatch({ type: 'TEST/changeFoo', payload: {} });
|
178 | expect(store.state.foo.list).to.eql([1, 2, 3]);
|
179 | });
|
180 | it('fires [changed] event', () => {
|
181 | const store = Store.create({ initial });
|
182 | const events = [];
|
183 | store.changed$.subscribe((e) => events.push(e));
|
184 | store.on('TEST/increment').subscribe((e) => {
|
185 | if (e.payload.by > 0) {
|
186 | const count = e.state.count + e.payload.by;
|
187 | const next = Object.assign(Object.assign({}, e.state), { count });
|
188 | e.change(next);
|
189 | }
|
190 | });
|
191 | store.dispatch({ type: 'TEST/increment', payload: { by: 90 } });
|
192 | store.dispatch({ type: 'TEST/increment', payload: { by: 0 } });
|
193 | store.dispatch({ type: 'TEST/increment', payload: { by: 2 } });
|
194 | expect(events.length).to.eql(2);
|
195 | const change1 = events[0];
|
196 | const change2 = events[1];
|
197 | expect(change1.type).to.eql('TEST/increment');
|
198 | expect(change1.event.type).to.eql('TEST/increment');
|
199 | expect(change1.event.payload.by).to.eql(90);
|
200 | expect(change2.type).to.eql('TEST/increment');
|
201 | expect(change2.event.type).to.eql('TEST/increment');
|
202 | expect(change2.event.payload.by).to.eql(2);
|
203 | expect(change1.from.count).to.eql(0);
|
204 | expect(change1.to.count).to.eql(90);
|
205 | expect(change2.from.count).to.eql(90);
|
206 | expect(change2.to.count).to.eql(92);
|
207 | });
|
208 | });
|
209 | describe('epics', () => {
|
210 | it('dispatches a follow-on event (sync)', () => {
|
211 | const store = Store.create({ initial });
|
212 | const events = [];
|
213 | store.event$.subscribe((e) => events.push(e));
|
214 | store.on('TEST/increment').subscribe((e) => {
|
215 | e.dispatch({ type: 'TEST/decrement', payload: { by: 2 } });
|
216 | });
|
217 | store.dispatch({ type: 'TEST/increment', payload: { by: 1 } });
|
218 | expect(events.length).to.eql(2);
|
219 | expect(events[0].type).to.eql('TEST/increment');
|
220 | expect(events[1].type).to.eql('TEST/decrement');
|
221 | });
|
222 | it('dispatches a follow-on event (async)', async () => {
|
223 | const store = Store.create({ initial });
|
224 | const events = [];
|
225 | store.event$.subscribe((e) => events.push(e));
|
226 | store.on('TEST/increment').subscribe(async (e) => {
|
227 | await time.wait(3);
|
228 | e.dispatch({ type: 'TEST/decrement', payload: { by: 2 } });
|
229 | });
|
230 | expect(events.length).to.eql(0);
|
231 | store.dispatch({ type: 'TEST/increment', payload: { by: 1 } });
|
232 | expect(events.length).to.eql(1);
|
233 | await time.wait(10);
|
234 | expect(events.length).to.eql(2);
|
235 | });
|
236 | });
|
237 | });
|