@jpmorganchase/mosaic-core 0.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ import PullDocs from '../PullDocs';
2
+ import SourceManager from '../SourceManager';
3
+ import UnionVolume from '../filesystems/UnionVolume';
4
+ import MutableVolume from '../filesystems/MutableVolume';
5
+
6
+ jest.mock('../SourceManager');
7
+
8
+ describe('GIVEN PullDocs', () => {
9
+ test('THEN it should instantiate correctly', () => {
10
+ expect(new PullDocs({ sources: [], plugins: [] })).toBeDefined();
11
+ });
12
+
13
+ test('THEN it should have an onSourceUpdate method', () => {
14
+ expect(new PullDocs({ sources: [], plugins: [] })).toHaveProperty('onSourceUpdate');
15
+ });
16
+
17
+ describe('WITH plugins specified', () => {
18
+ let pullDocs;
19
+
20
+ beforeEach(() => {
21
+ SourceManager.mockReset();
22
+ pullDocs = new PullDocs({
23
+ sources: [{ modulePath: 'source', name: 'name' }],
24
+ plugins: [{ modulePath: 'plugin', filter: /[a-z]$/ }]
25
+ });
26
+ });
27
+
28
+ test('THEN the SourceManager should be passed the plugins', () => {
29
+ expect(SourceManager).toHaveBeenCalledWith(
30
+ [
31
+ {
32
+ modulePath: expect.stringMatching(/\/packages\/plugins\/dist\/\$CodeModPlugin.js/),
33
+ options: {},
34
+ priority: Infinity
35
+ },
36
+ {
37
+ filter: /[a-z]$/,
38
+ modulePath: 'plugin'
39
+ },
40
+ {
41
+ modulePath: expect.stringMatching(/\/packages\/plugins\/dist\/\$TagPlugin.js/),
42
+ options: {}
43
+ },
44
+ {
45
+ modulePath: expect.stringMatching(/\/packages\/plugins\/dist\/\$AliasPlugin.js/),
46
+ options: {},
47
+ priority: -1
48
+ },
49
+ {
50
+ modulePath: expect.stringMatching(/\/packages\/plugins\/dist\/\$RefPlugin.js/),
51
+ options: {},
52
+ priority: -1
53
+ }
54
+ ],
55
+ [
56
+ {
57
+ filter: /\.json$/,
58
+ modulePath: expect.stringMatching(/\/packages\/serialisers\/dist\/json.js/),
59
+ options: {}
60
+ }
61
+ ],
62
+ ['.mdx'],
63
+ [],
64
+ expect.any(UnionVolume),
65
+ expect.any(MutableVolume)
66
+ );
67
+ });
68
+
69
+ describe('WITH sources specified', () => {
70
+ let pullDocs: PullDocs;
71
+
72
+ const source1 = { name: 'source', modulePath: 'source-module' };
73
+ const source2 = { name: 'source2', modulePath: 'source2-module' };
74
+
75
+ beforeEach(() => {
76
+ pullDocs = new PullDocs({
77
+ sources: [source1, source2],
78
+ plugins: []
79
+ });
80
+ });
81
+
82
+ describe('AND a source changes', () => {
83
+ test('THEN onSourceUpdate should be called', () => {
84
+ const onSourceUpdateSpy = jest.fn();
85
+ pullDocs.onSourceUpdate(onSourceUpdateSpy);
86
+
87
+ const onSourceUpdateCallback = (
88
+ SourceManager.prototype.onSourceUpdate as jest.MockedFunction<any>
89
+ ).mock.calls[0][0];
90
+ onSourceUpdateCallback();
91
+ expect(onSourceUpdateSpy).toHaveBeenCalled();
92
+ });
93
+ });
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,341 @@
1
+ import EventEmitter from 'events';
2
+
3
+ import WorkerSubscription, { EVENT } from '../WorkerSubscription';
4
+ import Source from '../Source';
5
+
6
+ jest.mock('../WorkerSubscription');
7
+ jest.mock('plugin', () => ({}), { virtual: true });
8
+
9
+ const utf8Encoder = new TextEncoder();
10
+
11
+ describe('GIVEN Source', () => {
12
+ test('THEN it should instantiate correctly', () => {
13
+ expect(
14
+ new Source({
15
+ modulePath: 'plugin',
16
+ namespace: 'test-namespace'
17
+ })
18
+ ).toBeDefined();
19
+ });
20
+
21
+ test('THEN sources should get a unique id', () => {
22
+ expect(
23
+ typeof new Source({
24
+ modulePath: 'plugin',
25
+ namespace: 'test-namespace'
26
+ }).id
27
+ ).toEqual('symbol');
28
+ expect(
29
+ new Source({
30
+ modulePath: 'plugin',
31
+ namespace: 'test-namespace'
32
+ }).id
33
+ ).not.toBe(
34
+ new Source({
35
+ modulePath: 'plugin',
36
+ namespace: 'test-namespace'
37
+ }).id
38
+ );
39
+ });
40
+
41
+ test('THEN it should have a use method', () => {
42
+ expect(
43
+ new Source({
44
+ modulePath: 'plugin',
45
+ namespace: 'test-namespace'
46
+ })
47
+ ).toHaveProperty('use');
48
+ });
49
+
50
+ test('THEN it should have an onStart method', () => {
51
+ expect(
52
+ new Source({
53
+ modulePath: 'plugin',
54
+ namespace: 'test-namespace'
55
+ })
56
+ ).toHaveProperty('onStart');
57
+ });
58
+
59
+ test('THEN it should have a start method', () => {
60
+ expect(
61
+ new Source({
62
+ modulePath: 'plugin',
63
+ namespace: 'test-namespace'
64
+ })
65
+ ).toHaveProperty('start');
66
+ });
67
+
68
+ test('THEN it should have a stop method', () => {
69
+ expect(
70
+ new Source({
71
+ modulePath: 'plugin',
72
+ namespace: 'test-namespace'
73
+ })
74
+ ).toHaveProperty('stop');
75
+ });
76
+
77
+ test('THEN it should have a onUpdate method', () => {
78
+ expect(
79
+ new Source({
80
+ modulePath: 'plugin',
81
+ namespace: 'test-namespace'
82
+ })
83
+ ).toHaveProperty('onUpdate');
84
+ });
85
+
86
+ test('THEN it should have a onError method', () => {
87
+ expect(
88
+ new Source({
89
+ modulePath: 'plugin',
90
+ namespace: 'test-namespace'
91
+ })
92
+ ).toHaveProperty('onError');
93
+ });
94
+
95
+ describe('WHEN calling `use`', () => {
96
+ let source: Source;
97
+
98
+ beforeEach(async () => {
99
+ source = new Source({
100
+ modulePath: 'plugin',
101
+ namespace: 'test-namespace'
102
+ });
103
+
104
+ source.use([{ modulePath: 'plugin', filter: /$/, options: { plugin: true } }]);
105
+ await source.start();
106
+ });
107
+
108
+ test('THEN the plugins should be passed to the worker', async () => {
109
+ expect(WorkerSubscription).toHaveBeenCalledWith({
110
+ ignorePages: undefined,
111
+ plugins: [{ modulePath: 'plugin', options: { plugin: true }, filter: /$/ }],
112
+ modulePath: 'plugin',
113
+ name: expect.stringMatching(/#5e543256/),
114
+ namespace: 'test-namespace',
115
+ options: undefined,
116
+ pageExtensions: undefined,
117
+ serialisers: []
118
+ });
119
+ });
120
+ });
121
+
122
+ describe('WHEN calling `start`', () => {
123
+ let source: Source;
124
+ beforeEach(() => {
125
+ source = new Source(
126
+ {
127
+ modulePath: 'plugin',
128
+ namespace: 'test-namespace'
129
+ },
130
+ { primary: true }
131
+ );
132
+ source.use([]);
133
+ (WorkerSubscription as jest.MockedFunction<any>).mockClear();
134
+ });
135
+
136
+ test('THEN the child worker should be created', async () => {
137
+ await source.start();
138
+ expect(WorkerSubscription).toHaveBeenCalledWith({
139
+ ignorePages: undefined,
140
+ modulePath: 'plugin',
141
+ namespace: 'test-namespace',
142
+ name: expect.stringMatching(/#c7b08e60/),
143
+ options: { primary: true },
144
+ pageExtensions: undefined,
145
+ plugins: [],
146
+ serialisers: []
147
+ });
148
+ });
149
+ });
150
+
151
+ describe('WHEN calling `stop`', () => {
152
+ let source;
153
+ let workerSubscriptionSingletonMock;
154
+
155
+ afterEach(() => {
156
+ (WorkerSubscription as jest.MockedFunction<any>).mockRestore();
157
+ });
158
+ beforeEach(async () => {
159
+ workerSubscriptionSingletonMock = {
160
+ stop: jest.fn(),
161
+ closed: false,
162
+ on() {},
163
+ once() {}
164
+ };
165
+ (WorkerSubscription as jest.MockedFunction<any>).mockImplementation(
166
+ () => workerSubscriptionSingletonMock
167
+ );
168
+ source = new Source({
169
+ modulePath: 'plugin',
170
+ namespace: 'test-namespace'
171
+ });
172
+ await source.start();
173
+ });
174
+ test('THEN the worker should throw if the source has not started', () => {
175
+ source.stop();
176
+ expect(workerSubscriptionSingletonMock.stop).toHaveBeenCalled();
177
+ workerSubscriptionSingletonMock.closed = true;
178
+ expect(() => source.stop()).toThrow(new Error('Cannot stop source that has not started'));
179
+ expect(workerSubscriptionSingletonMock.stop).toHaveBeenCalledTimes(1);
180
+ });
181
+ test('THEN the worker should be closed', () => {
182
+ source.stop();
183
+ expect(workerSubscriptionSingletonMock.stop).toHaveBeenCalled();
184
+ });
185
+ });
186
+
187
+ describe('WHEN events fire', () => {
188
+ const workerHandlers: {
189
+ exit: (code: number) => void;
190
+ start: () => void;
191
+ message: (data) => void;
192
+ error: (error: Error) => void;
193
+ } = {
194
+ exit: null,
195
+ start: null,
196
+ message: null,
197
+ error: null
198
+ };
199
+ let source;
200
+
201
+ afterEach(() => {
202
+ (WorkerSubscription.prototype.on as jest.MockedFunction<any>).mockRestore();
203
+ (WorkerSubscription.prototype.once as jest.MockedFunction<any>).mockRestore();
204
+ });
205
+ beforeEach(async () => {
206
+ const eventMap = {
207
+ [EVENT.UPDATE]: 'message',
208
+ [EVENT.EXIT]: 'exit',
209
+ [EVENT.ERROR]: 'error',
210
+ [EVENT.START]: 'start'
211
+ };
212
+ jest
213
+ .spyOn(WorkerSubscription.prototype, 'on')
214
+ .mockImplementation(function on(event: EVENT, handler) {
215
+ workerHandlers[eventMap[event]] = handler;
216
+ return this as EventEmitter;
217
+ } as any);
218
+ jest
219
+ .spyOn(WorkerSubscription.prototype, 'once')
220
+ .mockImplementation(function on(event: EVENT, handler) {
221
+ workerHandlers[eventMap[event]] = handler;
222
+ return this as EventEmitter;
223
+ } as any);
224
+
225
+ source = new Source({
226
+ modulePath: 'plugin',
227
+ namespace: 'test-namespace'
228
+ });
229
+ source.filesystem.reset = jest.fn();
230
+
231
+ await source.start();
232
+ });
233
+
234
+ describe('AND exceptions occur', () => {
235
+ test('THEN the worker should clear the filesystem on exit', () => {
236
+ workerHandlers.exit(1);
237
+ expect(source.filesystem).toEqual(null);
238
+ });
239
+ test('THEN the worker should remove listeners on exit', () => {
240
+ jest.spyOn(EventEmitter.prototype, 'removeAllListeners');
241
+ workerHandlers.exit(1);
242
+ expect(EventEmitter.prototype.removeAllListeners).toHaveBeenCalled();
243
+ });
244
+ test('THEN start handler should only fire once', () => {
245
+ const startSpy = jest.fn();
246
+ source.onStart(startSpy);
247
+ workerHandlers.start();
248
+ workerHandlers.start();
249
+ expect(startSpy).toHaveBeenCalledTimes(1);
250
+ });
251
+ test('THEN exit handler should only fire once', () => {
252
+ const exitSpy = jest.fn();
253
+ source.onExit(exitSpy);
254
+ workerHandlers.exit(0);
255
+ workerHandlers.exit(0);
256
+ expect(exitSpy).toHaveBeenCalledTimes(1);
257
+ });
258
+ test('THEN error handler should only fire once', () => {
259
+ const errorSpy = jest.fn();
260
+ source.onError(errorSpy);
261
+ workerHandlers.error(new Error('an exception that should only trigger an event once'));
262
+ workerHandlers.error(new Error('an exception that should only trigger an event once'));
263
+ expect(errorSpy).toHaveBeenCalledTimes(1);
264
+ });
265
+ });
266
+
267
+ describe('AND event handlers are attached', () => {
268
+ test('THEN the update handler should return a cleanup function', async () => {
269
+ const updateSpy = jest.fn();
270
+ const cleanup = source.onUpdate(updateSpy);
271
+ cleanup();
272
+ workerHandlers.message({ data: utf8Encoder.encode("{ key : 'test' }"), type: 'message' });
273
+ await new Promise(resolve => setTimeout(resolve));
274
+ expect(updateSpy).not.toHaveBeenCalled();
275
+ });
276
+ test('THEN the error handler should return a cleanup function', async () => {
277
+ const errorSpy = jest.fn();
278
+ const cleanup = source.onError(errorSpy);
279
+ cleanup();
280
+ workerHandlers.error(new Error('something broke'));
281
+ await new Promise(resolve => setTimeout(resolve));
282
+ expect(errorSpy).not.toHaveBeenCalled();
283
+ });
284
+ test('THEN the update handler should be fired on a message', async () => {
285
+ const updateSpy = jest.fn();
286
+ source.onUpdate(updateSpy);
287
+ workerHandlers.message({
288
+ data: { content: ['test'], meta: {}, data: { testValue: 'someValue' } },
289
+ type: 'message'
290
+ });
291
+ await new Promise(resolve => setTimeout(resolve));
292
+ expect(updateSpy).toHaveBeenCalledWith({
293
+ data: { testValue: 'someValue' },
294
+ pages: undefined,
295
+ symlinks: undefined
296
+ });
297
+ });
298
+
299
+ test('THEN the exit handler should unsubscribe all other handlers', async () => {
300
+ const exitSpy = jest.fn();
301
+ const errorSpy = jest.fn();
302
+ const updateSpy = jest.fn();
303
+ const startSpy = jest.fn();
304
+ source.onError(errorSpy);
305
+ source.onExit(exitSpy);
306
+ source.onStart(startSpy);
307
+ source.onUpdate(updateSpy);
308
+ workerHandlers.exit(0);
309
+ workerHandlers.error(new Error('i should never be handled'));
310
+ workerHandlers.message('i should be ignored');
311
+ workerHandlers.start();
312
+ expect(exitSpy).toHaveBeenCalledTimes(1);
313
+ expect(errorSpy).not.toHaveBeenCalled();
314
+ expect(startSpy).not.toHaveBeenCalled();
315
+ expect(updateSpy).not.toHaveBeenCalled();
316
+ });
317
+
318
+ test('THEN the start handler should be fired when a worker received a start event', () => {
319
+ const startSpy = jest.fn();
320
+ source.onStart(startSpy);
321
+ workerHandlers.start();
322
+ expect(startSpy).toHaveBeenCalledTimes(1);
323
+ });
324
+
325
+ test('THEN the exit handler should be fired when the exit event emits', () => {
326
+ const exitSpy = jest.fn();
327
+ source.onExit(exitSpy);
328
+ workerHandlers.exit(1);
329
+ expect(exitSpy).toHaveBeenCalledTimes(1);
330
+ });
331
+
332
+ test('THEN the error handler should be fired on an error', () => {
333
+ const error = new Error('something broke');
334
+ const errorSpy = jest.fn();
335
+ source.onError(errorSpy);
336
+ workerHandlers.error(error);
337
+ expect(errorSpy).toHaveBeenCalledWith(error);
338
+ });
339
+ });
340
+ });
341
+ });
@@ -0,0 +1,255 @@
1
+ import debounce from 'lodash/debounce';
2
+
3
+ import SourceManager from '../SourceManager';
4
+ import Source from '../Source';
5
+
6
+ jest.mock('../Source');
7
+ jest.mock('lodash/debounce');
8
+ // jest.mock('../worker/helpers/plugins', () => async (plugins, _fs) => {
9
+ // return async (_, input) => input;
10
+ // });
11
+
12
+ function scheduleOnUpdateCallback(message) {
13
+ setTimeout(() => {
14
+ const onUpdateCallback = getLastUpdateCallback();
15
+ onUpdateCallback(message);
16
+ });
17
+ }
18
+ function scheduleOnStartCallback() {
19
+ setTimeout(() => {
20
+ const onStartCallback = getOnStartCallback();
21
+ onStartCallback();
22
+ });
23
+ }
24
+
25
+ function getOnStartCallback() {
26
+ const onStartCallbackCalls = (Source.prototype.onStart as jest.MockedFunction<any>).mock.calls;
27
+ return onStartCallbackCalls[onStartCallbackCalls.length - 1][0];
28
+ }
29
+
30
+ function getLastUpdateCallback() {
31
+ const onUpdateCallbackCalls = (Source.prototype.onUpdate as jest.MockedFunction<any>).mock.calls;
32
+ return onUpdateCallbackCalls[onUpdateCallbackCalls.length - 1][0];
33
+ }
34
+
35
+ describe('GIVEN SourceManager', () => {
36
+ beforeEach(() => {
37
+ (debounce as jest.MockedFunction<any>).mockImplementation(
38
+ callback =>
39
+ (...args) =>
40
+ setTimeout(() => callback(...args))
41
+ );
42
+ });
43
+ test('THEN it should instantiate correctly', () => {
44
+ expect(new SourceManager([])).toBeDefined();
45
+ });
46
+
47
+ test('THEN it should have an addSource method', () => {
48
+ expect(new SourceManager([])).toHaveProperty('addSource');
49
+ });
50
+
51
+ test('THEN it should have an getSource method', () => {
52
+ expect(new SourceManager([])).toHaveProperty('getSource');
53
+ });
54
+
55
+ test('THEN it should have an destroyAll method', () => {
56
+ expect(new SourceManager([])).toHaveProperty('destroyAll');
57
+ });
58
+
59
+ describe('WHEN destroying a source', () => {
60
+ let sourceManager: SourceManager;
61
+
62
+ beforeEach(() => {
63
+ jest.spyOn(Source.prototype, 'onExit').mockReturnValue(jest.fn());
64
+ jest.spyOn(Source.prototype, 'onError').mockReturnValue(jest.fn());
65
+ jest.spyOn(Source.prototype, 'onUpdate').mockReturnValue(jest.fn());
66
+ jest.spyOn(Source.prototype, 'onStart').mockReturnValue(jest.fn());
67
+ jest.spyOn(Source.prototype, 'stop').mockReturnValue(jest.fn());
68
+
69
+ sourceManager = new SourceManager([]);
70
+ });
71
+
72
+ afterEach(() => {
73
+ (Source.prototype.stop as jest.MockedFunction<any>).mockReset();
74
+ (Source.prototype.onStart as jest.MockedFunction<any>).mockReset();
75
+ });
76
+
77
+ test('THEN calling `destroyAll` should stop all sources', async () => {
78
+ (Source.prototype as jest.MockedFunction<any>).id = '1';
79
+ scheduleOnStartCallback();
80
+ await sourceManager.addSource({ modulePath: '1', name: '1' }, {});
81
+ (Source.prototype as jest.MockedFunction<any>).id = '2';
82
+ scheduleOnStartCallback();
83
+ await sourceManager.addSource({ modulePath: '2', name: '2' }, {});
84
+ (Source.prototype as jest.MockedFunction<any>).id = '3';
85
+ scheduleOnStartCallback();
86
+ await sourceManager.addSource({ modulePath: '3', name: '3' }, {});
87
+ sourceManager.destroyAll();
88
+ expect(Source.prototype.stop).toHaveBeenCalledTimes(3);
89
+ });
90
+ });
91
+
92
+ describe('WHEN adding a source with plugins', () => {
93
+ let sourceManager: SourceManager;
94
+ beforeEach(async () => {
95
+ Source.prototype.constructorSpy.mockReset();
96
+ Source.prototype.use.mockReset();
97
+ sourceManager = new SourceManager([{ options: { plugins: true } }]);
98
+ scheduleOnStartCallback();
99
+ await sourceManager.addSource({ name: 'source', modulePath: 'source-module' }, {});
100
+ });
101
+ test('THEN the plugins should be used', () => {
102
+ expect(Source.prototype.use.mock.calls[0][0]).toEqual([{ options: { plugins: true } }]);
103
+ });
104
+ test('THEN the sources should be created for the new sources', async () => {
105
+ scheduleOnStartCallback();
106
+ await sourceManager.addSource(
107
+ { name: 'source2', modulePath: 'source-module2', options: { a: true } },
108
+ {}
109
+ );
110
+ expect(Source.prototype.constructorSpy.mock.calls[0][0]).toEqual({
111
+ name: 'source',
112
+ modulePath: 'source-module'
113
+ });
114
+ expect(Source.prototype.constructorSpy.mock.calls[1][0]).toEqual({
115
+ name: 'source2',
116
+ modulePath: 'source-module2',
117
+ options: { a: true }
118
+ });
119
+ });
120
+ });
121
+
122
+ describe('WHEN adding a source', () => {
123
+ let sourceManager: SourceManager;
124
+
125
+ afterEach(() => {});
126
+ beforeEach(() => {
127
+ Source.prototype.constructorSpy.mockReset();
128
+ sourceManager = new SourceManager([]);
129
+ jest.spyOn(Source.prototype, 'onExit').mockReturnValue(jest.fn());
130
+ jest.spyOn(Source.prototype, 'onError').mockReturnValue(jest.fn());
131
+ jest.spyOn(Source.prototype, 'onUpdate').mockReturnValue(jest.fn());
132
+ jest.spyOn(Source.prototype, 'onStart').mockReturnValue(jest.fn());
133
+ jest.spyOn(Source.prototype, 'stop').mockClear();
134
+ });
135
+ describe('AND the source changes', () => {
136
+ describe("AND the source hasn't initialised yet", () => {
137
+ test('THEN `onSourceUpdate` should not be called and an error should be thrown', async () => {
138
+ const onSourceUpdateSpy = jest.fn();
139
+ sourceManager.onSourceUpdate(onSourceUpdateSpy);
140
+ const sourceCreatorPromise = sourceManager.addSource(
141
+ { name: 'source', modulePath: 'source-module' },
142
+ {}
143
+ );
144
+ const onUpdateCallbackCalls = (Source.prototype.onUpdate as jest.MockedFunction<any>).mock
145
+ .calls;
146
+ const latestOnUpdateCallback = onUpdateCallbackCalls[onUpdateCallbackCalls.length - 1][0];
147
+ expect(latestOnUpdateCallback).toBeDefined();
148
+ latestOnUpdateCallback('message');
149
+
150
+ expect(() => sourceCreatorPromise).rejects.toThrow('message');
151
+ });
152
+ });
153
+
154
+ test('THEN `onSourceUpdate` should be called', async () => {
155
+ const onSourceUpdateSpy = jest.fn();
156
+ sourceManager.onSourceUpdate(onSourceUpdateSpy);
157
+
158
+ scheduleOnUpdateCallback('message');
159
+
160
+ await expect(() =>
161
+ sourceManager.addSource({ name: 'source2', modulePath: 'source2-module' }, {})
162
+ ).rejects.toThrow(
163
+ new Error("Source 'source2' received a message before it was initialised.")
164
+ );
165
+ });
166
+
167
+ test('THEN `onSourceUpdate` should be called with a filesystem', async () => {
168
+ const onSourceUpdateSpy = jest.fn();
169
+ sourceManager.onSourceUpdate(onSourceUpdateSpy);
170
+ scheduleOnStartCallback();
171
+ await sourceManager.addSource({ name: 'source', modulePath: 'source-module' }, {});
172
+
173
+ const latestOnUpdateCallback = getLastUpdateCallback();
174
+ expect(latestOnUpdateCallback).toBeDefined();
175
+ latestOnUpdateCallback(['message1', 'message1.2']);
176
+
177
+ scheduleOnStartCallback();
178
+ await sourceManager.addSource({ name: 'source', modulePath: 'source-module' }, {});
179
+
180
+ await new Promise(resolve => setTimeout(resolve));
181
+ expect(onSourceUpdateSpy.mock.calls[0][1].filesystem).toBeDefined();
182
+ });
183
+ });
184
+
185
+ test('THEN the source should be available via `getSource`', async () => {
186
+ scheduleOnStartCallback();
187
+ const source1 = await sourceManager.addSource(
188
+ { name: 'source', modulePath: 'source-module' },
189
+ {}
190
+ );
191
+ scheduleOnStartCallback();
192
+ await sourceManager.addSource({ name: 'source2', modulePath: 'source2-module' }, {});
193
+ expect(sourceManager.getSource(source1.id)).toBeDefined();
194
+ });
195
+
196
+ test('THEN the sources should be created for the new sources', async () => {
197
+ scheduleOnStartCallback();
198
+ await sourceManager.addSource(
199
+ { name: 'source', modulePath: 'source-module', options: { a: true } },
200
+ {}
201
+ );
202
+ scheduleOnStartCallback();
203
+ await sourceManager.addSource(
204
+ { name: 'source2', modulePath: 'source2-module', options: { b: true } },
205
+ {}
206
+ );
207
+
208
+ expect(Source.prototype.constructorSpy.mock.calls[0][0]).toEqual({
209
+ name: 'source',
210
+ modulePath: 'source-module',
211
+ options: { a: true }
212
+ });
213
+ expect(Source.prototype.constructorSpy.mock.calls[1][0]).toEqual({
214
+ name: 'source2',
215
+ modulePath: 'source2-module',
216
+ options: { b: true }
217
+ });
218
+ });
219
+
220
+ describe('AND an error is thrown or an early exit occurs', () => {
221
+ beforeEach(() => {
222
+ jest.spyOn(Source.prototype, 'onError').mockImplementation(() => jest.fn());
223
+ });
224
+ test('THEN an error should be reported for any early exit, as a rejected promise', async () => {
225
+ setTimeout(() => {
226
+ const onExitCallbackCalls = (Source.prototype.onExit as jest.MockedFunction<any>).mock
227
+ .calls;
228
+ const latestOnExitCallback = onExitCallbackCalls[onExitCallbackCalls.length - 1][0];
229
+ latestOnExitCallback();
230
+ });
231
+ await expect(() =>
232
+ sourceManager.addSource({ name: 'source', modulePath: 'source-module' }, {})
233
+ ).rejects.toThrow(new Error("Source 'source' silently exited before initialising."));
234
+ });
235
+ test('THEN the error should be reported in a rejected promise', async () => {
236
+ setTimeout(() => {
237
+ const onErrorCallbackCalls = (Source.prototype.onError as jest.MockedFunction<any>).mock
238
+ .calls;
239
+ const latestOnErrorCallback = onErrorCallbackCalls[onErrorCallbackCalls.length - 1][0];
240
+ latestOnErrorCallback(new Error('some error'));
241
+ });
242
+ await expect(() =>
243
+ sourceManager.addSource({ name: 'source', modulePath: 'source-module' }, {})
244
+ ).rejects.toThrow(new Error('some error'));
245
+ });
246
+ });
247
+
248
+ test('THEN it should call `start` on the Source', async () => {
249
+ jest.spyOn(Source.prototype, 'start');
250
+ scheduleOnStartCallback();
251
+ await sourceManager.addSource({ name: 'source', modulePath: 'source-module' }, {});
252
+ expect(Source.prototype.start).toHaveBeenCalled();
253
+ });
254
+ });
255
+ });