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 { 18 CdkVirtualScrollViewport, 19 ScrollingModule, 20} from '@angular/cdk/scrolling'; 21import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; 22import { 23 ComponentFixture, 24 ComponentFixtureAutoDetect, 25 TestBed, 26} from '@angular/core/testing'; 27import {FormsModule} from '@angular/forms'; 28import {MatDividerModule} from '@angular/material/divider'; 29import {MatFormFieldModule} from '@angular/material/form-field'; 30import {MatInputModule} from '@angular/material/input'; 31import {MatSelectModule} from '@angular/material/select'; 32import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 33import {assertDefined} from 'common/assert_utils'; 34import {PropertyTreeBuilder} from 'test/unit/property_tree_builder'; 35import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 36import {UnitTestUtils} from 'test/unit/utils'; 37import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters'; 38import {executeScrollComponentTests} from 'viewers/common/scroll_component_test_utils'; 39import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node'; 40import {ViewerEvents} from 'viewers/common/viewer_events'; 41import {CollapsedSectionsComponent} from 'viewers/components/collapsed_sections_component'; 42import {CollapsibleSectionTitleComponent} from 'viewers/components/collapsible_section_title_component'; 43import {PropertiesComponent} from 'viewers/components/properties_component'; 44import {SelectWithFilterComponent} from 'viewers/components/select_with_filter_component'; 45import {TransactionsScrollDirective} from './scroll_strategy/transactions_scroll_directive'; 46import {UiData, UiDataEntry} from './ui_data'; 47import {ViewerTransactionsComponent} from './viewer_transactions_component'; 48 49describe('ViewerTransactionsComponent', () => { 50 describe('Main component', () => { 51 let fixture: ComponentFixture<ViewerTransactionsComponent>; 52 let component: ViewerTransactionsComponent; 53 let htmlElement: HTMLElement; 54 55 beforeEach(async () => { 56 await TestBed.configureTestingModule({ 57 providers: [{provide: ComponentFixtureAutoDetect, useValue: true}], 58 imports: [ 59 ScrollingModule, 60 MatFormFieldModule, 61 FormsModule, 62 MatInputModule, 63 BrowserAnimationsModule, 64 MatSelectModule, 65 MatDividerModule, 66 ], 67 declarations: [ 68 ViewerTransactionsComponent, 69 TransactionsScrollDirective, 70 SelectWithFilterComponent, 71 CollapsedSectionsComponent, 72 CollapsibleSectionTitleComponent, 73 PropertiesComponent, 74 ], 75 schemas: [CUSTOM_ELEMENTS_SCHEMA], 76 }).compileComponents(); 77 78 fixture = TestBed.createComponent(ViewerTransactionsComponent); 79 component = fixture.componentInstance; 80 htmlElement = fixture.nativeElement; 81 82 component.inputData = makeUiData(0); 83 fixture.detectChanges(); 84 }); 85 86 it('can be created', () => { 87 expect(component).toBeTruthy(); 88 }); 89 90 it('renders filters', () => { 91 expect(htmlElement.querySelector('.entries .filters .pid')).toBeTruthy(); 92 expect(htmlElement.querySelector('.entries .filters .uid')).toBeTruthy(); 93 expect(htmlElement.querySelector('.entries .filters .type')).toBeTruthy(); 94 expect(htmlElement.querySelector('.entries .filters .id')).toBeTruthy(); 95 }); 96 97 it('renders entries', () => { 98 expect(htmlElement.querySelector('.scroll')).toBeTruthy(); 99 100 const entry = assertDefined(htmlElement.querySelector('.scroll .entry')); 101 expect(entry.innerHTML).toContain('1ns'); 102 expect(entry.innerHTML).toContain('-111'); 103 expect(entry.innerHTML).toContain('PID_VALUE'); 104 expect(entry.innerHTML).toContain('UID_VALUE'); 105 expect(entry.innerHTML).toContain('TYPE_VALUE'); 106 expect(entry.innerHTML).toContain('ID_VALUE'); 107 expect(entry.innerHTML).toContain('flag1 | flag2'); 108 }); 109 110 it('renders properties', () => { 111 expect(htmlElement.querySelector('.properties-view')).toBeTruthy(); 112 }); 113 114 it('applies transaction id filter correctly', async () => { 115 const allEntries = makeUiData(0).entries; 116 htmlElement.addEventListener( 117 ViewerEvents.TransactionIdFilterChanged, 118 (event) => { 119 if ((event as CustomEvent).detail.length === 0) { 120 component.uiData.entries = allEntries; 121 return; 122 } 123 component.uiData.entries = allEntries.filter((entry) => 124 (event as CustomEvent).detail.includes(entry.transactionId), 125 ); 126 }, 127 ); 128 await checkSelectFilter('.transaction-id'); 129 }); 130 131 it('applies vsync id filter correctly', async () => { 132 const allEntries = makeUiData(0).entries; 133 htmlElement.addEventListener( 134 ViewerEvents.VSyncIdFilterChanged, 135 (event) => { 136 if ((event as CustomEvent).detail.length === 0) { 137 component.uiData.entries = allEntries; 138 return; 139 } 140 component.uiData.entries = allEntries.filter((entry) => { 141 return (event as CustomEvent).detail.includes(`${entry.vsyncId}`); 142 }); 143 }, 144 ); 145 await checkSelectFilter('.vsyncid'); 146 }); 147 148 it('applies pid filter correctly', async () => { 149 const allEntries = makeUiData(0).entries; 150 htmlElement.addEventListener(ViewerEvents.PidFilterChanged, (event) => { 151 if ((event as CustomEvent).detail.length === 0) { 152 component.uiData.entries = allEntries; 153 return; 154 } 155 component.uiData.entries = allEntries.filter((entry) => { 156 return (event as CustomEvent).detail.includes(entry.pid); 157 }); 158 }); 159 await checkSelectFilter('.pid'); 160 }); 161 162 it('applies uid filter correctly', async () => { 163 const allEntries = makeUiData(0).entries; 164 htmlElement.addEventListener(ViewerEvents.UidFilterChanged, (event) => { 165 if ((event as CustomEvent).detail.length === 0) { 166 component.uiData.entries = allEntries; 167 return; 168 } 169 component.uiData.entries = allEntries.filter((entry) => { 170 return (event as CustomEvent).detail.includes(entry.uid); 171 }); 172 }); 173 await checkSelectFilter('.uid'); 174 }); 175 176 it('applies type filter correctly', async () => { 177 const allEntries = makeUiData(0).entries; 178 htmlElement.addEventListener(ViewerEvents.TypeFilterChanged, (event) => { 179 if ((event as CustomEvent).detail.length === 0) { 180 component.uiData.entries = allEntries; 181 return; 182 } 183 component.uiData.entries = allEntries.filter((entry) => { 184 return (event as CustomEvent).detail.includes(entry.type); 185 }); 186 }); 187 await checkSelectFilter('.type'); 188 }); 189 190 it('applies layer/display id filter correctly', async () => { 191 const allEntries = makeUiData(0).entries; 192 htmlElement.addEventListener( 193 ViewerEvents.LayerIdFilterChanged, 194 (event) => { 195 if ((event as CustomEvent).detail.length === 0) { 196 component.uiData.entries = allEntries; 197 return; 198 } 199 component.uiData.entries = allEntries.filter((entry) => { 200 return (event as CustomEvent).detail.includes( 201 entry.layerOrDisplayId, 202 ); 203 }); 204 }, 205 ); 206 await checkSelectFilter('.layer-or-display-id'); 207 }); 208 209 it('applies what filter correctly', async () => { 210 const allEntries = makeUiData(0).entries; 211 htmlElement.addEventListener(ViewerEvents.WhatFilterChanged, (event) => { 212 if ((event as CustomEvent).detail.length === 0) { 213 component.uiData.entries = allEntries; 214 return; 215 } 216 component.uiData.entries = allEntries.filter((entry) => { 217 return (event as CustomEvent).detail.some((allowed: string) => { 218 return entry.what.includes(allowed); 219 }); 220 }); 221 }); 222 await checkSelectFilter('.what'); 223 }); 224 225 it('scrolls to current entry on button click', () => { 226 const goToCurrentTimeButton = assertDefined( 227 htmlElement.querySelector('.go-to-current-time'), 228 ) as HTMLButtonElement; 229 const spy = spyOn( 230 assertDefined(component.scrollComponent), 231 'scrollToIndex', 232 ); 233 goToCurrentTimeButton.click(); 234 expect(spy).toHaveBeenCalledWith(1); 235 }); 236 237 it('changes selected entry on arrow key press', () => { 238 htmlElement.addEventListener( 239 ViewerEvents.LogChangedByKeyPress, 240 (event) => { 241 component.inputData = makeUiData((event as CustomEvent).detail); 242 fixture.detectChanges(); 243 }, 244 ); 245 246 // does not do anything if no prev entry available 247 component.handleKeyboardEvent( 248 new KeyboardEvent('keydown', {key: 'ArrowUp'}), 249 ); 250 expect(component.uiData.selectedEntryIndex).toEqual(0); 251 252 component.handleKeyboardEvent( 253 new KeyboardEvent('keydown', {key: 'ArrowDown'}), 254 ); 255 expect(component.uiData.selectedEntryIndex).toEqual(1); 256 257 component.handleKeyboardEvent( 258 new KeyboardEvent('keydown', {key: 'ArrowUp'}), 259 ); 260 expect(component.uiData.selectedEntryIndex).toEqual(0); 261 }); 262 263 it('propagates timestamp on click', () => { 264 component.inputData = makeUiData(0); 265 fixture.detectChanges(); 266 let index: number | undefined; 267 htmlElement.addEventListener(ViewerEvents.TimestampClick, (event) => { 268 index = (event as CustomEvent).detail.index; 269 }); 270 const logTimestampButton = assertDefined( 271 htmlElement.querySelector('.time button'), 272 ) as HTMLButtonElement; 273 logTimestampButton.click(); 274 275 expect(index).toEqual(0); 276 }); 277 278 it('creates collapsed sections with no buttons', () => { 279 UnitTestUtils.checkNoCollapsedSectionButtons(htmlElement); 280 }); 281 282 it('handles properties section collapse/expand', () => { 283 UnitTestUtils.checkSectionCollapseAndExpand( 284 htmlElement, 285 fixture, 286 '.properties-view', 287 'PROPERTIES - PROTO DUMP', 288 ); 289 }); 290 291 function makeUiData(selectedEntryIndex: number): UiData { 292 const propertiesTree = new PropertyTreeBuilder() 293 .setRootId('Transactions') 294 .setName('tree') 295 .setValue(null) 296 .build(); 297 298 const time = new PropertyTreeBuilder() 299 .setRootId(propertiesTree.id) 300 .setName('timestamp') 301 .setValue(TimestampConverterUtils.makeElapsedTimestamp(1n)) 302 .setFormatter(TIMESTAMP_NODE_FORMATTER) 303 .build(); 304 305 const entry = new UiDataEntry( 306 0, 307 time, 308 -111, 309 'PID_VALUE', 310 'UID_VALUE', 311 'TYPE_VALUE', 312 'LAYER_OR_DISPLAY_ID_VALUE', 313 'TRANSACTION_ID_VALUE', 314 'flag1 | flag2', 315 propertiesTree, 316 ); 317 318 const entry2 = new UiDataEntry( 319 1, 320 time, 321 -222, 322 'PID_VALUE_2', 323 'UID_VALUE_2', 324 'TYPE_VALUE_2', 325 'LAYER_OR_DISPLAY_ID_VALUE_2', 326 'TRANSACTION_ID_VALUE_2', 327 'flag3 | flag4', 328 propertiesTree, 329 ); 330 331 return new UiData( 332 ['-111', '-222'], 333 ['PID_VALUE', 'PID_VALUE_2'], 334 ['UID_VALUE', 'UID_VALUE_2'], 335 ['TYPE_VALUE', 'TYPE_VALUE_2'], 336 ['LAYER_OR_DISPLAY_ID_VALUE', 'LAYER_OR_DISPLAY_ID_VALUE_2'], 337 ['TRANSACTION_ID_VALUE', 'TRANSACTION_ID_VALUE_2'], 338 ['flag1', 'flag2', 'flag3', 'flag4'], 339 [entry, entry2], 340 1, 341 selectedEntryIndex, 342 0, 343 UiPropertyTreeNode.from(propertiesTree), 344 {}, 345 ); 346 } 347 348 async function checkSelectFilter(filterSelector: string) { 349 component.inputData = makeUiData(0); 350 fixture.detectChanges(); 351 expect(component.uiData.entries.length).toEqual(2); 352 const filterTrigger = assertDefined( 353 htmlElement.querySelector( 354 `.filters ${filterSelector} .mat-select-trigger`, 355 ), 356 ) as HTMLInputElement; 357 filterTrigger.click(); 358 await fixture.whenStable(); 359 360 const firstOption = assertDefined( 361 document.querySelector('.mat-select-panel .mat-option'), 362 ) as HTMLElement; 363 firstOption.click(); 364 fixture.detectChanges(); 365 expect(component.uiData.entries.length).toEqual(1); 366 367 firstOption.click(); 368 fixture.detectChanges(); 369 expect(component.uiData.entries.length).toEqual(2); 370 } 371 }); 372 373 describe('Scroll component', () => { 374 executeScrollComponentTests('entry', setUpTestEnvironment); 375 376 function makeUiDataForScroll(): UiData { 377 const propertiesTree = new PropertyTreeBuilder() 378 .setRootId('Transactions') 379 .setName('tree') 380 .setValue(null) 381 .build(); 382 383 const time = new PropertyTreeBuilder() 384 .setRootId(propertiesTree.id) 385 .setName('timestamp') 386 .setValue(TimestampConverterUtils.makeElapsedTimestamp(1n)) 387 .setFormatter(TIMESTAMP_NODE_FORMATTER) 388 .build(); 389 390 const uiData = new UiData( 391 [], 392 [], 393 [], 394 [], 395 [], 396 [], 397 [], 398 [], 399 0, 400 0, 401 0, 402 UiPropertyTreeNode.from(propertiesTree), 403 {}, 404 ); 405 const shortMessage = 'flag1 | flag2'; 406 const longMessage = shortMessage.repeat(20); 407 for (let i = 0; i < 200; i++) { 408 const entry = new UiDataEntry( 409 0, 410 time, 411 -111, 412 'PID_VALUE', 413 'UID_VALUE', 414 'TYPE_VALUE', 415 'LAYER_OR_DISPLAY_ID_VALUE', 416 'TRANSACTION_ID_VALUE', 417 i % 2 === 0 ? shortMessage : longMessage, 418 propertiesTree, 419 ); 420 uiData.entries.push(entry); 421 } 422 return uiData; 423 } 424 425 async function setUpTestEnvironment(): Promise< 426 [ 427 ComponentFixture<ViewerTransactionsComponent>, 428 HTMLElement, 429 CdkVirtualScrollViewport, 430 ] 431 > { 432 await TestBed.configureTestingModule({ 433 providers: [{provide: ComponentFixtureAutoDetect, useValue: true}], 434 imports: [ScrollingModule], 435 declarations: [ 436 ViewerTransactionsComponent, 437 TransactionsScrollDirective, 438 ], 439 schemas: [CUSTOM_ELEMENTS_SCHEMA], 440 }).compileComponents(); 441 const fixture = TestBed.createComponent(ViewerTransactionsComponent); 442 const transactionsComponent = fixture.componentInstance; 443 const htmlElement = fixture.nativeElement; 444 const viewport = assertDefined(transactionsComponent.scrollComponent); 445 transactionsComponent.inputData = makeUiDataForScroll(); 446 return [fixture, htmlElement, viewport]; 447 } 448 }); 449}); 450