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 {ClipboardModule} from '@angular/cdk/clipboard'; 18import {DragDropModule} from '@angular/cdk/drag-drop'; 19import {CdkMenuModule} from '@angular/cdk/menu'; 20import {ChangeDetectionStrategy, Component, ViewChild} from '@angular/core'; 21import {ComponentFixture, TestBed} from '@angular/core/testing'; 22import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 23import {MatButtonModule} from '@angular/material/button'; 24import {MatFormFieldModule} from '@angular/material/form-field'; 25import {MatIconModule} from '@angular/material/icon'; 26import {MatInputModule} from '@angular/material/input'; 27import {MatSelectModule} from '@angular/material/select'; 28import {MatTooltipModule} from '@angular/material/tooltip'; 29import {By} from '@angular/platform-browser'; 30import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 31import { 32 MatDrawer, 33 MatDrawerContainer, 34 MatDrawerContent, 35} from 'app/components/bottomnav/bottom_drawer_component'; 36import {TimelineData} from 'app/timeline_data'; 37import {assertDefined} from 'common/assert_utils'; 38import {PersistentStore} from 'common/store/persistent_store'; 39import {TimestampConverterUtils} from 'common/time/test_utils'; 40import {TimeRange} from 'common/time/time'; 41import { 42 ActiveTraceChanged, 43 ExpandedTimelineToggled, 44 InitializeTraceSearchRequest, 45 TraceAddRequest, 46 TracePositionUpdate, 47 TraceRemoveRequest, 48 TraceSearchCompleted, 49 TraceSearchInitialized, 50 TraceSearchRequest, 51 WinscopeEvent, 52} from 'messaging/winscope_event'; 53import {TracesBuilder} from 'test/unit/traces_builder'; 54import {TraceBuilder} from 'test/unit/trace_builder'; 55import {UnitTestUtils} from 'test/unit/utils'; 56import {Trace} from 'trace/trace'; 57import {Traces} from 'trace/traces'; 58import {TRACE_INFO} from 'trace/trace_info'; 59import {TracePosition} from 'trace/trace_position'; 60import {TraceType} from 'trace/trace_type'; 61import {QueryResult} from 'trace_processor/query_result'; 62import {CanvasDrawer} from './expanded-timeline/canvas_drawer'; 63import {DefaultTimelineRowComponent} from './expanded-timeline/default_timeline_row_component'; 64import {ExpandedTimelineComponent} from './expanded-timeline/expanded_timeline_component'; 65import {TransitionTimelineComponent} from './expanded-timeline/transition_timeline_component'; 66import {MiniTimelineDrawerImpl} from './mini-timeline/drawer/mini_timeline_drawer_impl'; 67import {MiniTimelineComponent} from './mini-timeline/mini_timeline_component'; 68import {SliderComponent} from './mini-timeline/slider_component'; 69import {TimelineComponent} from './timeline_component'; 70 71describe('TimelineComponent', () => { 72 const time90 = TimestampConverterUtils.makeRealTimestamp(90n); 73 const time100 = TimestampConverterUtils.makeRealTimestamp(100n); 74 const time101 = TimestampConverterUtils.makeRealTimestamp(101n); 75 const time105 = TimestampConverterUtils.makeRealTimestamp(105n); 76 const time110 = TimestampConverterUtils.makeRealTimestamp(110n); 77 const time112 = TimestampConverterUtils.makeRealTimestamp(112n); 78 79 const time2000 = TimestampConverterUtils.makeRealTimestamp(2000n); 80 const time3000 = TimestampConverterUtils.makeRealTimestamp(3000n); 81 const time4000 = TimestampConverterUtils.makeRealTimestamp(4000n); 82 const time6000 = TimestampConverterUtils.makeRealTimestamp(6000n); 83 const time8000 = TimestampConverterUtils.makeRealTimestamp(8000n); 84 85 const position90 = TracePosition.fromTimestamp(time90); 86 const position100 = TracePosition.fromTimestamp(time100); 87 const position105 = TracePosition.fromTimestamp(time105); 88 const position110 = TracePosition.fromTimestamp(time110); 89 const position112 = TracePosition.fromTimestamp(time112); 90 91 let fixture: ComponentFixture<TestHostComponent>; 92 let component: TestHostComponent; 93 let htmlElement: HTMLElement; 94 95 beforeEach(async () => { 96 await TestBed.configureTestingModule({ 97 imports: [ 98 FormsModule, 99 MatButtonModule, 100 MatFormFieldModule, 101 MatInputModule, 102 MatIconModule, 103 MatSelectModule, 104 MatTooltipModule, 105 ReactiveFormsModule, 106 BrowserAnimationsModule, 107 DragDropModule, 108 ClipboardModule, 109 CdkMenuModule, 110 ], 111 declarations: [ 112 TestHostComponent, 113 ExpandedTimelineComponent, 114 DefaultTimelineRowComponent, 115 MatDrawer, 116 MatDrawerContainer, 117 MatDrawerContent, 118 MiniTimelineComponent, 119 TimelineComponent, 120 SliderComponent, 121 TransitionTimelineComponent, 122 ], 123 }) 124 .overrideComponent(TimelineComponent, { 125 set: {changeDetection: ChangeDetectionStrategy.Default}, 126 }) 127 .compileComponents(); 128 fixture = TestBed.createComponent(TestHostComponent); 129 component = fixture.componentInstance; 130 htmlElement = fixture.nativeElement; 131 }); 132 133 it('can be created', () => { 134 expect(component).toBeTruthy(); 135 }); 136 137 it('can be expanded', () => { 138 const traces = new TracesBuilder() 139 .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110]) 140 .build(); 141 assertDefined(component.timelineData).initialize( 142 traces, 143 undefined, 144 TimestampConverterUtils.TIMESTAMP_CONVERTER, 145 ); 146 fixture.detectChanges(); 147 148 const timelineComponent = assertDefined(component.timeline); 149 150 const button = assertDefined( 151 htmlElement.querySelector(`.${timelineComponent.TOGGLE_BUTTON_CLASS}`), 152 ); 153 154 // initially not expanded 155 let expandedTimelineElement = fixture.debugElement.query( 156 By.directive(ExpandedTimelineComponent), 157 ); 158 expect(expandedTimelineElement).toBeFalsy(); 159 160 let isExpanded = false; 161 timelineComponent.setEmitEvent(async (event: WinscopeEvent) => { 162 expect(event).toBeInstanceOf(ExpandedTimelineToggled); 163 isExpanded = (event as ExpandedTimelineToggled).isTimelineExpanded; 164 }); 165 166 button.dispatchEvent(new Event('click')); 167 expandedTimelineElement = fixture.debugElement.query( 168 By.directive(ExpandedTimelineComponent), 169 ); 170 expect(expandedTimelineElement).toBeTruthy(); 171 expect(isExpanded).toBeTrue(); 172 173 button.dispatchEvent(new Event('click')); 174 expandedTimelineElement = fixture.debugElement.query( 175 By.directive(ExpandedTimelineComponent), 176 ); 177 expect(expandedTimelineElement).toBeFalsy(); 178 expect(isExpanded).toBeFalse(); 179 }); 180 181 it('handles empty traces', () => { 182 const traces = new TracesBuilder() 183 .setEntries(TraceType.SURFACE_FLINGER, []) 184 .build(); 185 assertDefined(assertDefined(component.timelineData)).initialize( 186 traces, 187 undefined, 188 TimestampConverterUtils.TIMESTAMP_CONVERTER, 189 ); 190 fixture.detectChanges(); 191 192 expect(htmlElement.querySelector('.time-selector')).toBeNull(); 193 expect(htmlElement.querySelector('.trace-selector')).toBeNull(); 194 195 const errorMessageContainer = assertDefined( 196 htmlElement.querySelector('.no-timeline-msg'), 197 ); 198 expect(errorMessageContainer.textContent).toContain('No timeline to show!'); 199 expect(errorMessageContainer.textContent).toContain( 200 'All loaded traces contain no timestamps.', 201 ); 202 203 checkNoTimelineNavigation(); 204 }); 205 206 it('handles some empty traces and some with one timestamp', async () => { 207 await loadTracesWithOneTimestamp(); 208 209 expect(htmlElement.querySelector('#time-selector')).toBeTruthy(); 210 const shownSelection = assertDefined( 211 htmlElement.querySelector('#trace-selector .shown-selection'), 212 ); 213 expect(shownSelection.innerHTML).toContain('Window Manager'); 214 expect(shownSelection.innerHTML).not.toContain('Surface Flinger'); 215 216 const errorMessageContainer = assertDefined( 217 htmlElement.querySelector('.no-timeline-msg'), 218 ); 219 expect(errorMessageContainer.textContent).toContain('No timeline to show!'); 220 expect(errorMessageContainer.textContent).toContain( 221 'Only a single timestamp has been recorded.', 222 ); 223 224 checkNoTimelineNavigation(); 225 }); 226 227 it('processes active trace input and updates selected traces', async () => { 228 loadAllTraces(); 229 fixture.detectChanges(); 230 231 const timelineComponent = assertDefined(component.timeline); 232 const nextEntryButton = assertDefined( 233 htmlElement.querySelector<HTMLElement>('#next_entry_button'), 234 ); 235 const prevEntryButton = assertDefined( 236 htmlElement.querySelector<HTMLElement>('#prev_entry_button'), 237 ); 238 239 timelineComponent.selectedTraces = [ 240 getLoadedTrace(TraceType.SURFACE_FLINGER), 241 ]; 242 fixture.detectChanges(); 243 checkActiveTraceSurfaceFlinger(nextEntryButton, prevEntryButton); 244 245 // setting same trace as active does not affect selected traces 246 await updateActiveTrace(TraceType.SURFACE_FLINGER); 247 expectSelectedTraceTypes([TraceType.SURFACE_FLINGER]); 248 checkActiveTraceSurfaceFlinger(nextEntryButton, prevEntryButton); 249 250 await updateActiveTrace(TraceType.SCREEN_RECORDING); 251 expectSelectedTraceTypes([ 252 TraceType.SURFACE_FLINGER, 253 TraceType.SCREEN_RECORDING, 254 ]); 255 testCurrentTimestampOnButtonClick(prevEntryButton, position110, 110n); 256 257 await updateActiveTrace(TraceType.WINDOW_MANAGER); 258 expectSelectedTraceTypes([ 259 TraceType.SURFACE_FLINGER, 260 TraceType.SCREEN_RECORDING, 261 TraceType.WINDOW_MANAGER, 262 ]); 263 checkActiveTraceWindowManager(nextEntryButton, prevEntryButton); 264 265 await updateActiveTrace(TraceType.PROTO_LOG); 266 expectSelectedTraceTypes([ 267 TraceType.SURFACE_FLINGER, 268 TraceType.SCREEN_RECORDING, 269 TraceType.WINDOW_MANAGER, 270 TraceType.PROTO_LOG, 271 ]); 272 testCurrentTimestampOnButtonClick(nextEntryButton, position100, 100n); 273 checkActiveTraceHasOneEntry(nextEntryButton, prevEntryButton); 274 275 // setting active trace that is already selected does not affect selection 276 await updateActiveTrace(TraceType.SCREEN_RECORDING); 277 expectSelectedTraceTypes([ 278 TraceType.SURFACE_FLINGER, 279 TraceType.SCREEN_RECORDING, 280 TraceType.WINDOW_MANAGER, 281 TraceType.PROTO_LOG, 282 ]); 283 testCurrentTimestampOnButtonClick(nextEntryButton, position110, 110n); 284 checkActiveTraceHasOneEntry(nextEntryButton, prevEntryButton); 285 }); 286 287 it('handles undefined active trace input', async () => { 288 const traces = new TracesBuilder() 289 .setTimestamps(TraceType.EVENT_LOG, [time100, time110]) 290 .build(); 291 292 const timelineData = assertDefined(component.timelineData); 293 timelineData.initialize( 294 traces, 295 undefined, 296 TimestampConverterUtils.TIMESTAMP_CONVERTER, 297 ); 298 timelineData.setPosition(position100); 299 fixture.detectChanges(); 300 const nextEntryButton = assertDefined( 301 htmlElement.querySelector<HTMLElement>('#next_entry_button'), 302 ); 303 const prevEntryButton = assertDefined( 304 htmlElement.querySelector<HTMLElement>('#prev_entry_button'), 305 ); 306 expect(timelineData.getActiveTrace()).toBeUndefined(); 307 expect(timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual( 308 100n, 309 ); 310 311 expect(prevEntryButton.getAttribute('disabled')).toEqual('true'); 312 expect(nextEntryButton.getAttribute('disabled')).toEqual('true'); 313 }); 314 315 it('handles ActiveTraceChanged event', async () => { 316 loadSfWmTraces(); 317 fixture.detectChanges(); 318 319 const timelineComponent = assertDefined(component.timeline); 320 const nextEntryButton = assertDefined( 321 htmlElement.querySelector<HTMLElement>('#next_entry_button'), 322 ); 323 const prevEntryButton = assertDefined( 324 htmlElement.querySelector<HTMLElement>('#prev_entry_button'), 325 ); 326 const spy = spyOn( 327 assertDefined(timelineComponent.miniTimeline?.drawer), 328 'draw', 329 ); 330 331 await updateActiveTrace(TraceType.SURFACE_FLINGER); 332 fixture.detectChanges(); 333 checkActiveTraceSurfaceFlinger(nextEntryButton, prevEntryButton); 334 expect(spy).toHaveBeenCalled(); 335 }); 336 337 it('updates trace selection using selector', async () => { 338 const allTraceTypes = [ 339 TraceType.SEARCH, 340 TraceType.SCREEN_RECORDING, 341 TraceType.SURFACE_FLINGER, 342 TraceType.WINDOW_MANAGER, 343 TraceType.PROTO_LOG, 344 TraceType.VIEW_CAPTURE, 345 ]; 346 loadAllTraces(); 347 const [spyQueryResult, spyIter] = 348 UnitTestUtils.makeSearchTraceSpies(time100); 349 const searchTrace = new TraceBuilder<QueryResult>() 350 .setEntries([spyQueryResult]) 351 .setTimestamps([time100]) 352 .setDescriptors(['test query', '0']) 353 .setType(TraceType.SEARCH) 354 .build(); 355 await component.timeline?.onWinscopeEvent(new TraceAddRequest(searchTrace)); 356 expectSelectedTraceTypes(allTraceTypes); 357 358 await openSelectPanel(); 359 360 const matOptions = 361 document.documentElement.querySelectorAll<HTMLInputElement>('mat-option'); 362 await UnitTestUtils.checkTooltips( 363 Array.from(matOptions), 364 [ 365 'test query, 0', 366 'mock_screen_recording', 367 'file descriptor', 368 'file descriptor', 369 'file descriptor', 370 'Test Window, mock_view_capture', 371 ], 372 fixture, 373 ); 374 expect(matOptions.item(0).textContent).toContain('Search test query'); 375 const sfOption = matOptions.item(2); 376 expect(sfOption.textContent).toContain('Surface Flinger'); 377 expect(sfOption.ariaDisabled).toEqual('true'); 378 for (const i of [1, 3, 4]) { 379 expect(matOptions.item(i).ariaDisabled).toEqual('false'); 380 } 381 382 matOptions.item(3).click(); 383 fixture.detectChanges(); 384 const expectedTypes = [ 385 TraceType.SEARCH, 386 TraceType.SCREEN_RECORDING, 387 TraceType.SURFACE_FLINGER, 388 TraceType.PROTO_LOG, 389 TraceType.VIEW_CAPTURE, 390 ]; 391 expectSelectedTraceTypes(expectedTypes); 392 const traceIcons = Array.from( 393 htmlElement.querySelectorAll<HTMLElement>( 394 '#trace-selector .shown-selection .mat-icon', 395 ), 396 ).slice(1); 397 traceIcons.forEach((el, index) => { 398 const text = el.textContent?.trim(); 399 const expectedType = expectedTypes[index]; 400 expect(text).toEqual(TRACE_INFO[expectedType].icon); 401 }); 402 await UnitTestUtils.checkTooltips( 403 traceIcons, 404 [ 405 'Search test query', 406 'Screen Recording mock_screen_recording', 407 TRACE_INFO[TraceType.SURFACE_FLINGER].name, 408 TRACE_INFO[TraceType.PROTO_LOG].name, 409 'View Capture Test Window', 410 ], 411 fixture, 412 ); 413 414 matOptions.item(3).click(); 415 fixture.detectChanges(); 416 expectSelectedTraceTypes(allTraceTypes); 417 const newIcons = htmlElement.querySelectorAll( 418 '#trace-selector .shown-selection .mat-icon', 419 ); 420 expect( 421 Array.from(newIcons) 422 .map((icon) => icon.textContent?.trim()) 423 .slice(1), 424 ).toEqual(allTraceTypes.map((type) => TRACE_INFO[type].icon)); 425 }); 426 427 it('update name and disables option for dumps', async () => { 428 loadAllTraces(component, fixture, false); 429 await openSelectPanel(); 430 431 const matOptions = 432 document.documentElement.querySelectorAll<HTMLInputElement>('mat-option'); // [WM, SF, SR, ProtoLog, VC] 433 434 for (const i of [0, 2, 4]) { 435 expect(matOptions.item(i).ariaDisabled).toEqual('false'); 436 } 437 for (const i of [1, 3]) { 438 expect(matOptions.item(i).ariaDisabled).toEqual('true'); 439 } 440 expect(matOptions.item(3).textContent).toContain('ProtoLog Dump'); 441 expect(matOptions.item(4).textContent).toContain( 442 'View Capture Test Window', 443 ); 444 }); 445 446 it('next button disabled if no next entry', () => { 447 loadSfWmTraces(); 448 const timelineData = assertDefined(component.timelineData); 449 450 expect(timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual( 451 100n, 452 ); 453 454 const nextEntryButton = assertDefined( 455 htmlElement.querySelector('#next_entry_button'), 456 ); 457 expect(nextEntryButton.getAttribute('disabled')).toBeFalsy(); 458 459 timelineData.setPosition(position90); 460 fixture.detectChanges(); 461 expect(nextEntryButton.getAttribute('disabled')).toBeFalsy(); 462 463 timelineData.setPosition(position110); 464 fixture.detectChanges(); 465 expect(nextEntryButton.getAttribute('disabled')).toBeTruthy(); 466 467 timelineData.setPosition(position112); 468 fixture.detectChanges(); 469 expect(nextEntryButton.getAttribute('disabled')).toBeTruthy(); 470 }); 471 472 it('prev button disabled if no prev entry', () => { 473 loadSfWmTraces(); 474 const timelineData = assertDefined(component.timelineData); 475 476 expect(timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual( 477 100n, 478 ); 479 const prevEntryButton = assertDefined( 480 htmlElement.querySelector('#prev_entry_button'), 481 ); 482 expect(prevEntryButton.getAttribute('disabled')).toBeTruthy(); 483 484 timelineData.setPosition(position90); 485 fixture.detectChanges(); 486 expect(prevEntryButton.getAttribute('disabled')).toBeTruthy(); 487 488 timelineData.setPosition(position110); 489 fixture.detectChanges(); 490 expect(prevEntryButton.getAttribute('disabled')).toBeFalsy(); 491 492 timelineData.setPosition(position112); 493 fixture.detectChanges(); 494 expect(prevEntryButton.getAttribute('disabled')).toBeFalsy(); 495 }); 496 497 it('next button enabled for different active viewers', async () => { 498 loadSfWmTraces(); 499 const nextEntryButton = assertDefined( 500 htmlElement.querySelector('#next_entry_button'), 501 ); 502 503 expect(nextEntryButton.getAttribute('disabled')).toBeNull(); 504 505 await updateActiveTrace(TraceType.WINDOW_MANAGER); 506 fixture.detectChanges(); 507 508 expect(nextEntryButton.getAttribute('disabled')).toBeNull(); 509 }); 510 511 it('changes timestamp on next entry button press', () => { 512 loadSfWmTraces(); 513 514 expect( 515 assertDefined(component.timelineData) 516 .getCurrentPosition() 517 ?.timestamp.getValueNs(), 518 ).toEqual(100n); 519 const nextEntryButton = assertDefined( 520 htmlElement.querySelector<HTMLElement>('#next_entry_button'), 521 ); 522 523 testCurrentTimestampOnButtonClick(nextEntryButton, position105, 110n); 524 525 testCurrentTimestampOnButtonClick(nextEntryButton, position100, 110n); 526 527 testCurrentTimestampOnButtonClick(nextEntryButton, position90, 100n); 528 529 // No change when we are already on the last timestamp of the active trace 530 testCurrentTimestampOnButtonClick(nextEntryButton, position110, 110n); 531 532 // No change when we are after the last entry of the active trace 533 testCurrentTimestampOnButtonClick(nextEntryButton, position112, 112n); 534 }); 535 536 it('changes timestamp on previous entry button press', () => { 537 loadSfWmTraces(); 538 539 expect( 540 assertDefined(component.timelineData) 541 .getCurrentPosition() 542 ?.timestamp.getValueNs(), 543 ).toEqual(100n); 544 const prevEntryButton = assertDefined( 545 htmlElement.querySelector<HTMLElement>('#prev_entry_button'), 546 ); 547 548 // In this state we are already on the first entry at timestamp 100, so 549 // there is no entry to move to before and we just don't update the timestamp 550 testCurrentTimestampOnButtonClick(prevEntryButton, position105, 105n); 551 552 testCurrentTimestampOnButtonClick(prevEntryButton, position110, 100n); 553 554 // Active entry here should be 110 so moving back means moving to 100. 555 testCurrentTimestampOnButtonClick(prevEntryButton, position112, 100n); 556 557 // No change when we are already on the first timestamp of the active trace 558 testCurrentTimestampOnButtonClick(prevEntryButton, position100, 100n); 559 560 // No change when we are before the first entry of the active trace 561 testCurrentTimestampOnButtonClick(prevEntryButton, position90, 90n); 562 }); 563 564 it('performs expected action on arrow key press depending on input form focus', () => { 565 loadSfWmTraces(); 566 const timelineComponent = assertDefined(component.timeline); 567 568 const spyNextEntry = spyOn(timelineComponent, 'moveToNextEntry'); 569 const spyPrevEntry = spyOn(timelineComponent, 'moveToPreviousEntry'); 570 571 document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'})); 572 fixture.detectChanges(); 573 expect(spyNextEntry).toHaveBeenCalled(); 574 575 const formElement = htmlElement.querySelector('.time-input input'); 576 const focusInEvent = new FocusEvent('focusin'); 577 Object.defineProperty(focusInEvent, 'target', {value: formElement}); 578 document.dispatchEvent(focusInEvent); 579 fixture.detectChanges(); 580 581 document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); 582 fixture.detectChanges(); 583 expect(spyPrevEntry).not.toHaveBeenCalled(); 584 585 const focusOutEvent = new FocusEvent('focusout'); 586 Object.defineProperty(focusOutEvent, 'target', {value: formElement}); 587 document.dispatchEvent(focusOutEvent); 588 fixture.detectChanges(); 589 590 document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); 591 fixture.detectChanges(); 592 expect(spyPrevEntry).toHaveBeenCalled(); 593 }); 594 595 it('updates position based on ns input field', () => { 596 loadSfWmTraces(); 597 598 expect( 599 assertDefined(component.timelineData) 600 .getCurrentPosition() 601 ?.timestamp.getValueNs(), 602 ).toEqual(100n); 603 604 const timeInputField = assertDefined( 605 document.querySelector<HTMLInputElement>('.time-input.nano'), 606 ); 607 608 testCurrentTimestampOnTimeInput( 609 timeInputField, 610 position105, 611 '110 ns', 612 110n, 613 ); 614 615 testCurrentTimestampOnTimeInput( 616 timeInputField, 617 position100, 618 '110 ns', 619 110n, 620 ); 621 622 testCurrentTimestampOnTimeInput(timeInputField, position90, '100 ns', 100n); 623 624 // No change when we are already on the last timestamp of the active trace 625 testCurrentTimestampOnTimeInput( 626 timeInputField, 627 position110, 628 '110 ns', 629 110n, 630 ); 631 632 // No change when we are after the last entry of the active trace 633 testCurrentTimestampOnTimeInput( 634 timeInputField, 635 position112, 636 '112 ns', 637 112n, 638 ); 639 }); 640 641 it('updates position based on human time input field using date time format', () => { 642 loadSfWmTraces(); 643 644 expect( 645 assertDefined(component.timelineData) 646 .getCurrentPosition() 647 ?.timestamp.getValueNs(), 648 ).toEqual(100n); 649 650 const timeInputField = assertDefined( 651 document.querySelector<HTMLInputElement>('.time-input.human'), 652 ); 653 654 testCurrentTimestampOnTimeInput( 655 timeInputField, 656 position105, 657 '1970-01-01, 00:00:00.000000110', 658 110n, 659 ); 660 661 testCurrentTimestampOnTimeInput( 662 timeInputField, 663 position100, 664 '1970-01-01, 00:00:00.000000110', 665 110n, 666 ); 667 668 testCurrentTimestampOnTimeInput( 669 timeInputField, 670 position90, 671 '1970-01-01, 00:00:00.000000100', 672 100n, 673 ); 674 675 // No change when we are already on the last timestamp of the active trace 676 testCurrentTimestampOnTimeInput( 677 timeInputField, 678 position110, 679 '1970-01-01, 00:00:00.000000110', 680 110n, 681 ); 682 683 // No change when we are after the last entry of the active trace 684 testCurrentTimestampOnTimeInput( 685 timeInputField, 686 position112, 687 '1970-01-01, 00:00:00.000000112', 688 112n, 689 ); 690 }); 691 692 it('updates position based on human time input field using ISO timestamp format', () => { 693 loadSfWmTraces(); 694 695 expect( 696 assertDefined(component.timelineData) 697 .getCurrentPosition() 698 ?.timestamp.valueOf(), 699 ).toEqual(100n); 700 701 const timeInputField = assertDefined( 702 document.querySelector<HTMLInputElement>('.time-input.human'), 703 ); 704 705 testCurrentTimestampOnTimeInput( 706 timeInputField, 707 position90, 708 '1970-01-01T00:00:00.000000100', 709 100n, 710 ); 711 }); 712 713 it('updates position based on human time input field using time-only format', () => { 714 loadSfWmTraces(); 715 716 expect( 717 assertDefined(component.timelineData) 718 .getCurrentPosition() 719 ?.timestamp.valueOf(), 720 ).toEqual(100n); 721 722 const timeInputField = assertDefined( 723 document.querySelector<HTMLInputElement>('.time-input.human'), 724 ); 725 726 testCurrentTimestampOnTimeInput( 727 timeInputField, 728 position105, 729 '00:00:00.000000110', 730 110n, 731 ); 732 }); 733 734 it('sets initial zoom of mini timeline from first non-SR viewer to end of all traces', () => { 735 loadAllTraces(); 736 const timelineComponent = assertDefined(component.timeline); 737 expect(timelineComponent.initialZoom).toEqual( 738 new TimeRange(time100, time112), 739 ); 740 }); 741 742 it('stores manual trace deselection and applies on new load', async () => { 743 loadAllTraces(); 744 const firstTimeline = assertDefined(component.timeline); 745 expectSelectedTraceTypes( 746 [ 747 TraceType.SCREEN_RECORDING, 748 TraceType.SURFACE_FLINGER, 749 TraceType.WINDOW_MANAGER, 750 TraceType.PROTO_LOG, 751 TraceType.VIEW_CAPTURE, 752 ], 753 firstTimeline, 754 ); 755 await openSelectPanel(); 756 clickTraceFromSelectPanel(2); 757 clickTraceFromSelectPanel(3); 758 clickTraceFromSelectPanel(4); 759 expectSelectedTraceTypes( 760 [TraceType.SCREEN_RECORDING, TraceType.SURFACE_FLINGER], 761 firstTimeline, 762 ); 763 764 const secondFixture = TestBed.createComponent(TestHostComponent); 765 const secondHost = secondFixture.componentInstance; 766 loadAllTraces(secondHost, secondFixture); 767 const secondTimeline = assertDefined(secondHost.timeline); 768 expectSelectedTraceTypes( 769 [TraceType.SCREEN_RECORDING, TraceType.SURFACE_FLINGER], 770 secondTimeline, 771 ); 772 773 clickTraceFromSelectPanel(2); 774 expectSelectedTraceTypes( 775 [TraceType.SCREEN_RECORDING, TraceType.SURFACE_FLINGER], 776 secondTimeline, 777 ); 778 779 const thirdFixture = TestBed.createComponent(TestHostComponent); 780 const thirdHost = thirdFixture.componentInstance; 781 loadAllTraces(thirdHost, thirdFixture); 782 const thirdTimeline = assertDefined(thirdHost.timeline); 783 expectSelectedTraceTypes( 784 [ 785 TraceType.SCREEN_RECORDING, 786 TraceType.SURFACE_FLINGER, 787 TraceType.WINDOW_MANAGER, 788 ], 789 thirdTimeline, 790 ); 791 }); 792 793 it('does not apply stored trace deselection on active trace', async () => { 794 loadAllTraces(); 795 const firstTimeline = assertDefined(component.timeline); 796 expectSelectedTraceTypes( 797 [ 798 TraceType.SCREEN_RECORDING, 799 TraceType.SURFACE_FLINGER, 800 TraceType.WINDOW_MANAGER, 801 TraceType.PROTO_LOG, 802 TraceType.VIEW_CAPTURE, 803 ], 804 firstTimeline, 805 ); 806 await updateActiveTrace(TraceType.PROTO_LOG); 807 await openSelectPanel(); 808 clickTraceFromSelectPanel(1); 809 clickTraceFromSelectPanel(4); 810 expectSelectedTraceTypes( 811 [ 812 TraceType.SCREEN_RECORDING, 813 TraceType.WINDOW_MANAGER, 814 TraceType.PROTO_LOG, 815 ], 816 firstTimeline, 817 ); 818 819 const secondFixture = TestBed.createComponent(TestHostComponent); 820 const secondHost = secondFixture.componentInstance; 821 loadAllTraces(secondHost, secondFixture); 822 const secondTimeline = assertDefined(secondHost.timeline); 823 expectSelectedTraceTypes( 824 [ 825 TraceType.SCREEN_RECORDING, 826 TraceType.SURFACE_FLINGER, 827 TraceType.WINDOW_MANAGER, 828 TraceType.PROTO_LOG, 829 ], 830 secondTimeline, 831 ); 832 }); 833 834 it('does not apply stored trace deselection if only one timestamp available', async () => { 835 loadAllTraces(); 836 await updateActiveTrace(TraceType.PROTO_LOG); 837 await openSelectPanel(); 838 clickTraceFromSelectPanel(2); 839 840 const secondFixture = TestBed.createComponent(TestHostComponent); 841 const secondHost = secondFixture.componentInstance; 842 const secondElement = secondFixture.nativeElement; 843 await loadTracesWithOneTimestamp(secondHost, secondFixture); 844 845 const shownSelection = assertDefined( 846 secondElement.querySelector('#trace-selector .shown-selection'), 847 ); 848 expect(shownSelection.innerHTML).toContain('Window Manager'); 849 expect(shownSelection.textContent).not.toContain('Surface Flinger'); 850 }); 851 852 it('does not store traces based on active view trace type', async () => { 853 loadAllTraces(); 854 expectSelectedTraceTypes( 855 [ 856 TraceType.SCREEN_RECORDING, 857 TraceType.SURFACE_FLINGER, 858 TraceType.WINDOW_MANAGER, 859 TraceType.PROTO_LOG, 860 TraceType.VIEW_CAPTURE, 861 ], 862 component.timeline, 863 ); 864 await openSelectPanel(); 865 clickTraceFromSelectPanel(3); 866 clickTraceFromSelectPanel(4); 867 expectSelectedTraceTypes( 868 [ 869 TraceType.SCREEN_RECORDING, 870 TraceType.SURFACE_FLINGER, 871 TraceType.WINDOW_MANAGER, 872 ], 873 component.timeline, 874 ); 875 await updateActiveTrace(TraceType.PROTO_LOG); 876 fixture.detectChanges(); 877 expectSelectedTraceTypes( 878 [ 879 TraceType.SCREEN_RECORDING, 880 TraceType.SURFACE_FLINGER, 881 TraceType.WINDOW_MANAGER, 882 TraceType.PROTO_LOG, 883 ], 884 component.timeline, 885 ); 886 887 const secondFixture = TestBed.createComponent(TestHostComponent); 888 const secondHost = secondFixture.componentInstance; 889 loadAllTraces(secondHost, secondFixture); 890 const secondTimeline = assertDefined(secondHost.timeline); 891 expectSelectedTraceTypes( 892 [ 893 TraceType.SCREEN_RECORDING, 894 TraceType.SURFACE_FLINGER, 895 TraceType.WINDOW_MANAGER, 896 ], 897 secondTimeline, 898 ); 899 }); 900 901 it('applies stored trace deselection between non-consecutive applicable sessions', async () => { 902 loadAllTraces(); 903 expectSelectedTraceTypes( 904 [ 905 TraceType.SCREEN_RECORDING, 906 TraceType.SURFACE_FLINGER, 907 TraceType.WINDOW_MANAGER, 908 TraceType.PROTO_LOG, 909 TraceType.VIEW_CAPTURE, 910 ], 911 component.timeline, 912 ); 913 await openSelectPanel(); 914 clickTraceFromSelectPanel(3); 915 clickTraceFromSelectPanel(4); 916 expectSelectedTraceTypes( 917 [ 918 TraceType.SCREEN_RECORDING, 919 TraceType.SURFACE_FLINGER, 920 TraceType.WINDOW_MANAGER, 921 ], 922 component.timeline, 923 ); 924 925 const secondFixture = TestBed.createComponent(TestHostComponent); 926 const secondHost = secondFixture.componentInstance; 927 loadSfWmTraces(secondHost, secondFixture); 928 const secondTimeline = assertDefined(secondHost.timeline); 929 expectSelectedTraceTypes( 930 [TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER], 931 secondTimeline, 932 ); 933 934 const thirdFixture = TestBed.createComponent(TestHostComponent); 935 const thirdHost = thirdFixture.componentInstance; 936 loadAllTraces(thirdHost, thirdFixture); 937 const thirdTimeline = assertDefined(thirdHost.timeline); 938 expectSelectedTraceTypes( 939 [ 940 TraceType.SCREEN_RECORDING, 941 TraceType.SURFACE_FLINGER, 942 TraceType.WINDOW_MANAGER, 943 ], 944 thirdTimeline, 945 ); 946 }); 947 948 it('shows all traces in new session that were not present (so not deselected) in previous session', async () => { 949 loadSfWmTraces(); 950 expectSelectedTraceTypes( 951 [TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER], 952 component.timeline, 953 ); 954 await openSelectPanel(); 955 clickTraceFromSelectPanel(1); 956 expectSelectedTraceTypes([TraceType.SURFACE_FLINGER], component.timeline); 957 958 const secondFixture = TestBed.createComponent(TestHostComponent); 959 const secondHost = secondFixture.componentInstance; 960 loadAllTraces(secondHost, secondFixture); 961 const secondTimeline = assertDefined(secondHost.timeline); 962 expectSelectedTraceTypes( 963 [ 964 TraceType.SCREEN_RECORDING, 965 TraceType.SURFACE_FLINGER, 966 TraceType.PROTO_LOG, 967 TraceType.VIEW_CAPTURE, 968 ], 969 secondTimeline, 970 ); 971 }); 972 973 it('toggles bookmark of current position', () => { 974 loadSfWmTraces(); 975 const timelineComponent = assertDefined(component.timeline); 976 expect(timelineComponent.bookmarks).toEqual([]); 977 expect(timelineComponent.currentPositionBookmarked()).toBeFalse(); 978 979 const bookmarkIcon = assertDefined( 980 htmlElement.querySelector<HTMLElement>('.bookmark-icon'), 981 ); 982 bookmarkIcon.click(); 983 fixture.detectChanges(); 984 985 expect(timelineComponent.bookmarks).toEqual([time100]); 986 expect(timelineComponent.currentPositionBookmarked()).toBeTrue(); 987 988 bookmarkIcon.click(); 989 fixture.detectChanges(); 990 expect(timelineComponent.bookmarks).toEqual([]); 991 expect(timelineComponent.currentPositionBookmarked()).toBeFalse(); 992 }); 993 994 it('toggles same bookmark if click within range', () => { 995 loadTracesWithLargeTimeRange(); 996 997 const timelineComponent = assertDefined(component.timeline); 998 expect(timelineComponent.bookmarks.length).toEqual(0); 999 1000 openContextMenu(); 1001 clickToggleBookmarkOption(); 1002 expect(timelineComponent.bookmarks.length).toEqual(1); 1003 1004 // click within marker y-pos, x-pos close enough to remove bookmark 1005 openContextMenu(5); 1006 clickToggleBookmarkOption(); 1007 expect(timelineComponent.bookmarks.length).toEqual(0); 1008 1009 openContextMenu(); 1010 clickToggleBookmarkOption(); 1011 expect(timelineComponent.bookmarks.length).toEqual(1); 1012 1013 // click within marker y-pos, x-pos too large so new bookmark added 1014 openContextMenu(20); 1015 clickToggleBookmarkOption(); 1016 expect(timelineComponent.bookmarks.length).toEqual(2); 1017 1018 openContextMenu(20); 1019 clickToggleBookmarkOption(); 1020 expect(timelineComponent.bookmarks.length).toEqual(1); 1021 1022 // click below marker y-pos, x-pos now too large so new bookmark added 1023 openContextMenu(5, true); 1024 clickToggleBookmarkOption(); 1025 expect(timelineComponent.bookmarks.length).toEqual(2); 1026 }); 1027 1028 it('removes all bookmarks', () => { 1029 loadSfWmTraces(); 1030 const timelineComponent = assertDefined(component.timeline); 1031 timelineComponent.bookmarks = [time100, time101, time112]; 1032 fixture.detectChanges(); 1033 1034 openContextMenu(); 1035 clickRemoveAllBookmarksOption(); 1036 expect(timelineComponent.bookmarks).toEqual([]); 1037 }); 1038 1039 it('updates active trace then trace position on mini timeline click', async () => { 1040 loadAllTraces(); 1041 const timelineComponent = assertDefined(component.timeline); 1042 1043 let firstEvent: WinscopeEvent | undefined; 1044 let activeTrace: Trace<object> | undefined; 1045 let position: TracePosition | undefined; 1046 timelineComponent.setEmitEvent(async (event: WinscopeEvent) => { 1047 if (!firstEvent) { 1048 expect(event).toBeInstanceOf(ActiveTraceChanged); 1049 firstEvent = event; 1050 activeTrace = (event as ActiveTraceChanged).trace; 1051 } else { 1052 expect(event).toBeInstanceOf(TracePositionUpdate); 1053 position = (event as TracePositionUpdate).position; 1054 } 1055 }); 1056 const miniTimelineComponent = assertDefined(timelineComponent.miniTimeline); 1057 const trace = assertDefined( 1058 component.timelineData.getTraces().getTrace(TraceType.WINDOW_MANAGER), 1059 ); 1060 spyOn( 1061 assertDefined(miniTimelineComponent.drawer), 1062 'getTraceClicked', 1063 ).and.returnValue(Promise.resolve(trace)); 1064 const canvas = miniTimelineComponent.getCanvas(); 1065 canvas.dispatchEvent(new MouseEvent('mousedown')); 1066 fixture.detectChanges(); 1067 await fixture.whenStable(); 1068 fixture.detectChanges(); 1069 await fixture.whenStable(); 1070 1071 expect(activeTrace).toEqual(trace); 1072 expect(position).toBeDefined(); 1073 }); 1074 1075 it('adds/removes trace and redraws timeline', async () => { 1076 loadSfWmTraces(); 1077 const timelineComponent = assertDefined(component.timeline); 1078 const initialTraces = timelineComponent.sortedTraces.slice(); 1079 const spy = spyOn( 1080 assertDefined(timelineComponent.miniTimeline?.drawer), 1081 'draw', 1082 ); 1083 const trace = UnitTestUtils.makeEmptyTrace(TraceType.SEARCH); 1084 1085 await timelineComponent.onWinscopeEvent(new TraceAddRequest(trace)); 1086 expect(spy).toHaveBeenCalledTimes(1); 1087 expect(timelineComponent.sortedTraces).not.toEqual(initialTraces); 1088 expect(timelineComponent.sortedTraces[0]).toEqual(trace); 1089 1090 await timelineComponent.onWinscopeEvent(new TraceRemoveRequest(trace)); 1091 expect(spy).toHaveBeenCalledTimes(2); 1092 expect(timelineComponent.sortedTraces).toEqual(initialTraces); 1093 }); 1094 1095 it('disables or enables timeline on winscope events', async () => { 1096 loadSfWmTraces(); 1097 const timelineComponent = assertDefined(component.timeline); 1098 checkTimelineEnabled(); 1099 1100 await timelineComponent.onWinscopeEvent(new InitializeTraceSearchRequest()); 1101 checkTimelineDisabled(); 1102 await timelineComponent.onWinscopeEvent(new TraceSearchInitialized([])); 1103 checkTimelineEnabled(); 1104 1105 await timelineComponent.onWinscopeEvent(new TraceSearchRequest('')); 1106 checkTimelineDisabled(); 1107 await timelineComponent.onWinscopeEvent(new TraceSearchCompleted()); 1108 checkTimelineEnabled(); 1109 }); 1110 1111 it('does not handle arrow key presses if component disabled', () => { 1112 loadSfWmTraces(); 1113 const timelineComponent = assertDefined(component.timeline); 1114 timelineComponent.isDisabled = true; 1115 fixture.detectChanges(); 1116 1117 const spyNextEntry = spyOn(timelineComponent, 'moveToNextEntry'); 1118 document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'})); 1119 fixture.detectChanges(); 1120 expect(spyNextEntry).not.toHaveBeenCalled(); 1121 }); 1122 1123 it('redraws both timelines on scroll', () => { 1124 loadSfWmTraces(); 1125 openExpandedTimeline(); 1126 const expandedDrawSpy = spyOn(CanvasDrawer.prototype, 'drawRect'); 1127 const miniDrawSpy = spyOn(MiniTimelineDrawerImpl.prototype, 'draw'); 1128 1129 // scroll from expanded timeline 1130 const wheelEvent = new WheelEvent('wheel'); 1131 spyOnProperty(wheelEvent, 'deltaY').and.returnValue(-200); 1132 spyOnProperty(wheelEvent, 'deltaX').and.returnValue(0); 1133 spyOnProperty(wheelEvent, 'y').and.returnValue(10); 1134 assertDefined(htmlElement.querySelector('single-timeline')).dispatchEvent( 1135 wheelEvent, 1136 ); 1137 fixture.detectChanges(); 1138 expect(expandedDrawSpy).toHaveBeenCalledTimes(5); // 3 entries total + 2 selected 1139 expect(miniDrawSpy).toHaveBeenCalledTimes(1); // all on one canvas so spy called once 1140 1141 // scroll from mini timeline 1142 expandedDrawSpy.calls.reset(); 1143 miniDrawSpy.calls.reset(); 1144 spyOnProperty(wheelEvent, 'target').and.returnValue( 1145 assertDefined(htmlElement.querySelector('#mini-timeline-canvas')), 1146 ); 1147 assertDefined(htmlElement.querySelector('mini-timeline')).dispatchEvent( 1148 wheelEvent, 1149 ); 1150 fixture.detectChanges(); 1151 expect(expandedDrawSpy).toHaveBeenCalledTimes(4); // 2 entries total + 2 selected 1152 expect(miniDrawSpy).toHaveBeenCalledTimes(1); 1153 }); 1154 1155 it('redraws both timelines on new position from expanded timeline click', () => { 1156 loadSfWmTraces(); 1157 openExpandedTimeline(); 1158 const expandedDrawSpy = spyOn(CanvasDrawer.prototype, 'drawRect'); 1159 const miniDrawSpy = spyOn(MiniTimelineDrawerImpl.prototype, 'draw'); 1160 1161 const clickEvent = new MouseEvent('mousedown'); 1162 spyOnProperty(clickEvent, 'offsetX').and.returnValue(0); 1163 spyOnProperty(clickEvent, 'offsetY').and.returnValue(0); 1164 assertDefined( 1165 htmlElement.querySelector<HTMLElement>('single-timeline #canvas'), 1166 ).dispatchEvent(clickEvent); 1167 fixture.detectChanges(); 1168 expect(expandedDrawSpy).toHaveBeenCalledTimes(3); // redraws SF timeline row 1169 expect(miniDrawSpy).toHaveBeenCalledTimes(1); // all on one canvas so spy called once 1170 }); 1171 1172 function loadSfWmTraces(hostComponent = component, hostFixture = fixture) { 1173 const traces = new TracesBuilder() 1174 .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110]) 1175 .setTimestamps(TraceType.WINDOW_MANAGER, [ 1176 time90, 1177 time101, 1178 time110, 1179 time112, 1180 ]) 1181 .build(); 1182 1183 const timelineData = assertDefined(hostComponent.timelineData); 1184 timelineData.initialize( 1185 traces, 1186 undefined, 1187 TimestampConverterUtils.TIMESTAMP_CONVERTER, 1188 ); 1189 timelineData.setPosition(position100); 1190 hostComponent.allTraces = hostComponent.timelineData.getTraces(); 1191 hostFixture.detectChanges(); 1192 } 1193 1194 function loadAllTraces( 1195 hostComponent = component, 1196 hostFixture = fixture, 1197 loadAllTraces = true, 1198 ) { 1199 const traces = new TracesBuilder() 1200 .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110]) 1201 .setTimestamps(TraceType.WINDOW_MANAGER, [ 1202 time90, 1203 time101, 1204 time110, 1205 time112, 1206 ]) 1207 .setTimestamps( 1208 TraceType.SCREEN_RECORDING, 1209 [time110], 1210 ['mock_screen_recording'], 1211 ) 1212 .setTimestamps(TraceType.PROTO_LOG, [time100]) 1213 .setTimestamps( 1214 TraceType.VIEW_CAPTURE, 1215 [time100], 1216 ['Test Window', 'mock_view_capture'], 1217 ) 1218 .build(); 1219 1220 let timelineDataTraces: Traces | undefined; 1221 if (loadAllTraces) { 1222 timelineDataTraces = traces; 1223 } else { 1224 timelineDataTraces = new Traces(); 1225 traces.forEachTrace((trace) => { 1226 if (trace.type !== TraceType.PROTO_LOG) { 1227 assertDefined(timelineDataTraces).addTrace(trace); 1228 } 1229 }); 1230 } 1231 1232 assertDefined(hostComponent.timelineData).initialize( 1233 timelineDataTraces, 1234 undefined, 1235 TimestampConverterUtils.TIMESTAMP_CONVERTER, 1236 ); 1237 hostComponent.allTraces = traces; 1238 hostFixture.detectChanges(); 1239 } 1240 1241 function loadTracesWithLargeTimeRange() { 1242 const traces = new TracesBuilder() 1243 .setTimestamps(TraceType.SURFACE_FLINGER, [ 1244 time100, 1245 time2000, 1246 time3000, 1247 time4000, 1248 ]) 1249 .setTimestamps(TraceType.WINDOW_MANAGER, [ 1250 time2000, 1251 time4000, 1252 time6000, 1253 time8000, 1254 ]) 1255 .build(); 1256 1257 const timelineData = assertDefined(component.timelineData); 1258 timelineData.initialize( 1259 traces, 1260 undefined, 1261 TimestampConverterUtils.TIMESTAMP_CONVERTER, 1262 ); 1263 timelineData.setPosition(position100); 1264 component.allTraces = timelineData.getTraces(); 1265 fixture.detectChanges(); 1266 } 1267 1268 function getLoadedTrace(type: TraceType): Trace<object> { 1269 const timelineData = assertDefined(component.timelineData); 1270 const trace = assertDefined( 1271 timelineData.getTraces().getTrace(type), 1272 ) as Trace<object>; 1273 return trace; 1274 } 1275 1276 async function loadTracesWithOneTimestamp( 1277 hostComponent = component, 1278 hostFixture = fixture, 1279 ) { 1280 const traces = new TracesBuilder() 1281 .setTimestamps(TraceType.SURFACE_FLINGER, []) 1282 .setTimestamps(TraceType.WINDOW_MANAGER, [time100]) 1283 .build(); 1284 assertDefined(hostComponent.timelineData).initialize( 1285 traces, 1286 undefined, 1287 TimestampConverterUtils.TIMESTAMP_CONVERTER, 1288 ); 1289 hostComponent.allTraces = traces; 1290 hostFixture.detectChanges(); 1291 await hostFixture.whenStable(); 1292 hostFixture.detectChanges(); 1293 } 1294 1295 async function updateActiveTrace(type: TraceType) { 1296 const trace = getLoadedTrace(type); 1297 const timelineData = assertDefined(component.timelineData); 1298 timelineData.trySetActiveTrace(trace); 1299 1300 const timelineComponent = assertDefined(component.timeline); 1301 await timelineComponent.onWinscopeEvent(new ActiveTraceChanged(trace)); 1302 } 1303 1304 function expectSelectedTraceTypes( 1305 expected: TraceType[], 1306 timelineComponent?: TimelineComponent, 1307 ) { 1308 const timeline = assertDefined(timelineComponent ?? component.timeline); 1309 const actual = timeline.selectedTraces.map((trace) => trace.type); 1310 expect(actual).toEqual(expected); 1311 } 1312 1313 function testCurrentTimestampOnButtonClick( 1314 button: HTMLElement, 1315 pos: TracePosition, 1316 expectedNs: bigint, 1317 ) { 1318 const timelineData = assertDefined(component.timelineData); 1319 timelineData.setPosition(pos); 1320 fixture.detectChanges(); 1321 button.click(); 1322 fixture.detectChanges(); 1323 expect(timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual( 1324 expectedNs, 1325 ); 1326 } 1327 1328 function testCurrentTimestampOnTimeInput( 1329 inputField: HTMLInputElement, 1330 pos: TracePosition, 1331 textInput: string, 1332 expectedNs: bigint, 1333 ) { 1334 const timelineData = assertDefined(component.timelineData); 1335 timelineData.setPosition(pos); 1336 fixture.detectChanges(); 1337 1338 inputField.value = textInput; 1339 inputField.dispatchEvent(new Event('change')); 1340 fixture.detectChanges(); 1341 1342 expect(timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual( 1343 expectedNs, 1344 ); 1345 } 1346 1347 async function openSelectPanel() { 1348 const selectTrigger = assertDefined( 1349 htmlElement.querySelector<HTMLElement>('.mat-select-trigger'), 1350 ); 1351 selectTrigger.click(); 1352 fixture.detectChanges(); 1353 await fixture.whenStable(); 1354 } 1355 1356 function clickTraceFromSelectPanel(index: number) { 1357 const matOptions = assertDefined( 1358 document.documentElement.querySelectorAll<HTMLElement>('mat-option'), 1359 ); 1360 matOptions.item(index).click(); 1361 fixture.detectChanges(); 1362 } 1363 1364 function checkActiveTraceSurfaceFlinger( 1365 nextEntryButton: HTMLElement, 1366 prevEntryButton: HTMLElement, 1367 ) { 1368 testCurrentTimestampOnButtonClick(prevEntryButton, position110, 100n); 1369 expect(prevEntryButton.getAttribute('disabled')).toEqual('true'); 1370 expect(nextEntryButton.getAttribute('disabled')).toBeNull(); 1371 testCurrentTimestampOnButtonClick(nextEntryButton, position100, 110n); 1372 expect(prevEntryButton.getAttribute('disabled')).toBeNull(); 1373 expect(nextEntryButton.getAttribute('disabled')).toEqual('true'); 1374 } 1375 1376 function checkActiveTraceWindowManager( 1377 nextEntryButton: HTMLElement, 1378 prevEntryButton: HTMLElement, 1379 ) { 1380 testCurrentTimestampOnButtonClick(prevEntryButton, position90, 90n); 1381 expect(prevEntryButton.getAttribute('disabled')).toEqual('true'); 1382 expect(nextEntryButton.getAttribute('disabled')).toBeNull(); 1383 testCurrentTimestampOnButtonClick(nextEntryButton, position90, 101n); 1384 expect(prevEntryButton.getAttribute('disabled')).toBeNull(); 1385 expect(nextEntryButton.getAttribute('disabled')).toBeNull(); 1386 testCurrentTimestampOnButtonClick(nextEntryButton, position110, 112n); 1387 expect(prevEntryButton.getAttribute('disabled')).toBeNull(); 1388 expect(nextEntryButton.getAttribute('disabled')).toEqual('true'); 1389 } 1390 1391 function checkActiveTraceHasOneEntry( 1392 nextEntryButton: HTMLElement, 1393 prevEntryButton: HTMLElement, 1394 ) { 1395 expect(prevEntryButton.getAttribute('disabled')).toEqual('true'); 1396 expect(nextEntryButton.getAttribute('disabled')).toEqual('true'); 1397 } 1398 1399 function checkNoTimelineNavigation() { 1400 const timelineComponent = assertDefined(component.timeline); 1401 // no expand button 1402 expect( 1403 htmlElement.querySelector(`.${timelineComponent.TOGGLE_BUTTON_CLASS}`), 1404 ).toBeNull(); 1405 1406 // no timelines shown 1407 const miniTimelineElement = fixture.debugElement.query( 1408 By.directive(MiniTimelineComponent), 1409 ); 1410 expect(miniTimelineElement).toBeFalsy(); 1411 1412 // arrow key presses don't do anything 1413 const spyNextEntry = spyOn(timelineComponent, 'moveToNextEntry'); 1414 const spyPrevEntry = spyOn(timelineComponent, 'moveToPreviousEntry'); 1415 1416 document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'})); 1417 fixture.detectChanges(); 1418 expect(spyNextEntry).not.toHaveBeenCalled(); 1419 1420 document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); 1421 fixture.detectChanges(); 1422 expect(spyPrevEntry).not.toHaveBeenCalled(); 1423 } 1424 1425 function openContextMenu(xOffset = 0, clickBelowMarker = false) { 1426 const miniTimelineCanvas = assertDefined( 1427 htmlElement.querySelector<HTMLElement>('#mini-timeline-canvas'), 1428 ); 1429 const yOffset = clickBelowMarker 1430 ? assertDefined(component.timeline?.miniTimeline?.drawer?.getHeight()) / 1431 6 + 1432 1 1433 : 0; 1434 1435 const event = new MouseEvent('contextmenu'); 1436 spyOnProperty(event, 'offsetX').and.returnValue( 1437 miniTimelineCanvas.offsetLeft + 1438 miniTimelineCanvas.offsetWidth / 2 + 1439 xOffset, 1440 ); 1441 spyOnProperty(event, 'offsetY').and.returnValue( 1442 miniTimelineCanvas.offsetTop + yOffset, 1443 ); 1444 miniTimelineCanvas.dispatchEvent(event); 1445 fixture.detectChanges(); 1446 } 1447 1448 function clickToggleBookmarkOption() { 1449 const menu = assertDefined(document.querySelector('.context-menu')); 1450 const toggleOption = assertDefined( 1451 menu.querySelector<HTMLElement>('.context-menu-item'), 1452 ); 1453 toggleOption.click(); 1454 fixture.detectChanges(); 1455 } 1456 1457 function clickRemoveAllBookmarksOption() { 1458 const menu = assertDefined(document.querySelector('.context-menu')); 1459 const options = assertDefined( 1460 menu.querySelectorAll<HTMLElement>('.context-menu-item'), 1461 ); 1462 options.item(1).click(); 1463 fixture.detectChanges(); 1464 } 1465 1466 function checkTimelineEnabled() { 1467 expect(htmlElement.querySelector('.disabled-component')).toBeNull(); 1468 expect(htmlElement.querySelector('.disabled-message')).toBeNull(); 1469 } 1470 1471 function checkTimelineDisabled() { 1472 expect(htmlElement.querySelector('.disabled-component')).toBeTruthy(); 1473 expect(htmlElement.querySelector('.disabled-message')).toBeTruthy(); 1474 } 1475 1476 function openExpandedTimeline() { 1477 const timelineComponent = assertDefined(component.timeline); 1478 assertDefined( 1479 htmlElement.querySelector<HTMLElement>( 1480 `.${timelineComponent.TOGGLE_BUTTON_CLASS}`, 1481 ), 1482 ).click(); 1483 fixture.detectChanges(); 1484 } 1485 1486 @Component({ 1487 selector: 'host-component', 1488 template: ` 1489 <timeline 1490 [allTraces]="allTraces" 1491 [timelineData]="timelineData" 1492 [store]="store"></timeline> 1493 `, 1494 }) 1495 class TestHostComponent { 1496 timelineData = new TimelineData(); 1497 allTraces = new Traces(); 1498 store = new PersistentStore(); 1499 1500 @ViewChild(TimelineComponent) 1501 timeline: TimelineComponent | undefined; 1502 1503 ngOnDestroy() { 1504 if (this.timeline) { 1505 this.store.clear(this.timeline.storeKeyDeselectedTraces); 1506 } 1507 } 1508 } 1509}); 1510