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 {Component, CUSTOM_ELEMENTS_SCHEMA, ViewChild} from '@angular/core'; 17import { 18 ComponentFixture, 19 ComponentFixtureAutoDetect, 20 TestBed, 21} from '@angular/core/testing'; 22import {MatIconModule} from '@angular/material/icon'; 23import {MatTooltipModule} from '@angular/material/tooltip'; 24import {assertDefined} from 'common/assert_utils'; 25import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder'; 26import {RectShowState} from 'viewers/common/rect_show_state'; 27import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_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 39 beforeEach(async () => { 40 await TestBed.configureTestingModule({ 41 providers: [{provide: ComponentFixtureAutoDetect, useValue: true}], 42 declarations: [ 43 TreeComponent, 44 TestHostComponent, 45 TreeNodeComponent, 46 HierarchyTreeNodeDataViewComponent, 47 PropertyTreeNodeDataViewComponent, 48 ], 49 imports: [MatTooltipModule, MatIconModule], 50 schemas: [CUSTOM_ELEMENTS_SCHEMA], 51 }).compileComponents(); 52 fixture = TestBed.createComponent(TestHostComponent); 53 component = fixture.componentInstance; 54 htmlElement = fixture.nativeElement; 55 fixture.detectChanges(); 56 }); 57 58 it('can be created', () => { 59 expect(component).toBeTruthy(); 60 }); 61 62 it('shows node', () => { 63 const treeNode = htmlElement.querySelector('tree-node'); 64 expect(treeNode).toBeTruthy(); 65 }); 66 67 it('can identify if a parent node has a selected child', () => { 68 const treeComponent = assertDefined(component.treeComponent); 69 expect(treeComponent.hasSelectedChild()).toBeFalse(); 70 component.highlightedItem = '3 Child3'; 71 fixture.detectChanges(); 72 expect(treeComponent.hasSelectedChild()).toBeTrue(); 73 }); 74 75 it('highlights node upon click', () => { 76 const treeNode = assertDefined(htmlElement.querySelector('tree-node')); 77 78 const spy = spyOn( 79 assertDefined(component.treeComponent).highlightedChange, 80 'emit', 81 ); 82 (treeNode as HTMLButtonElement).dispatchEvent( 83 new MouseEvent('click', {detail: 1}), 84 ); 85 fixture.detectChanges(); 86 expect(spy).toHaveBeenCalled(); 87 }); 88 89 it('toggles tree upon node double click', () => { 90 const treeComponent = assertDefined(component.treeComponent); 91 const treeNode = assertDefined(htmlElement.querySelector('tree-node')); 92 93 const currLocalExpandedState = treeComponent.localExpandedState; 94 (treeNode as HTMLButtonElement).dispatchEvent( 95 new MouseEvent('click', {detail: 2}), 96 ); 97 fixture.detectChanges(); 98 expect(!currLocalExpandedState).toEqual(treeComponent.localExpandedState); 99 }); 100 101 it('does not toggle tree in flat mode on double click', () => { 102 const treeComponent = assertDefined(component.treeComponent); 103 component.isFlattened = true; 104 fixture.detectChanges(); 105 const treeNode = assertDefined(htmlElement.querySelector('tree-node')); 106 107 const currLocalExpandedState = treeComponent.localExpandedState; 108 (treeNode as HTMLButtonElement).dispatchEvent( 109 new MouseEvent('click', {detail: 2}), 110 ); 111 fixture.detectChanges(); 112 expect(currLocalExpandedState).toEqual(treeComponent.localExpandedState); 113 }); 114 115 it('scrolls selected node only if not in view', () => { 116 const treeComponent = assertDefined(component.treeComponent); 117 const treeNode = assertDefined( 118 treeComponent.elementRef.nativeElement.querySelector(`#nodeChild79`), 119 ); 120 121 component.highlightedItem = 'Root node'; 122 fixture.detectChanges(); 123 124 const spy = spyOn(treeNode, 'scrollIntoView').and.callThrough(); 125 component.highlightedItem = '79 Child79'; 126 fixture.detectChanges(); 127 expect(spy).toHaveBeenCalledTimes(1); 128 129 component.highlightedItem = '78 Child78'; 130 fixture.detectChanges(); 131 expect(spy).toHaveBeenCalledTimes(1); 132 }); 133 134 it('sets initial expanded state to true by default', () => { 135 fixture.detectChanges(); 136 expect(assertDefined(component.treeComponent).isExpanded()).toBeTrue(); 137 }); 138 139 it('sets initial expanded state to false if collapse state exists in store', () => { 140 component.useStoredExpandedState = true; 141 const treeComponent = assertDefined(component.treeComponent); 142 // tree expanded by default 143 fixture.detectChanges(); 144 expect(treeComponent.isExpanded()).toBeTrue(); 145 146 // tree collapsed 147 treeComponent.toggleTree(); 148 fixture.detectChanges(); 149 expect(treeComponent.isExpanded()).toBeFalse(); 150 151 // tree collapsed state retained 152 component.tree = makeTree(); 153 fixture.detectChanges(); 154 expect(treeComponent.isExpanded()).toBeFalse(); 155 }); 156 157 it('renders show state button if applicable', () => { 158 expect(htmlElement.querySelector('.toggle-rect-show-state-btn')).toBeNull(); 159 160 component.rectIdToShowState = new Map([ 161 [component.tree.id, RectShowState.HIDE], 162 ]); 163 fixture.detectChanges(); 164 expect( 165 assertDefined(htmlElement.querySelector('.toggle-rect-show-state-btn')) 166 .textContent, 167 ).toContain('visibility_off'); 168 169 component.rectIdToShowState.set(component.tree.id, RectShowState.SHOW); 170 fixture.detectChanges(); 171 expect( 172 assertDefined(htmlElement.querySelector('.toggle-rect-show-state-btn')) 173 .textContent, 174 ).toContain('visibility'); 175 }); 176 177 it('handles show state button click', () => { 178 component.rectIdToShowState = new Map([ 179 [component.tree.id, RectShowState.HIDE], 180 ]); 181 fixture.detectChanges(); 182 const button = assertDefined( 183 htmlElement.querySelector('.toggle-rect-show-state-btn'), 184 ) as HTMLElement; 185 expect(button.textContent).toContain('visibility_off'); 186 187 let id: string | undefined; 188 let state: RectShowState | undefined; 189 htmlElement.addEventListener(ViewerEvents.RectShowStateChange, (event) => { 190 id = (event as CustomEvent).detail.rectId; 191 state = (event as CustomEvent).detail.state; 192 }); 193 button.click(); 194 fixture.detectChanges(); 195 expect(id).toEqual(component.tree.id); 196 expect(state).toEqual(RectShowState.SHOW); 197 }); 198 199 function makeTree() { 200 const children = []; 201 for (let i = 0; i < 80; i++) { 202 children.push({id: i, name: `Child${i}`}); 203 } 204 return UiHierarchyTreeNode.from( 205 new HierarchyTreeBuilder() 206 .setId('RootNode') 207 .setName('Root node') 208 .setChildren(children) 209 .build(), 210 ); 211 } 212 213 @Component({ 214 selector: 'host-component', 215 template: ` 216 <div class="tree-wrapper"> 217 <tree-view 218 [node]="tree" 219 [isFlattened]="isFlattened" 220 [isPinned]="false" 221 [highlightedItem]="highlightedItem" 222 [useStoredExpandedState]="useStoredExpandedState" 223 [itemsClickable]="true" 224 [rectIdToShowState]="rectIdToShowState"></tree-view> 225 </div> 226 `, 227 styles: [ 228 ` 229 .tree-wrapper { 230 height: 500px; 231 overflow: auto; 232 } 233 `, 234 ], 235 }) 236 class TestHostComponent { 237 tree: UiHierarchyTreeNode; 238 highlightedItem = ''; 239 isFlattened = false; 240 useStoredExpandedState = false; 241 rectIdToShowState: Map<string, RectShowState> | undefined; 242 243 constructor() { 244 this.tree = makeTree(); 245 } 246 247 @ViewChild(TreeComponent) 248 treeComponent: TreeComponent | undefined; 249 } 250}); 251