|  | // Copyright (C) 2018 The Android Open Source Project | 
|  | // | 
|  | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | // you may not use this file except in compliance with the License. | 
|  | // You may obtain a copy of the License at | 
|  | // | 
|  | //      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | // | 
|  | // Unless required by applicable law or agreed to in writing, software | 
|  | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | // See the License for the specific language governing permissions and | 
|  | // limitations under the License. | 
|  |  | 
|  | import {Child, Controller} from './controller'; | 
|  |  | 
|  | const _onCreate = jest.fn(); | 
|  | const _onDestroy = jest.fn(); | 
|  | const _run = jest.fn(); | 
|  |  | 
|  | type MockStates = 'idle' | 'state1' | 'state2' | 'state3'; | 
|  | class MockController extends Controller<MockStates> { | 
|  | constructor(public type: string) { | 
|  | super('idle'); | 
|  | _onCreate(this.type); | 
|  | } | 
|  |  | 
|  | run() { | 
|  | return _run(this.type); | 
|  | } | 
|  |  | 
|  | onDestroy() { | 
|  | return _onDestroy(this.type); | 
|  | } | 
|  | } | 
|  |  | 
|  | function runControllerTree(rootController: MockController): void { | 
|  | for (let runAgain = true, i = 0; runAgain; i++) { | 
|  | if (i >= 100) throw new Error('Controller livelock'); | 
|  | runAgain = rootController.invoke(); | 
|  | } | 
|  | } | 
|  |  | 
|  | beforeEach(() => { | 
|  | _onCreate.mockClear(); | 
|  | _onCreate.mockReset(); | 
|  | _onDestroy.mockClear(); | 
|  | _onDestroy.mockReset(); | 
|  | _run.mockClear(); | 
|  | _run.mockReset(); | 
|  | }); | 
|  |  | 
|  | test('singleControllerNoTransition', () => { | 
|  | const rootCtl = new MockController('root'); | 
|  | runControllerTree(rootCtl); | 
|  | expect(_run).toHaveBeenCalledTimes(1); | 
|  | expect(_run).toHaveBeenCalledWith('root'); | 
|  | }); | 
|  |  | 
|  | test('singleControllerThreeTransitions', () => { | 
|  | const rootCtl = new MockController('root'); | 
|  | _run.mockImplementation(() => { | 
|  | if (rootCtl.state === 'idle') { | 
|  | rootCtl.setState('state1'); | 
|  | } else if (rootCtl.state === 'state1') { | 
|  | rootCtl.setState('state2'); | 
|  | } | 
|  | }); | 
|  | runControllerTree(rootCtl); | 
|  | expect(_run).toHaveBeenCalledTimes(3); | 
|  | expect(_run).toHaveBeenCalledWith('root'); | 
|  | }); | 
|  |  | 
|  | test('nestedControllers', () => { | 
|  | const rootCtl = new MockController('root'); | 
|  | let nextState: MockStates = 'idle'; | 
|  | _run.mockImplementation((type: string) => { | 
|  | if (type !== 'root') return; | 
|  | rootCtl.setState(nextState); | 
|  | if (rootCtl.state === 'idle') return; | 
|  |  | 
|  | if (rootCtl.state === 'state1') { | 
|  | return [Child('child1', MockController, 'child1')]; | 
|  | } | 
|  | if (rootCtl.state === 'state2') { | 
|  | return [ | 
|  | Child('child1', MockController, 'child1'), | 
|  | Child('child2', MockController, 'child2'), | 
|  | ]; | 
|  | } | 
|  | if (rootCtl.state === 'state3') { | 
|  | return [ | 
|  | Child('child1', MockController, 'child1'), | 
|  | Child('child3', MockController, 'child3'), | 
|  | ]; | 
|  | } | 
|  | throw new Error('Not reached'); | 
|  | }); | 
|  | runControllerTree(rootCtl); | 
|  | expect(_run).toHaveBeenCalledWith('root'); | 
|  | expect(_run).toHaveBeenCalledTimes(1); | 
|  |  | 
|  | // Transition the root controller to state1. This will create the first child | 
|  | // and re-run both (because of the idle -> state1 transition). | 
|  | _run.mockClear(); | 
|  | _onCreate.mockClear(); | 
|  | nextState = 'state1'; | 
|  | runControllerTree(rootCtl); | 
|  | expect(_onCreate).toHaveBeenCalledWith('child1'); | 
|  | expect(_onCreate).toHaveBeenCalledTimes(1); | 
|  | expect(_run).toHaveBeenCalledWith('root'); | 
|  | expect(_run).toHaveBeenCalledWith('child1'); | 
|  | expect(_run).toHaveBeenCalledTimes(4); | 
|  |  | 
|  | // Transition the root controller to state2. This will create the 2nd child | 
|  | // and run the three of them (root + 2 chilren) two times. | 
|  | _run.mockClear(); | 
|  | _onCreate.mockClear(); | 
|  | nextState = 'state2'; | 
|  | runControllerTree(rootCtl); | 
|  | expect(_onCreate).toHaveBeenCalledWith('child2'); | 
|  | expect(_onCreate).toHaveBeenCalledTimes(1); | 
|  | expect(_run).toHaveBeenCalledWith('root'); | 
|  | expect(_run).toHaveBeenCalledWith('child1'); | 
|  | expect(_run).toHaveBeenCalledWith('child2'); | 
|  | expect(_run).toHaveBeenCalledTimes(6); | 
|  |  | 
|  | // Transition the root controller to state3. This will create the 3rd child | 
|  | // and remove the 2nd one. | 
|  | _run.mockClear(); | 
|  | _onCreate.mockClear(); | 
|  | nextState = 'state3'; | 
|  | runControllerTree(rootCtl); | 
|  | expect(_onCreate).toHaveBeenCalledWith('child3'); | 
|  | expect(_onDestroy).toHaveBeenCalledWith('child2'); | 
|  | expect(_onCreate).toHaveBeenCalledTimes(1); | 
|  | expect(_run).toHaveBeenCalledWith('root'); | 
|  | expect(_run).toHaveBeenCalledWith('child1'); | 
|  | expect(_run).toHaveBeenCalledWith('child3'); | 
|  | expect(_run).toHaveBeenCalledTimes(6); | 
|  |  | 
|  | // Finally transition back to the idle state. All children should be removed. | 
|  | _run.mockClear(); | 
|  | _onCreate.mockClear(); | 
|  | _onDestroy.mockClear(); | 
|  | nextState = 'idle'; | 
|  | runControllerTree(rootCtl); | 
|  | expect(_onDestroy).toHaveBeenCalledWith('child1'); | 
|  | expect(_onDestroy).toHaveBeenCalledWith('child3'); | 
|  | expect(_onCreate).toHaveBeenCalledTimes(0); | 
|  | expect(_onDestroy).toHaveBeenCalledTimes(2); | 
|  | expect(_run).toHaveBeenCalledWith('root'); | 
|  | expect(_run).toHaveBeenCalledTimes(2); | 
|  | }); |