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 {CommonModule} from '@angular/common';
17import {
18  ComponentFixture,
19  ComponentFixtureAutoDetect,
20  TestBed,
21} from '@angular/core/testing';
22import {FormsModule} from '@angular/forms';
23import {MatButtonModule} from '@angular/material/button';
24import {MatDividerModule} from '@angular/material/divider';
25import {MatFormFieldModule} from '@angular/material/form-field';
26import {MatIconModule} from '@angular/material/icon';
27import {MatInputModule} from '@angular/material/input';
28import {MatTooltipModule} from '@angular/material/tooltip';
29import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
30import {assertDefined} from 'common/assert_utils';
31import {PersistentStore} from 'common/persistent_store';
32import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder';
33import {TraceType} from 'trace/trace_type';
34import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
35import {ViewerEvents} from 'viewers/common/viewer_events';
36import {HierarchyTreeNodeDataViewComponent} from 'viewers/components/hierarchy_tree_node_data_view_component';
37import {TreeComponent} from 'viewers/components/tree_component';
38import {TreeNodeComponent} from 'viewers/components/tree_node_component';
39import {CollapsibleSectionTitleComponent} from './collapsible_section_title_component';
40import {HierarchyComponent} from './hierarchy_component';
41import {UserOptionsComponent} from './user_options_component';
42
43describe('HierarchyComponent', () => {
44  let fixture: ComponentFixture<HierarchyComponent>;
45  let component: HierarchyComponent;
46  let htmlElement: HTMLElement;
47
48  beforeEach(async () => {
49    await TestBed.configureTestingModule({
50      providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
51      declarations: [
52        HierarchyComponent,
53        TreeComponent,
54        TreeNodeComponent,
55        HierarchyTreeNodeDataViewComponent,
56        CollapsibleSectionTitleComponent,
57        UserOptionsComponent,
58      ],
59      imports: [
60        CommonModule,
61        MatButtonModule,
62        MatDividerModule,
63        MatInputModule,
64        MatFormFieldModule,
65        BrowserAnimationsModule,
66        FormsModule,
67        MatIconModule,
68        MatTooltipModule,
69      ],
70    }).compileComponents();
71
72    fixture = TestBed.createComponent(HierarchyComponent);
73    component = fixture.componentInstance;
74    htmlElement = fixture.nativeElement;
75
76    component.tree = UiHierarchyTreeNode.from(
77      new HierarchyTreeBuilder()
78        .setId('RootNode1')
79        .setName('Root node')
80        .setChildren([{id: 'Child1', name: 'Child node'}])
81        .build(),
82    );
83
84    component.store = new PersistentStore();
85    component.userOptions = {
86      showDiff: {
87        name: 'Show diff',
88        enabled: false,
89        isUnavailable: false,
90      },
91    };
92    component.dependencies = [TraceType.SURFACE_FLINGER];
93
94    fixture.detectChanges();
95  });
96
97  it('can be created', () => {
98    expect(component).toBeTruthy();
99  });
100
101  it('renders title', () => {
102    const title = htmlElement.querySelector('.hierarchy-title');
103    expect(title).toBeTruthy();
104  });
105
106  it('renders view controls', () => {
107    const viewControls = htmlElement.querySelector('.view-controls');
108    expect(viewControls).toBeTruthy();
109    const button = htmlElement.querySelector('.view-controls .user-option');
110    expect(button).toBeTruthy(); //renders at least one view control option
111  });
112
113  it('renders initial tree elements', () => {
114    const treeView = htmlElement.querySelector('tree-view');
115    expect(treeView).toBeTruthy();
116    expect(assertDefined(treeView).innerHTML).toContain('Root node');
117    expect(assertDefined(treeView).innerHTML).toContain('Child node');
118  });
119
120  it('renders subtrees', () => {
121    component.subtrees = [
122      UiHierarchyTreeNode.from(
123        new HierarchyTreeBuilder().setId('subtree').setName('subtree').build(),
124      ),
125    ];
126    fixture.detectChanges();
127    const subtree = assertDefined(
128      htmlElement.querySelector('.tree-wrapper .subtrees tree-view'),
129    );
130    expect(assertDefined(subtree).innerHTML).toContain('subtree');
131  });
132
133  it('renders pinned nodes', () => {
134    const pinnedNodesDiv = htmlElement.querySelector('.pinned-items');
135    expect(pinnedNodesDiv).toBeFalsy();
136
137    component.pinnedItems = [assertDefined(component.tree)];
138    fixture.detectChanges();
139    const pinnedNodeEl = htmlElement.querySelector('.pinned-items tree-node');
140    expect(pinnedNodeEl).toBeTruthy();
141  });
142
143  it('handles pinned node click', () => {
144    const node = assertDefined(component.tree);
145    component.pinnedItems = [node];
146    fixture.detectChanges();
147
148    let highlightedItem: UiHierarchyTreeNode | undefined;
149    htmlElement.addEventListener(
150      ViewerEvents.HighlightedNodeChange,
151      (event) => {
152        highlightedItem = (event as CustomEvent).detail.node;
153      },
154    );
155
156    const pinnedNodeEl = assertDefined(
157      htmlElement.querySelector('.pinned-items tree-node'),
158    );
159
160    (pinnedNodeEl as HTMLButtonElement).click();
161    fixture.detectChanges();
162    expect(highlightedItem).toEqual(node);
163  });
164
165  it('handles pinned item change from tree', () => {
166    let pinnedItem: UiHierarchyTreeNode | undefined;
167    htmlElement.addEventListener(
168      ViewerEvents.HierarchyPinnedChange,
169      (event) => {
170        pinnedItem = (event as CustomEvent).detail.pinnedItem;
171      },
172    );
173    const child = assertDefined(component.tree?.getChildByName('Child node'));
174    component.pinnedItems = [child];
175    fixture.detectChanges();
176
177    const pinButton = assertDefined(
178      htmlElement.querySelector('.pinned-items tree-node .pin-node-btn'),
179    );
180    (pinButton as HTMLButtonElement).click();
181    fixture.detectChanges();
182
183    expect(pinnedItem).toEqual(child);
184  });
185
186  it('handles change in filter', () => {
187    let filterString: string | undefined;
188    htmlElement.addEventListener(
189      ViewerEvents.HierarchyFilterChange,
190      (event) => {
191        filterString = (event as CustomEvent).detail.filterString;
192      },
193    );
194    const inputEl = assertDefined(
195      htmlElement.querySelector('.title-section input'),
196    ) as HTMLInputElement;
197
198    inputEl.value = 'Root';
199    inputEl.dispatchEvent(new Event('input'));
200    fixture.detectChanges();
201    expect(filterString).toBe('Root');
202  });
203
204  it('handles collapse button click', () => {
205    const spy = spyOn(component.collapseButtonClicked, 'emit');
206    const collapseButton = assertDefined(
207      htmlElement.querySelector('collapsible-section-title button'),
208    ) as HTMLButtonElement;
209    collapseButton.click();
210    fixture.detectChanges();
211    expect(spy).toHaveBeenCalled();
212  });
213});
214