UNPKG

11.9 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.take(6).map(x => String(x)).startWith('a'),
185 xs.never(),
186 ),
187 };
188 }
189
190 function driver(sink: Stream<string>) {
191 return sink.map(x => x.charCodeAt(0)).compose(delay(1));
192 }
193
194 const {sources, run} = setup(app, {other: driver});
195
196 let dispose: any;
197 sources.other.addListener({
198 next: x => {
199 assert.strictEqual(x, 97);
200 dispose(); // will trigger this listener's complete
201 },
202 error: err => done(err),
203 complete: () => done(),
204 });
205 dispose = run();
206 });
207
208 it('should support sink-only drivers', function(done) {
209 function app(sources: any): any {
210 return {
211 other: xs.of(1, 2, 3),
212 };
213 }
214
215 let driverCalled = false;
216 function driver(sink: Stream<string>) {
217 assert.strictEqual(typeof sink, 'object');
218 assert.strictEqual(typeof sink.fold, 'function');
219 driverCalled = true;
220 }
221
222 run(app, {other: driver});
223
224 assert.strictEqual(driverCalled, true);
225 done();
226 });
227
228 it('should not adapt() sinks', function(done) {
229 function app(sources: any): any {
230 return {
231 other: xs.of(1, 2, 3),
232 };
233 }
234
235 let driverCalled = false;
236 function driver(sink: Stream<string>) {
237 assert.strictEqual(typeof sink, 'object');
238 assert.strictEqual(typeof sink.fold, 'function');
239 driverCalled = true;
240 return xs.of(10, 20, 30);
241 }
242
243 setAdapt(stream => 'this not a stream');
244 run(app, {other: driver});
245 setAdapt(x => x);
246
247 assert.strictEqual(driverCalled, true);
248 done();
249 });
250
251 it('should adapt() a simple source (stream)', function(done) {
252 let appCalled = false;
253 function app(sources: any): any {
254 assert.strictEqual(typeof sources.other, 'string');
255 assert.strictEqual(sources.other, 'this is adapted');
256 appCalled = true;
257
258 return {
259 other: xs.of(1, 2, 3),
260 };
261 }
262
263 function driver(sink: Stream<string>) {
264 return xs.of(10, 20, 30);
265 }
266
267 setAdapt(stream => 'this is adapted');
268 run(app, {other: driver});
269 setAdapt(x => x);
270
271 assert.strictEqual(appCalled, true);
272 done();
273 });
274
275 it('should not work after has been disposed', function(done) {
276 type MySources = {
277 other: Stream<string>;
278 };
279
280 function app(sources: MySources) {
281 return {other: xs.periodic(100).map(i => i + 1)};
282 }
283 function driver(num$: Stream<number>): Stream<string> {
284 return num$.map(num => 'x' + num);
285 }
286
287 const {sources, run} = setup(app, {
288 other: driver,
289 });
290
291 let dispose: any;
292 sources.other.addListener({
293 next: x => {
294 assert.notStrictEqual(x, 'x3');
295 if (x === 'x2') {
296 dispose(); // will trigger this listener's complete
297 }
298 },
299 error: err => done(err),
300 complete: () => done(),
301 });
302 dispose = run();
303 });
304});
305
306describe('run', function() {
307 it('should be a function', function() {
308 assert.strictEqual(typeof run, 'function');
309 });
310
311 it('should throw if first argument is not a function', function() {
312 assert.throws(() => {
313 (run as any)('not a function');
314 }, /First argument given to Cycle must be the 'main' function/i);
315 });
316
317 it('should throw if second argument is not an object', function() {
318 assert.throws(() => {
319 (run as any)(() => {}, 'not an object');
320 }, /Second argument given to Cycle must be an object with driver functions/i);
321 });
322
323 it('should throw if second argument is an empty object', function() {
324 assert.throws(() => {
325 (run as any)(() => {}, {});
326 }, /Second argument given to Cycle must be an object with at least one/i);
327 });
328
329 it('should return a dispose function', function() {
330 let sandbox = sinon.sandbox.create();
331 const spy = sandbox.spy();
332
333 type NiceSources = {
334 other: Stream<string>;
335 };
336 type NiceSinks = {
337 other: Stream<string>;
338 };
339
340 function app(sources: NiceSources): NiceSinks {
341 return {
342 other: sources.other.take(1).startWith('a'),
343 };
344 }
345
346 function driver() {
347 return xs.of('b').debug(spy);
348 }
349
350 let dispose = run(app, {other: driver});
351 assert.strictEqual(typeof dispose, 'function');
352 sinon.assert.calledOnce(spy);
353 dispose();
354 });
355
356 it('should happen synchronously', function(done) {
357 let sandbox = sinon.sandbox.create();
358 const spy = sandbox.spy();
359 function app(sources: any): any {
360 sources.other.addListener({
361 next: () => {},
362 error: () => {},
363 complete: () => {},
364 });
365 return {
366 other: xs.of(10),
367 };
368 }
369 let mutable = 'correct';
370 function driver(sink: Stream<number>): Stream<string> {
371 return sink.map(x => 'a' + 10).debug(x => {
372 assert.strictEqual(x, 'a10');
373 assert.strictEqual(mutable, 'correct');
374 spy();
375 });
376 }
377 run(app, {other: driver});
378 mutable = 'wrong';
379 setTimeout(() => {
380 sinon.assert.calledOnce(spy);
381 done();
382 }, 20);
383 });
384
385 it('should support driver that asynchronously subscribes to sink', function(
386 done,
387 ) {
388 function app(sources: any): any {
389 return {
390 foo: xs.of(10),
391 };
392 }
393
394 const expected = [10];
395 function driver(sink: Stream<number>): Stream<any> {
396 setTimeout(() => {
397 sink.subscribe({
398 next(x) {
399 assert.strictEqual(x, expected.shift());
400 },
401 error() {},
402 complete() {},
403 });
404 });
405 return xs.never();
406 }
407
408 run(app, {foo: driver});
409
410 setTimeout(() => {
411 assert.strictEqual(expected.length, 0);
412 done();
413 }, 100);
414 });
415
416 it('should report errors from main() in the console', function(done) {
417 let sandbox = sinon.sandbox.create();
418 sandbox.stub(console, 'error');
419
420 function main(sources: any): any {
421 return {
422 other: sources.other.take(1).startWith('a').map(() => {
423 throw new Error('malfunction');
424 }),
425 };
426 }
427 function driver(sink: Stream<any>) {
428 sink.addListener({
429 next: () => {},
430 error: (err: any) => {},
431 });
432 return xs.of('b');
433 }
434
435 let caught = false;
436 try {
437 run(main, {other: driver});
438 } catch (e) {
439 assert.strictEqual(e.message, 'malfunction');
440 caught = true;
441 }
442 setTimeout(() => {
443 sinon.assert.calledOnce(console.error as any);
444 sinon.assert.calledWithExactly(
445 console.error as any,
446 sinon.match('malfunction'),
447 );
448
449 // Should be false because the error was already reported in the console.
450 // Otherwise we would have double reporting of the error.
451 assert.strictEqual(caught, false);
452
453 sandbox.restore();
454 done();
455 }, 80);
456 });
457});