xref: /aosp_15_r20/development/tools/winscope/src/viewers/components/tree_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 */
16
17import {Clipboard, ClipboardModule} from '@angular/cdk/clipboard';
18import {Component, CUSTOM_ELEMENTS_SCHEMA, 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      schemas: [CUSTOM_ELEMENTS_SCHEMA],
53    }).compileComponents();
54    fixture = TestBed.createComponent(TestHostComponent);
55    component = fixture.componentInstance;
56    htmlElement = fixture.nativeElement;
57  });
58
59  it('can be created', () => {
60    fixture.detectChanges();
61    expect(component).toBeTruthy();
62  });
63
64  it('shows node', () => {
65    fixture.detectChanges();
66    const treeNode = htmlElement.querySelector('tree-node');
67    expect(treeNode).toBeTruthy();
68  });
69
70  it('can identify if a parent node has a selected child', () => {
71    fixture.detectChanges();
72    const treeComponent = assertDefined(component.treeComponent);
73    expect(treeComponent.hasSelectedChild()).toBeFalse();
74    component.highlightedItem = '3 Child3';
75    fixture.detectChanges();
76    expect(treeComponent.hasSelectedChild()).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 treeComponent = assertDefined(component.treeComponent);
101    const treeNode = assertDefined(
102      htmlElement.querySelector<HTMLElement>('tree-node'),
103    );
104    const currLocalExpandedState = treeComponent.localExpandedState;
105    treeNode.dispatchEvent(new MouseEvent('click', {detail: 2}));
106    fixture.detectChanges();
107    expect(!currLocalExpandedState).toEqual(treeComponent.localExpandedState);
108  });
109
110  it('does not toggle tree in flat mode on double click', () => {
111    fixture.detectChanges();
112    const treeComponent = assertDefined(component.treeComponent);
113    component.isFlattened = true;
114    fixture.detectChanges();
115    const treeNode = assertDefined(
116      htmlElement.querySelector<HTMLElement>('tree-node'),
117    );
118
119    const currLocalExpandedState = treeComponent.localExpandedState;
120    treeNode.dispatchEvent(new MouseEvent('click', {detail: 2}));
121    fixture.detectChanges();
122    expect(currLocalExpandedState).toEqual(treeComponent.localExpandedState);
123  });
124
125  it('pins node on click', () => {
126    fixture.detectChanges();
127    const pinNodeButton = assertDefined(
128      htmlElement.querySelector<HTMLElement>('.pin-node-btn'),
129    );
130    const spy = spyOn(
131      assertDefined(component.treeComponent).pinnedItemChange,
132      'emit',
133    );
134    pinNodeButton.click();
135    fixture.detectChanges();
136    expect(spy).toHaveBeenCalled();
137  });
138
139  it('expands tree on expand tree button click', () => {
140    fixture.detectChanges();
141    const treeNode = assertDefined(
142      htmlElement.querySelector<HTMLElement>('tree-node'),
143    );
144    treeNode.dispatchEvent(new MouseEvent('click', {detail: 2}));
145    fixture.detectChanges();
146    expect(component.treeComponent?.localExpandedState).toEqual(false);
147    assertDefined(
148      htmlElement.querySelector<HTMLElement>('.expand-tree-btn'),
149    ).click();
150    fixture.detectChanges();
151    expect(component.treeComponent?.localExpandedState).toEqual(true);
152  });
153
154  it('scrolls selected node only if not in view', () => {
155    fixture.detectChanges();
156    const treeComponent = assertDefined(component.treeComponent);
157    const treeNode = assertDefined(
158      treeComponent.elementRef.nativeElement.querySelector(`#nodeChild79`),
159    );
160
161    component.highlightedItem = 'Root node';
162    fixture.detectChanges();
163
164    const spy = spyOn(treeNode, 'scrollIntoView').and.callThrough();
165    component.highlightedItem = '79 Child79';
166    fixture.detectChanges();
167    expect(spy).toHaveBeenCalledTimes(1);
168
169    component.highlightedItem = '78 Child78';
170    fixture.detectChanges();
171    expect(spy).toHaveBeenCalledTimes(1);
172  });
173
174  it('sets initial expanded state to true by default for leaf', () => {
175    fixture.detectChanges();
176    expect(assertDefined(component.treeComponent).isExpanded()).toBeTrue();
177  });
178
179  it('sets initial expanded state to true by default for non root', () => {
180    component.tree = component.tree.getAllChildren()[0];
181    fixture.detectChanges();
182    expect(assertDefined(component.treeComponent).isExpanded()).toBeTrue();
183  });
184
185  it('sets initial expanded state to false if collapse state exists in store', () => {
186    component.useStoredExpandedState = true;
187    fixture.detectChanges();
188    const treeComponent = assertDefined(component.treeComponent);
189    // tree expanded by default
190    expect(treeComponent.isExpanded()).toBeTrue();
191
192    // tree collapsed
193    treeComponent.toggleTree();
194    fixture.detectChanges();
195    expect(treeComponent.isExpanded()).toBeFalse();
196
197    // tree collapsed state retained
198    component.tree = makeTree();
199    fixture.detectChanges();
200    expect(treeComponent.isExpanded()).toBeFalse();
201  });
202
203  it('renders show state button if applicable', () => {
204    fixture.detectChanges();
205    expect(htmlElement.querySelector('.toggle-rect-show-state-btn')).toBeNull();
206    expect(htmlElement.querySelector('.children.with-gutter')).toBeNull();
207
208    component.rectIdToShowState = new Map([
209      [component.tree.id, RectShowState.HIDE],
210    ]);
211    fixture.detectChanges();
212    expect(htmlElement.querySelector('.children.with-gutter')).toBeTruthy();
213    expect(
214      assertDefined(htmlElement.querySelector('.toggle-rect-show-state-btn'))
215        .textContent,
216    ).toContain('visibility_off');
217
218    component.rectIdToShowState.set(component.tree.id, RectShowState.SHOW);
219    fixture.detectChanges();
220    expect(
221      assertDefined(htmlElement.querySelector('.toggle-rect-show-state-btn'))
222        .textContent,
223    ).toContain('visibility');
224  });
225
226  it('handles show state button click', () => {
227    component.rectIdToShowState = new Map([
228      [component.tree.id, RectShowState.HIDE],
229    ]);
230    fixture.detectChanges();
231    const button = assertDefined(
232      htmlElement.querySelector<HTMLElement>('.toggle-rect-show-state-btn'),
233    );
234    expect(button.textContent).toContain('visibility_off');
235
236    let id = '';
237    htmlElement.addEventListener(ViewerEvents.RectShowStateChange, (event) => {
238      const detail = (event as CustomEvent).detail;
239      id = detail.rectId;
240      component.rectIdToShowState?.set(detail.rectId, detail.state);
241    });
242    button.click();
243    fixture.detectChanges();
244    expect(component.rectIdToShowState.get(id)).toEqual(RectShowState.SHOW);
245
246    button.click();
247    fixture.detectChanges();
248    expect(component.rectIdToShowState.get(id)).toEqual(RectShowState.HIDE);
249  });
250
251  it('shows node at full opacity when applicable', () => {
252    fixture.detectChanges();
253    expect(htmlElement.querySelector('.node.full-opacity')).toBeTruthy();
254
255    component.rectIdToShowState = new Map([
256      [component.tree.id, RectShowState.SHOW],
257    ]);
258    fixture.detectChanges();
259    expect(htmlElement.querySelector('.node.full-opacity')).toBeTruthy();
260
261    component.tree = TreeNodeUtils.makeUiPropertyNode(
262      component.tree.id,
263      component.tree.name,
264      0,
265    );
266    fixture.detectChanges();
267    expect(htmlElement.querySelector('.node.full-opacity')).toBeTruthy();
268  });
269
270  it('shows node at non-full opacity when applicable', () => {
271    component.rectIdToShowState = new Map([]);
272    fixture.detectChanges();
273    expect(htmlElement.querySelector('.node.full-opacity')).toBeNull();
274
275    component.rectIdToShowState = new Map([
276      [component.tree.id, RectShowState.HIDE],
277    ]);
278    fixture.detectChanges();
279    expect(htmlElement.querySelector('.node.full-opacity')).toBeNull();
280  });
281
282  it('copies text via copy button without selecting node', () => {
283    fixture.detectChanges();
284
285    component.tree = TreeNodeUtils.makeUiPropertyNode(
286      component.tree.id,
287      component.tree.name,
288      0,
289    );
290    fixture.detectChanges();
291
292    const spy = spyOn(assertDefined(component.treeComponent), 'onNodeClick');
293    const copyButton = assertDefined(
294      htmlElement.querySelector<HTMLElement>('.icon-wrapper-copy button'),
295    );
296    copyButton.click();
297    fixture.detectChanges();
298    expect(mockCopyText).toHaveBeenCalled();
299    expect(spy).not.toHaveBeenCalled();
300  });
301
302  function makeTree() {
303    const children = [];
304    for (let i = 0; i < 80; i++) {
305      children.push({id: i, name: `Child${i}`});
306    }
307    return UiHierarchyTreeNode.from(
308      new HierarchyTreeBuilder()
309        .setId('RootNode')
310        .setName('Root node')
311        .setChildren(children)
312        .build(),
313    );
314  }
315
316  @Component({
317    selector: 'host-component',
318    template: `
319    <div class="tree-wrapper">
320      <tree-view
321        [node]="tree"
322        [isFlattened]="isFlattened"
323        [isPinned]="false"
324        [highlightedItem]="highlightedItem"
325        [useStoredExpandedState]="useStoredExpandedState"
326        [itemsClickable]="true"
327        [rectIdToShowState]="rectIdToShowState"></tree-view>
328    </div>
329    `,
330    styles: [
331      `
332      .tree-wrapper {
333        height: 500px;
334        overflow: auto;
335      }
336    `,
337    ],
338  })
339  class TestHostComponent {
340    tree: UiHierarchyTreeNode | UiPropertyTreeNode;
341    highlightedItem = '';
342    isFlattened = false;
343    useStoredExpandedState = false;
344    rectIdToShowState: Map<string, RectShowState> | undefined;
345
346    constructor() {
347      this.tree = makeTree();
348    }
349
350    @ViewChild(TreeComponent)
351    treeComponent: TreeComponent | undefined;
352  }
353});
354