Home | History | Annotate | Download | only in controller
      1 // Copyright (C) 2018 The Android Open Source Project
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //      http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 import {
     16   Child,
     17   Controller,
     18 } from './controller';
     19 
     20 const _onCreate = jest.fn();
     21 const _onDestroy = jest.fn();
     22 const _run = jest.fn();
     23 
     24 type MockStates = 'idle'|'state1'|'state2'|'state3';
     25 class MockController extends Controller<MockStates> {
     26   constructor(public type: string) {
     27     super('idle');
     28     _onCreate(this.type);
     29   }
     30 
     31   run() {
     32     return _run(this.type);
     33   }
     34 
     35   onDestroy() {
     36     return _onDestroy(this.type);
     37   }
     38 }
     39 
     40 function runControllerTree(rootController: MockController): void {
     41   for (let runAgain = true, i = 0; runAgain; i++) {
     42     if (i >= 100) throw new Error('Controller livelock');
     43     runAgain = rootController.invoke();
     44   }
     45 }
     46 
     47 beforeEach(() => {
     48   _onCreate.mockClear();
     49   _onCreate.mockReset();
     50   _onDestroy.mockClear();
     51   _onDestroy.mockReset();
     52   _run.mockClear();
     53   _run.mockReset();
     54 });
     55 
     56 test('singleControllerNoTransition', () => {
     57   const rootCtl = new MockController('root');
     58   runControllerTree(rootCtl);
     59   expect(_run).toHaveBeenCalledTimes(1);
     60   expect(_run).toHaveBeenCalledWith('root');
     61 });
     62 
     63 test('singleControllerThreeTransitions', () => {
     64   const rootCtl = new MockController('root');
     65   _run.mockImplementation(() => {
     66     if (rootCtl.state === 'idle') {
     67       rootCtl.setState('state1');
     68     } else if (rootCtl.state === 'state1') {
     69       rootCtl.setState('state2');
     70     }
     71   });
     72   runControllerTree(rootCtl);
     73   expect(_run).toHaveBeenCalledTimes(3);
     74   expect(_run).toHaveBeenCalledWith('root');
     75 });
     76 
     77 test('nestedControllers', () => {
     78   const rootCtl = new MockController('root');
     79   let nextState: MockStates = 'idle';
     80   _run.mockImplementation((type: string) => {
     81     if (type !== 'root') return;
     82     rootCtl.setState(nextState);
     83     if (rootCtl.state === 'idle') return;
     84 
     85     if (rootCtl.state === 'state1') {
     86       return [
     87         Child('child1', MockController, 'child1'),
     88       ];
     89     }
     90     if (rootCtl.state === 'state2') {
     91       return [
     92         Child('child1', MockController, 'child1'),
     93         Child('child2', MockController, 'child2'),
     94       ];
     95     }
     96     if (rootCtl.state === 'state3') {
     97       return [
     98         Child('child1', MockController, 'child1'),
     99         Child('child3', MockController, 'child3'),
    100       ];
    101     }
    102     throw new Error('Not reached');
    103   });
    104   runControllerTree(rootCtl);
    105   expect(_run).toHaveBeenCalledWith('root');
    106   expect(_run).toHaveBeenCalledTimes(1);
    107 
    108   // Transition the root controller to state1. This will create the first child
    109   // and re-run both (because of the idle -> state1 transition).
    110   _run.mockClear();
    111   _onCreate.mockClear();
    112   nextState = 'state1';
    113   runControllerTree(rootCtl);
    114   expect(_onCreate).toHaveBeenCalledWith('child1');
    115   expect(_onCreate).toHaveBeenCalledTimes(1);
    116   expect(_run).toHaveBeenCalledWith('root');
    117   expect(_run).toHaveBeenCalledWith('child1');
    118   expect(_run).toHaveBeenCalledTimes(4);
    119 
    120   // Transition the root controller to state2. This will create the 2nd child
    121   // and run the three of them (root + 2 chilren) two times.
    122   _run.mockClear();
    123   _onCreate.mockClear();
    124   nextState = 'state2';
    125   runControllerTree(rootCtl);
    126   expect(_onCreate).toHaveBeenCalledWith('child2');
    127   expect(_onCreate).toHaveBeenCalledTimes(1);
    128   expect(_run).toHaveBeenCalledWith('root');
    129   expect(_run).toHaveBeenCalledWith('child1');
    130   expect(_run).toHaveBeenCalledWith('child2');
    131   expect(_run).toHaveBeenCalledTimes(6);
    132 
    133   // Transition the root controller to state3. This will create the 3rd child
    134   // and remove the 2nd one.
    135   _run.mockClear();
    136   _onCreate.mockClear();
    137   nextState = 'state3';
    138   runControllerTree(rootCtl);
    139   expect(_onCreate).toHaveBeenCalledWith('child3');
    140   expect(_onDestroy).toHaveBeenCalledWith('child2');
    141   expect(_onCreate).toHaveBeenCalledTimes(1);
    142   expect(_run).toHaveBeenCalledWith('root');
    143   expect(_run).toHaveBeenCalledWith('child1');
    144   expect(_run).toHaveBeenCalledWith('child3');
    145   expect(_run).toHaveBeenCalledTimes(6);
    146 
    147   // Finally transition back to the idle state. All children should be removed.
    148   _run.mockClear();
    149   _onCreate.mockClear();
    150   _onDestroy.mockClear();
    151   nextState = 'idle';
    152   runControllerTree(rootCtl);
    153   expect(_onDestroy).toHaveBeenCalledWith('child1');
    154   expect(_onDestroy).toHaveBeenCalledWith('child3');
    155   expect(_onCreate).toHaveBeenCalledTimes(0);
    156   expect(_onDestroy).toHaveBeenCalledTimes(2);
    157   expect(_run).toHaveBeenCalledWith('root');
    158   expect(_run).toHaveBeenCalledTimes(2);
    159 });
    160