UNPKG

8.71 kBPlain TextView Raw
1import * as assert from 'assert';
2import * as sinon from 'sinon';
3import {run} from '../src/index';
4import {setAdapt} from '../src/adapt';
5import xs, {Stream} from 'xstream';
6
7describe('run', function() {
8 it('should be a function', function() {
9 assert.strictEqual(typeof run, 'function');
10 });
11
12 it('should throw if first argument is not a function', function() {
13 assert.throws(() => {
14 (run as any)('not a function');
15 }, /First argument given to Cycle must be the 'main' function/i);
16 });
17
18 it('should throw if second argument is not an object', function() {
19 assert.throws(() => {
20 (run as any)(() => {}, 'not an object');
21 }, /Second argument given to Cycle must be an object with driver functions/i);
22 });
23
24 it('should throw if second argument is an empty object', function() {
25 assert.throws(() => {
26 (run as any)(() => {}, {});
27 }, /Second argument given to Cycle must be an object with at least one/i);
28 });
29
30 it('should return a dispose function', function() {
31 const sandbox = sinon.createSandbox();
32 const spy = sandbox.spy();
33
34 type NiceSources = {
35 other: Stream<string>;
36 };
37 type NiceSinks = {
38 other: Stream<string>;
39 };
40
41 function app(sources: NiceSources): NiceSinks {
42 return {
43 other: sources.other.take(1).startWith('a'),
44 };
45 }
46
47 function driver() {
48 return xs.of('b').debug(spy);
49 }
50
51 const dispose = run(app, {other: driver});
52 assert.strictEqual(typeof dispose, 'function');
53 sinon.assert.calledOnce(spy);
54 dispose();
55 });
56
57 it('should support driver that asynchronously subscribes to sink', function(done) {
58 function app(sources: any): any {
59 return {
60 foo: xs.of(10),
61 };
62 }
63
64 const expected = [10];
65 function driver(sink: Stream<number>): Stream<any> {
66 const buffer: Array<number> = [];
67 sink.addListener({
68 next: x => {
69 buffer.push(x);
70 },
71 });
72 setTimeout(() => {
73 while (buffer.length > 0) {
74 const x = buffer.shift();
75 assert.strictEqual(x, expected.shift());
76 }
77 sink.subscribe({
78 next(x) {
79 assert.strictEqual(x, expected.shift());
80 },
81 error() {},
82 complete() {},
83 });
84 });
85 return xs.never();
86 }
87
88 run(app, {foo: driver});
89
90 setTimeout(() => {
91 assert.strictEqual(expected.length, 0);
92 done();
93 }, 100);
94 });
95
96 it('should forbid cross-driver synchronous races (#592)', function(done) {
97 this.timeout(4000);
98
99 function child(sources: any, num: number) {
100 const vdom$ = sources.HTTP
101 // .select('cat')
102 // .flatten()
103 .map((res: any) => res.body.name)
104 .map((name: string) => 'My name is ' + name);
105
106 const request$ =
107 num === 1
108 ? xs.of({
109 category: 'cat',
110 url: 'http://jsonplaceholder.typicode.com/users/1',
111 })
112 : xs.never();
113
114 return {
115 HTTP: request$,
116 DOM: vdom$,
117 };
118 }
119
120 function mainHTTPThenDOM(sources: any) {
121 const sinks$ = xs
122 .periodic(100)
123 .take(6)
124 .map(i => {
125 if (i % 2 === 1) {
126 return child(sources, i);
127 } else {
128 return {
129 HTTP: xs.empty(),
130 DOM: xs.of(''),
131 };
132 }
133 });
134
135 // order of sinks is important to reproduce the bug
136 return {
137 HTTP: sinks$.map(sinks => sinks.HTTP).flatten(),
138 DOM: sinks$.map(sinks => sinks.DOM).flatten(),
139 };
140 }
141
142 function mainDOMThenHTTP(sources: any) {
143 const sinks$ = xs
144 .periodic(100)
145 .take(6)
146 .map(i => {
147 if (i % 2 === 1) {
148 return child(sources, i);
149 } else {
150 return {
151 HTTP: xs.empty(),
152 DOM: xs.of(''),
153 };
154 }
155 });
156
157 // order of sinks is important to reproduce the bug
158 return {
159 DOM: sinks$.map(sinks => sinks.DOM).flatten(),
160 HTTP: sinks$.map(sinks => sinks.HTTP).flatten(),
161 };
162 }
163
164 let requestsSent = 0;
165 const expectedDOMSinks = [
166 /* HTTP then DOM: */ '',
167 'My name is Louis',
168 '',
169 '',
170 /* DOM then HTTP: */ '',
171 'My name is Louis',
172 '',
173 '',
174 ];
175
176 function domDriver(sink: Stream<string>) {
177 sink.addListener({
178 next: s => {
179 assert.strictEqual(s, expectedDOMSinks.shift());
180 },
181 error: (err: any) => {},
182 });
183 }
184
185 function httpDriver(sink: Stream<any>) {
186 const source = sink.map(req => ({body: {name: 'Louis'}}));
187 source.addListener({
188 next: x => {},
189 error: (err: any) => {},
190 });
191 return source.debug(x => {
192 requestsSent += 1;
193 });
194 }
195
196 // HTTP then DOM:
197 const dispose = run(mainHTTPThenDOM, {
198 HTTP: httpDriver,
199 DOM: domDriver,
200 });
201 setTimeout(() => {
202 assert.strictEqual(expectedDOMSinks.length, 4);
203 assert.strictEqual(requestsSent, 1);
204 dispose();
205
206 // DOM then HTTP:
207 run(mainDOMThenHTTP, {
208 HTTP: httpDriver,
209 DOM: domDriver,
210 });
211 setTimeout(() => {
212 assert.strictEqual(expectedDOMSinks.length, 0);
213 assert.strictEqual(requestsSent, 2);
214 done();
215 }, 1000);
216 }, 1000);
217 });
218
219 it('should report errors from main() in the console', function(done) {
220 const sandbox = sinon.createSandbox();
221 sandbox.stub(console, 'error');
222
223 function main(sources: any): any {
224 return {
225 other: sources.other
226 .take(1)
227 .startWith('a')
228 .map(() => {
229 throw new Error('malfunction');
230 }),
231 };
232 }
233 function driver(sink: Stream<any>) {
234 sink.addListener({
235 next: () => {},
236 error: (err: any) => {},
237 });
238 return xs.of('b');
239 }
240
241 let caught = false;
242 try {
243 run(main, {other: driver});
244 } catch (e) {
245 caught = true;
246 }
247 setTimeout(() => {
248 sinon.assert.calledOnce(console.error as any);
249 sinon.assert.calledWithExactly(
250 console.error as any,
251 sinon.match((err: any) => err.message === 'malfunction')
252 );
253
254 // Should be false because the error was already reported in the console.
255 // Otherwise we would have double reporting of the error.
256 assert.strictEqual(caught, false);
257
258 sandbox.restore();
259 done();
260 }, 80);
261 });
262
263 it('should call DevTool internal function to pass sinks', function() {
264 let window: any;
265 if (typeof global === 'object') {
266 (global as any).window = {};
267 window = (global as any).window;
268 }
269 const sandbox = sinon.createSandbox();
270 const spy = sandbox.spy();
271 window.CyclejsDevTool_startGraphSerializer = spy;
272
273 function app(ext: any): any {
274 return {
275 other: ext.other.take(1).startWith('a'),
276 };
277 }
278 function driver() {
279 return xs.of('b');
280 }
281 run(app, {other: driver});
282
283 sinon.assert.calledOnce(spy);
284 });
285
286 it('should adapt() a simple source (stream)', function(done) {
287 let appCalled = false;
288 function app(sources: any): any {
289 assert.strictEqual(typeof sources.other, 'string');
290 assert.strictEqual(sources.other, 'this is adapted');
291 appCalled = true;
292
293 return {
294 other: xs.of(1, 2, 3),
295 };
296 }
297
298 function driver(sink: Stream<string>) {
299 return xs.of(10, 20, 30);
300 }
301
302 setAdapt(stream => 'this is adapted');
303 run(app, {other: driver});
304 setAdapt(x => x);
305
306 assert.strictEqual(appCalled, true);
307 done();
308 });
309
310 it('should support sink-only drivers', function(done) {
311 function app(sources: any): any {
312 return {
313 other: xs.of(1, 2, 3),
314 };
315 }
316
317 let driverCalled = false;
318 function driver(sink: Stream<string>) {
319 assert.strictEqual(typeof sink, 'object');
320 assert.strictEqual(typeof sink.fold, 'function');
321 driverCalled = true;
322 }
323
324 run(app, {other: driver});
325
326 assert.strictEqual(driverCalled, true);
327 done();
328 });
329
330 it('should not adapt() sinks', function(done) {
331 function app(sources: any): any {
332 return {
333 other: xs.of(1, 2, 3),
334 };
335 }
336
337 let driverCalled = false;
338 function driver(sink: Stream<string>) {
339 assert.strictEqual(typeof sink, 'object');
340 assert.strictEqual(typeof sink.fold, 'function');
341 driverCalled = true;
342 return xs.of(10, 20, 30);
343 }
344
345 setAdapt(stream => 'this not a stream');
346 run(app, {other: driver});
347 setAdapt(x => x);
348
349 assert.strictEqual(driverCalled, true);
350 done();
351 });
352});