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 {Rect} from 'common/rect';
19import {TracePositionUpdate} from 'messaging/winscope_event';
20import {TimestampConverterUtils} from 'test/unit/timestamp_converter_utils';
21import {TreeNodeUtils} from 'test/unit/tree_node_utils';
22import {
23  AbstractHierarchyViewerPresenter,
24  NotifyHierarchyViewCallbackType,
25} from 'viewers/common/abstract_hierarchy_viewer_presenter';
26import {VISIBLE_CHIP} from 'viewers/common/chip';
27import {DiffType} from 'viewers/common/diff_type';
28import {RectShowState} from 'viewers/common/rect_show_state';
29import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
30import {UiTreeUtils} from 'viewers/common/ui_tree_utils';
31import {UserOptions} from 'viewers/common/user_options';
32import {UiDataHierarchy} from './ui_data_hierarchy';
33
34export abstract class AbstractHierarchyViewerPresenterTest {
35  execute() {
36    describe('AbstractHierarchyViewerPresenter', () => {
37      let uiData: UiDataHierarchy;
38      let presenter: AbstractHierarchyViewerPresenter;
39      beforeAll(async () => {
40        await this.setUpTestEnvironment();
41      });
42
43      beforeEach(() => {
44        presenter = this.createPresenter((newData) => {
45          uiData = newData;
46        });
47      });
48
49      it('is robust to empty trace', async () => {
50        const notifyViewCallback = (newData: UiDataHierarchy) => {
51          uiData = newData;
52        };
53        const presenter =
54          this.createPresenterWithEmptyTrace(notifyViewCallback);
55
56        const positionUpdateWithoutTraceEntry =
57          TracePositionUpdate.fromTimestamp(
58            TimestampConverterUtils.makeRealTimestamp(0n),
59          );
60        await presenter.onAppEvent(positionUpdateWithoutTraceEntry);
61
62        expect(Object.keys(uiData.hierarchyUserOptions).length).toBeGreaterThan(
63          0,
64        );
65        expect(
66          Object.keys(uiData.propertiesUserOptions).length,
67        ).toBeGreaterThan(0);
68        expect(uiData.hierarchyTrees).toBeFalsy();
69        if (this.shouldExecuteRectTests) {
70          expect(
71            Object.keys(assertDefined(uiData?.rectsUserOptions)).length,
72          ).toBeGreaterThan(0);
73        }
74      });
75
76      it('processes trace position updates', async () => {
77        await assertDefined(presenter).onAppEvent(
78          assertDefined(this.getPositionUpdate()),
79        );
80
81        expect(uiData.highlightedItem?.length).toEqual(0);
82        expect(Object.keys(uiData.hierarchyUserOptions).length).toBeGreaterThan(
83          0,
84        );
85        expect(
86          Object.keys(uiData.propertiesUserOptions).length,
87        ).toBeGreaterThan(0);
88        assertDefined(uiData.hierarchyTrees).forEach((tree) => {
89          expect(tree.getAllChildren().length > 0).toBeTrue();
90        });
91        if (this.shouldExecuteRectTests) {
92          expect(
93            Object.keys(assertDefined(uiData.rectsUserOptions)).length,
94          ).toBeGreaterThan(0);
95          expect(uiData.rectsToDraw?.length).toBeGreaterThan(0);
96          expect(uiData.displays?.length).toBeGreaterThan(0);
97        }
98      });
99
100      if (this.shouldExecuteShowDiffTests) {
101        it('disables show diff and generates non-diff tree if no prev entry available', async () => {
102          await presenter.onAppEvent(this.getPositionUpdate());
103
104          const hierarchyOpts = assertDefined(uiData.hierarchyUserOptions);
105          expect(hierarchyOpts['showDiff'].isUnavailable).toBeTrue();
106
107          const propertyOpts = assertDefined(uiData.propertiesUserOptions);
108          expect(propertyOpts['showDiff'].isUnavailable).toBeTrue();
109
110          assertDefined(uiData.hierarchyTrees).forEach((tree) => {
111            expect(tree.getAllChildren().length > 0).toBeTrue();
112          });
113        });
114      }
115
116      it('updates pinned items', () => {
117        expect(uiData.pinnedItems).toEqual([]);
118
119        const pinnedItem = TreeNodeUtils.makeUiHierarchyNode({
120          id: 'TestItem 4',
121          name: 'FirstPinnedItem',
122        });
123
124        presenter.onPinnedItemChange(pinnedItem);
125        expect(uiData.pinnedItems).toContain(pinnedItem);
126      });
127
128      it('updates highlighted property', () => {
129        expect(uiData.highlightedProperty).toEqual('');
130        const id = '4';
131        presenter.onHighlightedPropertyChange(id);
132        expect(uiData.highlightedProperty).toEqual(id);
133      });
134
135      it('filters hierarchy tree by visibility', async () => {
136        const userOptions: UserOptions = {
137          showOnlyVisible: {
138            name: 'Show only',
139            chip: VISIBLE_CHIP,
140            enabled: false,
141          },
142          flat: {
143            name: 'Flat',
144            enabled: true,
145          },
146        };
147
148        await presenter.onAppEvent(this.getPositionUpdate());
149        await presenter.onHierarchyUserOptionsChange(userOptions);
150
151        expect(this.getTotalHierarchyChildren(uiData)).toEqual(
152          this.getExpectedChildrenBeforeVisibilityFilter(),
153        );
154
155        userOptions['showOnlyVisible'].enabled = true;
156        await presenter.onHierarchyUserOptionsChange(userOptions);
157        expect(this.getTotalHierarchyChildren(uiData)).toEqual(
158          this.getExpectedChildrenAfterVisibilityFilter(),
159        );
160      });
161
162      if (this.shouldExecuteFlatTreeTest) {
163        it('flattens hierarchy tree', async () => {
164          //change flat view to true
165          const userOptions: UserOptions = {
166            showDiff: {
167              name: 'Show diff',
168              enabled: false,
169            },
170            simplifyNames: {
171              name: 'Simplify names',
172              enabled: false,
173            },
174            showOnlyVisible: {
175              name: 'Show only',
176              chip: VISIBLE_CHIP,
177              enabled: false,
178            },
179            flat: {
180              name: 'Flat',
181              enabled: true,
182            },
183          };
184
185          await presenter.onAppEvent(this.getPositionUpdate());
186          expect(this.getTotalHierarchyChildren(uiData)).toEqual(
187            this.getExpectedChildrenBeforeFlatFilter!(),
188          );
189
190          await presenter.onHierarchyUserOptionsChange(userOptions);
191          expect(uiData.hierarchyUserOptions).toEqual(userOptions);
192          expect(this.getTotalHierarchyChildren(uiData)).toEqual(
193            this.getExpectedChildrenAfterFlatFilter!(),
194          );
195          assertDefined(uiData.hierarchyTrees).forEach((tree) => {
196            tree.getAllChildren().forEach((child) => {
197              expect(child.getAllChildren().length).toEqual(0);
198            });
199          });
200        });
201      }
202
203      if (this.shouldExecuteSimplifyNamesTest) {
204        it('simplifies names in hierarchy tree', async () => {
205          const longName = assertDefined(this.treeNodeLongName);
206          const shortName = assertDefined(this.treeNodeShortName);
207          //change flat view to true
208          const userOptions: UserOptions = {
209            showDiff: {
210              name: 'Show diff',
211              enabled: false,
212            },
213            simplifyNames: {
214              name: 'Simplify names',
215              enabled: false,
216            },
217            showOnlyVisible: {
218              name: 'Show only',
219              chip: VISIBLE_CHIP,
220              enabled: false,
221            },
222            flat: {
223              name: 'Flat',
224              enabled: false,
225            },
226          };
227
228          await presenter.onAppEvent(this.getPositionUpdate());
229          let nodeWithLongName = assertDefined(
230            assertDefined(uiData.hierarchyTrees)[0].findDfs(
231              UiTreeUtils.makeIdFilter(longName),
232            ),
233          );
234          expect(nodeWithLongName.getDisplayName()).toEqual(shortName);
235
236          await presenter.onHierarchyUserOptionsChange(userOptions);
237          expect(uiData.hierarchyUserOptions).toEqual(userOptions);
238          nodeWithLongName = assertDefined(
239            assertDefined(uiData.hierarchyTrees)[0].findDfs(
240              UiTreeUtils.makeIdFilter(longName),
241            ),
242          );
243          expect(longName).toContain(nodeWithLongName.getDisplayName());
244        });
245      }
246
247      it('filters hierarchy tree by search string', async () => {
248        const userOptions: UserOptions = {
249          showDiff: {
250            name: 'Show diff',
251            enabled: false,
252          },
253          simplifyNames: {
254            name: 'Simplify names',
255            enabled: true,
256          },
257          showOnlyVisible: {
258            name: 'Show only',
259            chip: VISIBLE_CHIP,
260            enabled: false,
261          },
262          flat: {
263            name: 'Flat',
264            enabled: true,
265          },
266        };
267        await presenter.onAppEvent(this.getPositionUpdate());
268        await presenter.onHierarchyUserOptionsChange(userOptions);
269        expect(this.getTotalHierarchyChildren(uiData)).toEqual(
270          this.getExpectedHierarchyChildrenBeforeStringFilter(),
271        );
272
273        await presenter.onHierarchyFilterChange(this.hierarchyFilterString);
274        expect(this.getTotalHierarchyChildren(uiData)).toEqual(
275          this.expectedHierarchyChildrenAfterStringFilter,
276        );
277      });
278
279      it('sets properties tree and associated ui data from tree node', async () => {
280        await presenter.onAppEvent(this.getPositionUpdate());
281
282        const selectedTree = this.getSelectedTree();
283        await presenter.onHighlightedNodeChange(selectedTree);
284        const propertiesTree = assertDefined(uiData.propertiesTree);
285        expect(propertiesTree.id).toContain(selectedTree.id);
286        expect(propertiesTree.getAllChildren().length).toEqual(
287          this.numberOfNonDefaultProperties,
288        );
289        if (this.executeSpecializedChecksForPropertiesFromNode) {
290          this.executeSpecializedChecksForPropertiesFromNode(uiData);
291        }
292      });
293
294      it('after highlighting a node, updates properties tree on position update', async () => {
295        await presenter.onAppEvent(this.getPositionUpdate());
296        await presenter.onHighlightedNodeChange(
297          this.getSelectedTreeAfterPositionUpdate(),
298        );
299        this.executeChecksForPropertiesTreeAfterPositionUpdate(uiData);
300
301        const secondUpdate = this.getSecondPositionUpdate();
302        if (secondUpdate) {
303          await presenter.onAppEvent(secondUpdate);
304          assertDefined(
305            this.executeChecksForPropertiesTreeAfterSecondPositionUpdate,
306          )(uiData);
307        }
308      });
309
310      if (this.shouldExecuteShowDiffTests) {
311        it('updates properties tree to show diffs', async () => {
312          //change flat view to true
313          const userOptions: UserOptions = {
314            showDiff: {
315              name: 'Show diff',
316              enabled: true,
317            },
318          };
319
320          await presenter.onAppEvent(this.getShowDiffPositionUpdate());
321          await presenter.onHighlightedNodeChange(this.getSelectedTree());
322          const propertyName = assertDefined(this.propertyWithDiff);
323          expect(
324            assertDefined(
325              uiData.propertiesTree?.getChildByName(propertyName),
326            ).getDiff(),
327          ).toEqual(DiffType.NONE);
328
329          await presenter.onPropertiesUserOptionsChange(userOptions);
330          expect(uiData.propertiesUserOptions).toEqual(userOptions);
331          expect(
332            assertDefined(
333              uiData.propertiesTree?.getChildByName(propertyName),
334            ).getDiff(),
335          ).toEqual(assertDefined(this.expectedPropertyDiffType));
336        });
337      }
338
339      it('shows/hides defaults', async () => {
340        const userOptions: UserOptions = {
341          showDiff: {
342            name: 'Show diff',
343            enabled: true,
344          },
345          showDefaults: {
346            name: 'Show defaults',
347            enabled: true,
348          },
349        };
350
351        await presenter.onAppEvent(this.getPositionUpdate());
352        await presenter.onHighlightedNodeChange(this.getSelectedTree());
353        expect(
354          assertDefined(uiData.propertiesTree).getAllChildren().length,
355        ).toEqual(this.numberOfNonDefaultProperties);
356
357        await presenter.onPropertiesUserOptionsChange(userOptions);
358        expect(uiData.propertiesUserOptions).toEqual(userOptions);
359        expect(
360          assertDefined(uiData.propertiesTree).getAllChildren().length,
361        ).toEqual(
362          this.numberOfNonDefaultProperties + this.numberOfDefaultProperties,
363        );
364      });
365
366      it('filters properties tree', async () => {
367        await presenter.onAppEvent(this.getPositionUpdate());
368        await presenter.onHighlightedNodeChange(this.getSelectedTree());
369        expect(
370          assertDefined(uiData.propertiesTree).getAllChildren().length,
371        ).toEqual(this.numberOfNonDefaultProperties);
372
373        await presenter.onPropertiesFilterChange(this.propertiesFilterString);
374        expect(
375          assertDefined(uiData.propertiesTree).getAllChildren().length,
376        ).toEqual(this.numberOfFilteredProperties);
377      });
378
379      if (this.shouldExecuteRectTests) {
380        const totalRects = assertDefined(this.expectedTotalRects);
381        const visibleRects = assertDefined(this.expectedVisibleRects);
382
383        it('creates input data for rects view', async () => {
384          await presenter.onAppEvent(this.getPositionUpdate());
385          const rectsToDraw = assertDefined(uiData.rectsToDraw);
386          const expectedFirstRect = assertDefined(this.expectedFirstRect);
387          expect(rectsToDraw[0].x).toEqual(expectedFirstRect.x);
388          expect(rectsToDraw[0].y).toEqual(expectedFirstRect.y);
389          expect(rectsToDraw[0].w).toEqual(expectedFirstRect.w);
390          expect(rectsToDraw[0].h).toEqual(expectedFirstRect.h);
391          this.checkRectUiData(uiData, totalRects, totalRects, totalRects);
392        });
393
394        it('filters rects by visibility', async () => {
395          const userOptions: UserOptions = {
396            showOnlyVisible: {
397              name: 'Show only',
398              chip: VISIBLE_CHIP,
399              enabled: false,
400            },
401          };
402
403          await presenter.onAppEvent(this.getPositionUpdate());
404          presenter.onRectsUserOptionsChange(userOptions);
405          expect(uiData.rectsUserOptions).toEqual(userOptions);
406          this.checkRectUiData(uiData, totalRects, totalRects, totalRects);
407
408          userOptions['showOnlyVisible'].enabled = true;
409          presenter.onRectsUserOptionsChange(userOptions);
410          this.checkRectUiData(uiData, visibleRects, totalRects, visibleRects);
411        });
412
413        it('filters rects by show/hide state', async () => {
414          const userOptions: UserOptions = {
415            ignoreNonHidden: {
416              name: 'Ignore',
417              icon: 'visibility',
418              enabled: true,
419            },
420          };
421          presenter.onRectsUserOptionsChange(userOptions);
422          await presenter.onAppEvent(this.getPositionUpdate());
423          this.checkRectUiData(uiData, totalRects, totalRects, totalRects);
424
425          await presenter.onRectShowStateChange(
426            assertDefined(uiData.rectsToDraw)[0].id,
427            RectShowState.HIDE,
428          );
429          this.checkRectUiData(uiData, totalRects, totalRects, totalRects - 1);
430
431          userOptions['ignoreNonHidden'].enabled = false;
432          presenter.onRectsUserOptionsChange(userOptions);
433          this.checkRectUiData(
434            uiData,
435            totalRects - 1,
436            totalRects,
437            totalRects - 1,
438          );
439        });
440
441        it('handles both visibility and show/hide state in rects', async () => {
442          const userOptions: UserOptions = {
443            ignoreNonHidden: {
444              name: 'Ignore',
445              icon: 'visibility',
446              enabled: true,
447            },
448            showOnlyVisible: {
449              name: 'Show only',
450              chip: VISIBLE_CHIP,
451              enabled: false,
452            },
453          };
454          presenter.onRectsUserOptionsChange(userOptions);
455          await presenter.onAppEvent(this.getPositionUpdate());
456          this.checkRectUiData(uiData, totalRects, totalRects, totalRects);
457
458          await presenter.onRectShowStateChange(
459            assertDefined(uiData.rectsToDraw)[0].id,
460            RectShowState.HIDE,
461          );
462          this.checkRectUiData(uiData, totalRects, totalRects, totalRects - 1);
463
464          userOptions['ignoreNonHidden'].enabled = false;
465          presenter.onRectsUserOptionsChange(userOptions);
466          this.checkRectUiData(
467            uiData,
468            totalRects - 1,
469            totalRects,
470            totalRects - 1,
471          );
472
473          userOptions['showOnlyVisible'].enabled = true;
474          presenter.onRectsUserOptionsChange(userOptions);
475          this.checkRectUiData(
476            uiData,
477            visibleRects - 1,
478            totalRects,
479            visibleRects - 1,
480          );
481
482          userOptions['ignoreNonHidden'].enabled = true;
483          presenter.onRectsUserOptionsChange(userOptions);
484          this.checkRectUiData(
485            uiData,
486            visibleRects,
487            totalRects,
488            visibleRects - 1,
489          );
490        });
491
492        it('sets properties tree and associated ui data from rect', async () => {
493          await presenter.onAppEvent(this.getPositionUpdate());
494
495          const rect = assertDefined(uiData.rectsToDraw?.at(2));
496          await presenter.onHighlightedIdChange(rect.id);
497          const propertiesTree = assertDefined(uiData.propertiesTree);
498          expect(propertiesTree.id).toEqual(rect.id);
499          expect(propertiesTree.getAllChildren().length).toBeGreaterThan(0);
500
501          if (this.executeSpecializedChecksForPropertiesFromRect) {
502            this.executeSpecializedChecksForPropertiesFromRect(uiData);
503          }
504        });
505
506        it('after highlighting a rect, updates properties tree on position update', async () => {
507          await presenter.onAppEvent(this.getPositionUpdate());
508          await presenter.onHighlightedIdChange(
509            this.getSelectedTreeAfterPositionUpdate().id,
510          );
511          this.executeChecksForPropertiesTreeAfterPositionUpdate(uiData);
512
513          const secondUpdate = this.getSecondPositionUpdate();
514          if (secondUpdate) {
515            await presenter.onAppEvent(secondUpdate);
516            assertDefined(
517              this.executeChecksForPropertiesTreeAfterSecondPositionUpdate,
518            )(uiData);
519          }
520        });
521      }
522    });
523
524    if (this.executeSpecializedTests) {
525      this.executeSpecializedTests();
526    }
527  }
528
529  private getTotalHierarchyChildren(uiData: UiDataHierarchy) {
530    const children = assertDefined(uiData.hierarchyTrees).reduce(
531      (tot, tree) => (tot += tree.getAllChildren().length),
532      0,
533    );
534    return children;
535  }
536
537  private checkRectUiData(
538    uiData: UiDataHierarchy,
539    rectsToDraw: number,
540    allRects: number,
541    shownRects: number,
542  ) {
543    expect(assertDefined(uiData.rectsToDraw).length).toEqual(rectsToDraw);
544    const showStates = Array.from(
545      assertDefined(uiData.rectIdToShowState).values(),
546    );
547    expect(showStates.length).toEqual(allRects);
548    expect(showStates.filter((s) => s === RectShowState.SHOW).length).toEqual(
549      shownRects,
550    );
551  }
552
553  abstract readonly shouldExecuteFlatTreeTest: boolean;
554  abstract readonly shouldExecuteRectTests: boolean;
555  abstract readonly shouldExecuteShowDiffTests: boolean;
556  abstract readonly shouldExecuteSimplifyNamesTest: boolean;
557  abstract readonly numberOfDefaultProperties: number;
558  abstract readonly numberOfNonDefaultProperties: number;
559  abstract readonly propertiesFilterString: string;
560  abstract readonly numberOfFilteredProperties: number;
561  abstract readonly hierarchyFilterString: string;
562  abstract readonly expectedHierarchyChildrenAfterStringFilter: number;
563
564  readonly expectedFirstRect?: Rect;
565  readonly expectedTotalRects?: number;
566  readonly expectedVisibleRects?: number;
567  readonly treeNodeLongName?: string;
568  readonly treeNodeShortName?: string;
569  readonly propertyWithDiff?: string;
570  readonly expectedPropertyDiffType?: DiffType;
571
572  abstract setUpTestEnvironment(): Promise<void>;
573  abstract createPresenter(
574    callback: NotifyHierarchyViewCallbackType,
575  ): AbstractHierarchyViewerPresenter;
576  abstract createPresenterWithEmptyTrace(
577    callback: NotifyHierarchyViewCallbackType,
578  ): AbstractHierarchyViewerPresenter;
579  abstract getPositionUpdate(): TracePositionUpdate;
580  abstract getSecondPositionUpdate(): TracePositionUpdate | undefined;
581  abstract getShowDiffPositionUpdate(): TracePositionUpdate;
582  abstract getSelectedTree(): UiHierarchyTreeNode;
583  abstract getSelectedTreeAfterPositionUpdate(): UiHierarchyTreeNode;
584
585  abstract getExpectedChildrenBeforeVisibilityFilter(): number;
586  abstract getExpectedChildrenAfterVisibilityFilter(): number;
587  abstract getExpectedHierarchyChildrenBeforeStringFilter(): number;
588  abstract executeChecksForPropertiesTreeAfterPositionUpdate(
589    uiData: UiDataHierarchy,
590  ): void;
591
592  getExpectedChildrenBeforeFlatFilter?(): number;
593  getExpectedChildrenAfterFlatFilter?(): number;
594  executeSpecializedChecksForPropertiesFromNode?(uiData: UiDataHierarchy): void;
595  executeChecksForPropertiesTreeAfterSecondPositionUpdate?(
596    uiData: UiDataHierarchy,
597  ): void;
598  executeSpecializedChecksForPropertiesFromRect?(uiData: UiDataHierarchy): void;
599  executeSpecializedTests?(): void;
600}
601