Browse Source

CommandsObserver

Daniel Zlotin 6 years ago
parent
commit
1a6dd8b06e

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

9
 import { EventsRegistry } from './events/EventsRegistry';
9
 import { EventsRegistry } from './events/EventsRegistry';
10
 import { ComponentProvider } from 'react-native';
10
 import { ComponentProvider } from 'react-native';
11
 import { Element } from './adapters/Element';
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
 export class Navigation {
15
 export class Navigation {
15
   public readonly Element;
16
   public readonly Element;
23
   private readonly nativeCommandsSender;
24
   private readonly nativeCommandsSender;
24
   private readonly commands;
25
   private readonly commands;
25
   private readonly eventsRegistry;
26
   private readonly eventsRegistry;
27
+  private readonly commandsObserver;
28
+  private readonly componentEventsObserver;
26
 
29
 
27
   constructor() {
30
   constructor() {
28
     this.Element = Element;
31
     this.Element = Element;
34
     this.layoutTreeParser = new LayoutTreeParser();
37
     this.layoutTreeParser = new LayoutTreeParser();
35
     this.layoutTreeCrawler = new LayoutTreeCrawler(this.uniqueIdProvider, this.store);
38
     this.layoutTreeCrawler = new LayoutTreeCrawler(this.uniqueIdProvider, this.store);
36
     this.nativeCommandsSender = new NativeCommandsSender();
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
 import { NativeModules, NativeEventEmitter } from 'react-native';
1
 import { NativeModules, NativeEventEmitter } from 'react-native';
2
-
3
-export interface EventSubscription {
4
-  remove();
5
-}
2
+import { EventSubscription } from '../events/EventsRegistry';
6
 
3
 
7
 export class NativeEventsReceiver {
4
 export class NativeEventsReceiver {
8
   private emitter: NativeEventEmitter;
5
   private emitter: NativeEventEmitter;

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

1
+import * as _ from 'lodash';
1
 import { LayoutTreeParser } from './LayoutTreeParser';
2
 import { LayoutTreeParser } from './LayoutTreeParser';
2
 import { LayoutTreeCrawler } from './LayoutTreeCrawler';
3
 import { LayoutTreeCrawler } from './LayoutTreeCrawler';
3
 import { Store } from '../components/Store';
4
 import { Store } from '../components/Store';
4
 import { UniqueIdProvider } from '../adapters/UniqueIdProvider.mock';
5
 import { UniqueIdProvider } from '../adapters/UniqueIdProvider.mock';
5
 import { NativeCommandsSender } from '../adapters/NativeCommandsSender.mock';
6
 import { NativeCommandsSender } from '../adapters/NativeCommandsSender.mock';
6
 import { Commands } from './Commands';
7
 import { Commands } from './Commands';
8
+import { CommandsObserver } from '../events/CommandsObserver';
7
 
9
 
8
 describe('Commands', () => {
10
 describe('Commands', () => {
9
-  let uut;
11
+  let uut: Commands;
10
   let mockCommandsSender;
12
   let mockCommandsSender;
11
   let store;
13
   let store;
14
+  let commandsObserver: CommandsObserver;
12
 
15
 
13
   beforeEach(() => {
16
   beforeEach(() => {
14
     mockCommandsSender = new NativeCommandsSender();
17
     mockCommandsSender = new NativeCommandsSender();
15
     store = new Store();
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
   describe('setRoot', () => {
29
   describe('setRoot', () => {
183
 
192
 
184
   describe('pop', () => {
193
   describe('pop', () => {
185
     it('pops a component, passing componentId', () => {
194
     it('pops a component, passing componentId', () => {
186
-      uut.pop('theComponentId');
195
+      uut.pop('theComponentId', {});
187
       expect(mockCommandsSender.pop).toHaveBeenCalledTimes(1);
196
       expect(mockCommandsSender.pop).toHaveBeenCalledTimes(1);
188
-      expect(mockCommandsSender.pop).toHaveBeenCalledWith('theComponentId', undefined);
197
+      expect(mockCommandsSender.pop).toHaveBeenCalledWith('theComponentId', {});
189
     });
198
     });
190
     it('pops a component, passing componentId and options', () => {
199
     it('pops a component, passing componentId and options', () => {
191
       const options = {
200
       const options = {
203
 
212
 
204
     it('pop returns a promise that resolves to componentId', async () => {
213
     it('pop returns a promise that resolves to componentId', async () => {
205
       mockCommandsSender.pop.mockReturnValue(Promise.resolve('theComponentId'));
214
       mockCommandsSender.pop.mockReturnValue(Promise.resolve('theComponentId'));
206
-      const result = await uut.pop('theComponentId');
215
+      const result = await uut.pop('theComponentId', {});
207
       expect(result).toEqual('theComponentId');
216
       expect(result).toEqual('theComponentId');
208
     });
217
     });
209
   });
218
   });
282
       expect(mockCommandsSender.dismissOverlay).toHaveBeenCalledWith('Component1');
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
 import * as _ from 'lodash';
1
 import * as _ from 'lodash';
2
 import { OptionsProcessor } from './OptionsProcessor';
2
 import { OptionsProcessor } from './OptionsProcessor';
3
+import { CommandsObserver } from '../events/CommandsObserver';
3
 
4
 
4
 export class Commands {
5
 export class Commands {
5
-  private nativeCommandsSender;
6
-  private layoutTreeParser;
7
-  private layoutTreeCrawler;
8
   private optionsProcessor: OptionsProcessor;
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
     this.optionsProcessor = new OptionsProcessor(this.layoutTreeCrawler.store);
13
     this.optionsProcessor = new OptionsProcessor(this.layoutTreeCrawler.store);
15
   }
14
   }
16
 
15
 
17
-  setRoot(simpleApi) {
16
+  public setRoot(simpleApi) {
18
     const input = _.cloneDeep(simpleApi);
17
     const input = _.cloneDeep(simpleApi);
19
     const layout = this.layoutTreeParser.parse(input);
18
     const layout = this.layoutTreeParser.parse(input);
20
     this.layoutTreeCrawler.crawl(layout);
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
     const input = _.cloneDeep(options);
28
     const input = _.cloneDeep(options);
26
     this.optionsProcessor.processOptions(input);
29
     this.optionsProcessor.processOptions(input);
30
+
27
     this.nativeCommandsSender.setDefaultOptions(input);
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
     const input = _.cloneDeep(options);
36
     const input = _.cloneDeep(options);
32
     this.optionsProcessor.processOptions(input);
37
     this.optionsProcessor.processOptions(input);
38
+
33
     this.nativeCommandsSender.setOptions(componentId, input);
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
     const input = _.cloneDeep(simpleApi);
44
     const input = _.cloneDeep(simpleApi);
38
     const layout = this.layoutTreeParser.parse(input);
45
     const layout = this.layoutTreeParser.parse(input);
39
     this.layoutTreeCrawler.crawl(layout);
46
     this.layoutTreeCrawler.crawl(layout);
40
     return this.nativeCommandsSender.showModal(layout);
47
     return this.nativeCommandsSender.showModal(layout);
41
   }
48
   }
42
 
49
 
43
-  dismissModal(id) {
50
+  public dismissModal(id) {
44
     return this.nativeCommandsSender.dismissModal(id);
51
     return this.nativeCommandsSender.dismissModal(id);
45
   }
52
   }
46
 
53
 
47
-  dismissAllModals() {
54
+  public dismissAllModals() {
48
     return this.nativeCommandsSender.dismissAllModals();
55
     return this.nativeCommandsSender.dismissAllModals();
49
   }
56
   }
50
 
57
 
51
-  push(onComponentId, componentData) {
58
+  public push(onComponentId, componentData) {
52
     const input = _.cloneDeep(componentData);
59
     const input = _.cloneDeep(componentData);
53
     this.optionsProcessor.processOptions(input);
60
     this.optionsProcessor.processOptions(input);
54
     const layout = this.layoutTreeParser.parse(input);
61
     const layout = this.layoutTreeParser.parse(input);
56
     return this.nativeCommandsSender.push(onComponentId, layout);
63
     return this.nativeCommandsSender.push(onComponentId, layout);
57
   }
64
   }
58
 
65
 
59
-  pop(componentId, options) {
66
+  public pop(componentId, options) {
60
     return this.nativeCommandsSender.pop(componentId, options);
67
     return this.nativeCommandsSender.pop(componentId, options);
61
   }
68
   }
62
 
69
 
63
-  popTo(componentId) {
70
+  public popTo(componentId) {
64
     return this.nativeCommandsSender.popTo(componentId);
71
     return this.nativeCommandsSender.popTo(componentId);
65
   }
72
   }
66
 
73
 
67
-  popToRoot(componentId) {
74
+  public popToRoot(componentId) {
68
     return this.nativeCommandsSender.popToRoot(componentId);
75
     return this.nativeCommandsSender.popToRoot(componentId);
69
   }
76
   }
70
 
77
 
71
-  showOverlay(componentData) {
78
+  public showOverlay(componentData) {
72
     const input = _.cloneDeep(componentData);
79
     const input = _.cloneDeep(componentData);
73
     this.optionsProcessor.processOptions(input);
80
     this.optionsProcessor.processOptions(input);
74
 
81
 
78
     return this.nativeCommandsSender.showOverlay(layout);
85
     return this.nativeCommandsSender.showOverlay(layout);
79
   }
86
   }
80
 
87
 
81
-  dismissOverlay(componentId) {
88
+  public dismissOverlay(componentId) {
82
     return this.nativeCommandsSender.dismissOverlay(componentId);
89
     return this.nativeCommandsSender.dismissOverlay(componentId);
83
   }
90
   }
84
 }
91
 }

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

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

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

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

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

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

1
 import { EventsRegistry } from './EventsRegistry';
1
 import { EventsRegistry } from './EventsRegistry';
2
 import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver.mock';
2
 import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver.mock';
3
+import { CommandsObserver } from './CommandsObserver';
3
 
4
 
4
 describe('EventsRegistry', () => {
5
 describe('EventsRegistry', () => {
5
   let uut: EventsRegistry;
6
   let uut: EventsRegistry;
6
-  let mockNativeEventsReceiver;
7
+  const mockNativeEventsReceiver = new NativeEventsReceiver();
8
+  let commandsObserver: CommandsObserver;
7
 
9
 
8
   beforeEach(() => {
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
   it('exposes onAppLaunched event', () => {
15
   it('exposes onAppLaunched event', () => {
63
     mockNativeEventsReceiver.registerOnNavigationButtonPressed.mock.calls[0][0]({ componentId: 'theId', buttonId: 'theBtnId' });
65
     mockNativeEventsReceiver.registerOnNavigationButtonPressed.mock.calls[0][0]({ componentId: 'theId', buttonId: 'theBtnId' });
64
     expect(cb).toHaveBeenCalledWith('theId', 'theBtnId');
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
-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
   public onAppLaunched(callback: () => void): EventSubscription {
11
   public onAppLaunched(callback: () => void): EventSubscription {
11
     return this.nativeEventsReceiver.registerOnAppLaunched(callback);
12
     return this.nativeEventsReceiver.registerOnAppLaunched(callback);
22
   public onNavigationButtonPressed(callback: (componentId: string, buttonId: string) => void): EventSubscription {
23
   public onNavigationButtonPressed(callback: (componentId: string, buttonId: string) => void): EventSubscription {
23
     return this.nativeEventsReceiver.registerOnNavigationButtonPressed(({ componentId, buttonId }) => callback(componentId, buttonId));
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
 }