xref: /aosp_15_r20/development/tools/winscope/src/viewers/viewer_input/presenter_test.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright 2024 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 ANYf KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {assertDefined} from 'common/assert_utils';
18import {InMemoryStorage} from 'common/in_memory_storage';
19import {
20  TabbedViewSwitchRequest,
21  TracePositionUpdate,
22} from 'messaging/winscope_event';
23import {Transform} from 'parsers/surface_flinger/transform_utils';
24import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
25import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
26import {TracesBuilder} from 'test/unit/traces_builder';
27import {TraceBuilder} from 'test/unit/trace_builder';
28import {UnitTestUtils} from 'test/unit/utils';
29import {CustomQueryType} from 'trace/custom_query';
30import {Parser} from 'trace/parser';
31import {Trace} from 'trace/trace';
32import {Traces} from 'trace/traces';
33import {TraceRectBuilder} from 'trace/trace_rect_builder';
34import {TraceType} from 'trace/trace_type';
35import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
36import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
37import {NotifyLogViewCallbackType} from 'viewers/common/abstract_log_viewer_presenter';
38import {AbstractLogViewerPresenterTest} from 'viewers/common/abstract_log_viewer_presenter_test';
39import {VISIBLE_CHIP} from 'viewers/common/chip';
40import {LogSelectFilter} from 'viewers/common/log_filters';
41import {TextFilter} from 'viewers/common/text_filter';
42import {LogField, LogHeader} from 'viewers/common/ui_data_log';
43import {UserOptions} from 'viewers/common/user_options';
44import {ViewerEvents} from 'viewers/common/viewer_events';
45import {Presenter} from './presenter';
46import {UiData} from './ui_data';
47
48class PresenterInputTest extends AbstractLogViewerPresenterTest<UiData> {
49  override readonly expectedHeaders = [
50    {
51      header: new LogHeader({
52        name: 'Type',
53        cssClass: 'input-type inline',
54      }),
55    },
56    {
57      header: new LogHeader({
58        name: 'Source',
59        cssClass: 'input-source',
60      }),
61    },
62    {
63      header: new LogHeader({
64        name: 'Action',
65        cssClass: 'input-action',
66      }),
67    },
68    {
69      header: new LogHeader({
70        name: 'Device',
71        cssClass: 'input-device-id right-align',
72      }),
73    },
74    {
75      header: new LogHeader({
76        name: 'Display',
77        cssClass: 'input-display-id right-align',
78      }),
79    },
80    {
81      header: new LogHeader({
82        name: 'Details',
83        cssClass: 'input-details',
84      }),
85    },
86    {
87      header: new LogHeader(
88        {
89          name: 'Target Windows',
90          cssClass: 'input-windows',
91        },
92        new LogSelectFilter(
93          Array.from({length: 6}, () => ''),
94          true,
95          '100',
96          '100%',
97        ),
98      ),
99      options: [
100        this.wrappedName('win-212'),
101        this.wrappedName('win-64'),
102        this.wrappedName('win-82'),
103        this.wrappedName('win-75'),
104        this.wrappedName('win-zero-not-98'),
105        this.wrappedName('98'),
106      ],
107    },
108  ];
109  private trace: Trace<PropertyTreeNode> | undefined;
110  private surfaceFlingerTrace: Trace<HierarchyTreeNode> | undefined;
111  private positionUpdate: TracePositionUpdate | undefined;
112  private layerIdToName: Array<{id: number; name: string}> = [
113    {id: 0, name: 'win-zero-not-98'},
114    {id: 212, name: 'win-212'},
115    {id: 64, name: 'win-64'},
116    {id: 82, name: 'win-82'},
117    {id: 75, name: 'win-75'},
118    // The layer name for window with id 98 is omitted to test incomplete mapping.
119  ];
120
121  override async setUpTestEnvironment(): Promise<void> {
122    const parser = (await UnitTestUtils.getTracesParser([
123      'traces/perfetto/input-events.perfetto-trace',
124    ])) as Parser<PropertyTreeNode>;
125
126    this.trace = new TraceBuilder<PropertyTreeNode>()
127      .setType(TraceType.INPUT_EVENT_MERGED)
128      .setParser(parser)
129      .build();
130
131    this.surfaceFlingerTrace = new TraceBuilder<HierarchyTreeNode>()
132      .setType(TraceType.SURFACE_FLINGER)
133      .setEntries([])
134      .setParserCustomQueryResult(
135        CustomQueryType.SF_LAYERS_ID_AND_NAME,
136        this.layerIdToName,
137      )
138      .build();
139
140    this.positionUpdate = TracePositionUpdate.fromTraceEntry(
141      this.trace.getEntry(0),
142    );
143  }
144
145  override async createPresenterWithEmptyTrace(
146    callback: NotifyLogViewCallbackType<UiData>,
147  ): Promise<Presenter> {
148    const traces = new TracesBuilder()
149      .setEntries(TraceType.INPUT_EVENT_MERGED, [])
150      .build();
151    if (this.surfaceFlingerTrace !== undefined) {
152      traces.addTrace(this.surfaceFlingerTrace);
153    }
154    return PresenterInputTest.createPresenterWithTraces(traces, callback);
155  }
156
157  override async createPresenter(
158    callback: NotifyLogViewCallbackType<UiData>,
159  ): Promise<Presenter> {
160    const traces = new Traces();
161    traces.addTrace(assertDefined(this.trace));
162    if (this.surfaceFlingerTrace !== undefined) {
163      traces.addTrace(this.surfaceFlingerTrace);
164    }
165    const presenter = PresenterInputTest.createPresenterWithTraces(
166      traces,
167      callback,
168    );
169    await presenter.onAppEvent(this.getPositionUpdate()); // trigger initialization
170    return presenter;
171  }
172
173  private static createPresenterWithTraces(
174    traces: Traces,
175    callback: NotifyLogViewCallbackType<UiData>,
176  ): Presenter {
177    return new Presenter(
178      traces,
179      assertDefined(traces.getTrace(TraceType.INPUT_EVENT_MERGED)),
180      new InMemoryStorage(),
181      callback,
182    );
183  }
184
185  override getPositionUpdate(): TracePositionUpdate {
186    return assertDefined(this.positionUpdate);
187  }
188
189  override executePropertiesChecksForEmptyTrace(uiData: UiData) {
190    expect(uiData.highlightedProperty).toBeFalsy();
191    expect(uiData.dispatchPropertiesTree).toBeUndefined();
192    expect(uiData.dispatchPropertiesFilter).toBeDefined();
193  }
194
195  override executePropertiesChecksAfterPositionUpdate(uiData: UiData): void {
196    expect(uiData.entries.length).toEqual(8);
197    expect(uiData.currentIndex).toEqual(0);
198    expect(uiData.selectedIndex).toBeUndefined();
199    const curEntry = uiData.entries[0];
200    const expectedFields: LogField[] = [
201      {
202        spec: uiData.headers[0].spec,
203        value: 'MOTION',
204        propagateEntryTimestamp: true,
205      },
206      {spec: uiData.headers[1].spec, value: 'TOUCHSCREEN'},
207      {spec: uiData.headers[2].spec, value: 'DOWN'},
208      {spec: uiData.headers[3].spec, value: 4},
209      {spec: uiData.headers[4].spec, value: 0},
210      {spec: uiData.headers[5].spec, value: '[212, 64, 82, 75]'},
211      {
212        spec: uiData.headers[6].spec,
213        value: [
214          this.wrappedName('win-212'),
215          this.wrappedName('win-64'),
216          this.wrappedName('win-82'),
217          this.wrappedName('win-75'),
218          this.wrappedName('win-zero-not-98'),
219        ].join(', '),
220      },
221    ];
222    expectedFields.forEach((field) => {
223      expect(curEntry.fields).toContain(field);
224    });
225
226    const motionEvent = assertDefined(uiData.propertiesTree);
227    expect(motionEvent.getChildByName('eventId')?.getValue()).toEqual(
228      330184796,
229    );
230    expect(motionEvent.getChildByName('action')?.formattedValue()).toEqual(
231      'ACTION_DOWN',
232    );
233
234    const dispatchProperties = assertDefined(uiData.dispatchPropertiesTree);
235    expect(dispatchProperties.getAllChildren().length).toEqual(5);
236
237    expect(
238      dispatchProperties
239        .getChildByName('0')
240        ?.getChildByName('windowId')
241        ?.getDisplayName(),
242    ).toEqual('TargetWindow');
243    expect(
244      dispatchProperties
245        .getChildByName('0')
246        ?.getChildByName('windowId')
247        ?.formattedValue(),
248    ).toEqual('212 - win-212');
249  }
250
251  private expectEventPresented(
252    uiData: UiData,
253    eventId: number,
254    action: string,
255  ) {
256    const properties = assertDefined(uiData.propertiesTree);
257    expect(properties.getChildByName('action')?.formattedValue()).toEqual(
258      action,
259    );
260    expect(properties.getChildByName('eventId')?.getValue()).toEqual(eventId);
261  }
262
263  override executeSpecializedTests() {
264    describe('Specialized tests', () => {
265      const time0 = TimestampConverterUtils.makeRealTimestamp(0n);
266      const time10 = TimestampConverterUtils.makeRealTimestamp(10n);
267      const time19 = TimestampConverterUtils.makeRealTimestamp(19n);
268      const time20 = TimestampConverterUtils.makeRealTimestamp(20n);
269      const time25 = TimestampConverterUtils.makeRealTimestamp(25n);
270      const time30 = TimestampConverterUtils.makeRealTimestamp(30n);
271      const time35 = TimestampConverterUtils.makeRealTimestamp(35n);
272      const time36 = TimestampConverterUtils.makeRealTimestamp(36n);
273      const layerRect = new TraceRectBuilder()
274        .setX(0)
275        .setY(0)
276        .setWidth(1)
277        .setHeight(1)
278        .setId('layerRect')
279        .setName('layerRect')
280        .setCornerRadius(0)
281        .setTransform(Transform.EMPTY.matrix)
282        .setDepth(1)
283        .setGroupId(0)
284        .setIsVisible(true)
285        .setOpacity(1)
286        .setIsDisplay(false)
287        .setIsSpy(false)
288        .build();
289      const inputRect = new TraceRectBuilder()
290        .setX(2)
291        .setY(2)
292        .setWidth(3)
293        .setHeight(3)
294        .setId('inputRect')
295        .setName('inputRect')
296        .setCornerRadius(0)
297        .setTransform(Transform.EMPTY.matrix)
298        .setDepth(1)
299        .setGroupId(0)
300        .setIsVisible(true)
301        .setOpacity(1)
302        .setIsDisplay(false)
303        .setIsSpy(false)
304        .build();
305      const sfEntry0 = new HierarchyTreeBuilder()
306        .setId('LayerTraceEntry')
307        .setName('root')
308        .setChildren([
309          {
310            id: 1,
311            name: 'layer1',
312            rects: [layerRect],
313            secondaryRects: [inputRect],
314          },
315        ])
316        .build();
317      const sfEntry1 = new HierarchyTreeBuilder()
318        .setId('LayerTraceEntry')
319        .setName('root')
320        .setChildren([
321          {
322            id: 1,
323            name: 'layer1',
324            rects: [layerRect],
325            secondaryRects: [inputRect],
326            children: [
327              {
328                id: 2,
329                name: 'layer2',
330                rects: [layerRect],
331                secondaryRects: [inputRect],
332              },
333            ],
334          },
335        ])
336        .build();
337      const sfEntry2 = new HierarchyTreeBuilder()
338        .setId('LayerTraceEntry')
339        .setName('root')
340        .setChildren([
341          {
342            id: 1,
343            name: 'layer1',
344            rects: [layerRect],
345            secondaryRects: [inputRect],
346            children: [
347              {
348                id: 2,
349                name: 'layer2',
350                rects: [layerRect],
351                secondaryRects: [inputRect],
352              },
353            ],
354          },
355          {
356            id: 3,
357            name: 'layer3',
358            rects: [layerRect],
359            secondaryRects: [inputRect],
360          },
361        ])
362        .build();
363
364      let uiData: UiData = UiData.createEmpty();
365
366      beforeEach(async () => {
367        uiData = UiData.createEmpty();
368        await this.setUpTestEnvironment();
369      });
370
371      it('adds events listeners', async () => {
372        const element = document.createElement('div');
373        const presenter = await this.createPresenter(
374          (uiDataLog) => (uiData = uiDataLog as UiData),
375        );
376        presenter.addEventListeners(element);
377
378        const testId = 'testId';
379
380        let spy: jasmine.Spy = spyOn(presenter, 'onHighlightedPropertyChange');
381        element.dispatchEvent(
382          new CustomEvent(ViewerEvents.HighlightedPropertyChange, {
383            detail: {id: testId},
384          }),
385        );
386        expect(spy).toHaveBeenCalledWith(testId);
387
388        spy = spyOn(presenter, 'onHighlightedIdChange');
389        element.dispatchEvent(
390          new CustomEvent(ViewerEvents.HighlightedIdChange, {
391            detail: {id: testId},
392          }),
393        );
394        expect(spy).toHaveBeenCalledWith(testId);
395
396        spy = spyOn(presenter, 'onRectsUserOptionsChange');
397        const userOptions = {};
398        element.dispatchEvent(
399          new CustomEvent(ViewerEvents.RectsUserOptionsChange, {
400            detail: {userOptions},
401          }),
402        );
403        expect(spy).toHaveBeenCalledWith(userOptions);
404
405        spy = spyOn(presenter, 'onRectDoubleClick');
406        element.dispatchEvent(new CustomEvent(ViewerEvents.RectsDblClick));
407        expect(spy).toHaveBeenCalled();
408
409        spy = spyOn(presenter, 'onDispatchPropertiesFilterChange');
410        const filter = new TextFilter();
411        element.dispatchEvent(
412          new CustomEvent(ViewerEvents.DispatchPropertiesFilterChange, {
413            detail: filter,
414          }),
415        );
416        expect(spy).toHaveBeenCalledWith(filter);
417      });
418
419      it('updates selected entry', async () => {
420        const presenter = await this.createPresenter(
421          (uiDataLog) => (uiData = uiDataLog as UiData),
422        );
423
424        const keyEntry = assertDefined(this.trace).getEntry(7);
425        await presenter.onAppEvent(
426          TracePositionUpdate.fromTraceEntry(keyEntry),
427        );
428
429        this.expectEventPresented(uiData, 894093732, 'ACTION_UP');
430
431        const motionEntry = assertDefined(this.trace).getEntry(1);
432        await presenter.onAppEvent(
433          TracePositionUpdate.fromTraceEntry(motionEntry),
434        );
435
436        this.expectEventPresented(uiData, 1327679296, 'ACTION_OUTSIDE');
437
438        const motionDispatchProperties = assertDefined(
439          uiData.dispatchPropertiesTree,
440        );
441        expect(motionDispatchProperties.getAllChildren().length).toEqual(1);
442        expect(
443          motionDispatchProperties
444            .getChildByName('0')
445            ?.getChildByName('windowId')
446            ?.getValue(),
447        ).toEqual(98n);
448      });
449
450      it('finds entry by time', async () => {
451        const traces = new Traces();
452        traces.addTrace(assertDefined(this.trace));
453
454        const lastMotion = await assertDefined(this.trace).getEntry(5);
455        const firstKey = await assertDefined(this.trace).getEntry(6);
456        const diffNs =
457          firstKey.getTimestamp().getValueNs() -
458          lastMotion.getTimestamp().getValueNs();
459        const belowLastMotionTime = lastMotion.getTimestamp().minus(1n);
460        const midpointTime = lastMotion.getTimestamp().add(diffNs / 2n);
461        const aboveFirstKeyTime = firstKey.getTimestamp().add(1n);
462
463        const otherTrace = new TraceBuilder<string>()
464          .setType(TraceType.TEST_TRACE_STRING)
465          .setEntries(['event-log-00', 'event-log-01', 'event-log-02'])
466          .setTimestamps([belowLastMotionTime, midpointTime, aboveFirstKeyTime])
467          .build();
468        traces.addTrace(otherTrace);
469        const presenter = PresenterInputTest.createPresenterWithTraces(
470          traces,
471          (uiDataLog) => (uiData = uiDataLog as UiData),
472        );
473
474        await presenter.onAppEvent(
475          TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(0)),
476        );
477        this.expectEventPresented(uiData, 313395000, 'ACTION_MOVE');
478
479        await presenter.onAppEvent(
480          TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(1)),
481        );
482        this.expectEventPresented(uiData, 436499943, 'ACTION_UP');
483
484        await presenter.onAppEvent(
485          TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(2)),
486        );
487        this.expectEventPresented(uiData, 759309047, 'ACTION_DOWN');
488      });
489
490      it('finds closest input event by frame', async () => {
491        const parser = assertDefined(this.trace).getParser();
492        const traces = new Traces();
493
494        // FRAME:            0        1       2
495        // TEST(time):       0       19      35
496        // INPUT(time):     10    20,25   30,36
497        const trace = new TraceBuilder<PropertyTreeNode>()
498          .setType(TraceType.INPUT_EVENT_MERGED)
499          .setEntries([
500            await parser.getEntry(0),
501            await parser.getEntry(1),
502            await parser.getEntry(2),
503            await parser.getEntry(3),
504            await parser.getEntry(4),
505          ])
506          .setTimestamps([time10, time20, time25, time30, time36])
507          .setFrame(0, 0)
508          .setFrame(1, 1)
509          .setFrame(2, 1)
510          .setFrame(3, 2)
511          .setFrame(4, 2)
512          .build();
513        traces.addTrace(trace);
514
515        const otherTrace = new TraceBuilder<string>()
516          .setType(TraceType.TEST_TRACE_STRING)
517          .setEntries(['sf-event-00', 'sf-event-01', 'sf-event-02'])
518          .setTimestamps([time0, time19, time35])
519          .setFrame(0, 0)
520          .setFrame(1, 1)
521          .setFrame(2, 2)
522          .build();
523        traces.addTrace(otherTrace);
524
525        const presenter = PresenterInputTest.createPresenterWithTraces(
526          traces,
527          (uiDataLog) => (uiData = uiDataLog as UiData),
528        );
529
530        await presenter.onAppEvent(
531          TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(0)),
532        );
533        this.expectEventPresented(uiData, 330184796, 'ACTION_DOWN');
534
535        await presenter.onAppEvent(
536          TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(1)),
537        );
538        this.expectEventPresented(uiData, 1327679296, 'ACTION_OUTSIDE');
539
540        await presenter.onAppEvent(
541          TracePositionUpdate.fromTraceEntry(otherTrace.getEntry(2)),
542        );
543        this.expectEventPresented(uiData, 106022695, 'ACTION_MOVE');
544      });
545
546      it('no rects defined without SF trace', async () => {
547        this.surfaceFlingerTrace = undefined;
548
549        const presenter = await this.createPresenter(
550          (uiDataLog) => (uiData = uiDataLog as UiData),
551        );
552        await presenter.onAppEvent(this.getPositionUpdate());
553        expect(uiData.rectsToDraw).toBeUndefined();
554      });
555
556      it('empty trace no rects defined without SF trace', async () => {
557        this.surfaceFlingerTrace = undefined;
558
559        const presenter = await this.createPresenterWithEmptyTrace(
560          (uiDataLog) => (uiData = uiDataLog as UiData),
561        );
562        await presenter.onAppEvent(this.getPositionUpdate());
563        expect(uiData.rectsToDraw).toBeUndefined();
564      });
565
566      it('rects defined with SF trace', async () => {
567        assertDefined(this.surfaceFlingerTrace);
568        const presenter = await this.createPresenter(
569          (uiDataLog) => (uiData = uiDataLog as UiData),
570        );
571        await presenter.onAppEvent(this.getPositionUpdate());
572        expect(uiData.rectsToDraw).toBeDefined();
573        expect(uiData.rectsToDraw).toEqual([]);
574      });
575
576      it('empty trace rects defined with SF trace', async () => {
577        assertDefined(this.surfaceFlingerTrace);
578        const presenter = await this.createPresenterWithEmptyTrace(
579          (uiDataLog) => (uiData = uiDataLog as UiData),
580        );
581        await presenter.onAppEvent(this.getPositionUpdate());
582        expect(uiData.rectsToDraw).toBeDefined();
583        expect(uiData.rectsToDraw).toEqual([]);
584      });
585
586      it('extracts corresponding input rects from SF trace', async () => {
587        const parser = assertDefined(this.trace).getParser();
588        const traces = await getTracesWithSf(parser, this.layerIdToName);
589        const trace = assertDefined(
590          traces.getTrace(TraceType.INPUT_EVENT_MERGED),
591        );
592
593        const presenter = PresenterInputTest.createPresenterWithTraces(
594          traces,
595          (uiDataLog) => (uiData = uiDataLog as UiData),
596        );
597
598        await presenter.onAppEvent(
599          TracePositionUpdate.fromTraceEntry(trace.getEntry(0)),
600        );
601        expect(uiData.rectsToDraw).toEqual([]);
602
603        await presenter.onAppEvent(
604          TracePositionUpdate.fromTraceEntry(trace.getEntry(1)),
605        );
606        expect(uiData.rectsToDraw).toHaveSize(1);
607        expect(uiData.rectsToDraw?.at(0)?.id).toEqual('inputRect');
608
609        await presenter.onAppEvent(
610          TracePositionUpdate.fromTraceEntry(trace.getEntry(2)),
611        );
612        expect(uiData.rectsToDraw).toHaveSize(1);
613        expect(uiData.rectsToDraw?.at(0)?.id).toEqual('inputRect');
614
615        await presenter.onAppEvent(
616          TracePositionUpdate.fromTraceEntry(trace.getEntry(3)),
617        );
618        expect(uiData.rectsToDraw).toHaveSize(3);
619        uiData.rectsToDraw?.forEach((rect) =>
620          expect(rect.id).toEqual('inputRect'),
621        );
622      });
623
624      it('filters dispatch properties tree', async () => {
625        const presenter = await this.createPresenter(
626          (uiDataLog) => (uiData = uiDataLog as UiData),
627        );
628        await presenter.onAppEvent(this.getPositionUpdate());
629        await presenter.onLogEntryClick(3);
630        expect(
631          assertDefined(uiData.dispatchPropertiesTree).getAllChildren().length,
632        ).toEqual(5);
633        await presenter.onDispatchPropertiesFilterChange(new TextFilter('212'));
634        expect(
635          assertDefined(uiData.dispatchPropertiesTree).getAllChildren().length,
636        ).toEqual(1);
637      });
638
639      it('updates highlighted property', async () => {
640        const presenter = await this.createPresenter(
641          (uiDataLog) => (uiData = uiDataLog as UiData),
642        );
643        expect(uiData.highlightedProperty).toEqual('');
644        const id = '4';
645        presenter.onHighlightedPropertyChange(id);
646        expect(uiData.highlightedProperty).toEqual(id);
647        presenter.onHighlightedPropertyChange(id);
648        expect(uiData.highlightedProperty).toEqual('');
649      });
650
651      it('updates highlighted rect', async () => {
652        const parser = assertDefined(this.trace).getParser();
653        const traces = await getTracesWithSf(parser, this.layerIdToName);
654        const trace = assertDefined(
655          traces.getTrace(TraceType.INPUT_EVENT_MERGED),
656        );
657        const presenter = PresenterInputTest.createPresenterWithTraces(
658          traces,
659          (uiDataLog) => (uiData = uiDataLog as UiData),
660        );
661        await presenter.onAppEvent(
662          TracePositionUpdate.fromTraceEntry(trace.getEntry(1)),
663        );
664        expect(uiData.rectsToDraw).toHaveSize(1);
665
666        const rect = assertDefined(uiData.rectsToDraw)[0];
667        await presenter.onHighlightedIdChange(rect.id);
668        expect(uiData.highlightedRect).toEqual(rect.id);
669        await presenter.onHighlightedIdChange(rect.id);
670        expect(uiData.highlightedRect).toEqual('');
671      });
672
673      it('filters rects by having content or visibility', async () => {
674        const userOptions: UserOptions = {
675          showOnlyVisible: {
676            name: 'Show only',
677            chip: VISIBLE_CHIP,
678            enabled: false,
679          },
680          showOnlyWithContent: {
681            name: 'Has input',
682            icon: 'pan_tool_alt',
683            enabled: true,
684          },
685        };
686        const parser = assertDefined(this.trace).getParser();
687        const traces = await getTracesWithSf(parser, this.layerIdToName);
688        const trace = assertDefined(
689          traces.getTrace(TraceType.INPUT_EVENT_MERGED),
690        );
691        const presenter = PresenterInputTest.createPresenterWithTraces(
692          traces,
693          (uiDataLog) => (uiData = uiDataLog as UiData),
694        );
695        await presenter.onAppEvent(
696          TracePositionUpdate.fromTraceEntry(trace.getEntry(1)),
697        );
698        expect(uiData.rectsToDraw).toHaveSize(1);
699
700        await presenter.onRectsUserOptionsChange(userOptions);
701        expect(uiData.rectsUserOptions).toEqual(userOptions);
702        expect(uiData.rectsToDraw).toHaveSize(0);
703
704        userOptions['showOnlyVisible'].enabled = true;
705        userOptions['showOnlyWithContent'].enabled = false;
706        await presenter.onRectsUserOptionsChange(userOptions);
707        expect(uiData.rectsToDraw).toHaveSize(1);
708      });
709
710      it('emits event on rect double click', async () => {
711        const presenter = await this.createPresenter(
712          (uiDataLog) => (uiData = uiDataLog as UiData),
713        );
714        const spy = jasmine.createSpy();
715        presenter.setEmitEvent(spy);
716        await presenter.onRectDoubleClick();
717        expect(spy).toHaveBeenCalledWith(
718          new TabbedViewSwitchRequest(assertDefined(this.surfaceFlingerTrace)),
719        );
720      });
721
722      async function getTracesWithSf(
723        parser: Parser<PropertyTreeNode>,
724        layerIdToName: Array<{
725          id: number;
726          name: string;
727        }>,
728      ) {
729        const traces = new Traces();
730
731        // FRAME:         0     1   2   3
732        // INPUT(index):  0   1,2   -   3
733        // SF(index):     -     0   1   2
734        const trace = new TraceBuilder<PropertyTreeNode>()
735          .setType(TraceType.INPUT_EVENT_MERGED)
736          .setEntries([
737            await parser.getEntry(0),
738            await parser.getEntry(1),
739            await parser.getEntry(2),
740            await parser.getEntry(3),
741          ])
742          .setTimestamps([time10, time20, time25, time30])
743          .setFrame(0, 0)
744          .setFrame(1, 1)
745          .setFrame(2, 1)
746          .setFrame(3, 3)
747          .build();
748        traces.addTrace(trace);
749
750        const sfTrace = new TraceBuilder<HierarchyTreeNode>()
751          .setType(TraceType.SURFACE_FLINGER)
752          .setEntries([sfEntry0, sfEntry1, sfEntry2])
753          .setTimestamps([time0, time19, time35])
754          .setFrame(0, 1)
755          .setFrame(1, 2)
756          .setFrame(2, 3)
757          .setParserCustomQueryResult(
758            CustomQueryType.SF_LAYERS_ID_AND_NAME,
759            layerIdToName,
760          )
761          .build();
762        traces.addTrace(sfTrace);
763        return traces;
764      }
765    });
766  }
767
768  private wrappedName(name: string): string {
769    return `\u{200C}${name}\u{200C}`;
770  }
771}
772
773describe('PresenterInput', async () => {
774  new PresenterInputTest().execute();
775});
776