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 {ClipboardModule} from '@angular/cdk/clipboard'; 17import {OverlayModule} from '@angular/cdk/overlay'; 18import {CommonModule} from '@angular/common'; 19import {HttpClientModule} from '@angular/common/http'; 20import {ChangeDetectionStrategy} from '@angular/core'; 21import { 22 ComponentFixture, 23 ComponentFixtureAutoDetect, 24 TestBed, 25} from '@angular/core/testing'; 26import { 27 FormControl, 28 FormsModule, 29 ReactiveFormsModule, 30 Validators, 31} from '@angular/forms'; 32import {MatButtonModule} from '@angular/material/button'; 33import {MatCardModule} from '@angular/material/card'; 34import {MatDialogModule} from '@angular/material/dialog'; 35import {MatDividerModule} from '@angular/material/divider'; 36import {MatFormFieldModule} from '@angular/material/form-field'; 37import {MatIconModule} from '@angular/material/icon'; 38import {MatInputModule} from '@angular/material/input'; 39import {MatListModule} from '@angular/material/list'; 40import {MatProgressBarModule} from '@angular/material/progress-bar'; 41import {MatSelectModule} from '@angular/material/select'; 42import {MatSliderModule} from '@angular/material/slider'; 43import {MatSnackBarModule} from '@angular/material/snack-bar'; 44import {MatToolbarModule} from '@angular/material/toolbar'; 45import {MatTooltipModule} from '@angular/material/tooltip'; 46import {Title} from '@angular/platform-browser'; 47import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 48import {assertDefined} from 'common/assert_utils'; 49import {FileUtils} from 'common/file_utils'; 50import {UserNotifier} from 'common/user_notifier'; 51import { 52 FailedToInitializeTimelineData, 53 NoValidFiles, 54} from 'messaging/user_warnings'; 55import { 56 AppRefreshDumpsRequest, 57 ViewersLoaded, 58 ViewersUnloaded, 59} from 'messaging/winscope_event'; 60import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 61import {TracesBuilder} from 'test/unit/traces_builder'; 62import {waitToBeCalled} from 'test/utils'; 63import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component'; 64import {AdbProxyComponent} from './adb_proxy_component'; 65import {AppComponent} from './app_component'; 66import { 67 MatDrawer, 68 MatDrawerContainer, 69 MatDrawerContent, 70} from './bottomnav/bottom_drawer_component'; 71import {CollectTracesComponent} from './collect_traces_component'; 72import {ShortcutsComponent} from './shortcuts_component'; 73import {SnackBarComponent} from './snack_bar_component'; 74import {MiniTimelineComponent} from './timeline/mini-timeline/mini_timeline_component'; 75import {TimelineComponent} from './timeline/timeline_component'; 76import {TraceConfigComponent} from './trace_config_component'; 77import {TraceViewComponent} from './trace_view_component'; 78import {UploadTracesComponent} from './upload_traces_component'; 79import {WebAdbComponent} from './web_adb_component'; 80 81describe('AppComponent', () => { 82 let fixture: ComponentFixture<AppComponent>; 83 let component: AppComponent; 84 let htmlElement: HTMLElement; 85 let downloadTracesSpy: jasmine.Spy; 86 87 beforeEach(async () => { 88 await TestBed.configureTestingModule({ 89 providers: [Title, {provide: ComponentFixtureAutoDetect, useValue: true}], 90 imports: [ 91 CommonModule, 92 FormsModule, 93 MatCardModule, 94 MatButtonModule, 95 MatDividerModule, 96 MatFormFieldModule, 97 MatIconModule, 98 MatSelectModule, 99 MatSliderModule, 100 MatSnackBarModule, 101 MatToolbarModule, 102 MatTooltipModule, 103 ReactiveFormsModule, 104 MatInputModule, 105 BrowserAnimationsModule, 106 ClipboardModule, 107 MatDialogModule, 108 HttpClientModule, 109 MatListModule, 110 MatProgressBarModule, 111 OverlayModule, 112 ], 113 declarations: [ 114 AdbProxyComponent, 115 AppComponent, 116 CollectTracesComponent, 117 MatDrawer, 118 MatDrawerContainer, 119 MatDrawerContent, 120 MiniTimelineComponent, 121 TimelineComponent, 122 TraceConfigComponent, 123 TraceViewComponent, 124 UploadTracesComponent, 125 ViewerSurfaceFlingerComponent, 126 WebAdbComponent, 127 ShortcutsComponent, 128 SnackBarComponent, 129 ], 130 }) 131 .overrideComponent(AppComponent, { 132 set: {changeDetection: ChangeDetectionStrategy.Default}, 133 }) 134 .compileComponents(); 135 fixture = TestBed.createComponent(AppComponent); 136 component = fixture.componentInstance; 137 htmlElement = fixture.nativeElement; 138 component.filenameFormControl = new FormControl( 139 'winscope', 140 Validators.compose([ 141 Validators.required, 142 Validators.pattern(FileUtils.DOWNLOAD_FILENAME_REGEX), 143 ]), 144 ); 145 downloadTracesSpy = spyOn(component, 'downloadTraces'); 146 fixture.detectChanges(); 147 }); 148 149 it('can be created', () => { 150 expect(component).toBeTruthy(); 151 }); 152 153 it('has the expected title', () => { 154 expect(component.title).toEqual('winscope'); 155 }); 156 157 it('shows permanent header items on homepage', () => { 158 checkPermanentHeaderItems(); 159 }); 160 161 it('displays correct elements when no data loaded', () => { 162 component.dataLoaded = false; 163 component.showDataLoadedElements = false; 164 fixture.detectChanges(); 165 checkHomepage(); 166 }); 167 168 it('displays correct elements when data loaded', () => { 169 goToTraceView(); 170 checkTraceViewPage(); 171 172 spyOn(component, 'dumpsUploaded').and.returnValue(true); 173 fixture.detectChanges(); 174 expect(htmlElement.querySelector('.refresh-dumps')).toBeTruthy(); 175 }); 176 177 it('returns to homepage on upload new button click', async () => { 178 goToTraceView(); 179 checkTraceViewPage(); 180 181 assertDefined( 182 htmlElement.querySelector<HTMLButtonElement>('.upload-new'), 183 ).click(); 184 fixture.detectChanges(); 185 await fixture.whenStable(); 186 fixture.detectChanges(); 187 await fixture.whenStable(); 188 checkHomepage(); 189 }); 190 191 it('sends event on refresh dumps button click', async () => { 192 spyOn(component, 'dumpsUploaded').and.returnValue(true); 193 goToTraceView(); 194 checkTraceViewPage(); 195 196 const winscopeEventSpy = spyOn( 197 component.mediator, 198 'onWinscopeEvent', 199 ).and.callThrough(); 200 assertDefined( 201 htmlElement.querySelector<HTMLButtonElement>('.refresh-dumps'), 202 ).click(); 203 fixture.detectChanges(); 204 await fixture.whenStable(); 205 fixture.detectChanges(); 206 await fixture.whenStable(); 207 checkHomepage(); 208 expect(winscopeEventSpy).toHaveBeenCalledWith(new AppRefreshDumpsRequest()); 209 }); 210 211 it('shows download progress bar', () => { 212 component.showDataLoadedElements = true; 213 fixture.detectChanges(); 214 expect( 215 htmlElement.querySelector('.download-files-section mat-progress-bar'), 216 ).toBeNull(); 217 218 component.onProgressUpdate('Progress update', 10); 219 fixture.detectChanges(); 220 expect( 221 htmlElement.querySelector('.download-files-section mat-progress-bar'), 222 ).toBeTruthy(); 223 224 component.onOperationFinished(true); 225 fixture.detectChanges(); 226 expect( 227 htmlElement.querySelector('.download-files-section mat-progress-bar'), 228 ).toBeNull(); 229 }); 230 231 it('downloads traces on download button click and shows download progress bar', async () => { 232 component.showDataLoadedElements = true; 233 fixture.detectChanges(); 234 clickDownloadTracesButton(); 235 expect( 236 htmlElement.querySelector('.download-files-section mat-progress-bar'), 237 ).toBeTruthy(); 238 await waitToBeCalled(downloadTracesSpy); 239 }); 240 241 it('downloads traces after valid file name change', async () => { 242 component.showDataLoadedElements = true; 243 fixture.detectChanges(); 244 245 clickEditFilenameButton(); 246 updateFilenameInputAndDownloadTraces('Winscope2', true); 247 await waitToBeCalled(downloadTracesSpy); 248 expect(downloadTracesSpy).toHaveBeenCalledOnceWith( 249 jasmine.any(Blob), 250 'Winscope2.zip', 251 ); 252 253 downloadTracesSpy.calls.reset(); 254 255 // check it works twice in a row 256 clickEditFilenameButton(); 257 updateFilenameInputAndDownloadTraces('win_scope', true); 258 await waitToBeCalled(downloadTracesSpy); 259 expect(downloadTracesSpy).toHaveBeenCalledOnceWith( 260 jasmine.any(Blob), 261 'win_scope.zip', 262 ); 263 }); 264 265 it('changes page title based on archive name', async () => { 266 const pageTitle = TestBed.inject(Title); 267 component.timelineData.initialize( 268 new TracesBuilder().build(), 269 undefined, 270 TimestampConverterUtils.TIMESTAMP_CONVERTER, 271 ); 272 273 await component.onWinscopeEvent(new ViewersUnloaded()); 274 expect(pageTitle.getTitle()).toBe('Winscope'); 275 276 component.tracePipeline.getDownloadArchiveFilename = jasmine 277 .createSpy() 278 .and.returnValue('test_archive'); 279 await component.onWinscopeEvent(new ViewersLoaded([])); 280 fixture.detectChanges(); 281 expect(pageTitle.getTitle()).toBe('Winscope | test_archive'); 282 }); 283 284 it('does not download traces if invalid file name chosen', () => { 285 component.showDataLoadedElements = true; 286 fixture.detectChanges(); 287 288 clickEditFilenameButton(); 289 updateFilenameInputAndDownloadTraces('w?n$cope', false); 290 expect(downloadTracesSpy).not.toHaveBeenCalled(); 291 }); 292 293 it('behaves as expected when entering valid then invalid then valid file names', async () => { 294 component.showDataLoadedElements = true; 295 fixture.detectChanges(); 296 297 clickEditFilenameButton(); 298 updateFilenameInputAndDownloadTraces('Winscope2', true); 299 await waitToBeCalled(downloadTracesSpy); 300 expect(downloadTracesSpy).toHaveBeenCalledOnceWith( 301 jasmine.any(Blob), 302 'Winscope2.zip', 303 ); 304 downloadTracesSpy.calls.reset(); 305 306 clickEditFilenameButton(); 307 updateFilenameInputAndDownloadTraces('w?n$cope', false); 308 expect(downloadTracesSpy).not.toHaveBeenCalled(); 309 310 updateFilenameInputAndDownloadTraces('win.scope', true); 311 await waitToBeCalled(downloadTracesSpy); 312 expect(downloadTracesSpy).toHaveBeenCalledOnceWith( 313 jasmine.any(Blob), 314 'win.scope.zip', 315 ); 316 }); 317 318 it('validates filename on enter key, escape key or focus out', () => { 319 const spy = spyOn(component, 'trySubmitFilename'); 320 321 component.showDataLoadedElements = true; 322 fixture.detectChanges(); 323 clickEditFilenameButton(); 324 const inputField = assertDefined( 325 htmlElement.querySelector('.file-name-input-field'), 326 ); 327 const inputEl = assertDefined( 328 htmlElement.querySelector<HTMLInputElement>( 329 '.file-name-input-field input', 330 ), 331 ); 332 inputEl.value = 'valid_file_name'; 333 334 inputField.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'})); 335 fixture.detectChanges(); 336 expect(spy).toHaveBeenCalledTimes(1); 337 338 inputField.dispatchEvent(new KeyboardEvent('keydown', {key: 'Escape'})); 339 fixture.detectChanges(); 340 expect(spy).toHaveBeenCalledTimes(2); 341 342 inputField.dispatchEvent(new FocusEvent('focusout')); 343 fixture.detectChanges(); 344 expect(spy).toHaveBeenCalledTimes(3); 345 }); 346 347 it('downloads traces from upload traces section', () => { 348 const traces = assertDefined(component.tracePipeline.getTraces()); 349 spyOn(traces, 'getSize').and.returnValue(1); 350 fixture.detectChanges(); 351 const downloadButtonClickSpy = spyOn( 352 component, 353 'onDownloadTracesButtonClick', 354 ); 355 356 const downloadButton = assertDefined( 357 htmlElement.querySelector<HTMLElement>('upload-traces .download-btn'), 358 ); 359 downloadButton.click(); 360 fixture.detectChanges(); 361 expect(downloadButtonClickSpy).toHaveBeenCalledOnceWith( 362 component.uploadTracesComponent, 363 ); 364 }); 365 366 it('opens shortcuts dialog', () => { 367 expect(document.querySelector('shortcuts-panel')).toBeFalsy(); 368 const shortcutsButton = assertDefined( 369 htmlElement.querySelector<HTMLElement>('.shortcuts'), 370 ); 371 shortcutsButton.click(); 372 fixture.detectChanges(); 373 expect(document.querySelector('shortcuts-panel')).toBeTruthy(); 374 }); 375 376 it('sets snackbar opener to global user notifier', () => { 377 expect(document.querySelector('snack-bar')).toBeFalsy(); 378 UserNotifier.add(new NoValidFiles()); 379 UserNotifier.notify(); 380 expect(document.querySelector('snack-bar')).toBeTruthy(); 381 }); 382 383 it('does not open new snackbar until existing snackbar has been dismissed', async () => { 384 expect(document.querySelector('snack-bar')).toBeFalsy(); 385 const firstMessage = new NoValidFiles(); 386 UserNotifier.add(firstMessage); 387 UserNotifier.notify(); 388 fixture.detectChanges(); 389 await fixture.whenRenderingDone(); 390 let snackbar = assertDefined(document.querySelector('snack-bar')); 391 expect(snackbar.textContent).toContain(firstMessage.getMessage()); 392 393 const secondMessage = new FailedToInitializeTimelineData(); 394 UserNotifier.add(secondMessage); 395 UserNotifier.notify(); 396 fixture.detectChanges(); 397 await fixture.whenRenderingDone(); 398 snackbar = assertDefined(document.querySelector('snack-bar')); 399 expect(snackbar.textContent).toContain(firstMessage.getMessage()); 400 401 const closeButton = assertDefined( 402 snackbar.querySelector<HTMLElement>('.snack-bar-action'), 403 ); 404 closeButton.click(); 405 fixture.detectChanges(); 406 await fixture.whenRenderingDone(); 407 snackbar = assertDefined(document.querySelector('snack-bar')); 408 expect(snackbar.textContent).toContain(secondMessage.getMessage()); 409 }); 410 411 function goToTraceView() { 412 component.dataLoaded = true; 413 component.showDataLoadedElements = true; 414 component.timelineData.initialize( 415 new TracesBuilder().build(), 416 undefined, 417 TimestampConverterUtils.TIMESTAMP_CONVERTER, 418 ); 419 fixture.detectChanges(); 420 } 421 422 function updateFilenameInputAndDownloadTraces(name: string, valid: boolean) { 423 const inputEl = assertDefined( 424 htmlElement.querySelector<HTMLInputElement>( 425 '.file-name-input-field input', 426 ), 427 ); 428 const checkButton = assertDefined( 429 htmlElement.querySelector('.check-button'), 430 ); 431 inputEl.value = name; 432 inputEl.dispatchEvent(new Event('input')); 433 fixture.detectChanges(); 434 checkButton.dispatchEvent(new Event('click')); 435 fixture.detectChanges(); 436 437 const saveButton = assertDefined( 438 htmlElement.querySelector<HTMLButtonElement>('.save-button'), 439 ); 440 if (valid) { 441 assertDefined(htmlElement.querySelector('.download-file-info')); 442 expect(saveButton.disabled).toBeFalse(); 443 clickDownloadTracesButton(); 444 } else { 445 expect(htmlElement.querySelector('.download-file-info')).toBeFalsy(); 446 expect(saveButton.disabled).toBeTrue(); 447 } 448 } 449 450 function clickDownloadTracesButton() { 451 const downloadButton = assertDefined( 452 htmlElement.querySelector('.save-button'), 453 ); 454 downloadButton.dispatchEvent(new Event('click')); 455 fixture.detectChanges(); 456 } 457 458 function clickEditFilenameButton() { 459 const pencilButton = assertDefined( 460 htmlElement.querySelector('.edit-button'), 461 ); 462 pencilButton.dispatchEvent(new Event('click')); 463 fixture.detectChanges(); 464 } 465 466 function checkHomepage() { 467 expect(htmlElement.querySelector('.welcome-info')).toBeTruthy(); 468 expect(htmlElement.querySelector('.collect-traces-card')).toBeTruthy(); 469 expect(htmlElement.querySelector('.upload-traces-card')).toBeTruthy(); 470 expect(htmlElement.querySelector('.viewers')).toBeFalsy(); 471 expect(htmlElement.querySelector('.upload-new')).toBeFalsy(); 472 expect(htmlElement.querySelector('timeline')).toBeFalsy(); 473 checkPermanentHeaderItems(); 474 } 475 476 function checkTraceViewPage() { 477 expect(htmlElement.querySelector('.welcome-info')).toBeFalsy(); 478 expect(htmlElement.querySelector('.save-button')).toBeTruthy(); 479 expect(htmlElement.querySelector('.collect-traces-card')).toBeFalsy(); 480 expect(htmlElement.querySelector('.upload-traces-card')).toBeFalsy(); 481 expect(htmlElement.querySelector('.viewers')).toBeTruthy(); 482 expect(htmlElement.querySelector('.upload-new')).toBeTruthy(); 483 expect(htmlElement.querySelector('timeline')).toBeTruthy(); 484 checkPermanentHeaderItems(); 485 } 486 487 function checkPermanentHeaderItems() { 488 expect(htmlElement.querySelector('.app-title')).toBeTruthy(); 489 expect(htmlElement.querySelector('.shortcuts')).toBeTruthy(); 490 expect(htmlElement.querySelector('.documentation')).toBeTruthy(); 491 expect(htmlElement.querySelector('.report-bug')).toBeTruthy(); 492 expect(htmlElement.querySelector('.dark-mode')).toBeTruthy(); 493 } 494}); 495