// 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 { 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); });