1// Copyright (C) 2023 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 {assertExists} from '../base/logging'; 16import {Duration} from '../base/time'; 17import {TimeScale} from '../base/time_scale'; 18import {Track, TrackRenderContext} from '../public/track'; 19import {HighPrecisionTime} from '../base/high_precision_time'; 20import {HighPrecisionTimeSpan} from '../base/high_precision_time_span'; 21import {TrackManagerImpl} from '../core/track_manager'; 22 23function makeMockTrack() { 24 return { 25 onCreate: jest.fn(), 26 onUpdate: jest.fn(), 27 onDestroy: jest.fn(), 28 29 render: jest.fn(), 30 onFullRedraw: jest.fn(), 31 getSliceVerticalBounds: jest.fn(), 32 getHeight: jest.fn(), 33 getTrackShellButtons: jest.fn(), 34 onMouseMove: jest.fn(), 35 onMouseClick: jest.fn(), 36 onMouseOut: jest.fn(), 37 }; 38} 39 40async function settle() { 41 await new Promise((r) => setTimeout(r, 0)); 42} 43 44let mockTrack: ReturnType<typeof makeMockTrack>; 45let td: Track; 46let trackManager: TrackManagerImpl; 47const visibleWindow = new HighPrecisionTimeSpan(HighPrecisionTime.ZERO, 0); 48const dummyCtx: TrackRenderContext = { 49 trackUri: 'foo', 50 ctx: new CanvasRenderingContext2D(), 51 size: {width: 123, height: 123}, 52 visibleWindow, 53 resolution: Duration.ZERO, 54 timescale: new TimeScale(visibleWindow, {left: 0, right: 0}), 55}; 56 57beforeEach(() => { 58 mockTrack = makeMockTrack(); 59 td = { 60 uri: 'test', 61 title: 'foo', 62 track: mockTrack, 63 }; 64 trackManager = new TrackManagerImpl(); 65 trackManager.registerTrack(td); 66}); 67 68describe('TrackManager', () => { 69 it('calls track lifecycle hooks', async () => { 70 const entry = assertExists(trackManager.getTrackFSM(td.uri)); 71 72 entry.render(dummyCtx); 73 await settle(); 74 expect(mockTrack.onCreate).toHaveBeenCalledTimes(1); 75 expect(mockTrack.onUpdate).toHaveBeenCalledTimes(1); 76 77 // Double flush should destroy all tracks 78 trackManager.flushOldTracks(); 79 trackManager.flushOldTracks(); 80 await settle(); 81 expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1); 82 }); 83 84 it('calls onCrate lazily', async () => { 85 // Check we wait until the first call to render before calling onCreate 86 const entry = assertExists(trackManager.getTrackFSM(td.uri)); 87 await settle(); 88 expect(mockTrack.onCreate).not.toHaveBeenCalled(); 89 90 entry.render(dummyCtx); 91 await settle(); 92 expect(mockTrack.onCreate).toHaveBeenCalledTimes(1); 93 }); 94 95 it('reuses tracks', async () => { 96 const first = assertExists(trackManager.getTrackFSM(td.uri)); 97 trackManager.flushOldTracks(); 98 first.render(dummyCtx); 99 await settle(); 100 101 const second = assertExists(trackManager.getTrackFSM(td.uri)); 102 trackManager.flushOldTracks(); 103 second.render(dummyCtx); 104 await settle(); 105 106 expect(first).toBe(second); 107 // Ensure onCreate called only once 108 expect(mockTrack.onCreate).toHaveBeenCalledTimes(1); 109 }); 110 111 it('destroys tracks when they are not resolved for one cycle', async () => { 112 const entry = assertExists(trackManager.getTrackFSM(td.uri)); 113 entry.render(dummyCtx); 114 115 // Double flush should destroy all tracks 116 trackManager.flushOldTracks(); 117 trackManager.flushOldTracks(); 118 119 await settle(); 120 121 expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1); 122 }); 123 124 it('contains crash inside onCreate()', async () => { 125 const entry = assertExists(trackManager.getTrackFSM(td.uri)); 126 const e = new Error(); 127 128 // Mock crash inside onCreate 129 mockTrack.onCreate.mockImplementationOnce(() => { 130 throw e; 131 }); 132 133 entry.render(dummyCtx); 134 await settle(); 135 136 expect(mockTrack.onCreate).toHaveBeenCalledTimes(1); 137 expect(mockTrack.onUpdate).not.toHaveBeenCalled(); 138 expect(entry.getError()).toBe(e); 139 }); 140 141 it('contains crash inside onUpdate()', async () => { 142 const entry = assertExists(trackManager.getTrackFSM(td.uri)); 143 const e = new Error(); 144 145 // Mock crash inside onUpdate 146 mockTrack.onUpdate.mockImplementationOnce(() => { 147 throw e; 148 }); 149 150 entry.render(dummyCtx); 151 await settle(); 152 153 expect(mockTrack.onCreate).toHaveBeenCalledTimes(1); 154 expect(mockTrack.onUpdate).toHaveBeenCalledTimes(1); 155 expect(entry.getError()).toBe(e); 156 }); 157 158 it('handles dispose after crash', async () => { 159 const entry = assertExists(trackManager.getTrackFSM(td.uri)); 160 const e = new Error(); 161 162 // Mock crash inside onUpdate 163 mockTrack.onUpdate.mockImplementationOnce(() => { 164 throw e; 165 }); 166 167 entry.render(dummyCtx); 168 await settle(); 169 170 // Ensure we don't crash during the next render cycle 171 entry.render(dummyCtx); 172 await settle(); 173 }); 174}); 175