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