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