1/* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {AbtChromeExtensionProtocolStub} from 'abt_chrome_extension/abt_chrome_extension_protocol_stub'; 18import {CrossToolProtocolStub} from 'cross_tool/cross_tool_protocol_stub'; 19import {ProgressListenerStub} from 'interfaces/progress_listener_stub'; 20import {MockStorage} from 'test/unit/mock_storage'; 21import {UnitTestUtils} from 'test/unit/utils'; 22import {RealTimestamp} from 'trace/timestamp'; 23import {TraceFile} from 'trace/trace_file'; 24import {TracePosition} from 'trace/trace_position'; 25import {ViewerFactory} from 'viewers/viewer_factory'; 26import {ViewerStub} from 'viewers/viewer_stub'; 27import {AppComponentStub} from './components/app_component_stub'; 28import {SnackBarOpenerStub} from './components/snack_bar_opener_stub'; 29import {TimelineComponentStub} from './components/timeline/timeline_component_stub'; 30import {Mediator} from './mediator'; 31import {TimelineData} from './timeline_data'; 32import {TracePipeline} from './trace_pipeline'; 33 34describe('Mediator', () => { 35 const viewerStub = new ViewerStub('Title'); 36 let inputFiles: File[]; 37 let tracePipeline: TracePipeline; 38 let timelineData: TimelineData; 39 let abtChromeExtensionProtocol: AbtChromeExtensionProtocolStub; 40 let crossToolProtocol: CrossToolProtocolStub; 41 let appComponent: AppComponentStub; 42 let timelineComponent: TimelineComponentStub; 43 let uploadTracesComponent: ProgressListenerStub; 44 let collectTracesComponent: ProgressListenerStub; 45 let snackBarOpener: SnackBarOpenerStub; 46 let mediator: Mediator; 47 48 const TIMESTAMP_10 = new RealTimestamp(10n); 49 const TIMESTAMP_11 = new RealTimestamp(11n); 50 const POSITION_10 = TracePosition.fromTimestamp(TIMESTAMP_10); 51 const POSITION_11 = TracePosition.fromTimestamp(TIMESTAMP_11); 52 53 beforeAll(async () => { 54 inputFiles = [ 55 await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/SurfaceFlinger.pb'), 56 await UnitTestUtils.getFixtureFile('traces/elapsed_and_real_timestamp/WindowManager.pb'), 57 await UnitTestUtils.getFixtureFile( 58 'traces/elapsed_and_real_timestamp/screen_recording_metadata_v2.mp4' 59 ), 60 ]; 61 }); 62 63 beforeEach(async () => { 64 tracePipeline = new TracePipeline(); 65 timelineData = new TimelineData(); 66 abtChromeExtensionProtocol = new AbtChromeExtensionProtocolStub(); 67 crossToolProtocol = new CrossToolProtocolStub(); 68 appComponent = new AppComponentStub(); 69 timelineComponent = new TimelineComponentStub(); 70 uploadTracesComponent = new ProgressListenerStub(); 71 collectTracesComponent = new ProgressListenerStub(); 72 snackBarOpener = new SnackBarOpenerStub(); 73 mediator = new Mediator( 74 tracePipeline, 75 timelineData, 76 abtChromeExtensionProtocol, 77 crossToolProtocol, 78 appComponent, 79 snackBarOpener, 80 new MockStorage() 81 ); 82 mediator.setTimelineComponent(timelineComponent); 83 mediator.setUploadTracesComponent(uploadTracesComponent); 84 mediator.setCollectTracesComponent(collectTracesComponent); 85 86 spyOn(ViewerFactory.prototype, 'createViewers').and.returnValue([viewerStub]); 87 }); 88 89 it('handles uploaded traces from Winscope', async () => { 90 const spies = [ 91 spyOn(uploadTracesComponent, 'onProgressUpdate'), 92 spyOn(uploadTracesComponent, 'onOperationFinished'), 93 spyOn(timelineData, 'initialize').and.callThrough(), 94 spyOn(appComponent, 'onTraceDataLoaded'), 95 spyOn(viewerStub, 'onTracePositionUpdate'), 96 spyOn(timelineComponent, 'onTracePositionUpdate'), 97 spyOn(crossToolProtocol, 'sendTimestamp'), 98 ]; 99 100 await mediator.onWinscopeFilesUploaded(inputFiles); 101 102 expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalled(); 103 expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalled(); 104 expect(timelineData.initialize).not.toHaveBeenCalled(); 105 expect(appComponent.onTraceDataLoaded).not.toHaveBeenCalled(); 106 expect(viewerStub.onTracePositionUpdate).not.toHaveBeenCalled(); 107 108 spies.forEach((spy) => { 109 spy.calls.reset(); 110 }); 111 await mediator.onWinscopeViewTracesRequest(); 112 113 expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalled(); 114 expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalled(); 115 expect(timelineData.initialize).toHaveBeenCalledTimes(1); 116 expect(appComponent.onTraceDataLoaded).toHaveBeenCalledOnceWith([viewerStub]); 117 118 // propagates trace position on viewers creation 119 expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1); 120 expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1); 121 expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0); 122 }); 123 124 it('handles collected traces from Winscope', async () => { 125 const spies = [ 126 spyOn(collectTracesComponent, 'onProgressUpdate'), 127 spyOn(collectTracesComponent, 'onOperationFinished'), 128 spyOn(timelineData, 'initialize').and.callThrough(), 129 spyOn(appComponent, 'onTraceDataLoaded'), 130 spyOn(viewerStub, 'onTracePositionUpdate'), 131 spyOn(timelineComponent, 'onTracePositionUpdate'), 132 spyOn(crossToolProtocol, 'sendTimestamp'), 133 ]; 134 135 await mediator.onWinscopeFilesCollected(inputFiles); 136 137 expect(collectTracesComponent.onProgressUpdate).toHaveBeenCalled(); 138 expect(collectTracesComponent.onOperationFinished).toHaveBeenCalled(); 139 expect(timelineData.initialize).toHaveBeenCalledTimes(1); 140 expect(appComponent.onTraceDataLoaded).toHaveBeenCalledOnceWith([viewerStub]); 141 142 // propagates trace position on viewers creation 143 expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1); 144 expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1); 145 expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0); 146 }); 147 148 //TODO: test "bugreport data from cross-tool protocol" when FileUtils is fully compatible with 149 // Node.js (b/262269229). FileUtils#unzipFile() currently can't execute on Node.js. 150 151 //TODO: test "data from ABT chrome extension" when FileUtils is fully compatible with Node.js 152 // (b/262269229). 153 154 it('handles start download event from ABT chrome extension', () => { 155 spyOn(uploadTracesComponent, 'onProgressUpdate'); 156 expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(0); 157 158 abtChromeExtensionProtocol.onBuganizerAttachmentsDownloadStart(); 159 expect(uploadTracesComponent.onProgressUpdate).toHaveBeenCalledTimes(1); 160 }); 161 162 it('handles empty downloaded files from ABT chrome extension', async () => { 163 spyOn(uploadTracesComponent, 'onOperationFinished'); 164 expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(0); 165 166 // Pass files even if empty so that the upload component will update the progress bar 167 // and display error messages 168 await abtChromeExtensionProtocol.onBuganizerAttachmentsDownloaded([]); 169 expect(uploadTracesComponent.onOperationFinished).toHaveBeenCalledTimes(1); 170 }); 171 172 it('propagates trace position update from timeline component', async () => { 173 await loadTraceFiles(); 174 await mediator.onWinscopeViewTracesRequest(); 175 176 spyOn(viewerStub, 'onTracePositionUpdate'); 177 spyOn(timelineComponent, 'onTracePositionUpdate'); 178 spyOn(crossToolProtocol, 'sendTimestamp'); 179 expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(0); 180 expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0); 181 expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0); 182 183 // notify position 184 await mediator.onTimelineTracePositionUpdate(POSITION_10); 185 expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1); 186 expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1); 187 expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(1); 188 189 // notify position 190 await mediator.onTimelineTracePositionUpdate(POSITION_11); 191 expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(2); 192 expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(2); 193 expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(2); 194 }); 195 196 describe('timestamp received from remote tool', () => { 197 it('propagates trace position update', async () => { 198 await loadTraceFiles(); 199 await mediator.onWinscopeViewTracesRequest(); 200 201 spyOn(viewerStub, 'onTracePositionUpdate'); 202 spyOn(timelineComponent, 'onTracePositionUpdate'); 203 expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(0); 204 expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0); 205 206 // receive timestamp 207 await crossToolProtocol.onTimestampReceived(TIMESTAMP_10); 208 expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1); 209 expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(1); 210 211 // receive timestamp 212 await crossToolProtocol.onTimestampReceived(TIMESTAMP_11); 213 expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(2); 214 expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(2); 215 }); 216 217 it("doesn't propagate timestamp back to remote tool", async () => { 218 await loadTraceFiles(); 219 await mediator.onWinscopeViewTracesRequest(); 220 221 spyOn(viewerStub, 'onTracePositionUpdate'); 222 spyOn(crossToolProtocol, 'sendTimestamp'); 223 224 // receive timestamp 225 await crossToolProtocol.onTimestampReceived(TIMESTAMP_10); 226 expect(viewerStub.onTracePositionUpdate).toHaveBeenCalledTimes(1); 227 expect(crossToolProtocol.sendTimestamp).toHaveBeenCalledTimes(0); 228 }); 229 230 it('defers propagation till traces are loaded and visualized', async () => { 231 spyOn(timelineComponent, 'onTracePositionUpdate'); 232 233 // keep timestamp for later 234 await crossToolProtocol.onTimestampReceived(TIMESTAMP_10); 235 expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0); 236 237 // keep timestamp for later (replace previous one) 238 await crossToolProtocol.onTimestampReceived(TIMESTAMP_11); 239 expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledTimes(0); 240 241 // apply timestamp 242 await loadTraceFiles(); 243 await mediator.onWinscopeViewTracesRequest(); 244 expect(timelineComponent.onTracePositionUpdate).toHaveBeenCalledWith(POSITION_11); 245 }); 246 }); 247 248 const loadTraceFiles = async () => { 249 const traceFiles = inputFiles.map((file) => new TraceFile(file)); 250 const errors = await tracePipeline.loadTraceFiles(traceFiles); 251 expect(errors).toEqual([]); 252 }; 253}); 254