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, 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 {PropertyTreeBuilder} from 'test/unit/property_tree_builder'; 25import {DEFAULT_PROPERTY_FORMATTER} from 'trace/tree_node/formatters'; 26import {DiffType} from 'viewers/common/diff_type'; 27import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node'; 28import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node'; 29import {HierarchyTreeNodeDataViewComponent} from './hierarchy_tree_node_data_view_component'; 30import {PropertyTreeNodeDataViewComponent} from './property_tree_node_data_view_component'; 31import {TreeNodeComponent} from './tree_node_component'; 32 33describe('TreeNodeComponent', () => { 34 let fixture: ComponentFixture<TestHostComponent>; 35 let component: TestHostComponent; 36 let htmlElement: HTMLElement; 37 let mockCopyText: jasmine.Spy; 38 39 const propertiesTree = UiPropertyTreeNode.from( 40 new PropertyTreeBuilder() 41 .setRootId('test') 42 .setName('property tree') 43 .setChildren([ 44 {name: 'key1', value: 'value1', formatter: DEFAULT_PROPERTY_FORMATTER}, 45 {name: 'key2', children: [{name: 'key3'}]}, 46 ]) 47 .build(), 48 ); 49 propertiesTree.setIsRoot(true); 50 51 beforeEach(async () => { 52 mockCopyText = jasmine.createSpy(); 53 await TestBed.configureTestingModule({ 54 providers: [{provide: Clipboard, useValue: {copy: mockCopyText}}], 55 declarations: [ 56 TreeNodeComponent, 57 HierarchyTreeNodeDataViewComponent, 58 PropertyTreeNodeDataViewComponent, 59 TestHostComponent, 60 ], 61 imports: [MatIconModule, MatTooltipModule, ClipboardModule], 62 }).compileComponents(); 63 fixture = TestBed.createComponent(TestHostComponent); 64 component = fixture.componentInstance; 65 htmlElement = fixture.nativeElement; 66 fixture.detectChanges(); 67 }); 68 69 it('can be created', () => { 70 expect(component).toBeTruthy(); 71 }); 72 73 it('can generate hierarchy data view component', () => { 74 const treeNodeDataView = htmlElement.querySelector( 75 'hierarchy-tree-node-data-view', 76 ); 77 expect(treeNodeDataView).toBeTruthy(); 78 expect( 79 htmlElement.querySelector('property-tree-node-data-view'), 80 ).toBeNull(); 81 }); 82 83 it('can generate property data view component', () => { 84 component.node = propertiesTree; 85 fixture.detectChanges(); 86 const treeNodeDataView = htmlElement.querySelector( 87 'property-tree-node-data-view', 88 ); 89 expect(treeNodeDataView).toBeTruthy(); 90 expect( 91 htmlElement.querySelector('hierarchy-tree-node-data-view'), 92 ).toBeNull(); 93 }); 94 95 it('can trigger tree toggle on click of chevron', () => { 96 const treeNodeComponent = assertDefined(component.treeNodeComponent); 97 treeNodeComponent.showChevron = jasmine.createSpy().and.returnValue(true); 98 fixture.detectChanges(); 99 100 const spy = spyOn(treeNodeComponent.toggleTreeChange, 'emit'); 101 const toggleButton = assertDefined( 102 htmlElement.querySelector<HTMLElement>('.toggle-tree-btn'), 103 ); 104 toggleButton.click(); 105 expect(spy).toHaveBeenCalled(); 106 }); 107 108 it('can trigger tree expansion on click of expand tree button', () => { 109 const spy = spyOn( 110 assertDefined(component.treeNodeComponent).expandTreeChange, 111 'emit', 112 ); 113 const expandButton = assertDefined( 114 htmlElement.querySelector<HTMLElement>('.expand-tree-btn'), 115 ); 116 expandButton.click(); 117 expect(spy).toHaveBeenCalled(); 118 }); 119 120 it('can trigger tree expansion if node is selected and not in pinned section', () => { 121 const spy = spyOn( 122 assertDefined(component.treeNodeComponent).expandTreeChange, 123 'emit', 124 ); 125 component.isInPinnedSection = true; 126 component.isSelected = true; 127 fixture.detectChanges(); 128 expect(spy).not.toHaveBeenCalled(); 129 130 component.isSelected = false; 131 component.isInPinnedSection = false; 132 fixture.detectChanges(); 133 component.isSelected = true; 134 fixture.detectChanges(); 135 expect(spy).toHaveBeenCalledTimes(1); 136 }); 137 138 it('assigns diff css classes to expand tree button', () => { 139 const expandButton = assertDefined( 140 htmlElement.querySelector<HTMLElement>('.expand-tree-btn'), 141 ); 142 expect(expandButton.className).toEqual('icon-button expand-tree-btn'); 143 component.node = UiHierarchyTreeNode.from( 144 new HierarchyTreeBuilder() 145 .setId('LayerTraceEntry') 146 .setName('Added Diff') 147 .setChildren([ 148 {id: 1, name: 'Child 1', children: [{id: 2, name: 'Child 2'}]}, 149 ]) 150 .build(), 151 ); 152 component.node.getChildByName('Child 1')?.setDiff(DiffType.ADDED); 153 fixture.detectChanges(); 154 expect(expandButton.className).toEqual('icon-button expand-tree-btn added'); 155 156 component.node = UiHierarchyTreeNode.from( 157 new HierarchyTreeBuilder() 158 .setId('LayerTraceEntry') 159 .setName('Added Diff') 160 .setChildren([ 161 {id: 1, name: 'Child 1', children: [{id: 2, name: 'Child 2'}]}, 162 ]) 163 .build(), 164 ); 165 const child1 = assertDefined(component.node.getChildByName('Child 1')); 166 child1.setDiff(DiffType.ADDED); 167 child1.getChildByName('Child 2')?.setDiff(DiffType.DELETED); 168 fixture.detectChanges(); 169 expect(expandButton.className).toEqual( 170 'icon-button expand-tree-btn modified', 171 ); 172 }); 173 174 it('pins node on click', () => { 175 const treeNodeComponent = assertDefined(component.treeNodeComponent); 176 treeNodeComponent.showPinNodeIcon = jasmine 177 .createSpy() 178 .and.returnValue(true); 179 fixture.detectChanges(); 180 181 const spy = spyOn(treeNodeComponent.pinNodeChange, 'emit'); 182 const pinNodeButton = assertDefined( 183 htmlElement.querySelector<HTMLElement>('.pin-node-btn'), 184 ); 185 pinNodeButton.click(); 186 expect(spy).toHaveBeenCalledWith(component.node as UiHierarchyTreeNode); 187 }); 188 189 it('can trigger rect show state toggle on click of icon', () => { 190 const treeNodeComponent = assertDefined(component.treeNodeComponent); 191 treeNodeComponent.showStateIcon = 'visibility'; 192 fixture.detectChanges(); 193 194 const spy = spyOn(treeNodeComponent.rectShowStateChange, 'emit'); 195 const showStateButton = assertDefined( 196 htmlElement.querySelector<HTMLElement>('.toggle-rect-show-state-btn'), 197 ); 198 showStateButton.click(); 199 expect(spy).toHaveBeenCalled(); 200 }); 201 202 it('does not show copy button for hierarchy tree', () => { 203 expect(htmlElement.querySelector('.icon-wrapper-copy')).toBeNull(); 204 }); 205 206 it('does not show copy button for property tree node that is not leaf or root', () => { 207 component.node = assertDefined(propertiesTree.getChildByName('key2')); 208 fixture.detectChanges(); 209 expect(htmlElement.querySelector('.icon-wrapper-copy')).toBeNull(); 210 }); 211 212 it('copies node name for root of property tree node', () => { 213 component.node = propertiesTree; 214 fixture.detectChanges(); 215 const copyButton = assertDefined( 216 htmlElement.querySelector<HTMLElement>('.icon-wrapper-copy button'), 217 ); 218 copyButton.click(); 219 fixture.detectChanges(); 220 expect(mockCopyText).toHaveBeenCalledWith(propertiesTree.name); 221 }); 222 223 it('copies property name and value for leaf node', () => { 224 component.node = assertDefined(propertiesTree.getChildByName('key1')); 225 component.isLeaf = true; 226 fixture.detectChanges(); 227 const copyButton = assertDefined( 228 htmlElement.querySelector<HTMLElement>('.icon-wrapper-copy button'), 229 ); 230 copyButton.click(); 231 fixture.detectChanges(); 232 expect(mockCopyText).toHaveBeenCalledWith('key1: value1'); 233 }); 234 235 @Component({ 236 selector: 'host-component', 237 template: ` 238 <tree-node 239 [node]="node" 240 [isExpanded]="isExpanded" 241 [isPinned]="false" 242 [isInPinnedSection]="isInPinnedSection" 243 [isSelected]="isSelected" 244 [isLeaf]="isLeaf"></tree-node> 245 `, 246 }) 247 class TestHostComponent { 248 node: UiHierarchyTreeNode | UiPropertyTreeNode = UiHierarchyTreeNode.from( 249 new HierarchyTreeBuilder() 250 .setId('LayerTraceEntry') 251 .setName('4') 252 .setChildren([{id: 1, name: 'Child 1'}]) 253 .build(), 254 ); 255 256 isSelected = false; 257 isLeaf = false; 258 isExpanded = false; 259 isInPinnedSection = false; 260 261 @ViewChild(TreeNodeComponent) 262 treeNodeComponent: TreeNodeComponent | undefined; 263 } 264}); 265