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 {CommonModule} from '@angular/common'; 18import {HttpClientModule} from '@angular/common/http'; 19import {ChangeDetectionStrategy} from '@angular/core'; 20import { 21 ComponentFixture, 22 ComponentFixtureAutoDetect, 23 TestBed, 24} from '@angular/core/testing'; 25import { 26 FormControl, 27 FormsModule, 28 ReactiveFormsModule, 29 Validators, 30} from '@angular/forms'; 31import {MatButtonModule} from '@angular/material/button'; 32import {MatCardModule} from '@angular/material/card'; 33import {MatDialogModule} from '@angular/material/dialog'; 34import {MatDividerModule} from '@angular/material/divider'; 35import {MatFormFieldModule} from '@angular/material/form-field'; 36import {MatIconModule} from '@angular/material/icon'; 37import {MatInputModule} from '@angular/material/input'; 38import {MatSelectModule} from '@angular/material/select'; 39import {MatSliderModule} from '@angular/material/slider'; 40import {MatSnackBarModule} from '@angular/material/snack-bar'; 41import {MatToolbarModule} from '@angular/material/toolbar'; 42import {MatTooltipModule} from '@angular/material/tooltip'; 43import {Title} from '@angular/platform-browser'; 44import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 45import {assertDefined} from 'common/assert_utils'; 46import {FileUtils} from 'common/file_utils'; 47import { 48 AppRefreshDumpsRequest, 49 ViewersLoaded, 50 ViewersUnloaded, 51} from 'messaging/winscope_event'; 52import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 53import {TracesBuilder} from 'test/unit/traces_builder'; 54import {ViewerSurfaceFlingerComponent} from 'viewers/viewer_surface_flinger/viewer_surface_flinger_component'; 55import {AdbProxyComponent} from './adb_proxy_component'; 56import {AppComponent} from './app_component'; 57import { 58 MatDrawer, 59 MatDrawerContainer, 60 MatDrawerContent, 61} from './bottomnav/bottom_drawer_component'; 62import {CollectTracesComponent} from './collect_traces_component'; 63import {ShortcutsComponent} from './shortcuts_component'; 64import {MiniTimelineComponent} from './timeline/mini-timeline/mini_timeline_component'; 65import {TimelineComponent} from './timeline/timeline_component'; 66import {TraceConfigComponent} from './trace_config_component'; 67import {TraceViewComponent} from './trace_view_component'; 68import {UploadTracesComponent} from './upload_traces_component'; 69import {WebAdbComponent} from './web_adb_component'; 70 71describe('AppComponent', () => { 72 let fixture: ComponentFixture<AppComponent>; 73 let component: AppComponent; 74 let htmlElement: HTMLElement; 75 76 beforeEach(async () => { 77 await TestBed.configureTestingModule({ 78 providers: [Title, {provide: ComponentFixtureAutoDetect, useValue: true}], 79 imports: [ 80 CommonModule, 81 FormsModule, 82 MatCardModule, 83 MatButtonModule, 84 MatDividerModule, 85 MatFormFieldModule, 86 MatIconModule, 87 MatSelectModule, 88 MatSliderModule, 89 MatSnackBarModule, 90 MatToolbarModule, 91 MatTooltipModule, 92 ReactiveFormsModule, 93 MatInputModule, 94 BrowserAnimationsModule, 95 ClipboardModule, 96 MatDialogModule, 97 HttpClientModule, 98 ], 99 declarations: [ 100 AdbProxyComponent, 101 AppComponent, 102 CollectTracesComponent, 103 MatDrawer, 104 MatDrawerContainer, 105 MatDrawerContent, 106 MiniTimelineComponent, 107 TimelineComponent, 108 TraceConfigComponent, 109 TraceViewComponent, 110 UploadTracesComponent, 111 ViewerSurfaceFlingerComponent, 112 WebAdbComponent, 113 ShortcutsComponent, 114 ], 115 }) 116 .overrideComponent(AppComponent, { 117 set: {changeDetection: ChangeDetectionStrategy.Default}, 118 }) 119 .compileComponents(); 120 fixture = TestBed.createComponent(AppComponent); 121 component = fixture.componentInstance; 122 htmlElement = fixture.nativeElement; 123 component.filenameFormControl = new FormControl( 124 'winscope', 125 Validators.compose([ 126 Validators.required, 127 Validators.pattern(FileUtils.DOWNLOAD_FILENAME_REGEX), 128 ]), 129 ); 130 fixture.detectChanges(); 131 }); 132 133 it('can be created', () => { 134 expect(component).toBeTruthy(); 135 }); 136 137 it('has the expected title', () => { 138 expect(component.title).toEqual('winscope'); 139 }); 140 141 it('shows permanent header items on homepage', () => { 142 checkPermanentHeaderItems(); 143 }); 144 145 it('displays correct elements when no data loaded', () => { 146 component.dataLoaded = false; 147 component.showDataLoadedElements = false; 148 fixture.detectChanges(); 149 checkHomepage(); 150 }); 151 152 it('displays correct elements when data loaded', () => { 153 goToTraceView(); 154 checkTraceViewPage(); 155 156 spyOn(component, 'dumpsUploaded').and.returnValue(true); 157 fixture.detectChanges(); 158 expect(htmlElement.querySelector('.refresh-dumps')).toBeTruthy(); 159 }); 160 161 it('returns to homepage on upload new button click', async () => { 162 goToTraceView(); 163 checkTraceViewPage(); 164 165 (htmlElement.querySelector('.upload-new') as HTMLButtonElement).click(); 166 fixture.detectChanges(); 167 await fixture.whenStable(); 168 checkHomepage(); 169 }); 170 171 it('sends event on refresh dumps button click', async () => { 172 spyOn(component, 'dumpsUploaded').and.returnValue(true); 173 goToTraceView(); 174 checkTraceViewPage(); 175 176 const winscopeEventSpy = spyOn( 177 component.mediator, 178 'onWinscopeEvent', 179 ).and.callThrough(); 180 (htmlElement.querySelector('.refresh-dumps') as HTMLButtonElement).click(); 181 fixture.detectChanges(); 182 await fixture.whenStable(); 183 checkHomepage(); 184 expect(winscopeEventSpy).toHaveBeenCalledWith(new AppRefreshDumpsRequest()); 185 }); 186 187 it('downloads traces on download button click', () => { 188 component.showDataLoadedElements = true; 189 fixture.detectChanges(); 190 const spy = spyOn(component, 'downloadTraces'); 191 192 clickDownloadTracesButton(); 193 expect(spy).toHaveBeenCalledTimes(1); 194 195 clickDownloadTracesButton(); 196 expect(spy).toHaveBeenCalledTimes(2); 197 }); 198 199 it('downloads traces after valid file name change', () => { 200 component.showDataLoadedElements = true; 201 fixture.detectChanges(); 202 const spy = spyOn(component, 'downloadTraces'); 203 204 clickEditFilenameButton(); 205 updateFilenameInputAndDownloadTraces('Winscope2', true); 206 expect(spy).toHaveBeenCalledTimes(1); 207 208 // check it works twice in a row 209 clickEditFilenameButton(); 210 updateFilenameInputAndDownloadTraces('win_scope', true); 211 expect(spy).toHaveBeenCalledTimes(2); 212 }); 213 214 it('changes page title based on archive name', async () => { 215 const pageTitle = TestBed.inject(Title); 216 component.timelineData.initialize( 217 new TracesBuilder().build(), 218 undefined, 219 TimestampConverterUtils.TIMESTAMP_CONVERTER, 220 ); 221 222 await component.onWinscopeEvent(new ViewersUnloaded()); 223 expect(pageTitle.getTitle()).toBe('Winscope'); 224 225 component.tracePipeline.getDownloadArchiveFilename = jasmine 226 .createSpy() 227 .and.returnValue('test_archive'); 228 await component.onWinscopeEvent(new ViewersLoaded([])); 229 fixture.detectChanges(); 230 expect(pageTitle.getTitle()).toBe('Winscope | test_archive'); 231 }); 232 233 it('does not download traces if invalid file name chosen', () => { 234 component.showDataLoadedElements = true; 235 fixture.detectChanges(); 236 const spy = spyOn(component, 'downloadTraces'); 237 238 clickEditFilenameButton(); 239 updateFilenameInputAndDownloadTraces('w?n$cope', false); 240 expect(spy).not.toHaveBeenCalled(); 241 }); 242 243 it('behaves as expected when entering valid then invalid then valid file names', () => { 244 component.showDataLoadedElements = true; 245 fixture.detectChanges(); 246 247 const spy = spyOn(component, 'downloadTraces'); 248 249 clickEditFilenameButton(); 250 updateFilenameInputAndDownloadTraces('Winscope2', true); 251 expect(spy).toHaveBeenCalled(); 252 253 clickEditFilenameButton(); 254 updateFilenameInputAndDownloadTraces('w?n$cope', false); 255 expect(spy).toHaveBeenCalledTimes(1); 256 257 updateFilenameInputAndDownloadTraces('win.scope', true); 258 expect(spy).toHaveBeenCalledTimes(2); 259 }); 260 261 it('validates filename on enter key', () => { 262 const spy = spyOn(component, 'onCheckIconClick'); 263 264 component.showDataLoadedElements = true; 265 fixture.detectChanges(); 266 267 clickEditFilenameButton(); 268 269 const inputField = assertDefined( 270 htmlElement.querySelector('.file-name-input-field'), 271 ); 272 const inputEl = assertDefined( 273 htmlElement.querySelector('.file-name-input-field input'), 274 ); 275 (inputEl as HTMLInputElement).value = 'valid_file_name'; 276 inputField.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'})); 277 278 fixture.detectChanges(); 279 expect(spy).toHaveBeenCalled(); 280 }); 281 282 it('opens shortcuts dialog', () => { 283 expect(document.querySelector('shortcuts-panel')).toBeFalsy(); 284 const shortcutsButton = assertDefined( 285 htmlElement.querySelector('.shortcuts'), 286 ) as HTMLElement; 287 shortcutsButton.click(); 288 fixture.detectChanges(); 289 expect(document.querySelector('shortcuts-panel')).toBeTruthy(); 290 }); 291 292 function goToTraceView() { 293 component.dataLoaded = true; 294 component.showDataLoadedElements = true; 295 component.timelineData.initialize( 296 new TracesBuilder().build(), 297 undefined, 298 TimestampConverterUtils.TIMESTAMP_CONVERTER, 299 ); 300 fixture.detectChanges(); 301 } 302 303 function updateFilenameInputAndDownloadTraces(name: string, valid: boolean) { 304 const inputEl = assertDefined( 305 htmlElement.querySelector('.file-name-input-field input'), 306 ); 307 const checkButton = assertDefined( 308 htmlElement.querySelector('.check-button'), 309 ); 310 (inputEl as HTMLInputElement).value = name; 311 inputEl.dispatchEvent(new Event('input')); 312 fixture.detectChanges(); 313 checkButton.dispatchEvent(new Event('click')); 314 fixture.detectChanges(); 315 if (valid) { 316 assertDefined(htmlElement.querySelector('.download-file-info')); 317 expect( 318 (htmlElement.querySelector('.save-button') as HTMLButtonElement) 319 .disabled, 320 ).toBeFalse(); 321 clickDownloadTracesButton(); 322 } else { 323 expect(htmlElement.querySelector('.download-file-info')).toBeFalsy(); 324 expect( 325 (htmlElement.querySelector('.save-button') as HTMLButtonElement) 326 .disabled, 327 ).toBeTrue(); 328 } 329 } 330 331 function clickDownloadTracesButton() { 332 const downloadButton = assertDefined( 333 htmlElement.querySelector('.save-button'), 334 ); 335 downloadButton.dispatchEvent(new Event('click')); 336 fixture.detectChanges(); 337 } 338 339 function clickEditFilenameButton() { 340 const pencilButton = assertDefined( 341 htmlElement.querySelector('.edit-button'), 342 ); 343 pencilButton.dispatchEvent(new Event('click')); 344 fixture.detectChanges(); 345 } 346 347 function checkHomepage() { 348 expect(htmlElement.querySelector('.welcome-info')).toBeTruthy(); 349 expect(htmlElement.querySelector('.collect-traces-card')).toBeTruthy(); 350 expect(htmlElement.querySelector('.upload-traces-card')).toBeTruthy(); 351 expect(htmlElement.querySelector('.viewers')).toBeFalsy(); 352 expect(htmlElement.querySelector('.upload-new')).toBeFalsy(); 353 checkPermanentHeaderItems(); 354 } 355 356 function checkTraceViewPage() { 357 expect(htmlElement.querySelector('.welcome-info')).toBeFalsy(); 358 expect(htmlElement.querySelector('.save-button')).toBeTruthy(); 359 expect(htmlElement.querySelector('.collect-traces-card')).toBeFalsy(); 360 expect(htmlElement.querySelector('.upload-traces-card')).toBeFalsy(); 361 expect(htmlElement.querySelector('.viewers')).toBeTruthy(); 362 expect(htmlElement.querySelector('.upload-new')).toBeTruthy(); 363 checkPermanentHeaderItems(); 364 } 365 366 function checkPermanentHeaderItems() { 367 expect(htmlElement.querySelector('.app-title')).toBeTruthy(); 368 expect(htmlElement.querySelector('.shortcuts')).toBeTruthy(); 369 expect(htmlElement.querySelector('.documentation')).toBeTruthy(); 370 expect(htmlElement.querySelector('.report-bug')).toBeTruthy(); 371 expect(htmlElement.querySelector('.dark-mode')).toBeTruthy(); 372 } 373}); 374