Browse Source

CommandsObserver

Daniel Zlotin 6 years ago
parent
commit
1a6dd8b06e

+ 9
- 4
lib/src/Navigation.ts View File

@@ -9,7 +9,8 @@ import { LayoutTreeCrawler } from './commands/LayoutTreeCrawler';
9 9
 import { EventsRegistry } from './events/EventsRegistry';
10 10
 import { ComponentProvider } from 'react-native';
11 11
 import { Element } from './adapters/Element';
12
-import { ComponentEventsRegistry } from './events/ComponentEventsRegistry';
12
+import { ComponentEventsObserver } from './events/ComponentEventsObserver';
13
+import { CommandsObserver } from './events/CommandsObserver';
13 14
 
14 15
 export class Navigation {
15 16
   public readonly Element;
@@ -23,6 +24,8 @@ export class Navigation {
23 24
   private readonly nativeCommandsSender;
24 25
   private readonly commands;
25 26
   private readonly eventsRegistry;
27
+  private readonly commandsObserver;
28
+  private readonly componentEventsObserver;
26 29
 
27 30
   constructor() {
28 31
     this.Element = Element;
@@ -34,10 +37,12 @@ export class Navigation {
34 37
     this.layoutTreeParser = new LayoutTreeParser();
35 38
     this.layoutTreeCrawler = new LayoutTreeCrawler(this.uniqueIdProvider, this.store);
36 39
     this.nativeCommandsSender = new NativeCommandsSender();
37
-    this.commands = new Commands(this.nativeCommandsSender, this.layoutTreeParser, this.layoutTreeCrawler);
38
-    this.eventsRegistry = new EventsRegistry(this.nativeEventsReceiver);
40
+    this.commandsObserver = new CommandsObserver();
41
+    this.commands = new Commands(this.nativeCommandsSender, this.layoutTreeParser, this.layoutTreeCrawler, this.commandsObserver);
42
+    this.eventsRegistry = new EventsRegistry(this.nativeEventsReceiver, this.commandsObserver);
43
+    this.componentEventsObserver = new ComponentEventsObserver(this.eventsRegistry, this.store);
39 44
 
40
-    new ComponentEventsRegistry(this.eventsRegistry, this.store).registerForAllComponents();
45
+    this.componentEventsObserver.registerForAllComponents();
41 46
   }
42 47
 
43 48
   /**

+ 1
- 4
lib/src/adapters/NativeEventsReceiver.ts View File

@@ -1,8 +1,5 @@
1 1
 import { NativeModules, NativeEventEmitter } from 'react-native';
2
-
3
-export interface EventSubscription {
4
-  remove();
5
-}
2
+import { EventSubscription } from '../events/EventsRegistry';
6 3
 
7 4
 export class NativeEventsReceiver {
8 5
   private emitter: NativeEventEmitter;

+ 69
- 6
lib/src/commands/Commands.test.ts View File

@@ -1,20 +1,29 @@
1
+import * as _ from 'lodash';
1 2
 import { LayoutTreeParser } from './LayoutTreeParser';
2 3
 import { LayoutTreeCrawler } from './LayoutTreeCrawler';
3 4
 import { Store } from '../components/Store';
4 5
 import { UniqueIdProvider } from '../adapters/UniqueIdProvider.mock';
5 6
 import { NativeCommandsSender } from '../adapters/NativeCommandsSender.mock';
6 7
 import { Commands } from './Commands';
8
+import { CommandsObserver } from '../events/CommandsObserver';
7 9
 
8 10
 describe('Commands', () => {
9
-  let uut;
11
+  let uut: Commands;
10 12
   let mockCommandsSender;
11 13
   let store;
14
+  let commandsObserver: CommandsObserver;
12 15
 
13 16
   beforeEach(() => {
14 17
     mockCommandsSender = new NativeCommandsSender();
15 18
     store = new Store();
16
-
17
-    uut = new Commands(mockCommandsSender, new LayoutTreeParser(), new LayoutTreeCrawler(new UniqueIdProvider(), store));
19
+    commandsObserver = new CommandsObserver();
20
+
21
+    uut = new Commands(
22
+      mockCommandsSender,
23
+      new LayoutTreeParser(),
24
+      new LayoutTreeCrawler(new UniqueIdProvider(), store),
25
+      commandsObserver
26
+    );
18 27
   });
19 28
 
20 29
   describe('setRoot', () => {
@@ -183,9 +192,9 @@ describe('Commands', () => {
183 192
 
184 193
   describe('pop', () => {
185 194
     it('pops a component, passing componentId', () => {
186
-      uut.pop('theComponentId');
195
+      uut.pop('theComponentId', {});
187 196
       expect(mockCommandsSender.pop).toHaveBeenCalledTimes(1);
188
-      expect(mockCommandsSender.pop).toHaveBeenCalledWith('theComponentId', undefined);
197
+      expect(mockCommandsSender.pop).toHaveBeenCalledWith('theComponentId', {});
189 198
     });
190 199
     it('pops a component, passing componentId and options', () => {
191 200
       const options = {
@@ -203,7 +212,7 @@ describe('Commands', () => {
203 212
 
204 213
     it('pop returns a promise that resolves to componentId', async () => {
205 214
       mockCommandsSender.pop.mockReturnValue(Promise.resolve('theComponentId'));
206
-      const result = await uut.pop('theComponentId');
215
+      const result = await uut.pop('theComponentId', {});
207 216
       expect(result).toEqual('theComponentId');
208 217
     });
209 218
   });
@@ -282,4 +291,58 @@ describe('Commands', () => {
282 291
       expect(mockCommandsSender.dismissOverlay).toHaveBeenCalledWith('Component1');
283 292
     });
284 293
   });
294
+
295
+  describe('notifies commandsObserver', () => {
296
+    let cb;
297
+
298
+    beforeEach(() => {
299
+      cb = jest.fn();
300
+      const mockParser = { parse: () => 'parsed' };
301
+      const mockCrawler = { crawl: (x) => x };
302
+      commandsObserver.register(cb);
303
+      uut = new Commands(mockCommandsSender, mockParser, mockCrawler, commandsObserver);
304
+    });
305
+
306
+    it('always call last', () => {
307
+      const nativeCommandsSenderFns = _.functions(mockCommandsSender);
308
+      expect(nativeCommandsSenderFns.length).toBeGreaterThan(1);
309
+
310
+      // throw when calling any native commands sender
311
+      _.forEach(nativeCommandsSenderFns, (fn) => {
312
+        mockCommandsSender[fn].mockImplementation(() => {
313
+          throw new Error(`throwing from mockNativeCommandsSender`);
314
+        });
315
+      });
316
+
317
+      // call all commands on uut, all should throw, no commandObservers called
318
+      const uutFns = Object.getOwnPropertyNames(Commands.prototype);
319
+      const methods = _.filter(uutFns, (fn) => fn !== 'constructor');
320
+      expect(methods.sort()).toEqual(nativeCommandsSenderFns.sort());
321
+
322
+      _.forEach(methods, (m) => {
323
+        expect(() => uut[m]()).toThrow();
324
+        expect(cb).not.toHaveBeenCalled();
325
+      });
326
+    });
327
+
328
+    it('setRoot', () => {
329
+      uut.setRoot({});
330
+      expect(cb).toHaveBeenCalledTimes(1);
331
+      expect(cb).toHaveBeenCalledWith('setRoot', { layout: 'parsed' });
332
+    });
333
+
334
+    it('setDefaultOptions', () => {
335
+      const options = { x: 1 };
336
+      uut.setDefaultOptions(options);
337
+      expect(cb).toHaveBeenCalledTimes(1);
338
+      expect(cb).toHaveBeenCalledWith('setDefaultOptions', { options });
339
+    });
340
+
341
+    xit('setOptions', () => {
342
+      const options = { x: 1 };
343
+      uut.setOptions('compId', options);
344
+      expect(cb).toHaveBeenCalledTimes(1);
345
+      expect(cb).toHaveBeenCalledWith('setOptions', { componentId: 'compId', options: {} });
346
+    });
347
+  });
285 348
 });

+ 27
- 20
lib/src/commands/Commands.ts View File

@@ -1,54 +1,61 @@
1 1
 import * as _ from 'lodash';
2 2
 import { OptionsProcessor } from './OptionsProcessor';
3
+import { CommandsObserver } from '../events/CommandsObserver';
3 4
 
4 5
 export class Commands {
5
-  private nativeCommandsSender;
6
-  private layoutTreeParser;
7
-  private layoutTreeCrawler;
8 6
   private optionsProcessor: OptionsProcessor;
9 7
 
10
-  constructor(nativeCommandsSender, layoutTreeParser, layoutTreeCrawler) {
11
-    this.nativeCommandsSender = nativeCommandsSender;
12
-    this.layoutTreeParser = layoutTreeParser;
13
-    this.layoutTreeCrawler = layoutTreeCrawler;
8
+  constructor(
9
+    private readonly nativeCommandsSender,
10
+    private readonly layoutTreeParser,
11
+    private readonly layoutTreeCrawler,
12
+    private readonly commandsObserver: CommandsObserver) {
14 13
     this.optionsProcessor = new OptionsProcessor(this.layoutTreeCrawler.store);
15 14
   }
16 15
 
17
-  setRoot(simpleApi) {
16
+  public setRoot(simpleApi) {
18 17
     const input = _.cloneDeep(simpleApi);
19 18
     const layout = this.layoutTreeParser.parse(input);
20 19
     this.layoutTreeCrawler.crawl(layout);
21
-    return this.nativeCommandsSender.setRoot(layout);
20
+
21
+    const result = this.nativeCommandsSender.setRoot(layout);
22
+    this.commandsObserver.notify('setRoot', { layout });
23
+
24
+    return result;
22 25
   }
23 26
 
24
-  setDefaultOptions(options) {
27
+  public setDefaultOptions(options) {
25 28
     const input = _.cloneDeep(options);
26 29
     this.optionsProcessor.processOptions(input);
30
+
27 31
     this.nativeCommandsSender.setDefaultOptions(input);
32
+    this.commandsObserver.notify('setDefaultOptions', { options });
28 33
   }
29 34
 
30
-  setOptions(componentId, options) {
35
+  public setOptions(componentId, options) {
31 36
     const input = _.cloneDeep(options);
32 37
     this.optionsProcessor.processOptions(input);
38
+
33 39
     this.nativeCommandsSender.setOptions(componentId, input);
40
+    this.commandsObserver.notify('setOptions', { componentId, options });
34 41
   }
35 42
 
36
-  showModal(simpleApi) {
43
+  public showModal(simpleApi) {
37 44
     const input = _.cloneDeep(simpleApi);
38 45
     const layout = this.layoutTreeParser.parse(input);
39 46
     this.layoutTreeCrawler.crawl(layout);
40 47
     return this.nativeCommandsSender.showModal(layout);
41 48
   }
42 49
 
43
-  dismissModal(id) {
50
+  public dismissModal(id) {
44 51
     return this.nativeCommandsSender.dismissModal(id);
45 52
   }
46 53
 
47
-  dismissAllModals() {
54
+  public dismissAllModals() {
48 55
     return this.nativeCommandsSender.dismissAllModals();
49 56
   }
50 57
 
51
-  push(onComponentId, componentData) {
58
+  public push(onComponentId, componentData) {
52 59
     const input = _.cloneDeep(componentData);
53 60
     this.optionsProcessor.processOptions(input);
54 61
     const layout = this.layoutTreeParser.parse(input);
@@ -56,19 +63,19 @@ export class Commands {
56 63
     return this.nativeCommandsSender.push(onComponentId, layout);
57 64
   }
58 65
 
59
-  pop(componentId, options) {
66
+  public pop(componentId, options) {
60 67
     return this.nativeCommandsSender.pop(componentId, options);
61 68
   }
62 69
 
63
-  popTo(componentId) {
70
+  public popTo(componentId) {
64 71
     return this.nativeCommandsSender.popTo(componentId);
65 72
   }
66 73
 
67
-  popToRoot(componentId) {
74
+  public popToRoot(componentId) {
68 75
     return this.nativeCommandsSender.popToRoot(componentId);
69 76
   }
70 77
 
71
-  showOverlay(componentData) {
78
+  public showOverlay(componentData) {
72 79
     const input = _.cloneDeep(componentData);
73 80
     this.optionsProcessor.processOptions(input);
74 81
 
@@ -78,7 +85,7 @@ export class Commands {
78 85
     return this.nativeCommandsSender.showOverlay(layout);
79 86
   }
80 87
 
81
-  dismissOverlay(componentId) {
88
+  public dismissOverlay(componentId) {
82 89
     return this.nativeCommandsSender.dismissOverlay(componentId);
83 90
   }
84 91
 }

+ 47
- 0
lib/src/events/CommandsObserver.test.ts View File

@@ -0,0 +1,47 @@
1
+import { CommandsObserver } from './CommandsObserver';
2
+
3
+describe('CommandsObserver', () => {
4
+  let uut: CommandsObserver;
5
+  let cb1;
6
+  let cb2;
7
+
8
+  beforeEach(() => {
9
+    cb1 = jest.fn();
10
+    cb2 = jest.fn();
11
+    uut = new CommandsObserver();
12
+  });
13
+
14
+  it('register and notify listener', () => {
15
+    const theCommandName = 'theCommandName';
16
+    const theParams = { x: 1 };
17
+
18
+    uut.register(cb1);
19
+    uut.register(cb2);
20
+
21
+    expect(cb1).toHaveBeenCalledTimes(0);
22
+    expect(cb2).toHaveBeenCalledTimes(0);
23
+
24
+    uut.notify(theCommandName, theParams);
25
+
26
+    expect(cb1).toHaveBeenCalledTimes(1);
27
+    expect(cb1).toHaveBeenCalledWith(theCommandName, theParams);
28
+    expect(cb2).toHaveBeenCalledTimes(1);
29
+    expect(cb2).toHaveBeenCalledWith(theCommandName, theParams);
30
+  });
31
+
32
+  it('remove listener', () => {
33
+    uut.register(cb1);
34
+    const result = uut.register(cb2);
35
+    expect(result).toBeDefined();
36
+
37
+    uut.notify('commandName', {});
38
+    expect(cb1).toHaveBeenCalledTimes(1);
39
+    expect(cb2).toHaveBeenCalledTimes(1);
40
+
41
+    result.remove();
42
+
43
+    uut.notify('commandName', {});
44
+    expect(cb1).toHaveBeenCalledTimes(2);
45
+    expect(cb2).toHaveBeenCalledTimes(1);
46
+  });
47
+});

+ 20
- 0
lib/src/events/CommandsObserver.ts View File

@@ -0,0 +1,20 @@
1
+import * as _ from 'lodash';
2
+import { EventSubscription } from './EventsRegistry';
3
+
4
+export type CommandsListener = (name: string, params: {}) => void;
5
+
6
+export class CommandsObserver {
7
+  private readonly listeners = {};
8
+
9
+  public register(listener: CommandsListener): EventSubscription {
10
+    const id = _.uniqueId();
11
+    _.set(this.listeners, id, listener);
12
+    return {
13
+      remove: () => _.unset(this.listeners, id)
14
+    };
15
+  }
16
+
17
+  public notify(commandName: string, params: {}): void {
18
+    _.forEach(this.listeners, (listener) => listener(commandName, params));
19
+  }
20
+}

lib/src/events/ComponentEventsRegistry.test.ts → lib/src/events/ComponentEventsObserver.test.ts View File

@@ -1,8 +1,8 @@
1
-import { ComponentEventsRegistry } from './ComponentEventsRegistry';
1
+import { ComponentEventsObserver } from './ComponentEventsObserver';
2 2
 import { Store } from '../components/Store';
3 3
 
4 4
 describe(`ComponentEventRegistry`, () => {
5
-  let uut: ComponentEventsRegistry;
5
+  let uut: ComponentEventsObserver;
6 6
   let eventRegistry;
7 7
   let store: Store;
8 8
   let mockComponentRef;
@@ -24,7 +24,7 @@ describe(`ComponentEventRegistry`, () => {
24 24
     store = new Store();
25 25
     store.setRefForId(refId, mockComponentRef);
26 26
 
27
-    uut = new ComponentEventsRegistry(eventRegistry, store);
27
+    uut = new ComponentEventsObserver(eventRegistry, store);
28 28
   });
29 29
 
30 30
   it('register for lifecycle events on eventRegistry', () => {

lib/src/events/ComponentEventsRegistry.ts → lib/src/events/ComponentEventsObserver.ts View File

@@ -1,7 +1,7 @@
1 1
 import { EventsRegistry } from './EventsRegistry';
2 2
 import { Store } from '../components/Store';
3 3
 
4
-export class ComponentEventsRegistry {
4
+export class ComponentEventsObserver {
5 5
   constructor(private eventsRegistry: EventsRegistry, private store: Store) {
6 6
     this.componentDidAppear = this.componentDidAppear.bind(this);
7 7
     this.componentDidDisappear = this.componentDidDisappear.bind(this);

+ 22
- 3
lib/src/events/EventsRegistry.test.ts View File

@@ -1,13 +1,15 @@
1 1
 import { EventsRegistry } from './EventsRegistry';
2 2
 import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver.mock';
3
+import { CommandsObserver } from './CommandsObserver';
3 4
 
4 5
 describe('EventsRegistry', () => {
5 6
   let uut: EventsRegistry;
6
-  let mockNativeEventsReceiver;
7
+  const mockNativeEventsReceiver = new NativeEventsReceiver();
8
+  let commandsObserver: CommandsObserver;
7 9
 
8 10
   beforeEach(() => {
9
-    mockNativeEventsReceiver = new NativeEventsReceiver();
10
-    uut = new EventsRegistry(mockNativeEventsReceiver);
11
+    commandsObserver = new CommandsObserver();
12
+    uut = new EventsRegistry(mockNativeEventsReceiver, commandsObserver);
11 13
   });
12 14
 
13 15
   it('exposes onAppLaunched event', () => {
@@ -63,4 +65,21 @@ describe('EventsRegistry', () => {
63 65
     mockNativeEventsReceiver.registerOnNavigationButtonPressed.mock.calls[0][0]({ componentId: 'theId', buttonId: 'theBtnId' });
64 66
     expect(cb).toHaveBeenCalledWith('theId', 'theBtnId');
65 67
   });
68
+
69
+  it('exposes onNavigationCommand registers listener to commandObserver', () => {
70
+    const cb = jest.fn();
71
+    const result = uut.onNavigationCommand(cb);
72
+    expect(result).toBeDefined();
73
+    commandsObserver.notify('theCommandName', { x: 1 });
74
+    expect(cb).toHaveBeenCalledTimes(1);
75
+    expect(cb).toHaveBeenCalledWith('theCommandName', { x: 1 });
76
+  });
77
+
78
+  it('onNavigationCommand unregister', () => {
79
+    const cb = jest.fn();
80
+    const result = uut.onNavigationCommand(cb);
81
+    result.remove();
82
+    commandsObserver.notify('theCommandName', { x: 1 });
83
+    expect(cb).not.toHaveBeenCalled();
84
+  });
66 85
 });

+ 11
- 6
lib/src/events/EventsRegistry.ts View File

@@ -1,11 +1,12 @@
1
-import { EventSubscription, NativeEventsReceiver } from '../adapters/NativeEventsReceiver';
1
+import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver';
2
+import { CommandsObserver } from './CommandsObserver';
2 3
 
3
-export class EventsRegistry {
4
-  private nativeEventsReceiver: NativeEventsReceiver;
4
+export interface EventSubscription {
5
+  remove();
6
+}
5 7
 
6
-  constructor(nativeEventsReceiver: NativeEventsReceiver) {
7
-    this.nativeEventsReceiver = nativeEventsReceiver;
8
-  }
8
+export class EventsRegistry {
9
+  constructor(private nativeEventsReceiver: NativeEventsReceiver, private commandsObserver: CommandsObserver) { }
9 10
 
10 11
   public onAppLaunched(callback: () => void): EventSubscription {
11 12
     return this.nativeEventsReceiver.registerOnAppLaunched(callback);
@@ -22,4 +23,8 @@ export class EventsRegistry {
22 23
   public onNavigationButtonPressed(callback: (componentId: string, buttonId: string) => void): EventSubscription {
23 24
     return this.nativeEventsReceiver.registerOnNavigationButtonPressed(({ componentId, buttonId }) => callback(componentId, buttonId));
24 25
   }
26
+
27
+  public onNavigationCommand(callback: (name: string, params: any) => void): EventSubscription {
28
+    return this.commandsObserver.register(callback);
29
+  }
25 30
 }