1 | import * as assert from 'assert';
|
2 | import * as sinon from 'sinon';
|
3 | import {run} from '../src/index';
|
4 | import {setAdapt} from '../src/adapt';
|
5 | import xs, {Stream} from 'xstream';
|
6 |
|
7 | describe('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 |
|
102 |
|
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 |
|
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 |
|
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 | '',
|
167 | 'My name is Louis',
|
168 | '',
|
169 | '',
|
170 | '',
|
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 |
|
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 |
|
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 |
|
255 |
|
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 | });
|