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/persistent_store'; 39import {TimeRange} from 'common/time'; 40import { 41 ActiveTraceChanged, 42 ExpandedTimelineToggled, 43 WinscopeEvent, 44} from 'messaging/winscope_event'; 45import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 46import {TracesBuilder} from 'test/unit/traces_builder'; 47import {Trace} from 'trace/trace'; 48import {TRACE_INFO} from 'trace/trace_info'; 49import {TracePosition} from 'trace/trace_position'; 50import {TraceType} from 'trace/trace_type'; 51import {DefaultTimelineRowComponent} from './expanded-timeline/default_timeline_row_component'; 52import {ExpandedTimelineComponent} from './expanded-timeline/expanded_timeline_component'; 53import {TransitionTimelineComponent} from './expanded-timeline/transition_timeline_component'; 54import {MiniTimelineComponent} from './mini-timeline/mini_timeline_component'; 55import {SliderComponent} from './mini-timeline/slider_component'; 56import {TimelineComponent} from './timeline_component'; 57 58describe('TimelineComponent', () => { 59 const time90 = TimestampConverterUtils.makeRealTimestamp(90n); 60 const time100 = TimestampConverterUtils.makeRealTimestamp(100n); 61 const time101 = TimestampConverterUtils.makeRealTimestamp(101n); 62 const time105 = TimestampConverterUtils.makeRealTimestamp(105n); 63 const time110 = TimestampConverterUtils.makeRealTimestamp(110n); 64 const time112 = TimestampConverterUtils.makeRealTimestamp(112n); 65 66 const time1000 = TimestampConverterUtils.makeRealTimestamp(1000n); 67 const time2000 = TimestampConverterUtils.makeRealTimestamp(2000n); 68 const time3000 = TimestampConverterUtils.makeRealTimestamp(3000n); 69 const time4000 = TimestampConverterUtils.makeRealTimestamp(4000n); 70 const time6000 = TimestampConverterUtils.makeRealTimestamp(6000n); 71 const time8000 = TimestampConverterUtils.makeRealTimestamp(8000n); 72 73 const position90 = TracePosition.fromTimestamp(time90); 74 const position100 = TracePosition.fromTimestamp(time100); 75 const position105 = TracePosition.fromTimestamp(time105); 76 const position110 = TracePosition.fromTimestamp(time110); 77 const position112 = TracePosition.fromTimestamp(time112); 78 79 let fixture: ComponentFixture<TestHostComponent>; 80 let component: TestHostComponent; 81 let htmlElement: HTMLElement; 82 83 beforeEach(async () => { 84 await TestBed.configureTestingModule({ 85 imports: [ 86 FormsModule, 87 MatButtonModule, 88 MatFormFieldModule, 89 MatInputModule, 90 MatIconModule, 91 MatSelectModule, 92 MatTooltipModule, 93 ReactiveFormsModule, 94 BrowserAnimationsModule, 95 DragDropModule, 96 ClipboardModule, 97 CdkMenuModule, 98 ], 99 declarations: [ 100 TestHostComponent, 101 ExpandedTimelineComponent, 102 DefaultTimelineRowComponent, 103 MatDrawer, 104 MatDrawerContainer, 105 MatDrawerContent, 106 MiniTimelineComponent, 107 TimelineComponent, 108 SliderComponent, 109 TransitionTimelineComponent, 110 ], 111 }) 112 .overrideComponent(TimelineComponent, { 113 set: {changeDetection: ChangeDetectionStrategy.Default}, 114 }) 115 .compileComponents(); 116 fixture = TestBed.createComponent(TestHostComponent); 117 component = fixture.componentInstance; 118 htmlElement = fixture.nativeElement; 119 }); 120 121 it('can be created', () => { 122 expect(component).toBeTruthy(); 123 }); 124 125 it('can be expanded', () => { 126 const traces = new TracesBuilder() 127 .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110]) 128 .build(); 129 assertDefined(component.timelineData).initialize( 130 traces, 131 undefined, 132 TimestampConverterUtils.TIMESTAMP_CONVERTER, 133 ); 134 fixture.detectChanges(); 135 136 const timelineComponent = assertDefined(component.timeline); 137 138 const button = assertDefined( 139 htmlElement.querySelector(`.${timelineComponent.TOGGLE_BUTTON_CLASS}`), 140 ); 141 142 // initially not expanded 143 let expandedTimelineElement = fixture.debugElement.query( 144 By.directive(ExpandedTimelineComponent), 145 ); 146 expect(expandedTimelineElement).toBeFalsy(); 147 148 let isExpanded = false; 149 timelineComponent.setEmitEvent(async (event: WinscopeEvent) => { 150 expect(event).toBeInstanceOf(ExpandedTimelineToggled); 151 isExpanded = (event as ExpandedTimelineToggled).isTimelineExpanded; 152 }); 153 154 button.dispatchEvent(new Event('click')); 155 expandedTimelineElement = fixture.debugElement.query( 156 By.directive(ExpandedTimelineComponent), 157 ); 158 expect(expandedTimelineElement).toBeTruthy(); 159 expect(isExpanded).toBeTrue(); 160 161 button.dispatchEvent(new Event('click')); 162 expandedTimelineElement = fixture.debugElement.query( 163 By.directive(ExpandedTimelineComponent), 164 ); 165 expect(expandedTimelineElement).toBeFalsy(); 166 expect(isExpanded).toBeFalse(); 167 }); 168 169 it('handles empty traces', () => { 170 const traces = new TracesBuilder() 171 .setEntries(TraceType.SURFACE_FLINGER, []) 172 .build(); 173 assertDefined(assertDefined(component.timelineData)).initialize( 174 traces, 175 undefined, 176 TimestampConverterUtils.TIMESTAMP_CONVERTER, 177 ); 178 fixture.detectChanges(); 179 180 const timelineComponent = assertDefined(component.timeline); 181 182 // no expand button 183 const button = htmlElement.querySelector( 184 `.${timelineComponent.TOGGLE_BUTTON_CLASS}`, 185 ); 186 expect(button).toBeFalsy(); 187 188 // no timelines shown 189 const miniTimelineElement = fixture.debugElement.query( 190 By.directive(MiniTimelineComponent), 191 ); 192 expect(miniTimelineElement).toBeFalsy(); 193 194 // error message shown 195 const errorMessageContainer = assertDefined( 196 htmlElement.querySelector('.no-timestamps-msg'), 197 ); 198 expect(errorMessageContainer.textContent).toContain('No timeline to show!'); 199 200 // arrow key presses don't do anything 201 const spyNextEntry = spyOn(timelineComponent, 'moveToNextEntry'); 202 const spyPrevEntry = spyOn(timelineComponent, 'moveToPreviousEntry'); 203 204 document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'})); 205 fixture.detectChanges(); 206 expect(spyNextEntry).not.toHaveBeenCalled(); 207 208 document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); 209 fixture.detectChanges(); 210 expect(spyPrevEntry).not.toHaveBeenCalled(); 211 }); 212 213 it('handles some empty traces', () => { 214 const traces = new TracesBuilder() 215 .setTimestamps(TraceType.SURFACE_FLINGER, []) 216 .setTimestamps(TraceType.WINDOW_MANAGER, [time100]) 217 .build(); 218 assertDefined(component.timelineData).initialize( 219 traces, 220 undefined, 221 TimestampConverterUtils.TIMESTAMP_CONVERTER, 222 ); 223 fixture.detectChanges(); 224 }); 225 226 it('processes active trace input and updates selected traces', async () => { 227 loadAllTraces(); 228 fixture.detectChanges(); 229 230 const timelineComponent = assertDefined(component.timeline); 231 const nextEntryButton = assertDefined( 232 htmlElement.querySelector('#next_entry_button'), 233 ) as HTMLElement; 234 const prevEntryButton = assertDefined( 235 htmlElement.querySelector('#prev_entry_button'), 236 ) as HTMLElement; 237 238 timelineComponent.selectedTraces = [ 239 getLoadedTrace(TraceType.SURFACE_FLINGER), 240 ]; 241 fixture.detectChanges(); 242 checkActiveTraceSurfaceFlinger(nextEntryButton, prevEntryButton); 243 244 // setting same trace as active does not affect selected traces 245 await updateActiveTrace(TraceType.SURFACE_FLINGER); 246 expectSelectedTraceTypes([TraceType.SURFACE_FLINGER]); 247 checkActiveTraceSurfaceFlinger(nextEntryButton, prevEntryButton); 248 249 await updateActiveTrace(TraceType.SCREEN_RECORDING); 250 expectSelectedTraceTypes([ 251 TraceType.SURFACE_FLINGER, 252 TraceType.SCREEN_RECORDING, 253 ]); 254 testCurrentTimestampOnButtonClick(prevEntryButton, position110, 110n); 255 256 await updateActiveTrace(TraceType.WINDOW_MANAGER); 257 expectSelectedTraceTypes([ 258 TraceType.SURFACE_FLINGER, 259 TraceType.SCREEN_RECORDING, 260 TraceType.WINDOW_MANAGER, 261 ]); 262 checkActiveTraceWindowManager(nextEntryButton, prevEntryButton); 263 264 await updateActiveTrace(TraceType.PROTO_LOG); 265 expectSelectedTraceTypes([ 266 TraceType.SURFACE_FLINGER, 267 TraceType.SCREEN_RECORDING, 268 TraceType.WINDOW_MANAGER, 269 TraceType.PROTO_LOG, 270 ]); 271 testCurrentTimestampOnButtonClick(nextEntryButton, position100, 100n); 272 checkActiveTraceHasOneEntry(nextEntryButton, prevEntryButton); 273 274 // setting active trace that is already selected does not affect selection 275 await updateActiveTrace(TraceType.SCREEN_RECORDING); 276 expectSelectedTraceTypes([ 277 TraceType.SURFACE_FLINGER, 278 TraceType.SCREEN_RECORDING, 279 TraceType.WINDOW_MANAGER, 280 TraceType.PROTO_LOG, 281 ]); 282 testCurrentTimestampOnButtonClick(nextEntryButton, position110, 110n); 283 checkActiveTraceHasOneEntry(nextEntryButton, prevEntryButton); 284 }); 285 286 it('handles undefined active trace input', async () => { 287 const traces = new TracesBuilder() 288 .setTimestamps(TraceType.SCREEN_RECORDING, [time100, time110]) 289 .build(); 290 291 const timelineData = assertDefined(component.timelineData); 292 timelineData.initialize( 293 traces, 294 undefined, 295 TimestampConverterUtils.TIMESTAMP_CONVERTER, 296 ); 297 timelineData.setPosition(position100); 298 fixture.detectChanges(); 299 const nextEntryButton = assertDefined( 300 htmlElement.querySelector('#next_entry_button'), 301 ) as HTMLElement; 302 const prevEntryButton = assertDefined( 303 htmlElement.querySelector('#prev_entry_button'), 304 ) as HTMLElement; 305 expect(timelineData.getActiveTrace()).toBeUndefined(); 306 expect(timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual( 307 100n, 308 ); 309 310 expect(prevEntryButton.getAttribute('disabled')).toEqual('true'); 311 expect(nextEntryButton.getAttribute('disabled')).toEqual('true'); 312 }); 313 314 it('handles ActiveTraceChanged event', async () => { 315 loadSfWmTraces(); 316 fixture.detectChanges(); 317 318 const timelineComponent = assertDefined(component.timeline); 319 const nextEntryButton = assertDefined( 320 htmlElement.querySelector('#next_entry_button'), 321 ) as HTMLElement; 322 const prevEntryButton = assertDefined( 323 htmlElement.querySelector('#prev_entry_button'), 324 ) as HTMLElement; 325 const spy = spyOn( 326 assertDefined(timelineComponent.miniTimeline?.drawer), 327 'draw', 328 ); 329 330 await updateActiveTrace(TraceType.SURFACE_FLINGER); 331 fixture.detectChanges(); 332 checkActiveTraceSurfaceFlinger(nextEntryButton, prevEntryButton); 333 expect(spy).toHaveBeenCalled(); 334 }); 335 336 it('updates trace selection using selector', async () => { 337 const allTraceTypes = [ 338 TraceType.SCREEN_RECORDING, 339 TraceType.SURFACE_FLINGER, 340 TraceType.WINDOW_MANAGER, 341 TraceType.PROTO_LOG, 342 ]; 343 loadAllTraces(); 344 expectSelectedTraceTypes(allTraceTypes); 345 346 await openSelectPanel(); 347 348 const matOptions = assertDefined( 349 document.documentElement.querySelectorAll('mat-option'), 350 ); 351 const sfOption = assertDefined(matOptions.item(1)) as HTMLInputElement; 352 expect(sfOption.textContent).toContain('Surface Flinger'); 353 expect(sfOption.ariaDisabled).toEqual('true'); 354 for (const i of [0, 2, 3]) { 355 expect((matOptions.item(i) as HTMLInputElement).ariaDisabled).toEqual( 356 'false', 357 ); 358 } 359 360 (matOptions.item(2) as HTMLElement).click(); 361 fixture.detectChanges(); 362 expectSelectedTraceTypes([ 363 TraceType.SCREEN_RECORDING, 364 TraceType.SURFACE_FLINGER, 365 TraceType.PROTO_LOG, 366 ]); 367 const icons = htmlElement.querySelectorAll( 368 '#trace-selector .shown-selection .mat-icon', 369 ); 370 expect( 371 Array.from(icons) 372 .map((icon) => icon.textContent?.trim()) 373 .slice(1), 374 ).toEqual([ 375 TRACE_INFO[TraceType.SCREEN_RECORDING].icon, 376 TRACE_INFO[TraceType.SURFACE_FLINGER].icon, 377 TRACE_INFO[TraceType.PROTO_LOG].icon, 378 ]); 379 380 (matOptions.item(2) as HTMLElement).click(); 381 fixture.detectChanges(); 382 expectSelectedTraceTypes(allTraceTypes); 383 const newIcons = htmlElement.querySelectorAll( 384 '#trace-selector .shown-selection .mat-icon', 385 ); 386 expect( 387 Array.from(newIcons) 388 .map((icon) => icon.textContent?.trim()) 389 .slice(1), 390 ).toEqual([ 391 TRACE_INFO[TraceType.SCREEN_RECORDING].icon, 392 TRACE_INFO[TraceType.SURFACE_FLINGER].icon, 393 TRACE_INFO[TraceType.WINDOW_MANAGER].icon, 394 TRACE_INFO[TraceType.PROTO_LOG].icon, 395 ]); 396 }); 397 398 it('next button disabled if no next entry', () => { 399 loadSfWmTraces(); 400 const timelineData = assertDefined(component.timelineData); 401 402 expect(timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual( 403 100n, 404 ); 405 406 const nextEntryButton = assertDefined( 407 htmlElement.querySelector('#next_entry_button'), 408 ); 409 expect(nextEntryButton.getAttribute('disabled')).toBeFalsy(); 410 411 timelineData.setPosition(position90); 412 fixture.detectChanges(); 413 expect(nextEntryButton.getAttribute('disabled')).toBeFalsy(); 414 415 timelineData.setPosition(position110); 416 fixture.detectChanges(); 417 expect(nextEntryButton.getAttribute('disabled')).toBeTruthy(); 418 419 timelineData.setPosition(position112); 420 fixture.detectChanges(); 421 expect(nextEntryButton.getAttribute('disabled')).toBeTruthy(); 422 }); 423 424 it('prev button disabled if no prev entry', () => { 425 loadSfWmTraces(); 426 const timelineData = assertDefined(component.timelineData); 427 428 expect(timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual( 429 100n, 430 ); 431 const prevEntryButton = assertDefined( 432 htmlElement.querySelector('#prev_entry_button'), 433 ); 434 expect(prevEntryButton.getAttribute('disabled')).toBeTruthy(); 435 436 timelineData.setPosition(position90); 437 fixture.detectChanges(); 438 expect(prevEntryButton.getAttribute('disabled')).toBeTruthy(); 439 440 timelineData.setPosition(position110); 441 fixture.detectChanges(); 442 expect(prevEntryButton.getAttribute('disabled')).toBeFalsy(); 443 444 timelineData.setPosition(position112); 445 fixture.detectChanges(); 446 expect(prevEntryButton.getAttribute('disabled')).toBeFalsy(); 447 }); 448 449 it('next button enabled for different active viewers', async () => { 450 loadSfWmTraces(); 451 const nextEntryButton = assertDefined( 452 htmlElement.querySelector('#next_entry_button'), 453 ); 454 455 expect(nextEntryButton.getAttribute('disabled')).toBeNull(); 456 457 await updateActiveTrace(TraceType.WINDOW_MANAGER); 458 fixture.detectChanges(); 459 460 expect(nextEntryButton.getAttribute('disabled')).toBeNull(); 461 }); 462 463 it('changes timestamp on next entry button press', () => { 464 loadSfWmTraces(); 465 466 expect( 467 assertDefined(component.timelineData) 468 .getCurrentPosition() 469 ?.timestamp.getValueNs(), 470 ).toEqual(100n); 471 const nextEntryButton = assertDefined( 472 htmlElement.querySelector('#next_entry_button'), 473 ) as HTMLElement; 474 475 testCurrentTimestampOnButtonClick(nextEntryButton, position105, 110n); 476 477 testCurrentTimestampOnButtonClick(nextEntryButton, position100, 110n); 478 479 testCurrentTimestampOnButtonClick(nextEntryButton, position90, 100n); 480 481 // No change when we are already on the last timestamp of the active trace 482 testCurrentTimestampOnButtonClick(nextEntryButton, position110, 110n); 483 484 // No change when we are after the last entry of the active trace 485 testCurrentTimestampOnButtonClick(nextEntryButton, position112, 112n); 486 }); 487 488 it('changes timestamp on previous entry button press', () => { 489 loadSfWmTraces(); 490 491 expect( 492 assertDefined(component.timelineData) 493 .getCurrentPosition() 494 ?.timestamp.getValueNs(), 495 ).toEqual(100n); 496 const prevEntryButton = assertDefined( 497 htmlElement.querySelector('#prev_entry_button'), 498 ) as HTMLElement; 499 500 // In this state we are already on the first entry at timestamp 100, so 501 // there is no entry to move to before and we just don't update the timestamp 502 testCurrentTimestampOnButtonClick(prevEntryButton, position105, 105n); 503 504 testCurrentTimestampOnButtonClick(prevEntryButton, position110, 100n); 505 506 // Active entry here should be 110 so moving back means moving to 100. 507 testCurrentTimestampOnButtonClick(prevEntryButton, position112, 100n); 508 509 // No change when we are already on the first timestamp of the active trace 510 testCurrentTimestampOnButtonClick(prevEntryButton, position100, 100n); 511 512 // No change when we are before the first entry of the active trace 513 testCurrentTimestampOnButtonClick(prevEntryButton, position90, 90n); 514 }); 515 516 it('performs expected action on arrow key press depending on input form focus', () => { 517 loadSfWmTraces(); 518 const timelineComponent = assertDefined(component.timeline); 519 520 const spyNextEntry = spyOn(timelineComponent, 'moveToNextEntry'); 521 const spyPrevEntry = spyOn(timelineComponent, 'moveToPreviousEntry'); 522 523 document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowRight'})); 524 fixture.detectChanges(); 525 expect(spyNextEntry).toHaveBeenCalled(); 526 527 const formElement = htmlElement.querySelector('.time-input input'); 528 const focusInEvent = new FocusEvent('focusin'); 529 Object.defineProperty(focusInEvent, 'target', {value: formElement}); 530 document.dispatchEvent(focusInEvent); 531 fixture.detectChanges(); 532 533 document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); 534 fixture.detectChanges(); 535 expect(spyPrevEntry).not.toHaveBeenCalled(); 536 537 const focusOutEvent = new FocusEvent('focusout'); 538 Object.defineProperty(focusOutEvent, 'target', {value: formElement}); 539 document.dispatchEvent(focusOutEvent); 540 fixture.detectChanges(); 541 542 document.dispatchEvent(new KeyboardEvent('keydown', {key: 'ArrowLeft'})); 543 fixture.detectChanges(); 544 expect(spyPrevEntry).toHaveBeenCalled(); 545 }); 546 547 it('updates position based on ns input field', () => { 548 loadSfWmTraces(); 549 550 expect( 551 assertDefined(component.timelineData) 552 .getCurrentPosition() 553 ?.timestamp.getValueNs(), 554 ).toEqual(100n); 555 556 const timeInputField = assertDefined( 557 document.querySelector('.time-input.nano'), 558 ) as HTMLInputElement; 559 560 testCurrentTimestampOnTimeInput( 561 timeInputField, 562 position105, 563 '110 ns', 564 110n, 565 ); 566 567 testCurrentTimestampOnTimeInput( 568 timeInputField, 569 position100, 570 '110 ns', 571 110n, 572 ); 573 574 testCurrentTimestampOnTimeInput(timeInputField, position90, '100 ns', 100n); 575 576 // No change when we are already on the last timestamp of the active trace 577 testCurrentTimestampOnTimeInput( 578 timeInputField, 579 position110, 580 '110 ns', 581 110n, 582 ); 583 584 // No change when we are after the last entry of the active trace 585 testCurrentTimestampOnTimeInput( 586 timeInputField, 587 position112, 588 '112 ns', 589 112n, 590 ); 591 }); 592 593 it('updates position based on human time input field using date time format', () => { 594 loadSfWmTraces(); 595 596 expect( 597 assertDefined(component.timelineData) 598 .getCurrentPosition() 599 ?.timestamp.getValueNs(), 600 ).toEqual(100n); 601 602 const timeInputField = assertDefined( 603 document.querySelector('.time-input.human'), 604 ) as HTMLInputElement; 605 606 testCurrentTimestampOnTimeInput( 607 timeInputField, 608 position105, 609 '1970-01-01, 00:00:00.000000110', 610 110n, 611 ); 612 613 testCurrentTimestampOnTimeInput( 614 timeInputField, 615 position100, 616 '1970-01-01, 00:00:00.000000110', 617 110n, 618 ); 619 620 testCurrentTimestampOnTimeInput( 621 timeInputField, 622 position90, 623 '1970-01-01, 00:00:00.000000100', 624 100n, 625 ); 626 627 // No change when we are already on the last timestamp of the active trace 628 testCurrentTimestampOnTimeInput( 629 timeInputField, 630 position110, 631 '1970-01-01, 00:00:00.000000110', 632 110n, 633 ); 634 635 // No change when we are after the last entry of the active trace 636 testCurrentTimestampOnTimeInput( 637 timeInputField, 638 position112, 639 '1970-01-01, 00:00:00.000000112', 640 112n, 641 ); 642 }); 643 644 it('updates position based on human time input field using ISO timestamp format', () => { 645 loadSfWmTraces(); 646 647 expect( 648 assertDefined(component.timelineData) 649 .getCurrentPosition() 650 ?.timestamp.valueOf(), 651 ).toEqual(100n); 652 653 const timeInputField = assertDefined( 654 document.querySelector('.time-input.human'), 655 ) as HTMLInputElement; 656 657 testCurrentTimestampOnTimeInput( 658 timeInputField, 659 position90, 660 '1970-01-01T00:00:00.000000100', 661 100n, 662 ); 663 }); 664 665 it('updates position based on human time input field using time-only format', () => { 666 loadSfWmTraces(); 667 668 expect( 669 assertDefined(component.timelineData) 670 .getCurrentPosition() 671 ?.timestamp.valueOf(), 672 ).toEqual(100n); 673 674 const timeInputField = assertDefined( 675 document.querySelector('.time-input.human'), 676 ) as HTMLInputElement; 677 678 testCurrentTimestampOnTimeInput( 679 timeInputField, 680 position105, 681 '00:00:00.000000110', 682 110n, 683 ); 684 }); 685 686 it('sets initial zoom of mini timeline from first non-SR viewer to end of all traces', () => { 687 loadAllTraces(); 688 const timelineComponent = assertDefined(component.timeline); 689 expect(timelineComponent.initialZoom).toEqual( 690 new TimeRange(time100, time112), 691 ); 692 }); 693 694 it('stores manual trace deselection and applies on new load', async () => { 695 loadAllTraces(); 696 const firstTimeline = assertDefined(component.timeline); 697 expectSelectedTraceTypes( 698 [ 699 TraceType.SCREEN_RECORDING, 700 TraceType.SURFACE_FLINGER, 701 TraceType.WINDOW_MANAGER, 702 TraceType.PROTO_LOG, 703 ], 704 firstTimeline, 705 ); 706 await openSelectPanel(); 707 clickTraceFromSelectPanel(2); 708 clickTraceFromSelectPanel(3); 709 expectSelectedTraceTypes( 710 [TraceType.SCREEN_RECORDING, TraceType.SURFACE_FLINGER], 711 firstTimeline, 712 ); 713 714 const secondFixture = TestBed.createComponent(TestHostComponent); 715 const secondHost = secondFixture.componentInstance; 716 loadAllTraces(secondHost, secondFixture); 717 const secondTimeline = assertDefined(secondHost.timeline); 718 expectSelectedTraceTypes( 719 [TraceType.SCREEN_RECORDING, TraceType.SURFACE_FLINGER], 720 secondTimeline, 721 ); 722 723 clickTraceFromSelectPanel(2); 724 expectSelectedTraceTypes( 725 [TraceType.SCREEN_RECORDING, TraceType.SURFACE_FLINGER], 726 secondTimeline, 727 ); 728 729 const thirdFixture = TestBed.createComponent(TestHostComponent); 730 const thirdHost = thirdFixture.componentInstance; 731 loadAllTraces(thirdHost, thirdFixture); 732 const thirdTimeline = assertDefined(thirdHost.timeline); 733 expectSelectedTraceTypes( 734 [ 735 TraceType.SCREEN_RECORDING, 736 TraceType.SURFACE_FLINGER, 737 TraceType.WINDOW_MANAGER, 738 ], 739 thirdTimeline, 740 ); 741 }); 742 743 it('does not store traces based on active view trace type', async () => { 744 loadAllTraces(); 745 expectSelectedTraceTypes( 746 [ 747 TraceType.SCREEN_RECORDING, 748 TraceType.SURFACE_FLINGER, 749 TraceType.WINDOW_MANAGER, 750 TraceType.PROTO_LOG, 751 ], 752 component.timeline, 753 ); 754 await openSelectPanel(); 755 clickTraceFromSelectPanel(3); 756 expectSelectedTraceTypes( 757 [ 758 TraceType.SCREEN_RECORDING, 759 TraceType.SURFACE_FLINGER, 760 TraceType.WINDOW_MANAGER, 761 ], 762 component.timeline, 763 ); 764 await updateActiveTrace(TraceType.PROTO_LOG); 765 fixture.detectChanges(); 766 expectSelectedTraceTypes( 767 [ 768 TraceType.SCREEN_RECORDING, 769 TraceType.SURFACE_FLINGER, 770 TraceType.WINDOW_MANAGER, 771 TraceType.PROTO_LOG, 772 ], 773 component.timeline, 774 ); 775 776 const secondFixture = TestBed.createComponent(TestHostComponent); 777 const secondHost = secondFixture.componentInstance; 778 loadAllTraces(secondHost, secondFixture); 779 const secondTimeline = assertDefined(secondHost.timeline); 780 expectSelectedTraceTypes( 781 [ 782 TraceType.SCREEN_RECORDING, 783 TraceType.SURFACE_FLINGER, 784 TraceType.WINDOW_MANAGER, 785 ], 786 secondTimeline, 787 ); 788 }); 789 790 it('applies stored trace deselection between non-consecutive applicable sessions', async () => { 791 loadAllTraces(); 792 expectSelectedTraceTypes( 793 [ 794 TraceType.SCREEN_RECORDING, 795 TraceType.SURFACE_FLINGER, 796 TraceType.WINDOW_MANAGER, 797 TraceType.PROTO_LOG, 798 ], 799 component.timeline, 800 ); 801 await openSelectPanel(); 802 clickTraceFromSelectPanel(3); 803 expectSelectedTraceTypes( 804 [ 805 TraceType.SCREEN_RECORDING, 806 TraceType.SURFACE_FLINGER, 807 TraceType.WINDOW_MANAGER, 808 ], 809 component.timeline, 810 ); 811 812 const secondFixture = TestBed.createComponent(TestHostComponent); 813 const secondHost = secondFixture.componentInstance; 814 loadSfWmTraces(secondHost, secondFixture); 815 const secondTimeline = assertDefined(secondHost.timeline); 816 expectSelectedTraceTypes( 817 [TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER], 818 secondTimeline, 819 ); 820 821 const thirdFixture = TestBed.createComponent(TestHostComponent); 822 const thirdHost = thirdFixture.componentInstance; 823 loadAllTraces(thirdHost, thirdFixture); 824 const thirdTimeline = assertDefined(thirdHost.timeline); 825 expectSelectedTraceTypes( 826 [ 827 TraceType.SCREEN_RECORDING, 828 TraceType.SURFACE_FLINGER, 829 TraceType.WINDOW_MANAGER, 830 ], 831 thirdTimeline, 832 ); 833 }); 834 835 it('shows all traces in new session that were not present (so not deselected) in previous session', async () => { 836 loadSfWmTraces(); 837 expectSelectedTraceTypes( 838 [TraceType.SURFACE_FLINGER, TraceType.WINDOW_MANAGER], 839 component.timeline, 840 ); 841 await openSelectPanel(); 842 clickTraceFromSelectPanel(1); 843 expectSelectedTraceTypes([TraceType.SURFACE_FLINGER], component.timeline); 844 845 const secondFixture = TestBed.createComponent(TestHostComponent); 846 const secondHost = secondFixture.componentInstance; 847 loadAllTraces(secondHost, secondFixture); 848 const secondTimeline = assertDefined(secondHost.timeline); 849 expectSelectedTraceTypes( 850 [ 851 TraceType.SCREEN_RECORDING, 852 TraceType.SURFACE_FLINGER, 853 TraceType.PROTO_LOG, 854 ], 855 secondTimeline, 856 ); 857 }); 858 859 it('toggles bookmark of current position', () => { 860 loadSfWmTraces(); 861 const timelineComponent = assertDefined(component.timeline); 862 expect(timelineComponent.bookmarks).toEqual([]); 863 expect(timelineComponent.currentPositionBookmarked()).toBeFalse(); 864 865 const bookmarkIcon = assertDefined( 866 htmlElement.querySelector('.bookmark-icon'), 867 ) as HTMLElement; 868 bookmarkIcon.click(); 869 fixture.detectChanges(); 870 871 expect(timelineComponent.bookmarks).toEqual([time100]); 872 expect(timelineComponent.currentPositionBookmarked()).toBeTrue(); 873 874 bookmarkIcon.click(); 875 fixture.detectChanges(); 876 expect(timelineComponent.bookmarks).toEqual([]); 877 expect(timelineComponent.currentPositionBookmarked()).toBeFalse(); 878 }); 879 880 it('toggles same bookmark if click within range', () => { 881 loadTracesWithLargeTimeRange(); 882 883 const timelineComponent = assertDefined(component.timeline); 884 expect(timelineComponent.bookmarks.length).toEqual(0); 885 886 openContextMenu(); 887 clickToggleBookmarkOption(); 888 expect(timelineComponent.bookmarks.length).toEqual(1); 889 890 // click within marker y-pos, x-pos close enough to remove bookmark 891 openContextMenu(5); 892 clickToggleBookmarkOption(); 893 expect(timelineComponent.bookmarks.length).toEqual(0); 894 895 openContextMenu(); 896 clickToggleBookmarkOption(); 897 expect(timelineComponent.bookmarks.length).toEqual(1); 898 899 // click within marker y-pos, x-pos too large so new bookmark added 900 openContextMenu(20); 901 clickToggleBookmarkOption(); 902 expect(timelineComponent.bookmarks.length).toEqual(2); 903 904 openContextMenu(20); 905 clickToggleBookmarkOption(); 906 expect(timelineComponent.bookmarks.length).toEqual(1); 907 908 // click below marker y-pos, x-pos now too large so new bookmark added 909 openContextMenu(5, true); 910 clickToggleBookmarkOption(); 911 expect(timelineComponent.bookmarks.length).toEqual(2); 912 }); 913 914 it('removes all bookmarks', () => { 915 loadSfWmTraces(); 916 const timelineComponent = assertDefined(component.timeline); 917 timelineComponent.bookmarks = [time100, time101, time112]; 918 fixture.detectChanges(); 919 920 openContextMenu(); 921 clickRemoveAllBookmarksOption(); 922 expect(timelineComponent.bookmarks).toEqual([]); 923 }); 924 925 function loadSfWmTraces(hostComponent = component, hostFixture = fixture) { 926 const traces = new TracesBuilder() 927 .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110]) 928 .setTimestamps(TraceType.WINDOW_MANAGER, [ 929 time90, 930 time101, 931 time110, 932 time112, 933 ]) 934 .build(); 935 936 const timelineData = assertDefined(hostComponent.timelineData); 937 timelineData.initialize( 938 traces, 939 undefined, 940 TimestampConverterUtils.TIMESTAMP_CONVERTER, 941 ); 942 timelineData.setPosition(position100); 943 hostFixture.detectChanges(); 944 } 945 946 function loadAllTraces(hostComponent = component, hostFixture = fixture) { 947 const traces = new TracesBuilder() 948 .setTimestamps(TraceType.SURFACE_FLINGER, [time100, time110]) 949 .setTimestamps(TraceType.WINDOW_MANAGER, [ 950 time90, 951 time101, 952 time110, 953 time112, 954 ]) 955 .setTimestamps(TraceType.SCREEN_RECORDING, [time110]) 956 .setTimestamps(TraceType.PROTO_LOG, [time100]) 957 .build(); 958 959 assertDefined(hostComponent.timelineData).initialize( 960 traces, 961 undefined, 962 TimestampConverterUtils.TIMESTAMP_CONVERTER, 963 ); 964 hostFixture.detectChanges(); 965 } 966 967 function loadTracesWithLargeTimeRange() { 968 const traces = new TracesBuilder() 969 .setTimestamps(TraceType.SURFACE_FLINGER, [ 970 time100, 971 time2000, 972 time3000, 973 time4000, 974 ]) 975 .setTimestamps(TraceType.WINDOW_MANAGER, [ 976 time2000, 977 time4000, 978 time6000, 979 time8000, 980 ]) 981 .build(); 982 983 const timelineData = assertDefined(component.timelineData); 984 timelineData.initialize( 985 traces, 986 undefined, 987 TimestampConverterUtils.TIMESTAMP_CONVERTER, 988 ); 989 timelineData.setPosition(position100); 990 fixture.detectChanges(); 991 } 992 993 function getLoadedTrace(type: TraceType): Trace<object> { 994 const timelineData = assertDefined(component.timelineData); 995 const trace = assertDefined( 996 timelineData.getTraces().getTrace(type), 997 ) as Trace<object>; 998 return trace; 999 } 1000 1001 async function updateActiveTrace(type: TraceType) { 1002 const trace = getLoadedTrace(type); 1003 const timelineData = assertDefined(component.timelineData); 1004 timelineData.trySetActiveTrace(trace); 1005 1006 const timelineComponent = assertDefined(component.timeline); 1007 await timelineComponent.onWinscopeEvent(new ActiveTraceChanged(trace)); 1008 } 1009 1010 function expectSelectedTraceTypes( 1011 expected: TraceType[], 1012 timelineComponent?: TimelineComponent, 1013 ) { 1014 const timeline = assertDefined(timelineComponent ?? component.timeline); 1015 const actual = timeline.selectedTraces.map((trace) => trace.type); 1016 expect(actual).toEqual(expected); 1017 } 1018 1019 function testCurrentTimestampOnButtonClick( 1020 button: HTMLElement, 1021 pos: TracePosition, 1022 expectedNs: bigint, 1023 ) { 1024 const timelineData = assertDefined(component.timelineData); 1025 timelineData.setPosition(pos); 1026 fixture.detectChanges(); 1027 button.click(); 1028 fixture.detectChanges(); 1029 expect(timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual( 1030 expectedNs, 1031 ); 1032 } 1033 1034 function testCurrentTimestampOnTimeInput( 1035 inputField: HTMLInputElement, 1036 pos: TracePosition, 1037 textInput: string, 1038 expectedNs: bigint, 1039 ) { 1040 const timelineData = assertDefined(component.timelineData); 1041 timelineData.setPosition(pos); 1042 fixture.detectChanges(); 1043 1044 inputField.value = textInput; 1045 inputField.dispatchEvent(new Event('change')); 1046 fixture.detectChanges(); 1047 1048 expect(timelineData.getCurrentPosition()?.timestamp.getValueNs()).toEqual( 1049 expectedNs, 1050 ); 1051 } 1052 1053 async function openSelectPanel() { 1054 const selectTrigger = assertDefined( 1055 htmlElement.querySelector('.mat-select-trigger'), 1056 ); 1057 (selectTrigger as HTMLElement).click(); 1058 fixture.detectChanges(); 1059 await fixture.whenStable(); 1060 } 1061 1062 function clickTraceFromSelectPanel(index: number) { 1063 const matOptions = assertDefined( 1064 document.documentElement.querySelectorAll('mat-option'), 1065 ); 1066 (matOptions.item(index) as HTMLElement).click(); 1067 fixture.detectChanges(); 1068 } 1069 1070 function checkActiveTraceSurfaceFlinger( 1071 nextEntryButton: HTMLElement, 1072 prevEntryButton: HTMLElement, 1073 ) { 1074 testCurrentTimestampOnButtonClick(prevEntryButton, position110, 100n); 1075 expect(prevEntryButton.getAttribute('disabled')).toEqual('true'); 1076 expect(nextEntryButton.getAttribute('disabled')).toBeNull(); 1077 testCurrentTimestampOnButtonClick(nextEntryButton, position100, 110n); 1078 expect(prevEntryButton.getAttribute('disabled')).toBeNull(); 1079 expect(nextEntryButton.getAttribute('disabled')).toEqual('true'); 1080 } 1081 1082 function checkActiveTraceWindowManager( 1083 nextEntryButton: HTMLElement, 1084 prevEntryButton: HTMLElement, 1085 ) { 1086 testCurrentTimestampOnButtonClick(prevEntryButton, position90, 90n); 1087 expect(prevEntryButton.getAttribute('disabled')).toEqual('true'); 1088 expect(nextEntryButton.getAttribute('disabled')).toBeNull(); 1089 testCurrentTimestampOnButtonClick(nextEntryButton, position90, 101n); 1090 expect(prevEntryButton.getAttribute('disabled')).toBeNull(); 1091 expect(nextEntryButton.getAttribute('disabled')).toBeNull(); 1092 testCurrentTimestampOnButtonClick(nextEntryButton, position110, 112n); 1093 expect(prevEntryButton.getAttribute('disabled')).toBeNull(); 1094 expect(nextEntryButton.getAttribute('disabled')).toEqual('true'); 1095 } 1096 1097 function checkActiveTraceHasOneEntry( 1098 nextEntryButton: HTMLElement, 1099 prevEntryButton: HTMLElement, 1100 ) { 1101 expect(prevEntryButton.getAttribute('disabled')).toEqual('true'); 1102 expect(nextEntryButton.getAttribute('disabled')).toEqual('true'); 1103 } 1104 1105 function openContextMenu(xOffset = 0, clickBelowMarker = false) { 1106 const miniTimelineCanvas = assertDefined( 1107 htmlElement.querySelector('#mini-timeline-canvas'), 1108 ) as HTMLElement; 1109 const clickPosX = 1110 miniTimelineCanvas.offsetLeft + 1111 miniTimelineCanvas.offsetWidth / 2 + 1112 xOffset; 1113 const clickPosY = 1114 miniTimelineCanvas.offsetTop + (clickBelowMarker ? 1000 : 0); 1115 miniTimelineCanvas.dispatchEvent( 1116 new MouseEvent('contextmenu', { 1117 clientX: clickPosX, 1118 clientY: clickPosY, 1119 }), 1120 ); 1121 fixture.detectChanges(); 1122 } 1123 1124 function clickToggleBookmarkOption() { 1125 const menu = assertDefined(document.querySelector('.context-menu')); 1126 const toggleOption = assertDefined( 1127 menu.querySelector('.context-menu-item'), 1128 ) as HTMLElement; 1129 toggleOption.click(); 1130 fixture.detectChanges(); 1131 } 1132 1133 function clickRemoveAllBookmarksOption() { 1134 const menu = assertDefined(document.querySelector('.context-menu')); 1135 const options = assertDefined(menu.querySelectorAll('.context-menu-item')); 1136 (options.item(1) as HTMLElement).click(); 1137 fixture.detectChanges(); 1138 } 1139 1140 @Component({ 1141 selector: 'host-component', 1142 template: ` 1143 <timeline 1144 [timelineData]="timelineData" 1145 [store]="store"></timeline> 1146 `, 1147 }) 1148 class TestHostComponent { 1149 timelineData = new TimelineData(); 1150 store = new PersistentStore(); 1151 1152 @ViewChild(TimelineComponent) 1153 timeline: TimelineComponent | undefined; 1154 1155 ngOnDestroy() { 1156 if (this.timeline) { 1157 this.store.clear(this.timeline.storeKeyDeselectedTraces); 1158 } 1159 } 1160 } 1161}); 1162