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 {OverlayModule} from '@angular/cdk/overlay'; 18import {CommonModule} from '@angular/common'; 19import {Component, CUSTOM_ELEMENTS_SCHEMA, ViewChild} from '@angular/core'; 20import {ComponentFixture, TestBed} from '@angular/core/testing'; 21import {ReactiveFormsModule} from '@angular/forms'; 22import {MatButtonModule} from '@angular/material/button'; 23import {MatCardModule} from '@angular/material/card'; 24import {MatDividerModule} from '@angular/material/divider'; 25import {MatFormFieldModule} from '@angular/material/form-field'; 26import {MatIconModule} from '@angular/material/icon'; 27import {MatInputModule} from '@angular/material/input'; 28import {MatTabsModule} from '@angular/material/tabs'; 29import {MatTooltipModule} from '@angular/material/tooltip'; 30import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 31import {assertDefined} from 'common/assert_utils'; 32import {InMemoryStorage} from 'common/store/in_memory_storage'; 33import {TimestampConverterUtils} from 'common/time/test_utils'; 34import { 35 FilterPresetApplyRequest, 36 FilterPresetSaveRequest, 37 TabbedViewSwitchRequest, 38 WinscopeEvent, 39 WinscopeEventType, 40} from 'messaging/winscope_event'; 41import {TraceBuilder} from 'test/unit/trace_builder'; 42import {UnitTestUtils} from 'test/unit/utils'; 43import {TraceType} from 'trace/trace_type'; 44import {Viewer, ViewType} from 'viewers/viewer'; 45import {ViewerStub} from 'viewers/viewer_stub'; 46import {TraceViewComponent} from './trace_view_component'; 47 48describe('TraceViewComponent', () => { 49 const traceSf = UnitTestUtils.makeEmptyTrace(TraceType.SURFACE_FLINGER); 50 const traceWm = new TraceBuilder<object>() 51 .setType(TraceType.WINDOW_MANAGER) 52 .setEntries([{}]) 53 .setTimestamps([TimestampConverterUtils.makeZeroTimestamp()]) 54 .setDescriptors(['file_1', 'file_1']) 55 .build(); 56 const traceSr = UnitTestUtils.makeEmptyTrace(TraceType.SCREEN_RECORDING); 57 const traceProtolog = UnitTestUtils.makeEmptyTrace(TraceType.PROTO_LOG); 58 59 let fixture: ComponentFixture<TestHostComponent>; 60 let component: TestHostComponent; 61 let htmlElement: HTMLElement; 62 63 beforeEach(async () => { 64 await TestBed.configureTestingModule({ 65 declarations: [TestHostComponent, TraceViewComponent], 66 imports: [ 67 CommonModule, 68 MatCardModule, 69 MatDividerModule, 70 MatTabsModule, 71 MatTooltipModule, 72 OverlayModule, 73 MatButtonModule, 74 MatIconModule, 75 MatFormFieldModule, 76 BrowserAnimationsModule, 77 MatInputModule, 78 ReactiveFormsModule, 79 ], 80 schemas: [CUSTOM_ELEMENTS_SCHEMA], 81 }).compileComponents(); 82 fixture = TestBed.createComponent(TestHostComponent); 83 htmlElement = fixture.nativeElement; 84 component = fixture.componentInstance; 85 component.viewers = [ 86 new ViewerStub('Title0', 'Content0', traceSf, ViewType.TRACE_TAB), 87 new ViewerStub('Title1', 'Content1', traceWm, ViewType.TRACE_TAB), 88 new ViewerStub('Title2', 'Content2', traceSr, ViewType.OVERLAY), 89 new ViewerStub('Title3', 'Content3', traceProtolog, ViewType.TRACE_TAB), 90 ]; 91 fixture.detectChanges(); 92 }); 93 94 it('can be created', () => { 95 expect(component).toBeTruthy(); 96 }); 97 98 it('creates viewer tabs', () => { 99 const tabs = htmlElement.querySelectorAll('.tab'); 100 expect(tabs.length).toEqual(3); 101 expect(tabs.item(0).textContent).toContain('Title0'); 102 expect(tabs.item(1).textContent).toContain('Title1 Dump'); 103 }); 104 105 it('creates viewer overlay', () => { 106 const overlayContainer = assertDefined( 107 htmlElement.querySelector('.overlay-container'), 108 ); 109 expect(overlayContainer.textContent).toContain('Content2'); 110 }); 111 112 it('throws error if more than one overlay present', () => { 113 expect(() => { 114 component.viewers = [ 115 new ViewerStub('Title0', 'Content0', traceSf, ViewType.TRACE_TAB), 116 new ViewerStub('Title1', 'Content1', traceWm, ViewType.OVERLAY), 117 new ViewerStub('Title2', 'Content2', traceSr, ViewType.OVERLAY), 118 ]; 119 fixture.detectChanges(); 120 }).toThrowError(); 121 }); 122 123 it('switches view on click', () => { 124 const tabButtons = htmlElement.querySelectorAll<HTMLElement>('.tab'); 125 126 // Initially tab 0 127 fixture.detectChanges(); 128 let visibleTabContents = getVisibleTabContents(); 129 expect(visibleTabContents.length).toEqual(1); 130 expect(visibleTabContents[0].innerHTML).toEqual('Content0'); 131 132 // Switch to tab 1 133 tabButtons.item(1).click(); 134 fixture.detectChanges(); 135 visibleTabContents = getVisibleTabContents(); 136 expect(visibleTabContents.length).toEqual(1); 137 expect(visibleTabContents[0].innerHTML).toEqual('Content1'); 138 139 // Switch to tab 0 140 tabButtons.item(0).click(); 141 fixture.detectChanges(); 142 visibleTabContents = getVisibleTabContents(); 143 expect(visibleTabContents.length).toEqual(1); 144 expect(visibleTabContents[0].innerHTML).toEqual('Content0'); 145 }); 146 147 it("emits 'view switched' events", () => { 148 const traceViewComponent = assertDefined(component.traceViewComponent); 149 const tabButtons = htmlElement.querySelectorAll<HTMLElement>('.tab'); 150 151 const emitAppEvent = jasmine.createSpy(); 152 traceViewComponent.setEmitEvent(emitAppEvent); 153 154 expect(emitAppEvent).not.toHaveBeenCalled(); 155 156 tabButtons.item(1).click(); 157 expect(emitAppEvent).toHaveBeenCalledTimes(1); 158 expect(emitAppEvent).toHaveBeenCalledWith( 159 jasmine.objectContaining({ 160 type: WinscopeEventType.TABBED_VIEW_SWITCHED, 161 } as WinscopeEvent), 162 ); 163 164 tabButtons.item(0).click(); 165 expect(emitAppEvent).toHaveBeenCalledTimes(2); 166 expect(emitAppEvent).toHaveBeenCalledWith( 167 jasmine.objectContaining({ 168 type: WinscopeEventType.TABBED_VIEW_SWITCHED, 169 } as WinscopeEvent), 170 ); 171 }); 172 173 it("handles 'view switch' requests", async () => { 174 const traceViewComponent = assertDefined(component.traceViewComponent); 175 176 // Initially tab 0 177 let visibleTabContents = getVisibleTabContents(); 178 expect(visibleTabContents.length).toEqual(1); 179 expect(visibleTabContents[0].innerHTML).toEqual('Content0'); 180 181 // Switch to tab 1 182 await traceViewComponent.onWinscopeEvent( 183 new TabbedViewSwitchRequest(traceWm), 184 ); 185 fixture.detectChanges(); 186 visibleTabContents = getVisibleTabContents(); 187 expect(visibleTabContents.length).toEqual(1); 188 expect(visibleTabContents[0].innerHTML).toEqual('Content1'); 189 190 // Switch to tab 0 191 await traceViewComponent.onWinscopeEvent( 192 new TabbedViewSwitchRequest(traceSf), 193 ); 194 fixture.detectChanges(); 195 visibleTabContents = getVisibleTabContents(); 196 expect(visibleTabContents.length).toEqual(1); 197 expect(visibleTabContents[0].innerHTML).toEqual('Content0'); 198 }); 199 200 it('emits TabbedViewSwitched event on viewer changes', () => { 201 const traceViewComponent = assertDefined(component.traceViewComponent); 202 const emitAppEvent = jasmine.createSpy(); 203 traceViewComponent.setEmitEvent(emitAppEvent); 204 205 expect(emitAppEvent).not.toHaveBeenCalled(); 206 207 component.viewers = [new ViewerStub('Title1', 'Content1', traceWm)]; 208 fixture.detectChanges(); 209 210 expect(emitAppEvent).toHaveBeenCalledTimes(1); 211 expect(emitAppEvent).toHaveBeenCalledWith( 212 jasmine.objectContaining({ 213 type: WinscopeEventType.TABBED_VIEW_SWITCHED, 214 } as WinscopeEvent), 215 ); 216 }); 217 218 it('disables filter presets button for viewers without presets', () => { 219 const filterPresets = assertDefined( 220 htmlElement.querySelector<HTMLButtonElement>('.filter-presets'), 221 ); 222 expect(filterPresets.textContent).toContain('Filter Presets'); 223 expect(filterPresets.disabled).toBeFalse(); 224 const tabButtons = htmlElement.querySelectorAll<HTMLElement>('.tab'); 225 tabButtons.item(2).click(); 226 fixture.detectChanges(); 227 expect(filterPresets.disabled).toBeTrue(); 228 }); 229 230 it('saves preset by button', () => { 231 const emitAppEvent = jasmine.createSpy(); 232 component.traceViewComponent?.setEmitEvent(emitAppEvent); 233 openFilterPresets(); 234 235 const overlayPanel = assertDefined( 236 document.querySelector('.overlay-panel'), 237 ); 238 const existingPresets = assertDefined( 239 overlayPanel.querySelector('.existing-presets-section'), 240 ); 241 expect(existingPresets.textContent).toContain('No existing presets found'); 242 243 const saveButton = assertDefined( 244 overlayPanel.querySelector<HTMLButtonElement>('.save-field button'), 245 ); 246 expect(saveButton.disabled).toBeTrue(); 247 248 const inputEl = assertDefined( 249 overlayPanel.querySelector<HTMLInputElement>('.save-field input'), 250 ); 251 updateInputField(inputEl, 'Test Preset'); 252 saveButton.click(); 253 fixture.detectChanges(); 254 255 expect(emitAppEvent).toHaveBeenCalledWith( 256 new FilterPresetSaveRequest( 257 'Test Preset.Surface Flinger', 258 TraceType.SURFACE_FLINGER, 259 ), 260 ); 261 expect(existingPresets.textContent).toContain('Test Preset'); 262 expect(inputEl.value).toEqual(''); 263 expect(saveButton.disabled).toBeTrue(); 264 }); 265 266 it('saves preset by keydown', () => { 267 const emitAppEvent = jasmine.createSpy(); 268 component.traceViewComponent?.setEmitEvent(emitAppEvent); 269 openFilterPresets(); 270 271 const overlayPanel = assertDefined( 272 document.querySelector('.overlay-panel'), 273 ); 274 275 const inputEl = assertDefined( 276 overlayPanel.querySelector<HTMLInputElement>('.save-field input'), 277 ); 278 inputEl.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'})); 279 fixture.detectChanges(); 280 expect(emitAppEvent).not.toHaveBeenCalled(); 281 282 updateInputField(inputEl, 'Test Preset'); 283 inputEl.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'})); 284 fixture.detectChanges(); 285 286 expect(emitAppEvent).toHaveBeenCalledWith( 287 new FilterPresetSaveRequest( 288 'Test Preset.Surface Flinger', 289 TraceType.SURFACE_FLINGER, 290 ), 291 ); 292 }); 293 294 it('saves preset between sessions', () => { 295 savePresetByButton('Test Preset'); 296 297 component.showSecondComponent = true; 298 fixture.detectChanges(); 299 300 openFilterPresets(); 301 const existingPresets = assertDefined( 302 document.querySelector('.overlay-panel .existing-presets-section'), 303 ); 304 expect(existingPresets.textContent).toContain('Test Preset'); 305 }); 306 307 it('deletes preset', () => { 308 savePresetByButton('Test Preset'); 309 const saveButton = assertDefined( 310 document.querySelector<HTMLButtonElement>('.save-field button'), 311 ); 312 updateInputField( 313 assertDefined( 314 document.querySelector<HTMLInputElement>('.save-field input'), 315 ), 316 'Test Preset', 317 ); 318 expect(saveButton.disabled).toBeTrue(); 319 320 assertDefined( 321 document.querySelector<HTMLElement>('.delete-button'), 322 ).click(); 323 fixture.detectChanges(); 324 expect( 325 document.querySelector<HTMLElement>('.existing-presets-section') 326 ?.textContent, 327 ).toContain('No existing presets found'); 328 expect(saveButton.disabled).toBeFalse(); 329 }); 330 331 it('does not show presets for different trace', () => { 332 savePresetByButton('Test Preset'); 333 closeFilterPresets(); 334 335 const tabs = htmlElement.querySelectorAll<HTMLElement>('.tab'); 336 tabs.item(1).click(); 337 fixture.detectChanges(); 338 339 openFilterPresets(); 340 const existingPresets = assertDefined( 341 document.querySelector('.overlay-panel'), 342 ); 343 expect(existingPresets.textContent).toContain('No existing presets found'); 344 }); 345 346 it('emits apply preset request', () => { 347 const emitAppEvent = jasmine.createSpy(); 348 component.traceViewComponent?.setEmitEvent(emitAppEvent); 349 savePresetByButton('Test Preset'); 350 351 const preset = assertDefined( 352 document.querySelector<HTMLElement>( 353 '.overlay-panel .existing-preset button', 354 ), 355 ); 356 preset.click(); 357 fixture.detectChanges(); 358 359 expect(emitAppEvent).toHaveBeenCalledWith( 360 new FilterPresetApplyRequest( 361 'Test Preset.Surface Flinger', 362 TraceType.SURFACE_FLINGER, 363 ), 364 ); 365 }); 366 367 it('does not show global tab first', () => { 368 component.viewers = [ 369 new ViewerStub('Title0', 'Content0', undefined, ViewType.GLOBAL_SEARCH), 370 new ViewerStub('Title1', 'Content1', traceWm, ViewType.TRACE_TAB), 371 ]; 372 fixture.detectChanges(); 373 const visibleTabContents = getVisibleTabContents(); 374 expect(visibleTabContents.length).toEqual(1); 375 expect(visibleTabContents[0].innerHTML).toEqual('Content1'); 376 }); 377 378 it('shows tooltips for tabs with trace descriptors', async () => { 379 const tabs = htmlElement.querySelectorAll('.tab'); 380 const wmTab = tabs.item(1); 381 await UnitTestUtils.checkTooltips([wmTab], ['file_1'], fixture); 382 }); 383 384 function getVisibleTabContents() { 385 const contents: HTMLElement[] = []; 386 htmlElement 387 .querySelectorAll<HTMLElement>('.trace-view-content div') 388 .forEach((content) => { 389 if (content.style.display !== 'none') { 390 contents.push(content); 391 } 392 }); 393 return contents; 394 } 395 396 function savePresetByButton(presetName: string) { 397 openFilterPresets(); 398 const overlayPanel = assertDefined( 399 document.querySelector('.overlay-panel'), 400 ); 401 const saveButton = assertDefined( 402 overlayPanel.querySelector<HTMLButtonElement>('.save-field button'), 403 ); 404 405 const inputEl = assertDefined( 406 overlayPanel.querySelector<HTMLInputElement>('.save-field input'), 407 ); 408 updateInputField(inputEl, presetName); 409 saveButton.click(); 410 fixture.detectChanges(); 411 } 412 413 function openFilterPresets() { 414 const filterPresets = assertDefined( 415 htmlElement.querySelector<HTMLButtonElement>('.filter-presets'), 416 ); 417 filterPresets.click(); 418 fixture.detectChanges(); 419 } 420 421 function closeFilterPresets() { 422 assertDefined( 423 document.querySelector<HTMLElement>('.cdk-overlay-backdrop'), 424 ).click(); 425 fixture.detectChanges(); 426 } 427 428 function updateInputField(inputEl: HTMLInputElement, value: string) { 429 inputEl.value = value; 430 inputEl.dispatchEvent(new Event('input')); 431 fixture.detectChanges(); 432 } 433 434 @Component({ 435 selector: 'host-component', 436 template: ` 437 <trace-view 438 *ngIf="!showSecondComponent" 439 [viewers]="viewers" 440 [store]="store"></trace-view> 441 442 <trace-view 443 *ngIf="showSecondComponent" 444 [viewers]="viewers" 445 [store]="store"></trace-view> 446 `, 447 }) 448 class TestHostComponent { 449 viewers: Viewer[] = []; 450 store = new InMemoryStorage(); 451 showSecondComponent = false; 452 453 @ViewChild(TraceViewComponent) 454 traceViewComponent: TraceViewComponent | undefined; 455 } 456}); 457