• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/store/in_memory_storage';
20import {TimestampConverterUtils} from 'common/time/test_utils';
21import {
22  DarkModeToggled,
23  FilterPresetApplyRequest,
24  FilterPresetSaveRequest,
25  TracePositionUpdate,
26} from 'messaging/winscope_event';
27import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
28import {MockPresenter} from 'test/unit/mock_hierarchy_viewer_presenter';
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 event 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    spy = spyOn(presenter, 'onArrowPress');
263    element.dispatchEvent(
264      new CustomEvent(ViewerEvents.ArrowDownPress, {detail: storage}),
265    );
266    expect(spy).toHaveBeenCalledWith(storage, false);
267
268    element.dispatchEvent(
269      new CustomEvent(ViewerEvents.ArrowUpPress, {detail: storage}),
270    );
271    expect(spy).toHaveBeenCalledWith(storage, true);
272  });
273
274  it('is robust to empty trace', async () => {
275    const callback = (newData: UiDataHierarchy) => {
276      uiData = newData;
277    };
278    const trace = UnitTestUtils.makeEmptyTrace(TraceType.WINDOW_MANAGER);
279    const traces = new Traces();
280    traces.addTrace(trace);
281    const presenter = new MockPresenter(
282      trace,
283      traces,
284      new InMemoryStorage(),
285      callback,
286      undefined,
287    );
288    presenter.initializeRectsPresenter();
289
290    const positionUpdateWithoutTraceEntry = TracePositionUpdate.fromTimestamp(
291      TimestampConverterUtils.makeRealTimestamp(0n),
292    );
293    await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
294
295    expect(Object.keys(uiData.hierarchyUserOptions).length).toBeGreaterThan(0);
296    expect(Object.keys(uiData.propertiesUserOptions).length).toBeGreaterThan(0);
297    expect(uiData.hierarchyTrees).toBeUndefined();
298    expect(
299      Object.keys(assertDefined(uiData?.rectsUserOptions)).length,
300    ).toBeGreaterThan(0);
301  });
302
303  it('handles filter preset requests', async () => {
304    initializeRectsPresenter();
305    await presenter.onAppEvent(positionUpdate);
306    const saveEvent = new FilterPresetSaveRequest(
307      'TestPreset',
308      TraceType.TEST_TRACE_STRING,
309    );
310    expect(storage.get(saveEvent.name)).toBeUndefined();
311    await presenter.onAppEvent(saveEvent);
312    expect(storage.get(saveEvent.name)).toBeDefined();
313
314    await presenter.onHierarchyFilterChange(new TextFilter('Test Filter'));
315    await presenter.onHierarchyUserOptionsChange({});
316    await presenter.onPropertiesUserOptionsChange({});
317    await presenter.onPropertiesFilterChange(new TextFilter('Test Filter'));
318    presenter.onRectsUserOptionsChange({});
319    await presenter.onRectShowStateChange(
320      assertDefined(uiData.rectsToDraw)[0].id,
321      RectShowState.HIDE,
322    );
323    const currentUiData = uiData;
324
325    const applyEvent = new FilterPresetApplyRequest(
326      saveEvent.name,
327      TraceType.TEST_TRACE_STRING,
328    );
329    await presenter.onAppEvent(applyEvent);
330    expect(uiData).not.toEqual(currentUiData);
331  });
332
333  it('updates dark mode', async () => {
334    expect(uiData.isDarkMode).toBeFalse();
335    await presenter.onAppEvent(new DarkModeToggled(true));
336    expect(uiData.isDarkMode).toBeTrue();
337  });
338
339  it('disables show diff if no prev entry available', async () => {
340    const userOptions: UserOptions = {
341      showDiff: {name: '', enabled: false, isUnavailable: false},
342    };
343    await presenter.onHierarchyUserOptionsChange(userOptions);
344    await presenter.onPropertiesUserOptionsChange(userOptions);
345    await presenter.onAppEvent(positionUpdate);
346    expect(uiData.hierarchyUserOptions['showDiff'].isUnavailable).toBeTrue();
347    expect(uiData.propertiesUserOptions['showDiff'].isUnavailable).toBeTrue();
348  });
349
350  it('shows correct hierarchy tree name for entry', async () => {
351    const spy = spyOn(
352      assertDefined(positionUpdate.position.entry?.getFullTrace()),
353      'isDumpWithoutTimestamp',
354    );
355    spy.and.returnValue(false);
356    await presenter.onAppEvent(positionUpdate);
357    const entryNode = assertDefined(uiData.hierarchyTrees?.at(0));
358    expect(entryNode.getDisplayName()).toContain(
359      positionUpdate.position.timestamp.format(),
360    );
361
362    pinNode(entryNode);
363    spy.and.returnValue(true);
364    await presenter.onAppEvent(positionUpdate);
365    const newEntryNode = assertDefined(uiData.hierarchyTrees?.at(0));
366    expect(newEntryNode.getDisplayName()).toContain('Dump');
367    expect(uiData.pinnedItems).toEqual([newEntryNode]);
368  });
369
370  it('handles pinned item change', () => {
371    expect(uiData.pinnedItems).toEqual([]);
372    const item = TreeNodeUtils.makeUiHierarchyNode({id: '', name: ''});
373    presenter.onPinnedItemChange(item);
374    expect(uiData.pinnedItems).toEqual([item]);
375    presenter.onPinnedItemChange(item);
376    expect(uiData.pinnedItems).toEqual([]);
377  });
378
379  it('updates and applies hierarchy user options', async () => {
380    await presenter.onAppEvent(positionUpdate);
381    const userOptions: UserOptions = {flat: {name: '', enabled: true}};
382    await presenter.onHierarchyUserOptionsChange(userOptions);
383    expect(uiData.hierarchyUserOptions).toEqual(userOptions);
384    expect(uiData.hierarchyTrees?.at(0)?.getAllChildren().length).toEqual(3);
385  });
386
387  it('updates highlighted property', () => {
388    const id = '4';
389    presenter.onHighlightedPropertyChange(id);
390    expect(uiData.highlightedProperty).toEqual(id);
391    presenter.onHighlightedPropertyChange(id);
392    expect(uiData.highlightedProperty).toEqual('');
393  });
394
395  it('sets properties tree and associated ui data from tree node', async () => {
396    await presenter.onAppEvent(positionUpdate);
397    await presenter.onHighlightedNodeChange(selectedTree);
398    const propertiesTree = assertDefined(uiData.propertiesTree);
399    expect(propertiesTree.id).toContain(selectedTree.id);
400    expect(propertiesTree.getAllChildren().length).toEqual(2);
401  });
402
403  it('updates and applies properties user options, calculating diffs from prev hierarchy tree', async () => {
404    await presenter.onAppEvent(positionUpdate);
405    await presenter.onHighlightedIdChange(selectedTree.id);
406    await presenter.onAppEvent(secondPositionUpdate);
407    expect(
408      uiData.propertiesTree?.getChildByName('testProp')?.getDiff(),
409    ).toEqual(DiffType.NONE);
410
411    const userOptions: UserOptions = {showDiff: {name: '', enabled: true}};
412    await presenter.onPropertiesUserOptionsChange(userOptions);
413    expect(uiData.propertiesUserOptions).toEqual(userOptions);
414    expect(
415      uiData.propertiesTree?.getChildByName('testProp')?.getDiff(),
416    ).toEqual(DiffType.MODIFIED);
417  });
418
419  it('is robust to attempts to change rect user data if no rects presenter', async () => {
420    expect(() => presenter.onRectsUserOptionsChange({})).not.toThrowError();
421    await expectAsync(
422      presenter.onRectShowStateChange('', RectShowState.SHOW),
423    ).not.toBeRejected();
424  });
425
426  it('creates input data for rects view', async () => {
427    initializeRectsPresenter();
428    await presenter.onAppEvent(positionUpdate);
429    const rectsToDraw = assertDefined(uiData.rectsToDraw);
430    const expectedFirstRect = presenter.uiRects[0];
431    expect(rectsToDraw[0].x).toEqual(expectedFirstRect.x);
432    expect(rectsToDraw[0].y).toEqual(expectedFirstRect.y);
433    expect(rectsToDraw[0].w).toEqual(expectedFirstRect.w);
434    expect(rectsToDraw[0].h).toEqual(expectedFirstRect.h);
435    checkRectUiData(uiData, 3, 3, 3);
436  });
437
438  it('filters rects by visibility', async () => {
439    initializeRectsPresenter();
440    const userOptions: UserOptions = {
441      showOnlyVisible: {name: '', enabled: false},
442    };
443    await presenter.onAppEvent(positionUpdate);
444    presenter.onRectsUserOptionsChange(userOptions);
445    expect(uiData.rectsUserOptions).toEqual(userOptions);
446    checkRectUiData(uiData, 3, 3, 3);
447
448    userOptions['showOnlyVisible'].enabled = true;
449    presenter.onRectsUserOptionsChange(userOptions);
450    checkRectUiData(uiData, 2, 3, 2);
451  });
452
453  it('filters rects by show/hide state', async () => {
454    initializeRectsPresenter();
455    const userOptions: UserOptions = {
456      ignoreRectShowState: {
457        name: 'Ignore',
458        icon: 'visibility',
459        enabled: true,
460      },
461    };
462    await presenter.onAppEvent(positionUpdate);
463    presenter.onRectsUserOptionsChange(userOptions);
464    checkRectUiData(uiData, 3, 3, 3);
465
466    await presenter.onRectShowStateChange(
467      assertDefined(uiData.rectsToDraw)[0].id,
468      RectShowState.HIDE,
469    );
470    checkRectUiData(uiData, 3, 3, 2);
471
472    userOptions['ignoreRectShowState'].enabled = false;
473    presenter.onRectsUserOptionsChange(userOptions);
474    checkRectUiData(uiData, 2, 3, 2);
475  });
476
477  it('handles both visibility and show/hide state in rects', async () => {
478    initializeRectsPresenter();
479    const userOptions: UserOptions = {
480      ignoreRectShowState: {name: '', enabled: true},
481      showOnlyVisible: {name: '', enabled: false},
482    };
483    presenter.onRectsUserOptionsChange(userOptions);
484    await presenter.onAppEvent(positionUpdate);
485    checkRectUiData(uiData, 3, 3, 3);
486
487    await presenter.onRectShowStateChange(
488      assertDefined(uiData.rectsToDraw)[0].id,
489      RectShowState.HIDE,
490    );
491    checkRectUiData(uiData, 3, 3, 2);
492
493    userOptions['ignoreRectShowState'].enabled = false;
494    presenter.onRectsUserOptionsChange(userOptions);
495    checkRectUiData(uiData, 2, 3, 2);
496
497    userOptions['showOnlyVisible'].enabled = true;
498    presenter.onRectsUserOptionsChange(userOptions);
499    checkRectUiData(uiData, 1, 3, 1);
500
501    userOptions['ignoreRectShowState'].enabled = true;
502    presenter.onRectsUserOptionsChange(userOptions);
503    checkRectUiData(uiData, 2, 3, 1);
504  });
505
506  it('handles arrow up/down press', async () => {
507    await presenter.onAppEvent(positionUpdate);
508    await presenter.onArrowPress(storage, false);
509    expect(uiData.propertiesTree?.id).toContain('Test Trace entry');
510    await presenter.onArrowPress(storage, false);
511    expect(uiData.propertiesTree?.id).toContain('1 p1');
512    await presenter.onArrowPress(storage, true);
513    expect(uiData.propertiesTree?.id).toContain('Test Trace entry');
514  });
515
516  function pinNode(node: UiHierarchyTreeNode) {
517    presenter.onPinnedItemChange(node);
518    expect(uiData.pinnedItems).toEqual([node]);
519  }
520
521  function initializeRectsPresenter(p = presenter) {
522    p.initializeRectsPresenter();
523    p.uiRects = [
524      new UiRectBuilder()
525        .setX(0)
526        .setY(0)
527        .setWidth(1)
528        .setHeight(1)
529        .setLabel('test rect')
530        .setTransform(IDENTITY_MATRIX)
531        .setIsVisible(true)
532        .setIsDisplay(false)
533        .setIsActiveDisplay(true)
534        .setId('1 p1')
535        .setGroupId(0)
536        .setIsClickable(true)
537        .setCornerRadius(0)
538        .setDepth(0)
539        .build(),
540      new UiRectBuilder()
541        .setX(0)
542        .setY(0)
543        .setWidth(1)
544        .setHeight(1)
545        .setLabel('test rect 2')
546        .setTransform(IDENTITY_MATRIX)
547        .setIsVisible(true)
548        .setIsDisplay(false)
549        .setIsActiveDisplay(true)
550        .setId('3 c3')
551        .setGroupId(0)
552        .setIsClickable(true)
553        .setCornerRadius(0)
554        .setDepth(1)
555        .build(),
556      new UiRectBuilder()
557        .setX(0)
558        .setY(0)
559        .setWidth(1)
560        .setHeight(1)
561        .setLabel('test rect 3')
562        .setTransform(IDENTITY_MATRIX)
563        .setIsVisible(false)
564        .setIsDisplay(false)
565        .setIsActiveDisplay(true)
566        .setId('2 p2')
567        .setGroupId(0)
568        .setIsClickable(true)
569        .setCornerRadius(0)
570        .setDepth(2)
571        .build(),
572    ];
573    p.displays = [{displayId: 0, groupId: 0, name: 'Display', isActive: true}];
574  }
575
576  function checkRectUiData(
577    uiData: UiDataHierarchy,
578    rectsToDraw: number,
579    allRects: number,
580    shownRects: number,
581  ) {
582    expect(assertDefined(uiData.rectsToDraw).length).toEqual(rectsToDraw);
583    const showStates = Array.from(
584      assertDefined(uiData.rectIdToShowState).values(),
585    );
586    expect(showStates.length).toEqual(allRects);
587    expect(showStates.filter((s) => s === RectShowState.SHOW).length).toEqual(
588      shownRects,
589    );
590  }
591});
592