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 15import {Child, Controller} from './controller'; 16 17const _onCreate = jest.fn(); 18const _onDestroy = jest.fn(); 19const _run = jest.fn(); 20 21type MockStates = 'idle' | 'state1' | 'state2' | 'state3'; 22class MockController extends Controller<MockStates> { 23 constructor(public type: string) { 24 super('idle'); 25 _onCreate(this.type); 26 } 27 28 run() { 29 return _run(this.type); 30 } 31 32 onDestroy() { 33 return _onDestroy(this.type); 34 } 35} 36 37function runControllerTree(rootController: MockController): void { 38 for (let runAgain = true, i = 0; runAgain; i++) { 39 if (i >= 100) throw new Error('Controller livelock'); 40 runAgain = rootController.invoke(); 41 } 42} 43 44beforeEach(() => { 45 _onCreate.mockClear(); 46 _onCreate.mockReset(); 47 _onDestroy.mockClear(); 48 _onDestroy.mockReset(); 49 _run.mockClear(); 50 _run.mockReset(); 51}); 52 53test('singleControllerNoTransition', () => { 54 const rootCtl = new MockController('root'); 55 runControllerTree(rootCtl); 56 expect(_run).toHaveBeenCalledTimes(1); 57 expect(_run).toHaveBeenCalledWith('root'); 58}); 59 60test('singleControllerThreeTransitions', () => { 61 const rootCtl = new MockController('root'); 62 _run.mockImplementation(() => { 63 if (rootCtl.state === 'idle') { 64 rootCtl.setState('state1'); 65 } else if (rootCtl.state === 'state1') { 66 rootCtl.setState('state2'); 67 } 68 }); 69 runControllerTree(rootCtl); 70 expect(_run).toHaveBeenCalledTimes(3); 71 expect(_run).toHaveBeenCalledWith('root'); 72}); 73 74test('nestedControllers', () => { 75 const rootCtl = new MockController('root'); 76 let nextState: MockStates = 'idle'; 77 _run.mockImplementation((type: string) => { 78 if (type !== 'root') return; 79 rootCtl.setState(nextState); 80 if (rootCtl.state === 'idle') return; 81 82 if (rootCtl.state === 'state1') { 83 return [Child('child1', MockController, 'child1')]; 84 } 85 if (rootCtl.state === 'state2') { 86 return [ 87 Child('child1', MockController, 'child1'), 88 Child('child2', MockController, 'child2'), 89 ]; 90 } 91 if (rootCtl.state === 'state3') { 92 return [ 93 Child('child1', MockController, 'child1'), 94 Child('child3', MockController, 'child3'), 95 ]; 96 } 97 throw new Error('Not reached'); 98 }); 99 runControllerTree(rootCtl); 100 expect(_run).toHaveBeenCalledWith('root'); 101 expect(_run).toHaveBeenCalledTimes(1); 102 103 // Transition the root controller to state1. This will create the first child 104 // and re-run both (because of the idle -> state1 transition). 105 _run.mockClear(); 106 _onCreate.mockClear(); 107 nextState = 'state1'; 108 runControllerTree(rootCtl); 109 expect(_onCreate).toHaveBeenCalledWith('child1'); 110 expect(_onCreate).toHaveBeenCalledTimes(1); 111 expect(_run).toHaveBeenCalledWith('root'); 112 expect(_run).toHaveBeenCalledWith('child1'); 113 expect(_run).toHaveBeenCalledTimes(4); 114 115 // Transition the root controller to state2. This will create the 2nd child 116 // and run the three of them (root + 2 chilren) two times. 117 _run.mockClear(); 118 _onCreate.mockClear(); 119 nextState = 'state2'; 120 runControllerTree(rootCtl); 121 expect(_onCreate).toHaveBeenCalledWith('child2'); 122 expect(_onCreate).toHaveBeenCalledTimes(1); 123 expect(_run).toHaveBeenCalledWith('root'); 124 expect(_run).toHaveBeenCalledWith('child1'); 125 expect(_run).toHaveBeenCalledWith('child2'); 126 expect(_run).toHaveBeenCalledTimes(6); 127 128 // Transition the root controller to state3. This will create the 3rd child 129 // and remove the 2nd one. 130 _run.mockClear(); 131 _onCreate.mockClear(); 132 nextState = 'state3'; 133 runControllerTree(rootCtl); 134 expect(_onCreate).toHaveBeenCalledWith('child3'); 135 expect(_onDestroy).toHaveBeenCalledWith('child2'); 136 expect(_onCreate).toHaveBeenCalledTimes(1); 137 expect(_run).toHaveBeenCalledWith('root'); 138 expect(_run).toHaveBeenCalledWith('child1'); 139 expect(_run).toHaveBeenCalledWith('child3'); 140 expect(_run).toHaveBeenCalledTimes(6); 141 142 // Finally transition back to the idle state. All children should be removed. 143 _run.mockClear(); 144 _onCreate.mockClear(); 145 _onDestroy.mockClear(); 146 nextState = 'idle'; 147 runControllerTree(rootCtl); 148 expect(_onDestroy).toHaveBeenCalledWith('child1'); 149 expect(_onDestroy).toHaveBeenCalledWith('child3'); 150 expect(_onCreate).toHaveBeenCalledTimes(0); 151 expect(_onDestroy).toHaveBeenCalledTimes(2); 152 expect(_run).toHaveBeenCalledWith('root'); 153 expect(_run).toHaveBeenCalledTimes(2); 154}); 155