xref: /aosp_15_r20/development/tools/winscope/src/viewers/components/hierarchy_component_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 */
16import {ClipboardModule} from '@angular/cdk/clipboard';
17import {CommonModule} from '@angular/common';
18import {
19  ComponentFixture,
20  ComponentFixtureAutoDetect,
21  TestBed,
22} from '@angular/core/testing';
23import {FormsModule} from '@angular/forms';
24import {MatButtonModule} from '@angular/material/button';
25import {MatDividerModule} from '@angular/material/divider';
26import {MatFormFieldModule} from '@angular/material/form-field';
27import {MatIconModule} from '@angular/material/icon';
28import {MatInputModule} from '@angular/material/input';
29import {MatTooltipModule} from '@angular/material/tooltip';
30import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
31import {assertDefined} from 'common/assert_utils';
32import {FilterFlag} from 'common/filter_flag';
33import {PersistentStore} from 'common/persistent_store';
34import {DuplicateLayerIds, MissingLayerIds} from 'messaging/user_warnings';
35import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
36import {TraceType} from 'trace/trace_type';
37import {TextFilter} from 'viewers/common/text_filter';
38import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
39import {ViewerEvents} from 'viewers/common/viewer_events';
40import {HierarchyTreeNodeDataViewComponent} from 'viewers/components/hierarchy_tree_node_data_view_component';
41import {TreeComponent} from 'viewers/components/tree_component';
42import {TreeNodeComponent} from 'viewers/components/tree_node_component';
43import {CollapsibleSectionTitleComponent} from './collapsible_section_title_component';
44import {HierarchyComponent} from './hierarchy_component';
45import {SearchBoxComponent} from './search_box_component';
46import {UserOptionsComponent} from './user_options_component';
47
48describe('HierarchyComponent', () => {
49  let fixture: ComponentFixture<HierarchyComponent>;
50  let component: HierarchyComponent;
51  let htmlElement: HTMLElement;
52
53  beforeEach(async () => {
54    await TestBed.configureTestingModule({
55      providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
56      declarations: [
57        HierarchyComponent,
58        TreeComponent,
59        TreeNodeComponent,
60        HierarchyTreeNodeDataViewComponent,
61        CollapsibleSectionTitleComponent,
62        UserOptionsComponent,
63        SearchBoxComponent,
64      ],
65      imports: [
66        CommonModule,
67        MatButtonModule,
68        MatDividerModule,
69        MatInputModule,
70        MatFormFieldModule,
71        BrowserAnimationsModule,
72        FormsModule,
73        MatIconModule,
74        MatTooltipModule,
75        ClipboardModule,
76      ],
77    }).compileComponents();
78
79    fixture = TestBed.createComponent(HierarchyComponent);
80    component = fixture.componentInstance;
81    htmlElement = fixture.nativeElement;
82
83    component.tree = UiHierarchyTreeNode.from(
84      new HierarchyTreeBuilder()
85        .setId('RootNode1')
86        .setName('Root node')
87        .setChildren([{id: 'Child1', name: 'Child node'}])
88        .build(),
89    );
90
91    component.store = new PersistentStore();
92    component.userOptions = {
93      showDiff: {
94        name: 'Show diff',
95        enabled: false,
96        isUnavailable: false,
97      },
98    };
99    component.textFilter = new TextFilter();
100    component.dependencies = [TraceType.SURFACE_FLINGER];
101
102    fixture.detectChanges();
103  });
104
105  it('can be created', () => {
106    expect(component).toBeTruthy();
107  });
108
109  it('renders title', () => {
110    const title = htmlElement.querySelector('.hierarchy-title');
111    expect(title).toBeTruthy();
112  });
113
114  it('renders view controls', () => {
115    const viewControls = htmlElement.querySelector('.view-controls');
116    expect(viewControls).toBeTruthy();
117    const button = htmlElement.querySelector('.view-controls .user-option');
118    expect(button).toBeTruthy(); //renders at least one view control option
119  });
120
121  it('renders initial tree elements', () => {
122    const treeView = htmlElement.querySelector('tree-view');
123    expect(treeView).toBeTruthy();
124    expect(assertDefined(treeView).innerHTML).toContain('Root node');
125    expect(assertDefined(treeView).innerHTML).toContain('Child node');
126  });
127
128  it('renders subtrees', () => {
129    component.subtrees = [
130      UiHierarchyTreeNode.from(
131        new HierarchyTreeBuilder().setId('subtree').setName('subtree').build(),
132      ),
133    ];
134    fixture.detectChanges();
135    const subtree = assertDefined(
136      htmlElement.querySelector('.tree-wrapper .subtrees tree-view'),
137    );
138    expect(assertDefined(subtree).innerHTML).toContain('subtree');
139  });
140
141  it('renders pinned nodes', () => {
142    const pinnedNodesDiv = htmlElement.querySelector('.pinned-items');
143    expect(pinnedNodesDiv).toBeFalsy();
144
145    component.pinnedItems = [assertDefined(component.tree)];
146    fixture.detectChanges();
147    const pinnedNodeEl = htmlElement.querySelector('.pinned-items tree-node');
148    expect(pinnedNodeEl).toBeTruthy();
149  });
150
151  it('renders placeholder text', () => {
152    component.tree = undefined;
153    component.placeholderText = 'Placeholder text';
154    fixture.detectChanges();
155    expect(
156      htmlElement.querySelector('.placeholder-text')?.textContent,
157    ).toContain('Placeholder text');
158  });
159
160  it('handles pinned node click', () => {
161    const node = assertDefined(component.tree);
162    component.pinnedItems = [node];
163    fixture.detectChanges();
164
165    let highlightedItem: UiHierarchyTreeNode | undefined;
166    htmlElement.addEventListener(
167      ViewerEvents.HighlightedNodeChange,
168      (event) => {
169        highlightedItem = (event as CustomEvent).detail.node;
170      },
171    );
172
173    const pinnedNodeEl = assertDefined(
174      htmlElement.querySelector('.pinned-items tree-node'),
175    );
176
177    (pinnedNodeEl as HTMLButtonElement).click();
178    fixture.detectChanges();
179    expect(highlightedItem).toEqual(node);
180  });
181
182  it('handles pinned item change from tree', () => {
183    let pinnedItem: UiHierarchyTreeNode | undefined;
184    htmlElement.addEventListener(
185      ViewerEvents.HierarchyPinnedChange,
186      (event) => {
187        pinnedItem = (event as CustomEvent).detail.pinnedItem;
188      },
189    );
190    const child = assertDefined(component.tree?.getChildByName('Child node'));
191    component.pinnedItems = [child];
192    fixture.detectChanges();
193
194    const pinButton = assertDefined(
195      htmlElement.querySelector('.pinned-items tree-node .pin-node-btn'),
196    );
197    (pinButton as HTMLButtonElement).click();
198    fixture.detectChanges();
199
200    expect(pinnedItem).toEqual(child);
201  });
202
203  it('handles change in filter', () => {
204    let textFilter: TextFilter | undefined;
205    htmlElement.addEventListener(
206      ViewerEvents.HierarchyFilterChange,
207      (event) => {
208        textFilter = (event as CustomEvent).detail;
209      },
210    );
211    const inputEl = assertDefined(
212      htmlElement.querySelector<HTMLInputElement>('.title-section input'),
213    );
214    const flagButton = assertDefined(
215      htmlElement.querySelector<HTMLElement>('.search-box button'),
216    );
217    flagButton.click();
218    fixture.detectChanges();
219
220    inputEl.value = 'Root';
221    inputEl.dispatchEvent(new Event('input'));
222    fixture.detectChanges();
223    expect(textFilter).toEqual(new TextFilter('Root', [FilterFlag.MATCH_CASE]));
224  });
225
226  it('handles collapse button click', () => {
227    const spy = spyOn(component.collapseButtonClicked, 'emit');
228    const collapseButton = assertDefined(
229      htmlElement.querySelector('collapsible-section-title button'),
230    ) as HTMLButtonElement;
231    collapseButton.click();
232    fixture.detectChanges();
233    expect(spy).toHaveBeenCalled();
234  });
235
236  it('shows warnings', () => {
237    expect(htmlElement.querySelectorAll('.warning').length).toEqual(0);
238
239    const warning1 = new DuplicateLayerIds([123]);
240    component.tree?.addWarning(warning1);
241    const warning2 = new MissingLayerIds();
242    component.tree?.addWarning(warning2);
243    fixture.detectChanges();
244    const warnings = htmlElement.querySelectorAll('.warning');
245    expect(warnings.length).toEqual(2);
246    expect(warnings[0].textContent?.trim()).toEqual(
247      'warning ' + warning1.getMessage(),
248    );
249    expect(warnings[1].textContent?.trim()).toEqual(
250      'warning ' + warning2.getMessage(),
251    );
252  });
253
254  it('shows warning tooltip if text overflowing', () => {
255    const warning = new DuplicateLayerIds([123]);
256    component.tree?.addWarning(warning);
257    fixture.detectChanges();
258
259    const warningEl = assertDefined(htmlElement.querySelector('.warning'));
260    const msgEl = assertDefined(warningEl.querySelector('.warning-message'));
261
262    const spy = spyOnProperty(msgEl, 'scrollWidth').and.returnValue(
263      msgEl.clientWidth,
264    );
265    warningEl.dispatchEvent(new Event('mouseenter'));
266    fixture.detectChanges();
267    expect(
268      document.querySelector<HTMLElement>('.mat-tooltip-panel'),
269    ).toBeNull();
270    warningEl.dispatchEvent(new Event('mouseleave'));
271    fixture.detectChanges();
272
273    spy.and.returnValue(msgEl.clientWidth + 1);
274    fixture.detectChanges();
275    warningEl.dispatchEvent(new Event('mouseenter'));
276    fixture.detectChanges();
277    expect(
278      document.querySelector<HTMLElement>('.mat-tooltip-panel')?.textContent,
279    ).toEqual(warning.getMessage());
280  });
281});
282