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