1/* 2 * Copyright (C) 2023 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 {DragDropModule} from '@angular/cdk/drag-drop'; 18import {CdkMenuModule} from '@angular/cdk/menu'; 19import {ChangeDetectionStrategy, Component, ViewChild} from '@angular/core'; 20import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing'; 21import {FormsModule, ReactiveFormsModule} from '@angular/forms'; 22import {MatButtonModule} from '@angular/material/button'; 23import {MatFormFieldModule} from '@angular/material/form-field'; 24import {MatIconModule} from '@angular/material/icon'; 25import {MatInputModule} from '@angular/material/input'; 26import {MatSelectModule} from '@angular/material/select'; 27import {MatTooltipModule} from '@angular/material/tooltip'; 28import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 29import {TimelineData} from 'app/timeline_data'; 30import {assertDefined} from 'common/assert_utils'; 31import {TimeRange, Timestamp} from 'common/time'; 32import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils'; 33import {TracesBuilder} from 'test/unit/traces_builder'; 34import {dragElement} from 'test/utils'; 35import {Trace} from 'trace/trace'; 36import {TracePosition} from 'trace/trace_position'; 37import {TraceType} from 'trace/trace_type'; 38import {MiniTimelineComponent} from './mini_timeline_component'; 39import {SliderComponent} from './slider_component'; 40 41describe('MiniTimelineComponent', () => { 42 let fixture: ComponentFixture<TestHostComponent>; 43 let component: TestHostComponent; 44 let htmlElement: HTMLElement; 45 let timelineData: TimelineData; 46 47 const timestamp10 = TimestampConverterUtils.makeRealTimestamp(10n); 48 const timestamp15 = TimestampConverterUtils.makeRealTimestamp(15n); 49 const timestamp16 = TimestampConverterUtils.makeRealTimestamp(16n); 50 const timestamp20 = TimestampConverterUtils.makeRealTimestamp(20n); 51 const timestamp700 = TimestampConverterUtils.makeRealTimestamp(700n); 52 const timestamp810 = TimestampConverterUtils.makeRealTimestamp(810n); 53 const timestamp1000 = TimestampConverterUtils.makeRealTimestamp(1000n); 54 const timestamp1750 = TimestampConverterUtils.makeRealTimestamp(1750n); 55 const timestamp2000 = TimestampConverterUtils.makeRealTimestamp(2000n); 56 const timestamp3000 = TimestampConverterUtils.makeRealTimestamp(3000n); 57 const timestamp4000 = TimestampConverterUtils.makeRealTimestamp(4000n); 58 59 const position800 = TracePosition.fromTimestamp( 60 TimestampConverterUtils.makeRealTimestamp(800n), 61 ); 62 63 const traces = new TracesBuilder() 64 .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10]) 65 .setTimestamps(TraceType.TRANSACTIONS, [timestamp10, timestamp20]) 66 .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp20]) 67 .build(); 68 const traceSf = assertDefined(traces.getTrace(TraceType.SURFACE_FLINGER)); 69 const traceWm = assertDefined(traces.getTrace(TraceType.WINDOW_MANAGER)); 70 const traceTransactions = assertDefined( 71 traces.getTrace(TraceType.TRANSACTIONS), 72 ); 73 74 beforeEach(async () => { 75 await TestBed.configureTestingModule({ 76 imports: [ 77 FormsModule, 78 MatButtonModule, 79 MatFormFieldModule, 80 MatInputModule, 81 MatIconModule, 82 MatSelectModule, 83 MatTooltipModule, 84 ReactiveFormsModule, 85 BrowserAnimationsModule, 86 DragDropModule, 87 CdkMenuModule, 88 ], 89 declarations: [TestHostComponent, MiniTimelineComponent, SliderComponent], 90 }) 91 .overrideComponent(MiniTimelineComponent, { 92 set: {changeDetection: ChangeDetectionStrategy.Default}, 93 }) 94 .compileComponents(); 95 fixture = TestBed.createComponent(TestHostComponent); 96 component = fixture.componentInstance; 97 htmlElement = fixture.nativeElement; 98 99 timelineData = new TimelineData(); 100 await timelineData.initialize( 101 traces, 102 undefined, 103 TimestampConverterUtils.TIMESTAMP_CONVERTER, 104 ); 105 component.timelineData = timelineData; 106 expect(timelineData.getCurrentPosition()).toBeDefined(); 107 component.currentTracePosition = timelineData.getCurrentPosition()!; 108 component.selectedTraces = [traceSf]; 109 }); 110 111 it('can be created', () => { 112 expect(component).toBeTruthy(); 113 }); 114 115 it('redraws on resize', () => { 116 fixture.detectChanges(); 117 const miniTimelineComponent = assertDefined( 118 component.miniTimelineComponent, 119 ); 120 const spy = spyOn(assertDefined(miniTimelineComponent.drawer), 'draw'); 121 expect(spy).not.toHaveBeenCalled(); 122 123 miniTimelineComponent.onResize({} as Event); 124 125 expect(spy).toHaveBeenCalled(); 126 }); 127 128 it('resets zoom on reset zoom button click', () => { 129 const expectedZoomRange = new TimeRange(timestamp15, timestamp16); 130 timelineData.setZoom(expectedZoomRange); 131 132 let zoomRange = timelineData.getZoomRange(); 133 let fullRange = timelineData.getFullTimeRange(); 134 expect(zoomRange).toBe(expectedZoomRange); 135 expect(fullRange.from).toBe(timestamp10); 136 expect(fullRange.to).toBe(timestamp20); 137 138 fixture.detectChanges(); 139 140 const zoomButton = assertDefined( 141 htmlElement.querySelector('button#reset-zoom-btn'), 142 ) as HTMLButtonElement; 143 zoomButton.click(); 144 145 zoomRange = timelineData.getZoomRange(); 146 fullRange = timelineData.getFullTimeRange(); 147 expect(zoomRange).toBe(fullRange); 148 }); 149 150 it('show zoom controls when zoomed out', () => { 151 const zoomControlDiv = assertDefined( 152 htmlElement.querySelector('.zoom-control'), 153 ); 154 expect(window.getComputedStyle(zoomControlDiv).visibility).toBe('visible'); 155 156 const zoomButton = assertDefined( 157 htmlElement.querySelector('button#reset-zoom-btn'), 158 ) as HTMLButtonElement; 159 expect(window.getComputedStyle(zoomButton).visibility).toBe('visible'); 160 }); 161 162 it('shows zoom controls when zoomed in', () => { 163 const zoom = new TimeRange(timestamp15, timestamp16); 164 timelineData.setZoom(zoom); 165 166 fixture.detectChanges(); 167 168 const zoomControlDiv = assertDefined( 169 htmlElement.querySelector('.zoom-control'), 170 ); 171 expect(window.getComputedStyle(zoomControlDiv).visibility).toBe('visible'); 172 173 const zoomButton = assertDefined( 174 htmlElement.querySelector('button#reset-zoom-btn'), 175 ) as HTMLButtonElement; 176 expect(window.getComputedStyle(zoomButton).visibility).toBe('visible'); 177 }); 178 179 it('loads with initial zoom', () => { 180 const initialZoom = new TimeRange(timestamp15, timestamp16); 181 component.initialZoom = initialZoom; 182 fixture.detectChanges(); 183 const timelineData = assertDefined(component.timelineData); 184 const zoomRange = timelineData.getZoomRange(); 185 expect(zoomRange.from).toEqual(initialZoom.from); 186 expect(zoomRange.to).toEqual(initialZoom.to); 187 }); 188 189 it('updates timelineData on zoom changed', () => { 190 fixture.detectChanges(); 191 const zoom = new TimeRange(timestamp15, timestamp16); 192 assertDefined(component.miniTimelineComponent).onZoomChanged(zoom); 193 fixture.detectChanges(); 194 expect(timelineData.getZoomRange()).toBe(zoom); 195 }); 196 197 it('creates an appropriately sized canvas', () => { 198 fixture.detectChanges(); 199 const canvas = assertDefined(component.miniTimelineComponent).getCanvas(); 200 expect(canvas.width).toBeGreaterThan(100); 201 expect(canvas.height).toBeGreaterThan(10); 202 }); 203 204 it('getTracesToShow returns traces targeted by selectedTraces', () => { 205 fixture.detectChanges(); 206 const selectedTraces = assertDefined(component.selectedTraces); 207 const selectedTracesTypes = selectedTraces.map((trace) => trace.type); 208 209 const tracesToShow = assertDefined( 210 component.miniTimelineComponent, 211 ).getTracesToShow(); 212 const tracesToShowTypes = tracesToShow.map((trace) => trace.type); 213 214 expect(new Set(tracesToShowTypes)).toEqual(new Set(selectedTracesTypes)); 215 }); 216 217 it('getTracesToShow adds traces in correct order', () => { 218 component.selectedTraces = [traceWm, traceSf, traceTransactions]; 219 fixture.detectChanges(); 220 const tracesToShowTypes = assertDefined(component.miniTimelineComponent) 221 .getTracesToShow() 222 .map((trace) => trace.type); 223 expect(tracesToShowTypes).toEqual([ 224 TraceType.TRANSACTIONS, 225 TraceType.WINDOW_MANAGER, 226 TraceType.SURFACE_FLINGER, 227 ]); 228 }); 229 230 it('updates zoom when slider moved', fakeAsync(() => { 231 fixture.detectChanges(); 232 const initialZoom = new TimeRange(timestamp15, timestamp16); 233 assertDefined(component.miniTimelineComponent).onZoomChanged(initialZoom); 234 fixture.detectChanges(); 235 236 const slider = assertDefined(htmlElement.querySelector('.slider .handle')); 237 expect(window.getComputedStyle(slider).visibility).toEqual('visible'); 238 239 dragElement(fixture, slider, 100, 8); 240 241 const finalZoom = timelineData.getZoomRange(); 242 expect(finalZoom).not.toBe(initialZoom); 243 })); 244 245 it('zooms in/out with buttons', () => { 246 initializeTraces(); 247 248 const initialZoom = new TimeRange(timestamp700, timestamp810); 249 const miniTimelineComponent = assertDefined( 250 component.miniTimelineComponent, 251 ); 252 miniTimelineComponent.onZoomChanged(initialZoom); 253 miniTimelineComponent.currentTracePosition = position800; 254 255 fixture.detectChanges(); 256 257 const zoomInButton = assertDefined( 258 htmlElement.querySelector('#zoom-in-btn'), 259 ) as HTMLButtonElement; 260 const zoomOutButton = assertDefined( 261 htmlElement.querySelector('#zoom-out-btn'), 262 ) as HTMLButtonElement; 263 264 zoomInButton.click(); 265 fixture.detectChanges(); 266 const zoomedIn = timelineData.getZoomRange(); 267 checkZoomDifference(initialZoom, zoomedIn); 268 269 zoomOutButton.click(); 270 fixture.detectChanges(); 271 const zoomedOut = timelineData.getZoomRange(); 272 checkZoomDifference(zoomedOut, zoomedIn); 273 }); 274 275 it('cannot zoom out past full range', () => { 276 initializeTraces(); 277 278 const initialZoom = new TimeRange(timestamp10, timestamp1000); 279 assertDefined(component.miniTimelineComponent).onZoomChanged(initialZoom); 280 281 timelineData.setPosition(position800); 282 fixture.detectChanges(); 283 284 const zoomButton = assertDefined( 285 htmlElement.querySelector('#zoom-out-btn'), 286 ) as HTMLButtonElement; 287 288 zoomButton.click(); 289 fixture.detectChanges(); 290 291 let finalZoom = timelineData.getZoomRange(); 292 expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs()); 293 expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs()); 294 295 zoomOutByScrollWheel(); 296 297 finalZoom = timelineData.getZoomRange(); 298 expect(finalZoom.from.getValueNs()).toBe(initialZoom.from.getValueNs()); 299 expect(finalZoom.to.getValueNs()).toBe(initialZoom.to.getValueNs()); 300 }); 301 302 it('zooms in/out with scroll wheel', () => { 303 initializeTraces(); 304 305 let initialZoom = new TimeRange(timestamp10, timestamp1000); 306 const miniTimelineComponent = assertDefined( 307 component.miniTimelineComponent, 308 ); 309 miniTimelineComponent.onZoomChanged(initialZoom); 310 311 fixture.detectChanges(); 312 313 for (let i = 0; i < 10; i++) { 314 zoomInByScrollWheel(); 315 316 const finalZoom = timelineData.getZoomRange(); 317 checkZoomDifference(initialZoom, finalZoom); 318 initialZoom = finalZoom; 319 } 320 321 for (let i = 0; i < 9; i++) { 322 zoomOutByScrollWheel(); 323 324 const finalZoom = timelineData.getZoomRange(); 325 checkZoomDifference(finalZoom, initialZoom); 326 initialZoom = finalZoom; 327 } 328 }); 329 330 it('applies expanded timeline scroll wheel event', () => { 331 initializeTraces(); 332 333 const initialZoom = new TimeRange(timestamp10, timestamp1000); 334 fixture.detectChanges(); 335 assertDefined(component.miniTimelineComponent).onZoomChanged(initialZoom); 336 fixture.detectChanges(); 337 338 component.expandedTimelineScrollEvent = { 339 deltaY: -200, 340 deltaX: 0, 341 x: 10, // scrolling on pos 342 target: component.miniTimelineComponent?.getCanvas(), 343 } as unknown as WheelEvent; 344 fixture.detectChanges(); 345 346 const finalZoom = timelineData.getZoomRange(); 347 checkZoomDifference(initialZoom, finalZoom); 348 }); 349 350 it('opens context menu', () => { 351 fixture.detectChanges(); 352 expect(document.querySelector('.context-menu')).toBeFalsy(); 353 354 assertDefined(component.miniTimelineComponent) 355 .getCanvas() 356 .dispatchEvent(new MouseEvent('contextmenu')); 357 fixture.detectChanges(); 358 359 const menu = assertDefined(document.querySelector('.context-menu')); 360 const options = menu.querySelectorAll('.context-menu-item'); 361 expect(options.length).toEqual(2); 362 }); 363 364 it('adds bookmark', () => { 365 fixture.detectChanges(); 366 const miniTimelineComponent = assertDefined( 367 component.miniTimelineComponent, 368 ); 369 const spy = spyOn(miniTimelineComponent.onToggleBookmark, 'emit'); 370 371 miniTimelineComponent 372 .getCanvas() 373 .dispatchEvent(new MouseEvent('contextmenu')); 374 fixture.detectChanges(); 375 376 const menu = assertDefined(document.querySelector('.context-menu')); 377 const options = menu.querySelectorAll('.context-menu-item'); 378 expect(options.item(0).textContent).toContain('Add bookmark'); 379 (options.item(0) as HTMLElement).click(); 380 381 expect(spy).toHaveBeenCalledWith({ 382 range: new TimeRange(timestamp10, timestamp10), 383 rangeContainsBookmark: false, 384 }); 385 }); 386 387 it('removes bookmark', () => { 388 component.bookmarks = [timestamp10]; 389 fixture.detectChanges(); 390 const miniTimelineComponent = assertDefined( 391 component.miniTimelineComponent, 392 ); 393 const spy = spyOn(miniTimelineComponent.onToggleBookmark, 'emit'); 394 395 miniTimelineComponent 396 .getCanvas() 397 .dispatchEvent(new MouseEvent('contextmenu')); 398 fixture.detectChanges(); 399 400 const menu = assertDefined(document.querySelector('.context-menu')); 401 const options = menu.querySelectorAll('.context-menu-item'); 402 expect(options.item(0).textContent).toContain('Remove bookmark'); 403 (options.item(0) as HTMLElement).click(); 404 405 expect(spy).toHaveBeenCalledWith({ 406 range: new TimeRange(timestamp10, timestamp10), 407 rangeContainsBookmark: true, 408 }); 409 }); 410 411 it('removes all bookmarks', () => { 412 component.bookmarks = [timestamp10, timestamp1000]; 413 fixture.detectChanges(); 414 const miniTimelineComponent = assertDefined( 415 component.miniTimelineComponent, 416 ); 417 const spy = spyOn(miniTimelineComponent.onRemoveAllBookmarks, 'emit'); 418 419 miniTimelineComponent 420 .getCanvas() 421 .dispatchEvent(new MouseEvent('contextmenu')); 422 fixture.detectChanges(); 423 424 const menu = assertDefined(document.querySelector('.context-menu')); 425 const options = menu.querySelectorAll('.context-menu-item'); 426 expect(options.item(1).textContent).toContain('Remove all bookmarks'); 427 (options.item(1) as HTMLElement).click(); 428 429 expect(spy).toHaveBeenCalled(); 430 }); 431 432 it('zooms in/out on KeyW/KeyS press', () => { 433 initializeTracesForWASDZoom(); 434 435 const initialZoom = new TimeRange(timestamp1000, timestamp2000); 436 component.initialZoom = initialZoom; 437 fixture.detectChanges(); 438 439 zoomInByKeyW(); 440 const zoomedIn = timelineData.getZoomRange(); 441 checkZoomDifference(initialZoom, zoomedIn); 442 443 zoomOutByKeyS(); 444 const zoomedOut = timelineData.getZoomRange(); 445 checkZoomDifference(zoomedOut, zoomedIn); 446 }); 447 448 it('moves right/left on KeyD/KeyA press', () => { 449 initializeTracesForWASDZoom(); 450 451 const initialZoom = new TimeRange(timestamp1000, timestamp2000); 452 component.initialZoom = initialZoom; 453 fixture.detectChanges(); 454 455 while (timelineData.getZoomRange().to !== timestamp4000) { 456 document.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyD'})); 457 fixture.detectChanges(); 458 const zoomRange = timelineData.getZoomRange(); 459 const increase = 460 zoomRange.from.getValueNs() - initialZoom.from.getValueNs(); 461 expect(increase).toBeGreaterThan(0); 462 expect(zoomRange.to.getValueNs()).toEqual( 463 initialZoom.to.getValueNs() + increase, 464 ); 465 } 466 467 // cannot move past end of trace 468 const finalZoom = timelineData.getZoomRange(); 469 document.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyD'})); 470 fixture.detectChanges(); 471 expect(timelineData.getZoomRange()).toEqual(finalZoom); 472 473 while (timelineData.getZoomRange().from !== timestamp1000) { 474 document.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyA'})); 475 fixture.detectChanges(); 476 const zoomRange = timelineData.getZoomRange(); 477 const decrease = 478 finalZoom.from.getValueNs() - zoomRange.from.getValueNs(); 479 expect(decrease).toBeGreaterThan(0); 480 expect(zoomRange.to.getValueNs()).toEqual( 481 finalZoom.to.getValueNs() - decrease, 482 ); 483 } 484 485 // cannot move before start of trace 486 document.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyA'})); 487 fixture.detectChanges(); 488 expect(timelineData.getZoomRange()).toEqual(initialZoom); 489 }); 490 491 it('zooms in/out on mouse position if within current range', () => { 492 initializeTracesForWASDZoom(); 493 const initialZoom = new TimeRange(timestamp1000, timestamp4000); 494 component.initialZoom = initialZoom; 495 component.currentTracePosition = TracePosition.fromTimestamp(timestamp2000); 496 fixture.detectChanges(); 497 498 const miniTimelineComponent = assertDefined( 499 component.miniTimelineComponent, 500 ); 501 const canvas = miniTimelineComponent.getCanvas(); 502 const drawer = assertDefined(miniTimelineComponent.drawer); 503 const usableRange = drawer.getUsableRange(); 504 505 const mouseMoveEvent = new MouseEvent('mousemove'); 506 Object.defineProperty(mouseMoveEvent, 'target', {value: canvas}); 507 Object.defineProperty(mouseMoveEvent, 'offsetX', { 508 value: 509 (usableRange.to - usableRange.from) * 0.25 + drawer.getPadding().left, 510 }); 511 canvas.dispatchEvent(mouseMoveEvent); 512 fixture.detectChanges(); 513 514 const fullRangeQuarterTimestamp = timestamp1750; 515 checkZoomOnTimestamp( 516 fullRangeQuarterTimestamp, 517 1n, 518 4n, 519 zoomInByKeyW, 520 zoomOutByKeyS, 521 ); 522 checkZoomOnTimestamp( 523 fullRangeQuarterTimestamp, 524 1n, 525 4n, 526 zoomInByScrollWheel, 527 zoomOutByScrollWheel, 528 ); 529 }); 530 531 it('zooms in/out on current position if within current range and mouse position not available', () => { 532 initializeTracesForWASDZoom(); 533 const initialZoom = new TimeRange(timestamp1000, timestamp4000); 534 component.initialZoom = initialZoom; 535 component.currentTracePosition = TracePosition.fromTimestamp(timestamp1750); 536 fixture.detectChanges(); 537 538 const fullRangeQuarterTimestamp = timestamp1750; 539 checkZoomOnTimestamp( 540 fullRangeQuarterTimestamp, 541 1n, 542 4n, 543 zoomInByKeyW, 544 zoomOutByKeyS, 545 ); 546 checkZoomOnTimestamp( 547 fullRangeQuarterTimestamp, 548 1n, 549 4n, 550 zoomInByScrollWheel, 551 zoomOutByScrollWheel, 552 ); 553 554 const zoomInButton = assertDefined( 555 htmlElement.querySelector('#zoom-in-btn'), 556 ) as HTMLButtonElement; 557 const zoomOutButton = assertDefined( 558 htmlElement.querySelector('#zoom-out-btn'), 559 ) as HTMLButtonElement; 560 561 checkZoomOnTimestamp( 562 fullRangeQuarterTimestamp, 563 1n, 564 4n, 565 () => { 566 zoomInButton.click(); 567 fixture.detectChanges(); 568 }, 569 () => { 570 zoomOutButton.click(); 571 fixture.detectChanges(); 572 }, 573 ); 574 }); 575 576 it('zooms in/out on current position after mouse leaves canvas', () => { 577 initializeTracesForWASDZoom(); 578 const initialZoom = new TimeRange(timestamp1000, timestamp4000); 579 component.initialZoom = initialZoom; 580 component.currentTracePosition = TracePosition.fromTimestamp(timestamp1750); 581 fixture.detectChanges(); 582 583 const miniTimelineComponent = assertDefined( 584 component.miniTimelineComponent, 585 ); 586 const canvas = miniTimelineComponent.getCanvas(); 587 const drawer = assertDefined(miniTimelineComponent.drawer); 588 const usableRange = drawer.getUsableRange(); 589 590 const mouseMoveEvent = new MouseEvent('mousemove'); 591 Object.defineProperty(mouseMoveEvent, 'target', {value: canvas}); 592 Object.defineProperty(mouseMoveEvent, 'offsetX', { 593 value: (usableRange.to - usableRange.from) * 0.5, 594 }); 595 canvas.dispatchEvent(mouseMoveEvent); 596 fixture.detectChanges(); 597 canvas.dispatchEvent(new MouseEvent('mouseleave')); 598 fixture.detectChanges(); 599 600 const fullRangeQuarterTimestamp = timestamp1750; 601 checkZoomOnTimestamp( 602 fullRangeQuarterTimestamp, 603 1n, 604 4n, 605 zoomInByKeyW, 606 zoomOutByKeyS, 607 ); 608 checkZoomOnTimestamp( 609 fullRangeQuarterTimestamp, 610 1n, 611 4n, 612 zoomInByScrollWheel, 613 zoomOutByScrollWheel, 614 ); 615 616 const zoomInButton = assertDefined( 617 htmlElement.querySelector('#zoom-in-btn'), 618 ) as HTMLButtonElement; 619 const zoomOutButton = assertDefined( 620 htmlElement.querySelector('#zoom-out-btn'), 621 ) as HTMLButtonElement; 622 623 checkZoomOnTimestamp( 624 fullRangeQuarterTimestamp, 625 1n, 626 4n, 627 () => { 628 zoomInButton.click(); 629 fixture.detectChanges(); 630 }, 631 () => { 632 zoomOutButton.click(); 633 fixture.detectChanges(); 634 }, 635 ); 636 }); 637 638 it('zooms in/out on middle of slider bar if current position out of range and mouse position not available', () => { 639 initializeTracesForWASDZoom(); 640 const initialZoom = new TimeRange(timestamp2000, timestamp4000); 641 component.initialZoom = initialZoom; 642 component.currentTracePosition = TracePosition.fromTimestamp(timestamp1750); 643 fixture.detectChanges(); 644 645 const fullRangeMiddleTimestamp = timestamp3000; 646 checkZoomOnTimestamp( 647 fullRangeMiddleTimestamp, 648 1n, 649 2n, 650 zoomInByKeyW, 651 zoomOutByKeyS, 652 ); 653 checkZoomOnTimestamp( 654 fullRangeMiddleTimestamp, 655 1n, 656 2n, 657 zoomInByScrollWheel, 658 zoomOutByScrollWheel, 659 ); 660 661 const zoomInButton = assertDefined( 662 htmlElement.querySelector('#zoom-in-btn'), 663 ) as HTMLButtonElement; 664 const zoomOutButton = assertDefined( 665 htmlElement.querySelector('#zoom-out-btn'), 666 ) as HTMLButtonElement; 667 668 checkZoomOnTimestamp( 669 fullRangeMiddleTimestamp, 670 1n, 671 2n, 672 () => { 673 zoomInButton.click(); 674 fixture.detectChanges(); 675 }, 676 () => { 677 zoomOutButton.click(); 678 fixture.detectChanges(); 679 }, 680 ); 681 }); 682 683 it('zooms in/out on mouse position from expanded timeline', () => { 684 initializeTracesForWASDZoom(); 685 const initialZoom = new TimeRange(timestamp1000, timestamp4000); 686 component.initialZoom = initialZoom; 687 component.currentTracePosition = TracePosition.fromTimestamp(timestamp2000); 688 fixture.detectChanges(); 689 690 component.expandedTimelineMouseXRatio = 0.25; 691 fixture.detectChanges(); 692 693 const fullRangeQuarterTimestamp = timestamp1750; 694 checkZoomOnTimestamp( 695 fullRangeQuarterTimestamp, 696 1n, 697 4n, 698 zoomInByKeyW, 699 zoomOutByKeyS, 700 ); 701 checkZoomOnTimestamp( 702 fullRangeQuarterTimestamp, 703 1n, 704 4n, 705 zoomInByScrollWheel, 706 zoomOutByScrollWheel, 707 ); 708 }); 709 710 function initializeTraces() { 711 const timelineData = assertDefined(component.timelineData); 712 const traces = new TracesBuilder() 713 .setTimestamps(TraceType.SURFACE_FLINGER, [timestamp10]) 714 .setTimestamps(TraceType.WINDOW_MANAGER, [timestamp1000]) 715 .build(); 716 717 timelineData.initialize( 718 traces, 719 undefined, 720 TimestampConverterUtils.TIMESTAMP_CONVERTER, 721 ); 722 fixture.detectChanges(); 723 } 724 725 function initializeTracesForWASDZoom() { 726 const traces = new TracesBuilder() 727 .setTimestamps(TraceType.SURFACE_FLINGER, [ 728 timestamp1000, 729 timestamp2000, 730 timestamp4000, 731 ]) 732 .build(); 733 734 assertDefined(component.timelineData).initialize( 735 traces, 736 undefined, 737 TimestampConverterUtils.TIMESTAMP_CONVERTER, 738 ); 739 } 740 741 function checkZoomDifference( 742 biggerRange: TimeRange, 743 smallerRange: TimeRange, 744 ) { 745 expect(biggerRange).not.toBe(smallerRange); 746 expect(smallerRange.from.getValueNs()).toBeGreaterThanOrEqual( 747 Number(biggerRange.from.getValueNs()), 748 ); 749 expect(smallerRange.to.getValueNs()).toBeLessThanOrEqual( 750 Number(biggerRange.to.getValueNs()), 751 ); 752 } 753 754 function zoomInByKeyW() { 755 document.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyW'})); 756 fixture.detectChanges(); 757 } 758 759 function zoomOutByKeyS() { 760 document.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyS'})); 761 fixture.detectChanges(); 762 } 763 764 function zoomInByScrollWheel() { 765 assertDefined(component.miniTimelineComponent).onScroll({ 766 deltaY: -200, 767 deltaX: 0, 768 x: 10, // scrolling on pos 769 target: {id: 'mini-timeline-canvas', offsetLeft: 0}, 770 } as unknown as WheelEvent); 771 fixture.detectChanges(); 772 } 773 774 function zoomOutByScrollWheel() { 775 assertDefined(component.miniTimelineComponent).onScroll({ 776 deltaY: 200, 777 deltaX: 0, 778 x: 10, // scrolling on pos 779 target: {id: 'mini-timeline-canvas', offsetLeft: 0}, 780 } as unknown as WheelEvent); 781 fixture.detectChanges(); 782 } 783 784 function checkZoomOnTimestamp( 785 zoomOnTimestamp: Timestamp, 786 ratioNom: bigint, 787 ratioDenom: bigint, 788 zoomInAction: () => void, 789 zoomOutAction: () => void, 790 ) { 791 let currentZoom = timelineData.getZoomRange(); 792 for (let i = 0; i < 5; i++) { 793 zoomInAction(); 794 795 const zoomedIn = timelineData.getZoomRange(); 796 checkZoomDifference(currentZoom, zoomedIn); 797 currentZoom = zoomedIn; 798 799 const zoomedInTimestamp = zoomedIn.from.add( 800 (zoomedIn.to.minus(zoomedIn.from.getValueNs()).getValueNs() * 801 ratioNom) / 802 ratioDenom, 803 ); 804 expect( 805 Math.abs(Number(zoomedInTimestamp.minus(zoomOnTimestamp.getValueNs()))), 806 ).toBeLessThanOrEqual(5); 807 } 808 for (let i = 0; i < 4; i++) { 809 zoomOutAction(); 810 811 const zoomedOut = timelineData.getZoomRange(); 812 checkZoomDifference(zoomedOut, currentZoom); 813 currentZoom = zoomedOut; 814 815 const zoomedOutTimestamp = zoomedOut.from.add( 816 (zoomedOut.to.minus(zoomedOut.from.getValueNs()).getValueNs() * 817 ratioNom) / 818 ratioDenom, 819 ); 820 expect( 821 Math.abs( 822 Number(zoomedOutTimestamp.minus(zoomOnTimestamp.getValueNs())), 823 ), 824 ).toBeLessThanOrEqual(5); 825 } 826 } 827 828 @Component({ 829 selector: 'host-component', 830 template: ` 831 <mini-timeline 832 [timelineData]="timelineData" 833 [currentTracePosition]="currentTracePosition" 834 [selectedTraces]="selectedTraces" 835 [initialZoom]="initialZoom" 836 [expandedTimelineScrollEvent]="expandedTimelineScrollEvent" 837 [expandedTimelineMouseXRatio]="expandedTimelineMouseXRatio" 838 [bookmarks]="bookmarks"></mini-timeline> 839 `, 840 }) 841 class TestHostComponent { 842 timelineData = new TimelineData(); 843 currentTracePosition: TracePosition | undefined; 844 selectedTraces: Array<Trace<object>> = []; 845 initialZoom: TimeRange | undefined; 846 expandedTimelineScrollEvent: WheelEvent | undefined; 847 expandedTimelineMouseXRatio: number | undefined; 848 bookmarks: Timestamp[] = []; 849 850 @ViewChild(MiniTimelineComponent) 851 miniTimelineComponent: MiniTimelineComponent | undefined; 852 } 853}); 854