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