@expressive/mvc 0.53.6 → 0.54.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,527 +0,0 @@
1
- import { mockPromise, mockWarn } from '../mocks';
2
- import { Model } from '../model';
3
- import { set } from './set';
4
- const warn = mockWarn();
5
- describe("placeholder", () => {
6
- class Test extends Model {
7
- constructor() {
8
- super(...arguments);
9
- this.foobar = set();
10
- }
11
- }
12
- it('will suspend if value is accessed before assign', async () => {
13
- const instance = Test.new();
14
- const promise = mockPromise();
15
- const mockEffect = jest.fn((state) => {
16
- promise.resolve(state.foobar);
17
- });
18
- instance.get(mockEffect);
19
- expect(mockEffect).toBeCalledTimes(1);
20
- instance.foobar = "foo!";
21
- const result = await promise;
22
- expect(mockEffect).toBeCalledTimes(2);
23
- expect(result).toBe("foo!");
24
- });
25
- it("will resolve suspense after latest value", async () => {
26
- const test = Test.new();
27
- const foobar = jest.fn();
28
- const effect = jest.fn((state) => {
29
- foobar(state.foobar);
30
- });
31
- test.get(effect);
32
- expect(effect).toBeCalledTimes(1);
33
- expect(foobar).not.toBeCalled();
34
- test.foobar = "foo";
35
- test.foobar = "bar";
36
- await expect(test).toHaveUpdated("foobar");
37
- expect(effect).toBeCalledTimes(2);
38
- expect(foobar).not.toBeCalledWith("foo");
39
- expect(foobar).toBeCalledWith("bar");
40
- });
41
- it('will not suspend if value is defined', async () => {
42
- const instance = Test.new();
43
- instance.foobar = "bar!";
44
- const mockEffect = jest.fn((state) => {
45
- expect(state.foobar).toBe("bar!");
46
- });
47
- instance.get(mockEffect);
48
- expect(mockEffect).toBeCalledTimes(1);
49
- });
50
- });
51
- describe("callback", () => {
52
- it('will invoke callback on property put', async () => {
53
- class Subject extends Model {
54
- constructor() {
55
- super(...arguments);
56
- this.test = set(1, value => {
57
- didAssign(value + 1);
58
- });
59
- }
60
- }
61
- const state = Subject.new();
62
- const didAssign = jest.fn();
63
- const didUpdate = jest.fn();
64
- expect(didAssign).not.toBeCalled();
65
- state.set((key) => {
66
- if (key == "test")
67
- didUpdate();
68
- });
69
- state.test = 2;
70
- expect(didUpdate).toBeCalledTimes(1);
71
- expect(didAssign).toBeCalledWith(3);
72
- });
73
- it('will invoke return-callback on overwrite', async () => {
74
- class Subject extends Model {
75
- constructor() {
76
- super(...arguments);
77
- this.test = set(1, () => {
78
- return () => {
79
- callback(true);
80
- };
81
- });
82
- }
83
- }
84
- const callback = jest.fn();
85
- const state = Subject.new();
86
- state.test = 2;
87
- await expect(state).toHaveUpdated();
88
- expect(callback).not.toBeCalled();
89
- state.test = 3;
90
- await expect(state).toHaveUpdated();
91
- expect(callback).toBeCalledWith(true);
92
- });
93
- it('will assign a default value', async () => {
94
- class Subject extends Model {
95
- constructor() {
96
- super(...arguments);
97
- this.test = set("foo", value => {
98
- callback(value);
99
- });
100
- }
101
- }
102
- const callback = jest.fn();
103
- const state = Subject.new();
104
- expect(state.test).toBe("foo");
105
- state.test = "bar";
106
- await expect(state).toHaveUpdated();
107
- expect(callback).toBeCalledWith("bar");
108
- });
109
- it('will ignore effect promise', () => {
110
- class Subject extends Model {
111
- constructor() {
112
- super(...arguments);
113
- this.property = set(undefined, async () => { });
114
- }
115
- }
116
- const state = Subject.new();
117
- expect(() => state.property = "bar").not.toThrow();
118
- });
119
- it('will not suspend own property access', () => {
120
- class Subject extends Model {
121
- constructor() {
122
- super(...arguments);
123
- this.property = set(undefined, (_, previous) => {
124
- propertyWas = previous;
125
- });
126
- }
127
- }
128
- const state = Subject.new();
129
- let propertyWas;
130
- state.property = "bar";
131
- expect(propertyWas).toBe(undefined);
132
- state.property = "foo";
133
- expect(propertyWas).toBe("bar");
134
- });
135
- it('will reset nested effects', async () => {
136
- class Subject extends Model {
137
- constructor() {
138
- super(...arguments);
139
- this.name = "World";
140
- this.hello = set("Hello", async (value) => {
141
- this.get(({ name }) => {
142
- effect(`${value} ${name}!`);
143
- });
144
- });
145
- }
146
- }
147
- const effect = jest.fn();
148
- const state = Subject.new();
149
- state.hello = "Hola";
150
- await expect(state).toHaveUpdated();
151
- expect(effect).toBeCalledWith("Hola World!");
152
- state.hello = "Bonjour";
153
- await expect(state).toHaveUpdated();
154
- expect(effect).toBeCalledWith("Bonjour World!");
155
- state.name = "Earth";
156
- await expect(state).toHaveUpdated();
157
- expect(effect).toBeCalledWith("Bonjour Earth!");
158
- expect(effect).not.toBeCalledWith("Hola Earth!");
159
- });
160
- });
161
- describe("intercept", () => {
162
- it('will prevent update if callback returns false', async () => {
163
- class Subject extends Model {
164
- constructor() {
165
- super(...arguments);
166
- this.test = set("foo", value => {
167
- callback(value);
168
- return false;
169
- });
170
- }
171
- }
172
- const callback = jest.fn();
173
- const state = Subject.new();
174
- expect(state.test).toBe("foo");
175
- state.test = "bar";
176
- await expect(state).not.toHaveUpdated();
177
- expect(callback).toBeCalledWith("bar");
178
- expect(state.test).toBe("foo");
179
- });
180
- it('will not call prior cleanup if supressed', async () => {
181
- const cleanup = jest.fn();
182
- const setter = jest.fn(value => {
183
- return value === 3 ? false : cleanup;
184
- });
185
- class Test extends Model {
186
- constructor() {
187
- super(...arguments);
188
- this.value = set(1, setter);
189
- }
190
- }
191
- const subject = Test.new();
192
- subject.value = 2;
193
- expect(setter).toBeCalledWith(2, 1);
194
- await expect(subject).toHaveUpdated();
195
- expect(subject.value).toBe(2);
196
- // this update will be supressed by setter
197
- subject.value = 3;
198
- expect(setter).toBeCalledWith(3, 2);
199
- await expect(subject).not.toHaveUpdated();
200
- expect(cleanup).not.toBeCalled();
201
- subject.value = 4;
202
- expect(setter).toBeCalledWith(4, 2);
203
- expect(cleanup).toBeCalledTimes(1);
204
- expect(cleanup).toBeCalledWith(4);
205
- });
206
- });
207
- describe("factory", () => {
208
- it("will ignore setter if assigned", () => {
209
- const getValue = jest.fn(() => "foo");
210
- class Test extends Model {
211
- constructor() {
212
- super(...arguments);
213
- this.value = set(getValue);
214
- }
215
- }
216
- const test = Test.new();
217
- test.value = "bar";
218
- expect(test).toHaveUpdated();
219
- expect(test.value).toBe("bar");
220
- expect(getValue).not.toBeCalled();
221
- });
222
- it("will compute when accessed", () => {
223
- const factory = jest.fn(() => "Hello World");
224
- class Test extends Model {
225
- constructor() {
226
- super(...arguments);
227
- this.value = set(factory);
228
- }
229
- }
230
- const test = Test.new();
231
- expect(factory).not.toBeCalled();
232
- void test.value;
233
- expect(factory).toBeCalled();
234
- });
235
- it("will compute lazily", () => {
236
- const factory = jest.fn(async () => "Hello World");
237
- class Test extends Model {
238
- constructor() {
239
- super(...arguments);
240
- this.value = set(factory, false);
241
- }
242
- }
243
- Test.new();
244
- expect(factory).not.toBeCalled();
245
- });
246
- it('will bind factory function to self', async () => {
247
- class Test extends Model {
248
- constructor() {
249
- super(...arguments);
250
- // methods lose implicit this
251
- this.value = set(this.method);
252
- }
253
- async method() {
254
- expect(this).toBe(instance);
255
- }
256
- }
257
- const instance = Test.new();
258
- });
259
- it("will warn and rethrow error from factory", () => {
260
- class Test extends Model {
261
- constructor() {
262
- super(...arguments);
263
- this.memoized = set(this.failToGetSomething, true);
264
- }
265
- failToGetSomething() {
266
- throw new Error("Foobar");
267
- }
268
- }
269
- const attempt = () => Test.new("ID");
270
- expect(attempt).toThrowError("Foobar");
271
- expect(warn).toBeCalledWith(`Generating initial value for ID.memoized failed.`);
272
- });
273
- });
274
- describe("suspense", () => {
275
- it("will throw suspense-promise resembling an error", () => {
276
- const promise = mockPromise();
277
- class Test extends Model {
278
- constructor() {
279
- super(...arguments);
280
- this.value = set(promise, true);
281
- }
282
- }
283
- const instance = Test.new("ID");
284
- expect(() => instance.value).toThrowError(`ID.value is not yet available.`);
285
- promise.resolve();
286
- });
287
- it("will resolve when factory does", async () => {
288
- class Test extends Model {
289
- constructor() {
290
- super(...arguments);
291
- this.value = set(async () => "foobar", true);
292
- }
293
- }
294
- const test = Test.new();
295
- expect(() => test.value).toThrow(expect.any(Promise));
296
- await expect(test).toUpdate(0);
297
- expect(test.value).toBe("foobar");
298
- });
299
- it("will not suspend where already resolved", async () => {
300
- class Test extends Model {
301
- constructor() {
302
- super(...arguments);
303
- this.greet = set(async () => "Hello", true);
304
- this.name = set(async () => "World", true);
305
- this.value = set(() => this.greet + " " + this.name);
306
- }
307
- }
308
- const test = Test.new();
309
- try {
310
- void test.value;
311
- }
312
- catch (error) {
313
- if (error instanceof Promise)
314
- await error;
315
- else
316
- throw error;
317
- }
318
- expect(() => test.value).not.toThrow();
319
- });
320
- it("will suspend if required while still pending", () => {
321
- const promise = mockPromise();
322
- class Test extends Model {
323
- constructor() {
324
- super(...arguments);
325
- this.value = set(promise, true);
326
- }
327
- }
328
- const instance = Test.new();
329
- expect(() => instance.value).toThrow(expect.any(Promise));
330
- promise.resolve();
331
- });
332
- it("will be undefined if not required", async () => {
333
- const promise = mockPromise();
334
- const mock = jest.fn();
335
- class Test extends Model {
336
- constructor() {
337
- super(...arguments);
338
- this.value = set(promise);
339
- }
340
- }
341
- const test = Test.new();
342
- test.get($ => mock($.value));
343
- expect(mock).toBeCalledWith(undefined);
344
- promise.resolve("foobar");
345
- await expect(test).toUpdate(0);
346
- expect(mock).toBeCalledWith("foobar");
347
- });
348
- it("will suspend another factory", async () => {
349
- const greet = mockPromise();
350
- const name = mockPromise();
351
- const didEvaluate = jest.fn(function () {
352
- return this.greet + " " + this.name;
353
- });
354
- class Test extends Model {
355
- constructor() {
356
- super(...arguments);
357
- this.greet = set(greet, true);
358
- this.name = set(name, true);
359
- this.value = set(didEvaluate, true);
360
- }
361
- }
362
- const test = Test.new();
363
- test.get($ => void $.value);
364
- greet.resolve("Hello");
365
- await expect(test).toUpdate(0);
366
- name.resolve("World");
367
- await expect(test).toUpdate(0);
368
- expect(didEvaluate).toBeCalledTimes(3);
369
- expect(test.value).toBe("Hello World");
370
- });
371
- it("will suspend another factory (async)", async () => {
372
- const greet = mockPromise();
373
- const name = mockPromise();
374
- const didEvaluate = jest.fn(async function () {
375
- return this.greet + " " + this.name;
376
- });
377
- class Test extends Model {
378
- constructor() {
379
- super(...arguments);
380
- this.greet = set(() => greet, true);
381
- this.name = set(() => name, true);
382
- this.value = set(didEvaluate, true);
383
- }
384
- }
385
- const test = Test.new();
386
- test.get($ => void $.value);
387
- greet.resolve("Hello");
388
- await expect(test).toUpdate(0);
389
- name.resolve("World");
390
- await expect(test).toUpdate(0);
391
- expect(didEvaluate).toBeCalledTimes(3);
392
- expect(test.value).toBe("Hello World");
393
- });
394
- it("will nest suspense", async () => {
395
- const promise = mockPromise();
396
- const didUpdate = mockPromise();
397
- class Child extends Model {
398
- constructor() {
399
- super(...arguments);
400
- this.value = set(promise, true);
401
- }
402
- }
403
- class Test extends Model {
404
- constructor() {
405
- super(...arguments);
406
- this.child = new Child();
407
- this.childValue = set(() => {
408
- return this.child.value + " world!";
409
- }, true);
410
- }
411
- }
412
- const test = Test.new();
413
- const effect = jest.fn((state) => {
414
- didUpdate.resolve(state.childValue);
415
- });
416
- test.get(effect);
417
- expect(effect).toBeCalledTimes(1);
418
- expect(effect).not.toHaveReturned();
419
- promise.resolve("hello");
420
- await expect(didUpdate).resolves.toBe("hello world!");
421
- expect(effect).toBeCalledTimes(2);
422
- });
423
- it("will return undefined on nested suspense", async () => {
424
- const promise = mockPromise();
425
- class Test extends Model {
426
- constructor() {
427
- super(...arguments);
428
- this.asyncValue = set(() => promise, true);
429
- this.value = set(() => `Hello ${this.asyncValue}`);
430
- }
431
- }
432
- const test = Test.new();
433
- expect(test.value).toBeUndefined();
434
- promise.resolve("World");
435
- await expect(test).toUpdate();
436
- expect(test.value).toBe("Hello World");
437
- });
438
- it("will squash repeating suspense", async () => {
439
- let pending = mockPromise();
440
- let suspend = true;
441
- const compute = jest.fn(() => {
442
- if (suspend)
443
- throw pending;
444
- return `OK I'm unblocked.`;
445
- });
446
- class Test extends Model {
447
- constructor() {
448
- super(...arguments);
449
- this.message = set(compute, true);
450
- }
451
- }
452
- const test = Test.new();
453
- const didEvaluate = mockPromise();
454
- const effect = jest.fn((state) => {
455
- didEvaluate.resolve(state.message);
456
- });
457
- test.get(effect);
458
- expect(effect).toBeCalled();
459
- expect(effect).not.toHaveReturned();
460
- expect(compute).toBeCalledTimes(1);
461
- pending.resolve();
462
- pending = mockPromise();
463
- // TODO: why does this not work when `.set(0)` is used?
464
- await test.set();
465
- // expect eval to run again because promise resolved.
466
- expect(compute).toBeCalledTimes(2);
467
- suspend = false;
468
- pending.resolve();
469
- await didEvaluate;
470
- expect(test.message).toBe("OK I'm unblocked.");
471
- expect(compute).toBeCalledTimes(3);
472
- expect(effect).toBeCalledTimes(2);
473
- expect(effect).toHaveReturnedTimes(1);
474
- });
475
- it("will squash multiple dependancies", async () => {
476
- const promise = mockPromise();
477
- const promise2 = mockPromise();
478
- class Test extends Model {
479
- constructor() {
480
- super(...arguments);
481
- this.a = set(promise, true);
482
- this.b = set(promise2, true);
483
- this.sum = set(this.getSum);
484
- }
485
- getSum() {
486
- const { a, b } = this;
487
- return `Answer is ${a + b}.`;
488
- }
489
- }
490
- const test = Test.new();
491
- const effect = jest.fn((state) => void state.sum);
492
- test.get(effect);
493
- expect(effect).toBeCalled();
494
- promise.resolve(10);
495
- await expect(test).toUpdate();
496
- expect(effect).toBeCalledTimes(1);
497
- promise2.resolve(20);
498
- await expect(test).toUpdate();
499
- expect(test.sum).toBe("Answer is 30.");
500
- expect(effect).toBeCalledTimes(2);
501
- });
502
- it('will refresh and throw if async rejects', async () => {
503
- const promise = mockPromise();
504
- class Test extends Model {
505
- constructor() {
506
- super(...arguments);
507
- this.value = set(promise, true);
508
- }
509
- }
510
- const instance = Test.new();
511
- let didThrow;
512
- instance.get(state => {
513
- try {
514
- void state.value;
515
- }
516
- catch (err) {
517
- didThrow = err;
518
- if (err instanceof Promise)
519
- throw err;
520
- }
521
- });
522
- expect(didThrow).toBeInstanceOf(Promise);
523
- promise.reject("oh no");
524
- await new Promise(res => setTimeout(res, 10));
525
- expect(didThrow).toBe("oh no");
526
- });
527
- });