| /* |
| * 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 {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 {checkTooltips, DOMTestHelper} from 'test/unit/dom_test_utils'; |
| import {HierarchyTreeBuilder} from 'test/unit/hierarchy_tree_builder'; |
| import {TRACE_INFO} from 'trace_api/trace_info'; |
| import {TraceType} from 'trace_api/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 component: HierarchyComponent; |
| let dom: DOMTestHelper<HierarchyComponent>; |
| |
| beforeEach(async () => { |
| await TestBed.configureTestingModule({ |
| providers: [{provide: ComponentFixtureAutoDetect, useValue: true}], |
| imports: [ |
| HierarchyComponent, |
| HierarchyTreeNodeDataViewComponent, |
| CollapsibleSectionTitleComponent, |
| UserOptionsComponent, |
| SearchBoxComponent, |
| TreeComponent, |
| TreeNodeComponent, |
| CommonModule, |
| MatButtonModule, |
| MatDividerModule, |
| MatInputModule, |
| MatFormFieldModule, |
| BrowserAnimationsModule, |
| FormsModule, |
| MatIconModule, |
| MatTooltipModule, |
| ClipboardModule, |
| ], |
| }).compileComponents(); |
| const fixture = TestBed.createComponent(HierarchyComponent); |
| component = fixture.componentInstance; |
| dom = new DOMTestHelper(fixture, 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]; |
| |
| dom.detectChanges(); |
| }); |
| |
| it('can be created', () => { |
| expect(component).toBeTruthy(); |
| }); |
| |
| it('renders title', () => { |
| expect(dom.find('.hierarchy-title')).toBeDefined(); |
| }); |
| |
| it('renders view controls', () => { |
| expect(dom.find('.view-controls')).toBeDefined(); |
| expect(dom.find('.view-controls .user-option')).toBeDefined(); //renders at least one view control option |
| }); |
| |
| it('renders initial tree elements', () => { |
| const treeView = dom.get('tree-view'); |
| treeView.checkText('Root node'); |
| treeView.checkText('Child node'); |
| }); |
| |
| it('renders multiple trees', () => { |
| component.trees = [ |
| component.trees[0], |
| UiHierarchyTreeNode.from( |
| new HierarchyTreeBuilder().setId('subtree').setName('subtree').build(), |
| ), |
| ]; |
| dom.detectChanges(); |
| const trees = dom.findAll('.tree-wrapper .tree'); |
| expect(trees.length).toBe(2); |
| trees[1].checkText('subtree'); |
| }); |
| |
| it('renders pinned nodes', () => { |
| expect(dom.find('.pinned-items')).toBeUndefined(); |
| component.pinnedItems = assertDefined(component.trees); |
| dom.detectChanges(); |
| expect(dom.find('.pinned-items tree-node')).toBeDefined(); |
| }); |
| |
| it('renders placeholder text', () => { |
| component.trees = []; |
| component.placeholderText = 'Placeholder text.'; |
| dom.detectChanges(); |
| |
| const placeholderText = dom.get('.placeholder-text'); |
| placeholderText.checkTextExact( |
| 'Placeholder text.' + |
| ` There may be no ${ |
| TRACE_INFO[component.dependencies[0]].name |
| } state associated with the current state in the active trace.` + |
| ' Try changing timeline position.', |
| ); |
| |
| component.dependencies = []; |
| dom.detectChanges(); |
| placeholderText.checkTextExact( |
| 'Placeholder text.' + |
| ' There may be no state for this trace associated with the current state in the active trace.' + |
| ' Try changing timeline position.', |
| ); |
| }); |
| |
| it('handles pinned node click', () => { |
| const node = assertDefined(component.trees[0]); |
| component.pinnedItems = [node]; |
| dom.detectChanges(); |
| |
| let highlightedItem: UiHierarchyTreeNode | undefined; |
| dom.addEventListener(ViewerEvents.HighlightedNodeChange, (event) => { |
| highlightedItem = (event as CustomEvent).detail.node; |
| }); |
| |
| dom.findAndClick('.pinned-items tree-node'); |
| expect(highlightedItem).toEqual(node); |
| }); |
| |
| it('handles pinned item change from tree', () => { |
| let pinnedItem: UiHierarchyTreeNode | undefined; |
| dom.addEventListener(ViewerEvents.HierarchyPinnedChange, (event) => { |
| pinnedItem = (event as CustomEvent).detail.pinnedItem; |
| }); |
| const child = assertDefined( |
| component.trees[0].getChildByName('Child node'), |
| ); |
| component.pinnedItems = [child]; |
| dom.detectChanges(); |
| |
| dom.findAndClick('.pinned-items tree-node .pin-node-btn'); |
| expect(pinnedItem).toEqual(child); |
| }); |
| |
| it('handles change in filter', () => { |
| let textFilter: TextFilter | undefined; |
| dom.addEventListener(ViewerEvents.HierarchyFilterChange, (event) => { |
| textFilter = (event as CustomEvent).detail; |
| }); |
| dom.findAndClick('.search-box button'); |
| dom.findAndDispatchInput('.title-section', 'Root'); |
| expect(textFilter).toEqual(new TextFilter('Root', [FilterFlag.MATCH_CASE])); |
| }); |
| |
| it('handles collapse button click', () => { |
| const spy = spyOn(component.collapseButtonClicked, 'emit'); |
| dom.findAndClick('collapsible-section-title button'); |
| expect(spy).toHaveBeenCalled(); |
| }); |
| |
| it('shows warnings from all trees', () => { |
| expect(dom.find('.warning')).toBeUndefined(); |
| |
| component.trees = [ |
| component.trees[0], |
| UiHierarchyTreeNode.from(component.trees[0]), |
| ]; |
| dom.detectChanges(); |
| const warning1 = new DuplicateLayerIds([123]); |
| component.trees[0].addWarning(warning1); |
| const warning2 = new MissingLayerIds(); |
| component.trees[1].addWarning(warning2); |
| dom.detectChanges(); |
| const warnings = dom.findAll('.warning'); |
| expect(warnings.length).toBe(2); |
| warnings[0].checkTextExact('warning ' + warning1.getMessage()); |
| warnings[1].checkTextExact('warning ' + warning2.getMessage()); |
| }); |
| |
| it('shows warning tooltip if text overflowing', () => { |
| const warning = new DuplicateLayerIds([123]); |
| component.trees[0].addWarning(warning); |
| dom.detectChanges(); |
| |
| const warningEl = dom.get('.warning'); |
| const msgEl = dom.get('.warning-message').getHTMLElement(); |
| |
| const spy = spyOnProperty(msgEl, 'scrollWidth').and.returnValue( |
| msgEl.clientWidth, |
| ); |
| checkTooltips([warningEl], [undefined]); |
| |
| spy.and.returnValue(msgEl.clientWidth + 1); |
| dom.detectChanges(); |
| checkTooltips([warningEl], [warning.getMessage()]); |
| }); |
| |
| it('handles arrow down key press', () => { |
| testArrowKeyPress(ViewerEvents.ArrowDownPress); |
| }); |
| |
| it('handles arrow up key press', () => { |
| testArrowKeyPress(ViewerEvents.ArrowUpPress); |
| }); |
| |
| function testArrowKeyPress(viewerEvent: string) { |
| let storage: InMemoryStorage | undefined; |
| dom.addEventListener(viewerEvent, (event) => { |
| storage = (event as CustomEvent).detail; |
| }); |
| let keydown: () => void; |
| if (viewerEvent === ViewerEvents.ArrowDownPress) { |
| keydown = () => dom.keydownArrowDown(true); |
| } else { |
| keydown = () => dom.keydownArrowUp(true); |
| } |
| keydown(); |
| expect(storage).toEqual(component.treeStorage); |
| |
| storage = undefined; |
| dom.getHTMLElement().style.height = '0px'; |
| dom.detectChanges(); |
| keydown(); |
| expect(storage).toBeUndefined(); |
| } |
| }); |