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 { 16 Child, 17 Controller, 18} from './controller'; 19 20const _onCreate = jest.fn(); 21const _onDestroy = jest.fn(); 22const _run = jest.fn(); 23 24type MockStates = 'idle'|'state1'|'state2'|'state3'; 25class 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 40function 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 47beforeEach(() => { 48 _onCreate.mockClear(); 49 _onCreate.mockReset(); 50 _onDestroy.mockClear(); 51 _onDestroy.mockReset(); 52 _run.mockClear(); 53 _run.mockReset(); 54}); 55 56test('singleControllerNoTransition', () => { 57 const rootCtl = new MockController('root'); 58 runControllerTree(rootCtl); 59 expect(_run).toHaveBeenCalledTimes(1); 60 expect(_run).toHaveBeenCalledWith('root'); 61}); 62 63test('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 77test('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