xref: /aosp_15_r20/development/tools/winscope/src/viewers/common/hierarchy_viewer_presenter_test.ts (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
1/*
2 * Copyright (C) 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 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 {assertDefined} from 'common/assert_utils';
18import {IDENTITY_MATRIX} from 'common/geometry/transform_matrix';
19import {InMemoryStorage} from 'common/in_memory_storage';
20import {
21  DarkModeToggled,
22  FilterPresetApplyRequest,
23  FilterPresetSaveRequest,
24  TracePositionUpdate,
25} from 'messaging/winscope_event';
26import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
27import {MockPresenter} from 'test/unit/mock_hierarchy_viewer_presenter';
28import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
29import {TraceBuilder} from 'test/unit/trace_builder';
30import {TreeNodeUtils} from 'test/unit/tree_node_utils';
31import {UnitTestUtils} from 'test/unit/utils';
32import {Trace} from 'trace/trace';
33import {Traces} from 'trace/traces';
34import {TraceType} from 'trace/trace_type';
35import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
36import {TextFilter} from 'viewers/common/text_filter';
37import {UiRectBuilder} from 'viewers/components/rects/ui_rect_builder';
38import {DiffType} from './diff_type';
39import {RectShowState} from './rect_show_state';
40import {UiDataHierarchy} from './ui_data_hierarchy';
41import {UiHierarchyTreeNode} from './ui_hierarchy_tree_node';
42import {UserOptions} from './user_options';
43import {ViewerEvents} from './viewer_events';
44
45describe('AbstractHierarchyViewerPresenter', () => {
46  const timestamp1 = TimestampConverterUtils.makeElapsedTimestamp(1n);
47  const timestamp2 = TimestampConverterUtils.makeElapsedTimestamp(2n);
48  let uiData: UiDataHierarchy;
49  let presenter: MockPresenter;
50  let trace: Trace<HierarchyTreeNode>;
51  let traces: Traces;
52  let positionUpdate: TracePositionUpdate;
53  let secondPositionUpdate: TracePositionUpdate;
54  let selectedTree: UiHierarchyTreeNode;
55  let storage: InMemoryStorage;
56
57  beforeAll(async () => {
58    jasmine.addCustomEqualityTester(TreeNodeUtils.treeNodeEqualityTester);
59    trace = new TraceBuilder<HierarchyTreeNode>()
60      .setType(TraceType.SURFACE_FLINGER)
61      .setEntries([
62        new HierarchyTreeBuilder()
63          .setId('Test Trace')
64          .setName('entry')
65          .setChildren([
66            {
67              id: '1',
68              name: 'p1',
69              properties: {isComputedVisible: true, testProp: true},
70              children: [
71                {id: '3', name: 'c3', properties: {isComputedVisible: true}},
72              ],
73            },
74            {id: '2', name: 'p2', properties: {isComputedVisible: false}},
75          ])
76          .build(),
77        new HierarchyTreeBuilder()
78          .setId('Test Trace')
79          .setName('entry')
80          .setChildren([
81            {
82              id: '1',
83              name: 'p1',
84              properties: {isComputedVisible: true, testProp: false},
85            },
86            {id: '2', name: 'p2'},
87          ])
88          .build(),
89      ])
90      .setTimestamps([timestamp1, timestamp2])
91      .build();
92    selectedTree = UiHierarchyTreeNode.from(
93      assertDefined((await trace.getEntry(0).getValue()).getChildByName('p1')),
94    );
95    positionUpdate = TracePositionUpdate.fromTraceEntry(trace.getEntry(0));
96    secondPositionUpdate = TracePositionUpdate.fromTraceEntry(
97      trace.getEntry(1),
98    );
99    traces = new Traces();
100    traces.addTrace(trace);
101  });
102
103  beforeEach(() => {
104    storage = new InMemoryStorage();
105    presenter = new MockPresenter(
106      trace,
107      traces,
108      storage,
109      (newData) => {
110        uiData = newData;
111      },
112      undefined,
113    );
114  });
115
116  it('clears ui data before throwing error on corrupted trace', async () => {
117    const notifyViewCallback = (newData: UiDataHierarchy) => {
118      uiData = newData;
119    };
120    const trace = new TraceBuilder<HierarchyTreeNode>()
121      .setType(TraceType.SURFACE_FLINGER)
122      .setEntries([selectedTree])
123      .setTimestamps([timestamp1])
124      .setIsCorrupted(true)
125      .build();
126    const traces = new Traces();
127    traces.addTrace(trace);
128    const presenter = new MockPresenter(
129      trace,
130      traces,
131      new InMemoryStorage(),
132      notifyViewCallback,
133      undefined,
134    );
135    initializeRectsPresenter(presenter);
136
137    try {
138      await presenter.onAppEvent(
139        TracePositionUpdate.fromTraceEntry(trace.getEntry(0)),
140      );
141      fail('error should be thrown for corrupted trace');
142    } catch (e) {
143      expect(Object.keys(uiData.hierarchyUserOptions).length).toBeGreaterThan(
144        0,
145      );
146      expect(Object.keys(uiData.propertiesUserOptions).length).toBeGreaterThan(
147        0,
148      );
149      expect(uiData.hierarchyTrees).toBeUndefined();
150      expect(uiData.propertiesTree).toBeUndefined();
151      expect(uiData.highlightedItem).toEqual('');
152      expect(uiData.highlightedProperty).toEqual('');
153      expect(uiData.pinnedItems.length).toEqual(0);
154      expect(
155        Object.keys(assertDefined(uiData?.rectsUserOptions)).length,
156      ).toBeGreaterThan(0);
157      expect(uiData.rectsToDraw).toEqual([]);
158    }
159  });
160
161  it('processes trace position updates', async () => {
162    initializeRectsPresenter();
163    pinNode(selectedTree);
164    await presenter.onAppEvent(positionUpdate);
165
166    expect(uiData.highlightedItem?.length).toEqual(0);
167    expect(Object.keys(uiData.hierarchyUserOptions).length).toBeGreaterThan(0);
168    expect(Object.keys(uiData.propertiesUserOptions).length).toBeGreaterThan(0);
169    assertDefined(uiData.hierarchyTrees).forEach((tree) => {
170      expect(tree.getAllChildren().length > 0).toBeTrue();
171    });
172    expect(uiData.pinnedItems.length).toBeGreaterThan(0);
173    expect(
174      Object.keys(assertDefined(uiData.rectsUserOptions)).length,
175    ).toBeGreaterThan(0);
176    expect(uiData.rectsToDraw?.length).toBeGreaterThan(0);
177    expect(uiData.displays?.length).toBeGreaterThan(0);
178  });
179
180  it('adds events listeners', () => {
181    const element = document.createElement('div');
182    presenter.addEventListeners(element);
183
184    let spy: jasmine.Spy = spyOn(presenter, 'onPinnedItemChange');
185    const node = TreeNodeUtils.makeUiHierarchyNode({name: 'test'});
186    element.dispatchEvent(
187      new CustomEvent(ViewerEvents.HierarchyPinnedChange, {
188        detail: {pinnedItem: node},
189      }),
190    );
191    expect(spy).toHaveBeenCalledWith(node);
192
193    spy = spyOn(presenter, 'onHighlightedIdChange');
194    element.dispatchEvent(
195      new CustomEvent(ViewerEvents.HighlightedIdChange, {
196        detail: {id: 'test'},
197      }),
198    );
199    expect(spy).toHaveBeenCalledWith('test');
200
201    spy = spyOn(presenter, 'onHighlightedPropertyChange');
202    element.dispatchEvent(
203      new CustomEvent(ViewerEvents.HighlightedPropertyChange, {
204        detail: {id: 'test'},
205      }),
206    );
207    expect(spy).toHaveBeenCalledWith('test');
208
209    spy = spyOn(presenter, 'onHierarchyUserOptionsChange');
210    element.dispatchEvent(
211      new CustomEvent(ViewerEvents.HierarchyUserOptionsChange, {
212        detail: {userOptions: {}},
213      }),
214    );
215    expect(spy).toHaveBeenCalledWith({});
216
217    spy = spyOn(presenter, 'onHierarchyFilterChange');
218    const filter = new TextFilter();
219    element.dispatchEvent(
220      new CustomEvent(ViewerEvents.HierarchyFilterChange, {detail: filter}),
221    );
222    expect(spy).toHaveBeenCalledWith(filter);
223
224    spy = spyOn(presenter, 'onPropertiesUserOptionsChange');
225    element.dispatchEvent(
226      new CustomEvent(ViewerEvents.PropertiesUserOptionsChange, {
227        detail: {userOptions: {}},
228      }),
229    );
230    expect(spy).toHaveBeenCalledWith({});
231
232    spy = spyOn(presenter, 'onPropertiesFilterChange');
233    element.dispatchEvent(
234      new CustomEvent(ViewerEvents.PropertiesFilterChange, {
235        detail: filter,
236      }),
237    );
238    expect(spy).toHaveBeenCalledWith(filter);
239
240    spy = spyOn(presenter, 'onHighlightedNodeChange');
241    element.dispatchEvent(
242      new CustomEvent(ViewerEvents.HighlightedNodeChange, {detail: {node}}),
243    );
244    expect(spy).toHaveBeenCalledWith(node);
245
246    spy = spyOn(presenter, 'onRectShowStateChange');
247    element.dispatchEvent(
248      new CustomEvent(ViewerEvents.RectShowStateChange, {
249        detail: {rectId: 'test', state: RectShowState.HIDE},
250      }),
251    );
252    expect(spy).toHaveBeenCalledWith('test', RectShowState.HIDE);
253
254    spy = spyOn(presenter, 'onRectsUserOptionsChange');
255    element.dispatchEvent(
256      new CustomEvent(ViewerEvents.RectsUserOptionsChange, {
257        detail: {userOptions: {}},
258      }),
259    );
260    expect(spy).toHaveBeenCalledWith({});
261  });
262
263  it('is robust to empty trace', async () => {
264    const callback = (newData: UiDataHierarchy) => {
265      uiData = newData;
266    };
267    const trace = UnitTestUtils.makeEmptyTrace(TraceType.WINDOW_MANAGER);
268    const traces = new Traces();
269    traces.addTrace(trace);
270    const presenter = new MockPresenter(
271      trace,
272      traces,
273      new InMemoryStorage(),
274      callback,
275      undefined,
276    );
277    presenter.initializeRectsPresenter();
278
279    const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp(
280      TimestampConverterUtils.makeRealTimestamp(0n),
281    );
282    await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
283
284    expect(Object.keys(uiData.hierarchyUserOptions).length).toBeGreaterThan(0);
285    expect(Object.keys(uiData.propertiesUserOptions).length).toBeGreaterThan(0);
286    expect(uiData.hierarchyTrees).toBeUndefined();
287    expect(
288      Object.keys(assertDefined(uiData?.rectsUserOptions)).length,
289    ).toBeGreaterThan(0);
290  });
291
292  it('handles filter preset requests', async () => {
293    initializeRectsPresenter();
294    await presenter.onAppEvent(positionUpdate);
295    const saveEvent = new FilterPresetSaveRequest(
296      'TestPreset',
297      TraceType.TEST_TRACE_STRING,
298    );
299    expect(storage.get(saveEvent.name)).toBeUndefined();
300    await presenter.onAppEvent(saveEvent);
301    expect(storage.get(saveEvent.name)).toBeDefined();
302
303    await presenter.onHierarchyFilterChange(new TextFilter('Test Filter'));
304    await presenter.onHierarchyUserOptionsChange({});
305    await presenter.onPropertiesUserOptionsChange({});
306    await presenter.onPropertiesFilterChange(new TextFilter('Test Filter'));
307    presenter.onRectsUserOptionsChange({});
308    await presenter.onRectShowStateChange(
309      assertDefined(uiData.rectsToDraw)[0].id,
310      RectShowState.HIDE,
311    );
312    const currentUiData = uiData;
313
314    const applyEvent = new FilterPresetApplyRequest(
315      saveEvent.name,
316      TraceType.TEST_TRACE_STRING,
317    );
318    await presenter.onAppEvent(applyEvent);
319    expect(uiData).not.toEqual(currentUiData);
320  });
321
322  it('updates dark mode', async () => {
323    expect(uiData.isDarkMode).toBeFalse();
324    await presenter.onAppEvent(new DarkModeToggled(true));
325    expect(uiData.isDarkMode).toBeTrue();
326  });
327
328  it('disables show diff if no prev entry available', async () => {
329    const userOptions: UserOptions = {
330      showDiff: {name: '', enabled: false, isUnavailable: false},
331    };
332    await presenter.onHierarchyUserOptionsChange(userOptions);
333    await presenter.onPropertiesUserOptionsChange(userOptions);
334    await presenter.onAppEvent(positionUpdate);
335    expect(uiData.hierarchyUserOptions['showDiff'].isUnavailable).toBeTrue();
336    expect(uiData.propertiesUserOptions['showDiff'].isUnavailable).toBeTrue();
337  });
338
339  it('shows correct hierarchy tree name for entry', async () => {
340    const spy = spyOn(
341      assertDefined(positionUpdate.position.entry?.getFullTrace()),
342      'isDumpWithoutTimestamp',
343    );
344    spy.and.returnValue(false);
345    await presenter.onAppEvent(positionUpdate);
346    const entryNode = assertDefined(uiData.hierarchyTrees?.at(0));
347    expect(entryNode.getDisplayName()).toContain(
348      positionUpdate.position.timestamp.format(),
349    );
350
351    pinNode(entryNode);
352    spy.and.returnValue(true);
353    await presenter.onAppEvent(positionUpdate);
354    const newEntryNode = assertDefined(uiData.hierarchyTrees?.at(0));
355    expect(newEntryNode.getDisplayName()).toContain('Dump');
356    expect(uiData.pinnedItems).toEqual([newEntryNode]);
357  });
358
359  it('handles pinned item change', () => {
360    expect(uiData.pinnedItems).toEqual([]);
361    const item = TreeNodeUtils.makeUiHierarchyNode({id: '', name: ''});
362    presenter.onPinnedItemChange(item);
363    expect(uiData.pinnedItems).toEqual([item]);
364    presenter.onPinnedItemChange(item);
365    expect(uiData.pinnedItems).toEqual([]);
366  });
367
368  it('updates and applies hierarchy user options', async () => {
369    await presenter.onAppEvent(positionUpdate);
370    const userOptions: UserOptions = {flat: {name: '', enabled: true}};
371    await presenter.onHierarchyUserOptionsChange(userOptions);
372    expect(uiData.hierarchyUserOptions).toEqual(userOptions);
373    expect(uiData.hierarchyTrees?.at(0)?.getAllChildren().length).toEqual(3);
374  });
375
376  it('updates highlighted property', () => {
377    const id = '4';
378    presenter.onHighlightedPropertyChange(id);
379    expect(uiData.highlightedProperty).toEqual(id);
380    presenter.onHighlightedPropertyChange(id);
381    expect(uiData.highlightedProperty).toEqual('');
382  });
383
384  it('sets properties tree and associated ui data from tree node', async () => {
385    await presenter.onAppEvent(positionUpdate);
386    await presenter.onHighlightedNodeChange(selectedTree);
387    const propertiesTree = assertDefined(uiData.propertiesTree);
388    expect(propertiesTree.id).toContain(selectedTree.id);
389    expect(propertiesTree.getAllChildren().length).toEqual(2);
390  });
391
392  it('updates and applies properties user options, calculating diffs from prev hierarchy tree', async () => {
393    await presenter.onAppEvent(positionUpdate);
394    await presenter.onHighlightedIdChange(selectedTree.id);
395    await presenter.onAppEvent(secondPositionUpdate);
396    expect(
397      uiData.propertiesTree?.getChildByName('testProp')?.getDiff(),
398    ).toEqual(DiffType.NONE);
399
400    const userOptions: UserOptions = {showDiff: {name: '', enabled: true}};
401    await presenter.onPropertiesUserOptionsChange(userOptions);
402    expect(uiData.propertiesUserOptions).toEqual(userOptions);
403    expect(
404      uiData.propertiesTree?.getChildByName('testProp')?.getDiff(),
405    ).toEqual(DiffType.MODIFIED);
406  });
407
408  it('is robust to attempts to change rect user data if no rects presenter', async () => {
409    expect(() => presenter.onRectsUserOptionsChange({})).not.toThrowError();
410    await expectAsync(
411      presenter.onRectShowStateChange('', RectShowState.SHOW),
412    ).not.toBeRejected();
413  });
414
415  it('creates input data for rects view', async () => {
416    initializeRectsPresenter();
417    await presenter.onAppEvent(positionUpdate);
418    const rectsToDraw = assertDefined(uiData.rectsToDraw);
419    const expectedFirstRect = presenter.uiRects[0];
420    expect(rectsToDraw[0].x).toEqual(expectedFirstRect.x);
421    expect(rectsToDraw[0].y).toEqual(expectedFirstRect.y);
422    expect(rectsToDraw[0].w).toEqual(expectedFirstRect.w);
423    expect(rectsToDraw[0].h).toEqual(expectedFirstRect.h);
424    checkRectUiData(uiData, 3, 3, 3);
425  });
426
427  it('filters rects by visibility', async () => {
428    initializeRectsPresenter();
429    const userOptions: UserOptions = {
430      showOnlyVisible: {name: '', enabled: false},
431    };
432    await presenter.onAppEvent(positionUpdate);
433    presenter.onRectsUserOptionsChange(userOptions);
434    expect(uiData.rectsUserOptions).toEqual(userOptions);
435    checkRectUiData(uiData, 3, 3, 3);
436
437    userOptions['showOnlyVisible'].enabled = true;
438    presenter.onRectsUserOptionsChange(userOptions);
439    checkRectUiData(uiData, 2, 3, 2);
440  });
441
442  it('filters rects by show/hide state', async () => {
443    initializeRectsPresenter();
444    const userOptions: UserOptions = {
445      ignoreRectShowState: {
446        name: 'Ignore',
447        icon: 'visibility',
448        enabled: true,
449      },
450    };
451    await presenter.onAppEvent(positionUpdate);
452    presenter.onRectsUserOptionsChange(userOptions);
453    checkRectUiData(uiData, 3, 3, 3);
454
455    await presenter.onRectShowStateChange(
456      assertDefined(uiData.rectsToDraw)[0].id,
457      RectShowState.HIDE,
458    );
459    checkRectUiData(uiData, 3, 3, 2);
460
461    userOptions['ignoreRectShowState'].enabled = false;
462    presenter.onRectsUserOptionsChange(userOptions);
463    checkRectUiData(uiData, 2, 3, 2);
464  });
465
466  it('handles both visibility and show/hide state in rects', async () => {
467    initializeRectsPresenter();
468    const userOptions: UserOptions = {
469      ignoreRectShowState: {name: '', enabled: true},
470      showOnlyVisible: {name: '', enabled: false},
471    };
472    presenter.onRectsUserOptionsChange(userOptions);
473    await presenter.onAppEvent(positionUpdate);
474    checkRectUiData(uiData, 3, 3, 3);
475
476    await presenter.onRectShowStateChange(
477      assertDefined(uiData.rectsToDraw)[0].id,
478      RectShowState.HIDE,
479    );
480    checkRectUiData(uiData, 3, 3, 2);
481
482    userOptions['ignoreRectShowState'].enabled = false;
483    presenter.onRectsUserOptionsChange(userOptions);
484    checkRectUiData(uiData, 2, 3, 2);
485
486    userOptions['showOnlyVisible'].enabled = true;
487    presenter.onRectsUserOptionsChange(userOptions);
488    checkRectUiData(uiData, 1, 3, 1);
489
490    userOptions['ignoreRectShowState'].enabled = true;
491    presenter.onRectsUserOptionsChange(userOptions);
492    checkRectUiData(uiData, 2, 3, 1);
493  });
494
495  function pinNode(node: UiHierarchyTreeNode) {
496    presenter.onPinnedItemChange(node);
497    expect(uiData.pinnedItems).toEqual([node]);
498  }
499
500  function initializeRectsPresenter(p = presenter) {
501    p.initializeRectsPresenter();
502    p.uiRects = [
503      new UiRectBuilder()
504        .setX(0)
505        .setY(0)
506        .setWidth(1)
507        .setHeight(1)
508        .setLabel('test rect')
509        .setTransform(IDENTITY_MATRIX)
510        .setIsVisible(true)
511        .setIsDisplay(false)
512        .setIsActiveDisplay(true)
513        .setId('1 p1')
514        .setGroupId(0)
515        .setIsClickable(true)
516        .setCornerRadius(0)
517        .setDepth(0)
518        .build(),
519      new UiRectBuilder()
520        .setX(0)
521        .setY(0)
522        .setWidth(1)
523        .setHeight(1)
524        .setLabel('test rect 2')
525        .setTransform(IDENTITY_MATRIX)
526        .setIsVisible(true)
527        .setIsDisplay(false)
528        .setIsActiveDisplay(true)
529        .setId('3 c3')
530        .setGroupId(0)
531        .setIsClickable(true)
532        .setCornerRadius(0)
533        .setDepth(1)
534        .build(),
535      new UiRectBuilder()
536        .setX(0)
537        .setY(0)
538        .setWidth(1)
539        .setHeight(1)
540        .setLabel('test rect 3')
541        .setTransform(IDENTITY_MATRIX)
542        .setIsVisible(false)
543        .setIsDisplay(false)
544        .setIsActiveDisplay(true)
545        .setId('2 p2')
546        .setGroupId(0)
547        .setIsClickable(true)
548        .setCornerRadius(0)
549        .setDepth(2)
550        .build(),
551    ];
552    p.displays = [{displayId: 0, groupId: 0, name: 'Display', isActive: true}];
553  }
554
555  function checkRectUiData(
556    uiData: UiDataHierarchy,
557    rectsToDraw: number,
558    allRects: number,
559    shownRects: number,
560  ) {
561    expect(assertDefined(uiData.rectsToDraw).length).toEqual(rectsToDraw);
562    const showStates = Array.from(
563      assertDefined(uiData.rectIdToShowState).values(),
564    );
565    expect(showStates.length).toEqual(allRects);
566    expect(showStates.filter((s) => s === RectShowState.SHOW).length).toEqual(
567      shownRects,
568    );
569  }
570});
571