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