/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {ClipboardModule} from '@angular/cdk/clipboard'; import {CommonModule} from '@angular/common'; import { ComponentFixture, ComponentFixtureAutoDetect, TestBed, } from '@angular/core/testing'; import {FormsModule} from '@angular/forms'; import {MatButtonModule} from '@angular/material/button'; import {MatDividerModule} from '@angular/material/divider'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatIconModule} from '@angular/material/icon'; import {MatInputModule} from '@angular/material/input'; import {MatTooltipModule} from '@angular/material/tooltip'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {assertDefined} from 'common/assert_utils'; import {FilterFlag} from 'common/filter_flag'; import {InMemoryStorage} from 'common/store/in_memory_storage'; import {PersistentStore} from 'common/store/persistent_store'; import {DuplicateLayerIds, MissingLayerIds} from 'messaging/user_warnings'; import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder'; import {UnitTestUtils} from 'test/unit/utils'; import {TraceType} from 'trace/trace_type'; import {TextFilter} from 'viewers/common/text_filter'; import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node'; import {ViewerEvents} from 'viewers/common/viewer_events'; import {HierarchyTreeNodeDataViewComponent} from 'viewers/components/hierarchy_tree_node_data_view_component'; import {TreeComponent} from 'viewers/components/tree_component'; import {TreeNodeComponent} from 'viewers/components/tree_node_component'; import {CollapsibleSectionTitleComponent} from './collapsible_section_title_component'; import {HierarchyComponent} from './hierarchy_component'; import {SearchBoxComponent} from './search_box_component'; import {UserOptionsComponent} from './user_options_component'; describe('HierarchyComponent', () => { let fixture: ComponentFixture; let component: HierarchyComponent; let htmlElement: HTMLElement; beforeEach(async () => { await TestBed.configureTestingModule({ providers: [{provide: ComponentFixtureAutoDetect, useValue: true}], declarations: [ HierarchyComponent, TreeComponent, TreeNodeComponent, HierarchyTreeNodeDataViewComponent, CollapsibleSectionTitleComponent, UserOptionsComponent, SearchBoxComponent, ], imports: [ CommonModule, MatButtonModule, MatDividerModule, MatInputModule, MatFormFieldModule, BrowserAnimationsModule, FormsModule, MatIconModule, MatTooltipModule, ClipboardModule, ], }).compileComponents(); fixture = TestBed.createComponent(HierarchyComponent); component = fixture.componentInstance; htmlElement = fixture.nativeElement; component.trees = [ UiHierarchyTreeNode.from( new HierarchyTreeBuilder() .setId('RootNode1') .setName('Root node') .setChildren([{id: 'Child1', name: 'Child node'}]) .build(), ), ]; component.store = new PersistentStore(); component.userOptions = { showDiff: { name: 'Show diff', enabled: false, isUnavailable: false, }, }; component.textFilter = new TextFilter(); component.dependencies = [TraceType.SURFACE_FLINGER]; fixture.detectChanges(); }); it('can be created', () => { expect(component).toBeTruthy(); }); it('renders title', () => { const title = htmlElement.querySelector('.hierarchy-title'); expect(title).toBeTruthy(); }); it('renders view controls', () => { const viewControls = htmlElement.querySelector('.view-controls'); expect(viewControls).toBeTruthy(); const button = htmlElement.querySelector('.view-controls .user-option'); expect(button).toBeTruthy(); //renders at least one view control option }); it('renders initial tree elements', () => { const treeView = htmlElement.querySelector('tree-view'); expect(treeView).toBeTruthy(); expect(assertDefined(treeView).innerHTML).toContain('Root node'); expect(assertDefined(treeView).innerHTML).toContain('Child node'); }); it('renders multiple trees', () => { component.trees = [ component.trees[0], UiHierarchyTreeNode.from( new HierarchyTreeBuilder().setId('subtree').setName('subtree').build(), ), ]; fixture.detectChanges(); const trees = assertDefined( htmlElement.querySelectorAll('.tree-wrapper .tree'), ); expect(trees.length).toEqual(2); expect(trees.item(1).textContent).toContain('subtree'); }); it('renders pinned nodes', () => { const pinnedNodesDiv = htmlElement.querySelector('.pinned-items'); expect(pinnedNodesDiv).toBeFalsy(); component.pinnedItems = assertDefined(component.trees); fixture.detectChanges(); const pinnedNodeEl = htmlElement.querySelector('.pinned-items tree-node'); expect(pinnedNodeEl).toBeTruthy(); }); it('renders placeholder text', () => { component.trees = []; component.placeholderText = 'Placeholder text.'; fixture.detectChanges(); expect( htmlElement.querySelector('.placeholder-text')?.textContent?.trim(), ).toEqual('Placeholder text. Try changing timeline position.'); }); it('handles pinned node click', () => { const node = assertDefined(component.trees[0]); component.pinnedItems = [node]; fixture.detectChanges(); let highlightedItem: UiHierarchyTreeNode | undefined; htmlElement.addEventListener( ViewerEvents.HighlightedNodeChange, (event) => { highlightedItem = (event as CustomEvent).detail.node; }, ); const pinnedNodeEl = assertDefined( htmlElement.querySelector('.pinned-items tree-node'), ); (pinnedNodeEl as HTMLButtonElement).click(); fixture.detectChanges(); expect(highlightedItem).toEqual(node); }); it('handles pinned item change from tree', () => { let pinnedItem: UiHierarchyTreeNode | undefined; htmlElement.addEventListener( ViewerEvents.HierarchyPinnedChange, (event) => { pinnedItem = (event as CustomEvent).detail.pinnedItem; }, ); const child = assertDefined( component.trees[0].getChildByName('Child node'), ); component.pinnedItems = [child]; fixture.detectChanges(); const pinButton = assertDefined( htmlElement.querySelector('.pinned-items tree-node .pin-node-btn'), ); (pinButton as HTMLButtonElement).click(); fixture.detectChanges(); expect(pinnedItem).toEqual(child); }); it('handles change in filter', () => { let textFilter: TextFilter | undefined; htmlElement.addEventListener( ViewerEvents.HierarchyFilterChange, (event) => { textFilter = (event as CustomEvent).detail; }, ); const inputEl = assertDefined( htmlElement.querySelector('.title-section input'), ); const flagButton = assertDefined( htmlElement.querySelector('.search-box button'), ); flagButton.click(); fixture.detectChanges(); inputEl.value = 'Root'; inputEl.dispatchEvent(new Event('input')); fixture.detectChanges(); expect(textFilter).toEqual(new TextFilter('Root', [FilterFlag.MATCH_CASE])); }); it('handles collapse button click', () => { const spy = spyOn(component.collapseButtonClicked, 'emit'); const collapseButton = assertDefined( htmlElement.querySelector('collapsible-section-title button'), ) as HTMLButtonElement; collapseButton.click(); fixture.detectChanges(); expect(spy).toHaveBeenCalled(); }); it('shows warnings from all trees', () => { expect(htmlElement.querySelectorAll('.warning').length).toEqual(0); component.trees = [ component.trees[0], UiHierarchyTreeNode.from(component.trees[0]), ]; fixture.detectChanges(); const warning1 = new DuplicateLayerIds([123]); component.trees[0].addWarning(warning1); const warning2 = new MissingLayerIds(); component.trees[1].addWarning(warning2); fixture.detectChanges(); const warnings = htmlElement.querySelectorAll('.warning'); expect(warnings.length).toEqual(2); expect(warnings[0].textContent?.trim()).toEqual( 'warning ' + warning1.getMessage(), ); expect(warnings[1].textContent?.trim()).toEqual( 'warning ' + warning2.getMessage(), ); }); it('shows warning tooltip if text overflowing', () => { const warning = new DuplicateLayerIds([123]); component.trees[0].addWarning(warning); fixture.detectChanges(); const warningEl = assertDefined(htmlElement.querySelector('.warning')); const msgEl = assertDefined(warningEl.querySelector('.warning-message')); const spy = spyOnProperty(msgEl, 'scrollWidth').and.returnValue( msgEl.clientWidth, ); UnitTestUtils.checkTooltips([warningEl], [undefined], fixture); spy.and.returnValue(msgEl.clientWidth + 1); fixture.detectChanges(); UnitTestUtils.checkTooltips([warningEl], [warning.getMessage()], fixture); }); it('handles arrow down key press', () => { testArrowKeyPress(ViewerEvents.ArrowDownPress, 'ArrowDown'); }); it('handles arrow up key press', () => { testArrowKeyPress(ViewerEvents.ArrowUpPress, 'ArrowUp'); }); function testArrowKeyPress(viewerEvent: string, key: string) { let storage: InMemoryStorage | undefined; htmlElement.addEventListener(viewerEvent, (event) => { storage = (event as CustomEvent).detail; }); const event = new KeyboardEvent('keydown', {key}); document.dispatchEvent(event); expect(storage).toEqual(component.treeStorage); storage = undefined; htmlElement.style.height = '0px'; fixture.detectChanges(); document.dispatchEvent(event); expect(storage).toBeUndefined(); } });