UNPKG

14.4 kBPlain TextView Raw
1import 'mocha';
2import * as assert from 'assert';
3import * as sinon from 'sinon';
4import {run, setup, Sources, Sinks, Driver} from '../lib/cjs/index';
5import {setAdapt} from '../lib/adapt';
6import xs, {Stream} from 'xstream';
7import concat from 'xstream/extra/concat';
8import delay from 'xstream/extra/delay';
9
10let window: any;
11if (typeof global === 'object') {
12 (global as any).window = {};
13 window = (global as any).window;
14}
15
16describe('setup', function() {
17 it('should be a function', function() {
18 assert.strictEqual(typeof setup, 'function');
19 });
20
21 it('should throw if first argument is not a function', function() {
22 assert.throws(() => {
23 (setup as any)('not a function');
24 }, /First argument given to Cycle must be the 'main' function/i);
25 });
26
27 it('should throw if second argument is not an object', function() {
28 assert.throws(() => {
29 (setup as any)(() => {}, 'not an object');
30 }, /Second argument given to Cycle must be an object with driver functions/i);
31 });
32
33 it('should throw if second argument is an empty object', function() {
34 assert.throws(() => {
35 (setup as any)(() => {}, {});
36 }, /Second argument given to Cycle must be an object with at least one/i);
37 });
38
39 it('should return sinks object and sources object', function() {
40 function app(ext: any): any {
41 return {
42 other: ext.other.take(1).startWith('a'),
43 };
44 }
45 function driver() {
46 return xs.of('b');
47 }
48 let {sinks, sources} = setup(app, {other: driver});
49 assert.strictEqual(typeof sinks, 'object');
50 assert.strictEqual(typeof sinks.other.addListener, 'function');
51 assert.strictEqual(typeof sources, 'object');
52 assert.notStrictEqual(typeof sources.other, 'undefined');
53 assert.notStrictEqual(sources.other, null);
54 assert.strictEqual(typeof sources.other.addListener, 'function');
55 });
56
57 it('should type-check keyof sources and sinks in main and drivers', function() {
58 type Sources = {
59 str: Stream<string>;
60 obj: Stream<object>;
61 };
62
63 function app(sources: Sources) {
64 return {
65 str: sources.str.take(1).startWith('a'), // good
66 // str: sources.obj.mapTo('good'), // good
67 // strTYPO: sources.str.take(1).startWith('a'), // bad
68 // str: xs.of(123), // bad
69 num: xs.of(100), // good
70 // numTYPO: xs.of(100), // bad
71 // num: xs.of('BAD TYPE'), // bad
72 };
73 }
74
75 const stringDriver: Driver<Stream<string>, Stream<string>> = (
76 sink: Stream<string>,
77 ) => xs.of('b');
78
79 const numberWriteOnlyDriver: Driver<Stream<number>, void> = (
80 sink: Stream<number>,
81 ) => {};
82
83 const objectReadOnlyDriver: Driver<void, Stream<object>> = () => xs.of({});
84
85 setup(app, {
86 str: stringDriver,
87 num: numberWriteOnlyDriver,
88 obj: objectReadOnlyDriver,
89 });
90 });
91
92 it('should type-check keyof sources and sinks, supporting interfaces', function() {
93 interface Sources {
94 str: Stream<string>;
95 obj: Stream<object>;
96 }
97
98 interface Sinks {
99 str: Stream<string>;
100 num: Stream<number>;
101 }
102
103 function app(sources: Sources): Sinks {
104 return {
105 str: sources.str.take(1).startWith('a'), // good
106 // str: sources.obj.mapTo('good'), // good
107 // strTYPO: sources.str.take(1).startWith('a'), // bad
108 // str: xs.of(123), // bad
109 num: xs.of(100), // good
110 // numTYPO: xs.of(100), // bad
111 // num: xs.of('BAD TYPE'), // bad
112 };
113 }
114
115 const stringDriver: Driver<Stream<string>, Stream<string>> = (
116 sink: Stream<string>,
117 ) => xs.of('b');
118
119 const numberWriteOnlyDriver: Driver<Stream<number>, void> = (
120 sink: Stream<number>,
121 ) => {};
122
123 const objectReadOnlyDriver: Driver<void, Stream<object>> = () => xs.of({});
124
125 setup(app, {
126 str: stringDriver,
127 num: numberWriteOnlyDriver,
128 obj: objectReadOnlyDriver,
129 });
130 });
131
132 it('should type-check and allow more drivers than sinks', function() {
133 type Sources = {
134 str: Stream<string>;
135 num: Stream<number>;
136 obj: Stream<object>;
137 };
138
139 function app(sources: Sources) {
140 return {};
141 }
142
143 function stringDriver(sink: Stream<string>) {
144 return xs.of('b');
145 }
146
147 const numberDriver = (sink: Stream<number>) => xs.of(100);
148
149 const objectReadOnlyDriver = () => xs.of({});
150
151 setup(app, {
152 str: stringDriver,
153 num: numberDriver,
154 obj: objectReadOnlyDriver,
155 });
156 });
157
158 it('should call DevTool internal function to pass sinks', function() {
159 let sandbox = sinon.sandbox.create();
160 let spy = sandbox.spy();
161 window['CyclejsDevTool_startGraphSerializer'] = spy;
162
163 function app(ext: any): any {
164 return {
165 other: ext.other.take(1).startWith('a'),
166 };
167 }
168 function driver() {
169 return xs.of('b');
170 }
171 run(app, {other: driver});
172
173 sinon.assert.calledOnce(spy);
174 });
175
176 it('should return a run() which in turn returns a dispose()', function(done) {
177 type TestSources = {
178 other: Stream<number>;
179 };
180
181 function app(sources: TestSources) {
182 return {
183 other: concat(
184 sources.other
185 .take(6)
186 .map(x => String(x))
187 .startWith('a'),
188 xs.never(),
189 ),
190 };
191 }
192
193 function driver(sink: Stream<string>) {
194 return sink.map(x => x.charCodeAt(0)).compose(delay(1));
195 }
196
197 const {sources, run} = setup(app, {other: driver});
198
199 let dispose: any;
200 sources.other.addListener({
201 next: x => {
202 assert.strictEqual(x, 97);
203 dispose(); // will trigger this listener's complete
204 },
205 error: err => done(err),
206 complete: () => done(),
207 });
208 dispose = run();
209 });
210
211 it('should support sink-only drivers', function(done) {
212 function app(sources: any): any {
213 return {
214 other: xs.of(1, 2, 3),
215 };
216 }
217
218 let driverCalled = false;
219 function driver(sink: Stream<string>) {
220 assert.strictEqual(typeof sink, 'object');
221 assert.strictEqual(typeof sink.fold, 'function');
222 driverCalled = true;
223 }
224
225 run(app, {other: driver});
226
227 assert.strictEqual(driverCalled, true);
228 done();
229 });
230
231 it('should not adapt() sinks', function(done) {
232 function app(sources: any): any {
233 return {
234 other: xs.of(1, 2, 3),
235 };
236 }
237
238 let driverCalled = false;
239 function driver(sink: Stream<string>) {
240 assert.strictEqual(typeof sink, 'object');
241 assert.strictEqual(typeof sink.fold, 'function');
242 driverCalled = true;
243 return xs.of(10, 20, 30);
244 }
245
246 setAdapt(stream => 'this not a stream');
247 run(app, {other: driver});
248 setAdapt(x => x);
249
250 assert.strictEqual(driverCalled, true);
251 done();
252 });
253
254 it('should adapt() a simple source (stream)', function(done) {
255 let appCalled = false;
256 function app(sources: any): any {
257 assert.strictEqual(typeof sources.other, 'string');
258 assert.strictEqual(sources.other, 'this is adapted');
259 appCalled = true;
260
261 return {
262 other: xs.of(1, 2, 3),
263 };
264 }
265
266 function driver(sink: Stream<string>) {
267 return xs.of(10, 20, 30);
268 }
269
270 setAdapt(stream => 'this is adapted');
271 run(app, {other: driver});
272 setAdapt(x => x);
273
274 assert.strictEqual(appCalled, true);
275 done();
276 });
277
278 it('should not work after has been disposed', function(done) {
279 type MySources = {
280 other: Stream<string>;
281 };
282
283 function app(sources: MySources) {
284 return {other: xs.periodic(100).map(i => i + 1)};
285 }
286 function driver(num$: Stream<number>): Stream<string> {
287 return num$.map(num => 'x' + num);
288 }
289
290 const {sources, run} = setup(app, {
291 other: driver,
292 });
293
294 let dispose: any;
295 sources.other.addListener({
296 next: x => {
297 assert.notStrictEqual(x, 'x3');
298 if (x === 'x2') {
299 dispose(); // will trigger this listener's complete
300 }
301 },
302 error: err => done(err),
303 complete: () => done(),
304 });
305 dispose = run();
306 });
307});
308
309describe('run', function() {
310 it('should be a function', function() {
311 assert.strictEqual(typeof run, 'function');
312 });
313
314 it('should throw if first argument is not a function', function() {
315 assert.throws(() => {
316 (run as any)('not a function');
317 }, /First argument given to Cycle must be the 'main' function/i);
318 });
319
320 it('should throw if second argument is not an object', function() {
321 assert.throws(() => {
322 (run as any)(() => {}, 'not an object');
323 }, /Second argument given to Cycle must be an object with driver functions/i);
324 });
325
326 it('should throw if second argument is an empty object', function() {
327 assert.throws(() => {
328 (run as any)(() => {}, {});
329 }, /Second argument given to Cycle must be an object with at least one/i);
330 });
331
332 it('should return a dispose function', function() {
333 let sandbox = sinon.sandbox.create();
334 const spy = sandbox.spy();
335
336 type NiceSources = {
337 other: Stream<string>;
338 };
339 type NiceSinks = {
340 other: Stream<string>;
341 };
342
343 function app(sources: NiceSources): NiceSinks {
344 return {
345 other: sources.other.take(1).startWith('a'),
346 };
347 }
348
349 function driver() {
350 return xs.of('b').debug(spy);
351 }
352
353 let dispose = run(app, {other: driver});
354 assert.strictEqual(typeof dispose, 'function');
355 sinon.assert.calledOnce(spy);
356 dispose();
357 });
358
359 it('should support driver that asynchronously subscribes to sink', function(
360 done,
361 ) {
362 function app(sources: any): any {
363 return {
364 foo: xs.of(10),
365 };
366 }
367
368 const expected = [10];
369 function driver(sink: Stream<number>): Stream<any> {
370 const buffer: Array<number> = [];
371 sink.addListener({
372 next: x => {
373 buffer.push(x);
374 },
375 });
376 setTimeout(() => {
377 while (buffer.length > 0) {
378 const x = buffer.shift();
379 assert.strictEqual(x, expected.shift());
380 }
381 sink.subscribe({
382 next(x) {
383 assert.strictEqual(x, expected.shift());
384 },
385 error() {},
386 complete() {},
387 });
388 });
389 return xs.never();
390 }
391
392 run(app, {foo: driver});
393
394 setTimeout(() => {
395 assert.strictEqual(expected.length, 0);
396 done();
397 }, 100);
398 });
399
400 it('should forbid cross-driver synchronous races (#592)', function(done) {
401 this.timeout(4000);
402
403 function child(sources: any, num: number) {
404 const vdom$ = sources.HTTP
405 // .select('cat')
406 // .flatten()
407 .map((res: any) => res.body.name)
408 .map((name: string) => 'My name is ' + name);
409
410 const request$ =
411 num === 1
412 ? xs.of({
413 category: 'cat',
414 url: 'http://jsonplaceholder.typicode.com/users/1',
415 })
416 : xs.never();
417
418 return {
419 HTTP: request$,
420 DOM: vdom$,
421 };
422 }
423
424 function mainHTTPThenDOM(sources: any) {
425 const sinks$ = xs
426 .periodic(100)
427 .take(6)
428 .map(i => {
429 if (i % 2 === 1) {
430 return child(sources, i);
431 } else {
432 return {
433 HTTP: xs.empty(),
434 DOM: xs.of(''),
435 };
436 }
437 });
438
439 // order of sinks is important to reproduce the bug
440 return {
441 HTTP: sinks$.map(sinks => sinks.HTTP).flatten(),
442 DOM: sinks$.map(sinks => sinks.DOM).flatten(),
443 };
444 }
445
446 function mainDOMThenHTTP(sources: any) {
447 const sinks$ = xs
448 .periodic(100)
449 .take(6)
450 .map(i => {
451 if (i % 2 === 1) {
452 return child(sources, i);
453 } else {
454 return {
455 HTTP: xs.empty(),
456 DOM: xs.of(''),
457 };
458 }
459 });
460
461 // order of sinks is important to reproduce the bug
462 return {
463 DOM: sinks$.map(sinks => sinks.DOM).flatten(),
464 HTTP: sinks$.map(sinks => sinks.HTTP).flatten(),
465 };
466 }
467
468 let requestsSent = 0;
469 const expectedDOMSinks = [
470 /* HTTP then DOM: */ '',
471 'My name is Louis',
472 '',
473 '',
474 /* DOM then HTTP: */ '',
475 'My name is Louis',
476 '',
477 '',
478 ];
479
480 function domDriver(sink: Stream<string>) {
481 sink.addListener({
482 next: s => {
483 assert.strictEqual(s, expectedDOMSinks.shift());
484 },
485 error: (err: any) => {},
486 });
487 }
488
489 function httpDriver(sink: Stream<any>) {
490 const source = sink.map(req => ({body: {name: 'Louis'}}));
491 source.addListener({
492 next: x => {},
493 error: (err: any) => {},
494 });
495 return source.debug(x => {
496 requestsSent += 1;
497 });
498 }
499
500 // HTTP then DOM:
501 const dispose = run(mainHTTPThenDOM, {
502 HTTP: httpDriver,
503 DOM: domDriver,
504 });
505 setTimeout(() => {
506 assert.strictEqual(expectedDOMSinks.length, 4);
507 assert.strictEqual(requestsSent, 1);
508 dispose();
509
510 // DOM then HTTP:
511 run(mainDOMThenHTTP, {
512 HTTP: httpDriver,
513 DOM: domDriver,
514 });
515 setTimeout(() => {
516 assert.strictEqual(expectedDOMSinks.length, 0);
517 assert.strictEqual(requestsSent, 2);
518 done();
519 }, 1000);
520 }, 1000);
521 });
522
523 it('should report errors from main() in the console', function(done) {
524 const sandbox = sinon.sandbox.create();
525 sandbox.stub(console, 'error');
526
527 function main(sources: any): any {
528 return {
529 other: sources.other
530 .take(1)
531 .startWith('a')
532 .map(() => {
533 throw new Error('malfunction');
534 }),
535 };
536 }
537 function driver(sink: Stream<any>) {
538 sink.addListener({
539 next: () => {},
540 error: (err: any) => {},
541 });
542 return xs.of('b');
543 }
544
545 let caught = false;
546 try {
547 run(main, {other: driver});
548 } catch (e) {
549 caught = true;
550 }
551 setTimeout(() => {
552 sinon.assert.calledOnce(console.error as any);
553 sinon.assert.calledWithExactly(
554 console.error as any,
555 sinon.match((err: any) => err.message === 'malfunction'),
556 );
557
558 // Should be false because the error was already reported in the console.
559 // Otherwise we would have double reporting of the error.
560 assert.strictEqual(caught, false);
561
562 sandbox.restore();
563 done();
564 }, 80);
565 });
566});