/// /// Copyright 2015-2026 Micro Focus or one of its affiliates. /// /// 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 { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ENTER } from '@angular/cdk/keycodes'; import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { BehaviorSubject } from 'rxjs'; import { dispatchKeyboardEvent, dispatchMouseEvent } from '../../common/testing/index'; import { AccessibilityModule } from '../../directives/accessibility/index'; import { ResizeDimensions, ResizeService } from '../../directives/resize/index'; import { IconModule } from '../icon/index'; import { OrganizationChartComponent, OrganizationChartNode } from './organization-chart.component'; import { OrganizationChartModule } from './organization-chart.module'; export class MockResizeService { addResizeListener(target: HTMLElement): BehaviorSubject { return new BehaviorSubject({ width: target.offsetWidth, height: target.offsetHeight, }); } // eslint-disable-next-line @typescript-eslint/no-empty-function removeResizeListener(_target: HTMLElement): void {} } @Component({ selector: 'app-organization-chart', template: `
{{ data.name }} {{ focused }}
`, imports: [AccessibilityModule, IconModule, CommonModule, OrganizationChartModule], }) export class OrganizationChartTestComponent { dataset: OrganizationChartNode = { id: 0, data: { name: 'Tony Stark', }, children: [ { id: 1, data: { name: 'Carol Danvers', }, }, { id: 2, data: { name: 'Peter Parker', }, }, { id: 3, data: { name: 'Bruce Banner', }, }, ], }; selected: OrganizationChartNode; canReveal: boolean = false; // eslint-disable-next-line @typescript-eslint/no-empty-function onReveal(): void {} } export interface Employee { name: string; } describe('Organization Chart Component', () => { let component: OrganizationChartTestComponent; let fixture: ComponentFixture; let element: HTMLElement; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ AccessibilityModule, IconModule, CommonModule, OrganizationChartTestComponent, OrganizationChartComponent, ], providers: [{ provide: ResizeService, useClass: MockResizeService }], }).compileComponents(); fixture = TestBed.createComponent(OrganizationChartTestComponent); component = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); await fixture.whenStable(); }); afterEach(() => fixture.nativeElement.remove()); it('should create the component', () => { expect(component).toBeTruthy(); }); it('should initially show the correct nodes', () => { const nodes = getNodes(); expect(nodes.length).toBe(1); }); it('should show the correct content in the root node', () => { const [name, isFocused] = getNodeContents(getRootNode()); expect(name).toBe('Tony Stark'); expect(isFocused).toBe(false); }); it('should not show the reveal button when `showReveal` is false', () => { expect(getRevealButton().hasAttribute('hidden')).toBeTruthy(); }); it('should not show the reveal button when `showReveal` is true', () => { component.canReveal = true; fixture.detectChanges(); expect(getRevealButton().hasAttribute('hidden')).toBeFalsy(); }); it('should emit whenever the reveal button is pressed', () => { component.canReveal = true; fixture.detectChanges(); const onRevealSpy = spyOn(component, 'onReveal'); dispatchMouseEvent(getRevealButton(), 'click'); // expect the onReveal function to be called expect(onRevealSpy).toHaveBeenCalledTimes(1); }); it('should select the root node by default', () => { fixture.detectChanges(); expect(getRootNode().classList.contains('ux-organization-chart-node-selected')).toBeTruthy(); expect(component.selected).toEqual(component.dataset); }); it('should expand nodes on click if they have children', async () => { await clickOnNode(getRootNode()); expect(getNodes().length).toBe(4); }); it('should expand nodes on enter if they have children', async () => { getRootNode().focus(); await keydownOnNode(getRootNode(), ENTER); expect(getNodes().length).toBe(4); }); it('should collapse expanded nodes on click', async () => { await clickOnNode(getRootNode()); expect(getNodes().length).toBe(4); await clickOnNode(getRootNode()); expect(getNodes().length).toBe(1); }); it('should collapse expanded nodes on enter key', async () => { getRootNode().focus(); await keydownOnNode(getRootNode(), ENTER); expect(getNodes().length).toBe(4); await keydownOnNode(getRootNode(), ENTER); expect(getNodes().length).toBe(1); }); it('should focus a child node on click', async () => { await clickOnNode(getRootNode()); await clickOnNode(getNodes().item(1)); expect( getNodes().item(1).classList.contains('ux-organization-chart-node-selected') ).toBeTruthy(); expect(component.selected).toEqual(component.dataset.children[0]); }); function getNodes(): NodeListOf { return element.querySelectorAll('.ux-organization-chart-node'); } function getRootNode(): HTMLElement { return getNodes().item(0) as HTMLElement; } function getNodeContents(node: HTMLElement): [string, boolean] { const data: HTMLSpanElement = node.querySelector('.node-name'); const focused: HTMLSpanElement = node.querySelector('.node-focused'); return [data.innerText, coerceBooleanProperty(focused.innerText)]; } function getRevealButton(): HTMLButtonElement | null { return element.querySelector('.ux-organization-chart-reveal'); } function clickOnNode(node: HTMLElement): Promise { return new Promise(resolve => { dispatchMouseEvent(node, 'click'); setTimeout(() => resolve(), 100); }); } function keydownOnNode(node: HTMLElement, keyCode: number): Promise { return new Promise(resolve => { dispatchKeyboardEvent(node, 'keydown', keyCode); setTimeout(() => resolve(), 100); }); } });