• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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