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