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 */ 16import {ComponentFixture, TestBed} from '@angular/core/testing'; 17import {MatCardModule} from '@angular/material/card'; 18import {MatIconModule} from '@angular/material/icon'; 19import {MatListModule} from '@angular/material/list'; 20import {MatProgressBarModule} from '@angular/material/progress-bar'; 21import {MatSnackBar, MatSnackBarModule} from '@angular/material/snack-bar'; 22import {MatTooltipModule} from '@angular/material/tooltip'; 23import {By} from '@angular/platform-browser'; 24import {FilesSource} from 'app/files_source'; 25import {TracePipeline} from 'app/trace_pipeline'; 26import {assertDefined} from 'common/assert_utils'; 27import {TimestampConverterUtils} from 'common/time/test_utils'; 28import { 29 AppTraceViewRequest, 30 AppTraceViewRequestHandled, 31} from 'messaging/winscope_event'; 32import {getFixtureFile} from 'test/unit/fixture_utils'; 33import {TraceBuilder} from 'test/unit/trace_builder'; 34import {Traces} from 'trace/traces'; 35import {LoadProgressComponent} from './load_progress_component'; 36import {UploadTracesComponent} from './upload_traces_component'; 37 38describe('UploadTracesComponent', () => { 39 const uploadSelector = '.upload-btn'; 40 const clearAllSelector = '.clear-all-btn'; 41 const viewTracesSelector = '.load-btn'; 42 const removeTraceSelector = '.uploaded-files button'; 43 let fixture: ComponentFixture<UploadTracesComponent>; 44 let component: UploadTracesComponent; 45 let htmlElement: HTMLElement; 46 let validSfFile: File; 47 let validWmFile: File; 48 49 beforeEach(async () => { 50 await TestBed.configureTestingModule({ 51 imports: [ 52 MatCardModule, 53 MatSnackBarModule, 54 MatListModule, 55 MatIconModule, 56 MatProgressBarModule, 57 MatTooltipModule, 58 ], 59 providers: [MatSnackBar], 60 declarations: [UploadTracesComponent, LoadProgressComponent], 61 }).compileComponents(); 62 fixture = TestBed.createComponent(UploadTracesComponent); 63 component = fixture.componentInstance; 64 htmlElement = fixture.nativeElement; 65 component.tracePipeline = new TracePipeline(); 66 validSfFile = await getFixtureFile( 67 'traces/elapsed_and_real_timestamp/SurfaceFlinger.pb', 68 ); 69 validWmFile = await getFixtureFile( 70 'traces/elapsed_and_real_timestamp/WindowManager.pb', 71 ); 72 fixture.detectChanges(); 73 }); 74 75 it('can be created', () => { 76 expect(component).toBeTruthy(); 77 }); 78 79 it('renders the expected card title', () => { 80 expect(htmlElement.querySelector('.title')?.innerHTML).toContain( 81 'Upload Traces', 82 ); 83 }); 84 85 it('handles file upload via drag and drop', () => { 86 const spy = spyOn(component.filesUploaded, 'emit'); 87 dropFileAndGetTransferredFiles(false); 88 expect(spy).not.toHaveBeenCalled(); 89 const files = dropFileAndGetTransferredFiles(); 90 expect(spy).toHaveBeenCalledOnceWith(files); 91 }); 92 93 it('handles file upload via upload button click', async () => { 94 await loadFiles([validSfFile]); 95 const spy = spyOn(component.filesUploaded, 'emit'); 96 addFileByClickAndGetTransferredFiles(false); 97 expect(spy).not.toHaveBeenCalled(); 98 const files = addFileByClickAndGetTransferredFiles(); 99 expect(spy).toHaveBeenCalledOnceWith(files); 100 }); 101 102 it('displays only load progress bar on progress update (no existing files)', () => { 103 component.onProgressUpdate(undefined, undefined); 104 fixture.detectChanges(); 105 checkOnlyProgressBarShowing(); 106 107 component.onOperationFinished(); 108 fixture.detectChanges(); 109 expect(htmlElement.querySelector('load-progress')).toBeNull(); 110 assertDefined(htmlElement.querySelector('.drop-info')); 111 }); 112 113 it('displays only load progress bar on progress update (existing files)', async () => { 114 await loadFiles([validSfFile]); 115 component.onProgressUpdate(undefined, undefined); 116 fixture.detectChanges(); 117 checkOnlyProgressBarShowing(); 118 119 component.onOperationFinished(); 120 fixture.detectChanges(); 121 expect(htmlElement.querySelector('load-progress')).toBeNull(); 122 assertDefined(htmlElement.querySelector('.trace-actions-container')); 123 assertDefined(htmlElement.querySelector('.uploaded-files')); 124 }); 125 126 it('shows progress bar with custom message', () => { 127 component.onProgressUpdate('Updating', undefined); 128 fixture.detectChanges(); 129 checkOnlyProgressBarShowing('Updating'); 130 }); 131 132 it('updates progress bar percentage only if sufficient time has passed', () => { 133 component.onProgressUpdate(undefined, 10); 134 fixture.detectChanges(); 135 const progressBar = fixture.debugElement.query( 136 By.directive(LoadProgressComponent), 137 ).componentInstance as LoadProgressComponent; 138 expect(progressBar.progressPercentage).toEqual(10); 139 140 component.onProgressUpdate(undefined, 20); 141 fixture.detectChanges(); 142 expect(progressBar.progressPercentage).toEqual(10); 143 144 const now = Date.now(); 145 spyOn(Date, 'now').and.returnValue(now + 500); 146 component.onProgressUpdate(undefined, 20); 147 fixture.detectChanges(); 148 expect(progressBar.progressPercentage).toEqual(20); 149 }); 150 151 it('can display uploaded traces', async () => { 152 await loadFiles([validSfFile]); 153 assertDefined(htmlElement.querySelector('.uploaded-files')); 154 assertDefined(htmlElement.querySelector('.trace-actions-container')); 155 }); 156 157 it('can remove one of two uploaded traces', async () => { 158 await loadFiles([validSfFile, validWmFile]); 159 expect(component.tracePipeline?.getTraces().getSize()).toBe(2); 160 161 const spy = spyOn(component, 'onOperationFinished'); 162 removeTrace(); 163 assertDefined(htmlElement.querySelector('.uploaded-files')); 164 expect(spy).toHaveBeenCalled(); 165 expect(component.tracePipeline?.getTraces().getSize()).toBe(1); 166 }); 167 168 it('handles removal of the only uploaded trace', async () => { 169 await loadFiles([validSfFile]); 170 171 const spy = spyOn(component, 'onOperationFinished'); 172 removeTrace(); 173 assertDefined(htmlElement.querySelector('.drop-info')); 174 expect(spy).toHaveBeenCalled(); 175 expect(component.tracePipeline?.getTraces().getSize()).toBe(0); 176 }); 177 178 it('can remove all uploaded traces', async () => { 179 await loadFiles([validSfFile, validWmFile]); 180 expect(component.tracePipeline?.getTraces().getSize()).toBe(2); 181 182 const spy = spyOn(component, 'onOperationFinished'); 183 const clearAllButton = getButton(clearAllSelector); 184 clearAllButton.click(); 185 fixture.detectChanges(); 186 assertDefined(htmlElement.querySelector('.drop-info')); 187 expect(spy).toHaveBeenCalled(); 188 expect(component.tracePipeline?.getTraces().getSize()).toBe(0); 189 }); 190 191 it('can emit view traces event', async () => { 192 await loadFiles([validSfFile]); 193 194 const spy = spyOn(component.viewTracesButtonClick, 'emit'); 195 getButton(viewTracesSelector).click(); 196 fixture.detectChanges(); 197 expect(spy).toHaveBeenCalled(); 198 }); 199 200 it('shows warning elements for traces without visualization', async () => { 201 const shellTransitionFile = await getFixtureFile( 202 'traces/elapsed_and_real_timestamp/shell_transition_trace.pb', 203 ); 204 await loadFiles([shellTransitionFile]); 205 206 expect(htmlElement.querySelector('.warning-icon')).toBeTruthy(); 207 expect(getButton(viewTracesSelector).disabled).toBeTrue(); 208 }); 209 210 it('shows error elements for corrupted traces', async () => { 211 const corruptedTrace = new TraceBuilder<string>() 212 .setEntries(['entry-0']) 213 .setTimestamps([TimestampConverterUtils.makeZeroTimestamp()]) 214 .build(); 215 corruptedTrace.setCorruptedState(true); 216 const traces = new Traces(); 217 traces.addTrace(corruptedTrace); 218 spyOn(assertDefined(component.tracePipeline), 'getTraces').and.returnValue( 219 traces, 220 ); 221 fixture.detectChanges(); 222 223 expect(htmlElement.querySelector('.error-icon')).toBeTruthy(); 224 expect(getButton(viewTracesSelector).disabled).toBeTrue(); 225 }); 226 227 it('emits download traces event', async () => { 228 await loadFiles([validSfFile]); 229 230 const spy = spyOn(component.downloadTracesClick, 'emit'); 231 const downloadTracesButton = assertDefined( 232 htmlElement.querySelector<HTMLElement>('.download-btn'), 233 ); 234 downloadTracesButton.click(); 235 fixture.detectChanges(); 236 expect(spy).toHaveBeenCalled(); 237 }); 238 239 it('disables edit/view traces functionality on trace view request events', async () => { 240 await loadFiles([validSfFile]); 241 const buttons = [ 242 getButton(viewTracesSelector), 243 getButton(removeTraceSelector), 244 getButton(clearAllSelector), 245 getButton(uploadSelector), 246 ]; 247 const dropBox = assertDefined( 248 htmlElement.querySelector<HTMLElement>('.drop-box'), 249 ); 250 const spy = spyOn(component.filesUploaded, 'emit'); 251 252 await component.onWinscopeEvent(new AppTraceViewRequest()); 253 fixture.detectChanges(); 254 buttons.forEach((button) => { 255 expect(button.disabled).toBeTrue(); 256 }); 257 dropFileAndGetTransferredFiles(); 258 addFileByClickAndGetTransferredFiles(true, dropBox); 259 expect(spy).not.toHaveBeenCalled(); 260 261 await component.onWinscopeEvent(new AppTraceViewRequestHandled()); 262 fixture.detectChanges(); 263 buttons.forEach((button) => { 264 expect(button.disabled).toBeFalse(); 265 }); 266 const files = dropFileAndGetTransferredFiles(); 267 expect(spy).toHaveBeenCalledOnceWith(files); 268 spy.calls.reset(); 269 addFileByClickAndGetTransferredFiles(true, dropBox); 270 expect(spy).toHaveBeenCalledOnceWith(files); 271 }); 272 273 async function loadFiles(files: File[]) { 274 const tracePipeline = assertDefined(component.tracePipeline); 275 tracePipeline.clear(); 276 await tracePipeline.loadFiles(files, FilesSource.TEST, undefined); 277 fixture.detectChanges(); 278 } 279 280 function dropFileAndGetTransferredFiles(withFile = true): File[] { 281 const dropbox = assertDefined(htmlElement.querySelector('.drop-box')); 282 let dataTransfer: DataTransfer | undefined; 283 if (withFile) { 284 dataTransfer = new DataTransfer(); 285 dataTransfer.items.add(validSfFile); 286 } 287 dropbox.dispatchEvent(new DragEvent('drop', {dataTransfer})); 288 fixture.detectChanges(); 289 return Array.from(dataTransfer?.files ?? []); 290 } 291 292 function addFileByClickAndGetTransferredFiles( 293 withFile = true, 294 clickEl: HTMLElement = getButton(uploadSelector), 295 ): File[] { 296 const dataTransfer = new DataTransfer(); 297 if (withFile) dataTransfer.items.add(validSfFile); 298 const fileList = dataTransfer.files; 299 300 const fileInput = assertDefined( 301 htmlElement.querySelector<HTMLInputElement>('.drop-box input'), 302 ); 303 clickEl.addEventListener('click', () => { 304 fileInput.files = fileList; 305 }); 306 307 clickEl.click(); 308 fixture.detectChanges(); 309 fileInput.dispatchEvent(new Event('change')); 310 fixture.detectChanges(); 311 return Array.from(fileList); 312 } 313 314 function removeTrace() { 315 getButton(removeTraceSelector).click(); 316 fixture.detectChanges(); 317 } 318 319 function getButton(selector: string): HTMLButtonElement { 320 return assertDefined( 321 htmlElement.querySelector<HTMLButtonElement>(selector), 322 ); 323 } 324 325 function checkOnlyProgressBarShowing(expectedMessage = 'Loading...') { 326 const progressBar = assertDefined( 327 htmlElement.querySelector('load-progress'), 328 ); 329 expect(progressBar.textContent).toEqual(expectedMessage); 330 expect(htmlElement.querySelector('.trace-actions-container')).toBeNull(); 331 expect(htmlElement.querySelector('.uploaded-files')).toBeNull(); 332 expect(htmlElement.querySelector('.drop-info')).toBeNull(); 333 } 334}); 335