1 | import 'mocha';
|
2 | import * as assert from 'assert';
|
3 | import * as sinon from 'sinon';
|
4 | import {run, setup, Sources, Sinks, Driver} from '../lib/cjs/index';
|
5 | import {setAdapt} from '../lib/adapt';
|
6 | import xs, {Stream} from 'xstream';
|
7 | import concat from 'xstream/extra/concat';
|
8 | import delay from 'xstream/extra/delay';
|
9 |
|
10 | let window: any;
|
11 | if (typeof global === 'object') {
|
12 | (global as any).window = {};
|
13 | window = (global as any).window;
|
14 | }
|
15 |
|
16 | describe('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'),
|
66 |
|
67 |
|
68 |
|
69 | num: xs.of(100),
|
70 |
|
71 |
|
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'),
|
106 |
|
107 |
|
108 |
|
109 | num: xs.of(100),
|
110 |
|
111 |
|
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();
|
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();
|
297 | }
|
298 | },
|
299 | error: err => done(err),
|
300 | complete: () => done(),
|
301 | });
|
302 | dispose = run();
|
303 | });
|
304 | });
|
305 |
|
306 | describe('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 |
|
450 |
|
451 | assert.strictEqual(caught, false);
|
452 |
|
453 | sandbox.restore();
|
454 | done();
|
455 | }, 80);
|
456 | });
|
457 | });
|