• 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 {Clipboard, ClipboardModule} from '@angular/cdk/clipboard';
18import {Component, ViewChild} from '@angular/core';
19import {ComponentFixture, TestBed} from '@angular/core/testing';
20import {MatIconModule} from '@angular/material/icon';
21import {MatTooltipModule} from '@angular/material/tooltip';
22import {assertDefined} from 'common/assert_utils';
23import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
24import {TreeNodeUtils} from 'test/unit/tree_node_utils';
25import {RectShowState} from 'viewers/common/rect_show_state';
26import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
27import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
28import {ViewerEvents} from 'viewers/common/viewer_events';
29import {HierarchyTreeNodeDataViewComponent} from './hierarchy_tree_node_data_view_component';
30import {PropertyTreeNodeDataViewComponent} from './property_tree_node_data_view_component';
31import {TreeComponent} from './tree_component';
32import {TreeNodeComponent} from './tree_node_component';
33
34describe('TreeComponent', () => {
35  let fixture: ComponentFixture<TestHostComponent>;
36  let component: TestHostComponent;
37  let htmlElement: HTMLElement;
38  let mockCopyText: jasmine.Spy;
39
40  beforeEach(async () => {
41    mockCopyText = jasmine.createSpy();
42    await TestBed.configureTestingModule({
43      providers: [{provide: Clipboard, useValue: {copy: mockCopyText}}],
44      declarations: [
45        TreeComponent,
46        TestHostComponent,
47        TreeNodeComponent,
48        HierarchyTreeNodeDataViewComponent,
49        PropertyTreeNodeDataViewComponent,
50      ],
51      imports: [MatTooltipModule, MatIconModule, ClipboardModule],
52    }).compileComponents();
53    fixture = TestBed.createComponent(TestHostComponent);
54    component = fixture.componentInstance;
55    htmlElement = fixture.nativeElement;
56  });
57
58  it('can be created', () => {
59    fixture.detectChanges();
60    expect(component).toBeTruthy();
61  });
62
63  it('shows node', () => {
64    fixture.detectChanges();
65    expect(htmlElement.querySelector('tree-node')).toBeTruthy();
66  });
67
68  it('can identify if a parent node has a selected child', () => {
69    fixture.detectChanges();
70    const treeNode = assertDefined(
71      htmlElement.querySelector<HTMLElement>('tree-node'),
72    );
73    expect(treeNode.className.includes('child-selected')).toBeFalse();
74    component.highlightedItem = '3 Child3';
75    fixture.detectChanges();
76    expect(treeNode.className.includes('child-selected')).toBeTrue();
77  });
78
79  it('highlights node and inner node upon click', () => {
80    fixture.detectChanges();
81    const treeNodes = assertDefined(
82      htmlElement.querySelectorAll<HTMLElement>('tree-node'),
83    );
84
85    const spy = spyOn(
86      assertDefined(component.treeComponent).highlightedChange,
87      'emit',
88    );
89    treeNodes.item(0).dispatchEvent(new MouseEvent('click', {detail: 1}));
90    fixture.detectChanges();
91    expect(spy).toHaveBeenCalledTimes(1);
92
93    treeNodes.item(1).click();
94    fixture.detectChanges();
95    expect(spy).toHaveBeenCalledTimes(2);
96  });
97
98  it('toggles tree upon node double click', () => {
99    fixture.detectChanges();
100    const toggleButton = assertDefined(
101      htmlElement.querySelector('.toggle-tree-btn'),
102    );
103    expect(toggleButton.textContent?.trim()).toEqual('arrow_drop_down');
104    checkIsExpanded(true);
105
106    doubleClickFirstNode();
107    expect(toggleButton.textContent?.trim()).toEqual('chevron_right');
108    checkIsExpanded(false);
109  });
110
111  it('does not toggle tree in flat mode on double click', () => {
112    fixture.detectChanges();
113    component.isFlattened = true;
114    fixture.detectChanges();
115    doubleClickFirstNode();
116    checkIsExpanded(true);
117  });
118
119  it('pins node on click', () => {
120    fixture.detectChanges();
121    const pinNodeButton = assertDefined(
122      htmlElement.querySelector<HTMLElement>('.pin-node-btn'),
123    );
124    const spy = spyOn(
125      assertDefined(component.treeComponent).pinnedItemChange,
126      'emit',
127    );
128    pinNodeButton.click();
129    fixture.detectChanges();
130    expect(spy).toHaveBeenCalled();
131  });
132
133  it('expands tree on expand tree button click', () => {
134    fixture.detectChanges();
135    doubleClickFirstNode();
136    checkIsExpanded(false);
137
138    assertDefined(
139      htmlElement.querySelector<HTMLElement>('.expand-tree-btn'),
140    ).click();
141    fixture.detectChanges();
142    checkIsExpanded(true);
143  });
144
145  it('expands tree recursively on node selection', () => {
146    fixture.detectChanges();
147    doubleClickFirstNode();
148    checkIsExpanded(false);
149    component.highlightedItem = '79 Child79';
150    fixture.detectChanges();
151    checkIsExpanded(true);
152  });
153
154  it('scrolls selected node only if not in view', () => {
155    fixture.detectChanges();
156    checkNodeScrolling();
157  });
158
159  it('scrolls selected node if not in view even if pinned', () => {
160    component.pinnedItems = [
161      assertDefined(component.tree.getChildByName('Child78')),
162      assertDefined(component.tree.getChildByName('Child79')),
163    ];
164    fixture.detectChanges();
165    checkNodeScrolling();
166  });
167
168  it('sets initial expanded state to true by default for leaf', () => {
169    fixture.detectChanges();
170    checkIsExpanded(true);
171  });
172
173  it('sets initial expanded state to true by default for non root', () => {
174    const child = component.tree.getAllChildren()[0] as UiHierarchyTreeNode;
175    const innerChild = UiHierarchyTreeNode.from(
176      new HierarchyTreeBuilder()
177        .setId('InnerChild')
178        .setName('child')
179        .setChildren([])
180        .build(),
181    );
182    child.addOrReplaceChild(innerChild);
183    component.tree = child;
184    fixture.detectChanges();
185    checkIsExpanded(true);
186  });
187
188  it('sets initial expanded state to false if collapse state exists in store', () => {
189    component.useStoredExpandedState = true;
190    fixture.detectChanges();
191    // tree expanded by default
192    checkIsExpanded(true);
193
194    // tree collapsed
195    doubleClickFirstNode();
196    checkIsExpanded(false);
197
198    // tree collapsed state retained
199    component.tree = makeTree();
200    fixture.detectChanges();
201    checkIsExpanded(false);
202  });
203
204  it('renders show state button if applicable', () => {
205    fixture.detectChanges();
206    expect(htmlElement.querySelector('.toggle-rect-show-state-btn')).toBeNull();
207    expect(htmlElement.querySelector('.children.with-gutter')).toBeNull();
208
209    component.rectIdToShowState = new Map([
210      [component.tree.id, RectShowState.HIDE],
211    ]);
212    fixture.detectChanges();
213    expect(htmlElement.querySelector('.children.with-gutter')).toBeTruthy();
214    expect(
215      assertDefined(htmlElement.querySelector('.toggle-rect-show-state-btn'))
216        .textContent,
217    ).toContain('visibility_off');
218
219    component.rectIdToShowState.set(component.tree.id, RectShowState.SHOW);
220    fixture.detectChanges();
221    expect(
222      assertDefined(htmlElement.querySelector('.toggle-rect-show-state-btn'))
223        .textContent,
224    ).toContain('visibility');
225  });
226
227  it('handles show state button click', () => {
228    component.rectIdToShowState = new Map([
229      [component.tree.id, RectShowState.HIDE],
230    ]);
231    fixture.detectChanges();
232    const button = assertDefined(
233      htmlElement.querySelector<HTMLElement>('.toggle-rect-show-state-btn'),
234    );
235    expect(button.textContent).toContain('visibility_off');
236
237    let id = '';
238    htmlElement.addEventListener(ViewerEvents.RectShowStateChange, (event) => {
239      const detail = (event as CustomEvent).detail;
240      id = detail.rectId;
241      component.rectIdToShowState?.set(detail.rectId, detail.state);
242    });
243    button.click();
244    fixture.detectChanges();
245    expect(component.rectIdToShowState.get(id)).toEqual(RectShowState.SHOW);
246
247    button.click();
248    fixture.detectChanges();
249    expect(component.rectIdToShowState.get(id)).toEqual(RectShowState.HIDE);
250  });
251
252  it('shows node at full opacity when applicable', () => {
253    fixture.detectChanges();
254    expect(htmlElement.querySelector('.node.full-opacity')).toBeTruthy();
255
256    component.rectIdToShowState = new Map([
257      [component.tree.id, RectShowState.SHOW],
258    ]);
259    fixture.detectChanges();
260    expect(htmlElement.querySelector('.node.full-opacity')).toBeTruthy();
261
262    component.tree = TreeNodeUtils.makeUiPropertyNode(
263      component.tree.id,
264      component.tree.name,
265      0,
266    );
267    fixture.detectChanges();
268    expect(htmlElement.querySelector('.node.full-opacity')).toBeTruthy();
269  });
270
271  it('shows node at non-full opacity when applicable', () => {
272    component.rectIdToShowState = new Map([]);
273    fixture.detectChanges();
274    expect(htmlElement.querySelector('.node.full-opacity')).toBeNull();
275
276    component.rectIdToShowState = new Map([
277      [component.tree.id, RectShowState.HIDE],
278    ]);
279    fixture.detectChanges();
280    expect(htmlElement.querySelector('.node.full-opacity')).toBeNull();
281  });
282
283  it('copies text via copy button without selecting node', () => {
284    fixture.detectChanges();
285
286    component.tree = TreeNodeUtils.makeUiPropertyNode(
287      component.tree.id,
288      component.tree.name,
289      0,
290    );
291    fixture.detectChanges();
292
293    const spy = spyOn(assertDefined(component.treeComponent), 'onNodeClick');
294    const copyButton = assertDefined(
295      htmlElement.querySelector<HTMLElement>('.icon-wrapper-copy button'),
296    );
297    copyButton.click();
298    fixture.detectChanges();
299    expect(mockCopyText).toHaveBeenCalled();
300    expect(spy).not.toHaveBeenCalled();
301  });
302
303  function makeTree() {
304    const children = [];
305    for (let i = 0; i < 80; i++) {
306      children.push({id: i, name: `Child${i}`});
307    }
308    return UiHierarchyTreeNode.from(
309      new HierarchyTreeBuilder()
310        .setId('RootNode')
311        .setName('Root node')
312        .setChildren(children)
313        .build(),
314    );
315  }
316
317  function doubleClickFirstNode() {
318    assertDefined(
319      htmlElement.querySelector<HTMLElement>('tree-node'),
320    ).dispatchEvent(new MouseEvent('click', {detail: 2}));
321    fixture.detectChanges();
322  }
323
324  function checkIsExpanded(isExpanded: boolean) {
325    expect(htmlElement.querySelector<HTMLElement>('.children')?.hidden).toEqual(
326      !isExpanded,
327    );
328  }
329
330  function checkNodeScrolling() {
331    const treeNode = assertDefined(htmlElement.querySelector(`#nodeChild79`));
332    const spy = spyOn(treeNode, 'scrollIntoView').and.callThrough();
333
334    component.highlightedItem = 'Root node';
335    fixture.detectChanges();
336
337    component.highlightedItem = '79 Child79';
338    fixture.detectChanges();
339    expect(spy).toHaveBeenCalledTimes(1);
340
341    component.highlightedItem = '78 Child78';
342    fixture.detectChanges();
343    expect(spy).toHaveBeenCalledTimes(1);
344  }
345
346  @Component({
347    selector: 'host-component',
348    template: `
349    <div class="tree-wrapper">
350      <tree-view
351        [node]="tree"
352        [isFlattened]="isFlattened"
353        [pinnedItems]="pinnedItems"
354        [highlightedItem]="highlightedItem"
355        [useStoredExpandedState]="useStoredExpandedState"
356        [itemsClickable]="true"
357        [rectIdToShowState]="rectIdToShowState"></tree-view>
358    </div>
359    `,
360    styles: [
361      `
362      .tree-wrapper {
363        height: 500px;
364        overflow: auto;
365      }
366    `,
367    ],
368  })
369  class TestHostComponent {
370    tree: UiHierarchyTreeNode | UiPropertyTreeNode;
371    highlightedItem = '';
372    isFlattened = false;
373    useStoredExpandedState = false;
374    rectIdToShowState: Map<string, RectShowState> | undefined;
375    pinnedItems: Array<UiHierarchyTreeNode | UiPropertyTreeNode> = [];
376
377    constructor() {
378      this.tree = makeTree();
379    }
380
381    @ViewChild(TreeComponent)
382    treeComponent: TreeComponent | undefined;
383  }
384});
385