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