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