/// /// 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 { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { dispatchMouseEvent } from '../../common/testing/index'; import { TooltipModule } from '../../components/tooltip/index'; import { AccessibilityModule } from '../../directives/accessibility/index'; import { ResizeModule } from '../../directives/resize/index'; import { ColorServiceModule, colorSets } from '../../services/color'; import { SankeyLink } from './interfaces/link.interface'; import { SankeyNode } from './interfaces/node.interface'; import { SankeyNodeDirective } from './sankey-chart-node.directive'; import { SankeyChartComponent } from './sankey-chart.component'; import { SankeyChartModule } from './sankey-chart.module'; @Component({ selector: 'app-sankey-chart', template: ` {{ node.data.name }} - {{ active }} `, styles: [ ` ux-sankey-chart { width: 800px; height: 800px; } `, ], imports: [AccessibilityModule, CommonModule, TooltipModule, ResizeModule, SankeyChartModule], }) export class SankeyChartTestComponent { nodes: ReadonlyArray> = [ { id: SankeyNodeId.FileSystem, data: { name: 'File System Windows', files: 2_100_000, space: 23_100_000_000 }, }, { id: SankeyNodeId.Exchange, data: { name: 'Exchange', files: 3_700_000, space: 40_700_000_000 }, }, { id: SankeyNodeId.Office365, data: { name: 'Office 365 Exchange', files: 1_600_000, space: 17_600_000_000 }, }, { id: SankeyNodeId.SharePoint, data: { name: 'Sharepoint', files: 5_300_000, space: 58_300_000_000 }, }, { id: SankeyNodeId.Text, data: { name: 'Text', files: 2_300_000, space: 18_700_000_000 }, }, { id: SankeyNodeId.Audio, data: { name: 'Audio', files: 1_300_000, space: 17_600_000_000 }, }, { id: SankeyNodeId.Video, data: { name: 'Video', files: 2_500_000, space: 23_100_000_000 }, }, { id: SankeyNodeId.Entity, data: { name: 'Entity', files: 2_950_000, space: 22_000_000_000 }, }, { id: SankeyNodeId.Metadata, data: { name: 'Metadata', files: 1_450_000, space: 12_100_000_000 }, }, { id: SankeyNodeId.Other, data: { name: 'Other', files: 2_200_000, space: 20_900_000_000 }, }, { id: SankeyNodeId.EmployeeData, data: { name: 'Employee data', files: 3_030_000, space: 23_100_000_000 }, }, { id: SankeyNodeId.HealthData, data: { name: 'Health data', files: 1_400_000, space: 17_600_000_000 }, }, { id: SankeyNodeId.FinancialData, data: { name: 'Financial', files: 2_540_000, space: 17_600_000_000 }, }, { id: SankeyNodeId.PersonalData, data: { name: 'Personal', files: 2_480_000, space: 20_900_000_000 }, }, { id: SankeyNodeId.OtherData, data: { name: 'Other', files: 2_800_000, space: 19_800_000_000 }, }, ]; links: ReadonlyArray = [ { source: SankeyNodeId.FileSystem, target: SankeyNodeId.Video, value: 950_000 }, { source: SankeyNodeId.FileSystem, target: SankeyNodeId.Metadata, value: 200_000 }, { source: SankeyNodeId.FileSystem, target: SankeyNodeId.Other, value: 950_000 }, { source: SankeyNodeId.Exchange, target: SankeyNodeId.Text, value: 2_000_000 }, { source: SankeyNodeId.Exchange, target: SankeyNodeId.Entity, value: 1_700_000 }, { source: SankeyNodeId.Office365, target: SankeyNodeId.Text, value: 300_000 }, { source: SankeyNodeId.Office365, target: SankeyNodeId.Audio, value: 1_000_000 }, { source: SankeyNodeId.Office365, target: SankeyNodeId.Other, value: 300_000 }, { source: SankeyNodeId.SharePoint, target: SankeyNodeId.Audio, value: 300_000 }, { source: SankeyNodeId.SharePoint, target: SankeyNodeId.Video, value: 1_250_000 }, { source: SankeyNodeId.SharePoint, target: SankeyNodeId.Entity, value: 1_250_000 }, { source: SankeyNodeId.SharePoint, target: SankeyNodeId.Metadata, value: 1_250_000 }, { source: SankeyNodeId.SharePoint, target: SankeyNodeId.Other, value: 1_250_000 }, { source: SankeyNodeId.Text, target: SankeyNodeId.EmployeeData, value: 1_150_000 }, { source: SankeyNodeId.Text, target: SankeyNodeId.FinancialData, value: 920_000 }, { source: SankeyNodeId.Text, target: SankeyNodeId.PersonalData, value: 230_000 }, { source: SankeyNodeId.Audio, target: SankeyNodeId.HealthData, value: 520_000 }, { source: SankeyNodeId.Audio, target: SankeyNodeId.OtherData, value: 780_000 }, { source: SankeyNodeId.Video, target: SankeyNodeId.PersonalData, value: 2_250_000 }, { source: SankeyNodeId.Video, target: SankeyNodeId.OtherData, value: 250_000 }, { source: SankeyNodeId.Entity, target: SankeyNodeId.OtherData, value: 1_770_000 }, { source: SankeyNodeId.Entity, target: SankeyNodeId.FinancialData, value: 1_180_000 }, { source: SankeyNodeId.Metadata, target: SankeyNodeId.EmployeeData, value: 1_000_000 }, { source: SankeyNodeId.Other, target: SankeyNodeId.HealthData, value: 880_000 }, { source: SankeyNodeId.Other, target: SankeyNodeId.EmployeeData, value: 880_000 }, { source: SankeyNodeId.Other, target: SankeyNodeId.FinancialData, value: 440_000 }, ]; columns: string[] = ['Repositories', 'Extraction', 'Classification']; } export interface SankeyNodeData { name: string; files: number; space: number; } export enum SankeyNodeId { FileSystem, Exchange, Office365, SharePoint, Text, Audio, Video, Entity, Metadata, Other, EmployeeData, HealthData, FinancialData, PersonalData, OtherData, } describe('Sankey Chart Component', () => { let component: SankeyChartTestComponent; let fixture: ComponentFixture; let element: HTMLElement; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ AccessibilityModule, CommonModule, TooltipModule, ResizeModule, BrowserAnimationsModule, ColorServiceModule.forRoot(colorSets.keppel), SankeyChartTestComponent, SankeyChartComponent, SankeyNodeDirective, SankeyChartModule, ], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(SankeyChartTestComponent); component = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); }); it('should create the chart', () => { expect(component).toBeTruthy(); }); it('should show the correct column headings', () => { const titles = element.querySelectorAll('.ux-sankey-chart-column-title'); expect(titles.length).toBe(component.columns.length); for (let idx = 0; idx < titles.length; idx++) { expect(titles.item(idx).innerText).toBe(component.columns[idx]); } }); it('should display the correct number of nodes', () => { const nodes = element.querySelectorAll('.ux-sankey-chart-node'); expect(nodes.length).toBe(15); }); it('should display the correct number of links', () => { const links = element.querySelectorAll('.ux-sankey-chart-link'); expect(links.length).toBe(26); }); it('should display the correct node content', () => { const nodes = element.querySelectorAll('.node-state'); expect(nodes.length).toBe(15); for (let idx = 0; idx < nodes.length; idx++) { const node = nodes.item(idx); expect((node as HTMLElement).innerText).toBe(component.nodes[idx].data.name); } }); it('should not show any nodes or links initially active', () => { const activeNodes = element.querySelectorAll('.ux-sankey-chart-node-active'); const activeLinks = element.querySelectorAll('.ux-sankey-chart-link-active'); expect(activeNodes.length).toBe(0); expect(activeLinks.length).toBe(0); }); it('should show active state on node hover', () => { // hover over the first node const node = element.querySelectorAll('.ux-sankey-chart-node').item(0); dispatchMouseEvent(node, 'mouseenter'); // ensure that the node is active expect(node.classList.contains('ux-sankey-chart-node-active')).toBeTruthy(); // the correct number of links and nodes should now be active const activeNodes = element.querySelectorAll('.ux-sankey-chart-node-active'); const activeLinks = element.querySelectorAll('.ux-sankey-chart-link-active'); expect(activeNodes.length).toBe(4); expect(activeLinks.length).toBe(3); }); it('should remove active state when node hover leaves', () => { // hover over the first node const node = element.querySelectorAll('.ux-sankey-chart-node').item(0); dispatchMouseEvent(node, 'mouseenter'); // the correct number of links and nodes should now be active let activeNodes = element.querySelectorAll('.ux-sankey-chart-node-active'); let activeLinks = element.querySelectorAll('.ux-sankey-chart-link-active'); expect(activeNodes.length).toBe(4); expect(activeLinks.length).toBe(3); dispatchMouseEvent(node, 'mouseleave'); // the correct number of links and nodes should now be active activeNodes = element.querySelectorAll('.ux-sankey-chart-node-active'); activeLinks = element.querySelectorAll('.ux-sankey-chart-link-active'); expect(activeNodes.length).toBe(0); expect(activeLinks.length).toBe(0); }); it('should show active state on link hover', () => { // hover over the first link const link = element.querySelectorAll('.ux-sankey-chart-link').item(0); dispatchMouseEvent(link, 'mouseenter'); // ensure that the link is active expect(link.classList.contains('ux-sankey-chart-link-active')).toBeTruthy(); // the correct number of links and nodes should now be active const activeNodes = element.querySelectorAll('.ux-sankey-chart-node-active'); const activeLinks = element.querySelectorAll('.ux-sankey-chart-link-active'); expect(activeNodes.length).toBe(2); expect(activeLinks.length).toBe(1); }); it('should not active state when link hover leaves', () => { // hover over the first link const link = element.querySelectorAll('.ux-sankey-chart-link').item(0); dispatchMouseEvent(link, 'mouseenter'); // the correct number of links and nodes should now be active let activeNodes = element.querySelectorAll('.ux-sankey-chart-node-active'); let activeLinks = element.querySelectorAll('.ux-sankey-chart-link-active'); expect(activeNodes.length).toBe(2); expect(activeLinks.length).toBe(1); dispatchMouseEvent(link, 'mouseleave'); // the correct number of links and nodes should now be active activeNodes = element.querySelectorAll('.ux-sankey-chart-node-active'); activeLinks = element.querySelectorAll('.ux-sankey-chart-link-active'); expect(activeNodes.length).toBe(0); expect(activeLinks.length).toBe(0); }); it('should update TemplateRef context on node hover', () => { // hover over the first node const node = element.querySelectorAll('.ux-sankey-chart-node').item(0); const activeState = node.querySelector('.active-state') as HTMLElement; expect(activeState.innerText).toBe('false'); dispatchMouseEvent(node, 'mouseenter'); expect(activeState.innerText).toBe('true'); }); it('should update TemplateRef context on link hover', () => { // hover over the first link const node = element.querySelectorAll('.ux-sankey-chart-node').item(0); const link = element.querySelectorAll('.ux-sankey-chart-link').item(0); const activeState = node.querySelector('.active-state') as HTMLElement; expect(activeState.innerText).toBe('false'); dispatchMouseEvent(link, 'mouseenter'); expect(activeState.innerText).toBe('true'); }); it('should not intially show a tooltip', () => { const tooltip = element.querySelector('ux-tooltip'); expect(tooltip).toBeFalsy(); }); it('should show a tooltip on link hover', () => { const link = element.querySelectorAll('.ux-sankey-chart-link').item(0); dispatchMouseEvent(link, 'mouseenter'); const tooltip = element.querySelector('ux-tooltip'); expect(tooltip).toBeTruthy(); }); it('should show a the correct tooltip content on link hover', () => { const link = element.querySelectorAll('.ux-sankey-chart-link').item(0); dispatchMouseEvent(link, 'mouseenter'); const tooltip = element.querySelector('ux-tooltip') as HTMLElement; expect(tooltip.innerText).toBe('950,000 items'); }); it('should show a falloff indicator when the outputs are less than the inputs', () => { const falloff = element.querySelectorAll('.ux-sankey-chart-falloff-indicator'); expect(falloff.length).toBe(2); }); it('should show a tooltip on falloff hover', () => { const falloff = element.querySelectorAll('.ux-sankey-chart-falloff-indicator').item(0); dispatchMouseEvent(falloff, 'mouseenter'); const tooltip = element.querySelector('ux-tooltip'); expect(tooltip).toBeTruthy(); }); it('should show a the correct tooltip content', () => { const falloff = element.querySelectorAll('.ux-sankey-chart-falloff-indicator').item(0); dispatchMouseEvent(falloff, 'mouseenter'); const tooltip = element.querySelector('ux-tooltip') as HTMLElement; expect(tooltip.innerText).toBe('450,000 items'); }); it('should resize nodes when container is resized', done => { const chart = element.querySelector('ux-sankey-chart') as HTMLElement; const node = element.querySelectorAll('.ux-sankey-chart-node').item(0) as HTMLElement; expect(node.offsetWidth).toBe(150); chart.style.width = '900px'; setTimeout(() => { expect(node.offsetWidth).toBe(170); done(); }, 100); }); }); @Component({ selector: 'app-sankey-chart-min-height', template: ` {{ node.data.name }} - {{ active }} `, styles: [ ` ux-sankey-chart { width: 800px; height: 800px; } .chart { width: 800px; height: 800px; } `, ], imports: [AccessibilityModule, CommonModule, TooltipModule, ResizeModule, SankeyChartModule], }) export class SankeyChartMinHeightTestComponent { nodes: ReadonlyArray> = [ { id: 0, data: { name: 'Node0' }, }, { id: 1, data: { name: 'Node1' }, }, { id: 2, data: { name: 'Node2' }, }, { id: 3, data: { name: 'Node3' }, }, { id: 4, data: { name: 'Node4' }, }, { id: 5, data: { name: 'Node5' }, }, { id: 6, data: { name: 'Node6' }, }, ]; links: ReadonlyArray = [ { source: 0, target: 6, value: 100 }, { source: 1, target: 6, value: 200 }, { source: 2, target: 6, value: 400 }, { source: 3, target: 6, value: 800 }, { source: 4, target: 6, value: 1600 }, { source: 5, target: 6, value: 3200 }, ]; columns: string[] = ['Column A', 'Column B']; minHeight = 0; getNodeHeight(parentElement: HTMLElement, id: string): number { const element = parentElement.querySelector(`#${id}`).closest('.ux-sankey-chart-node'); return element.clientHeight; } } describe('Sankey Chart with minHeight', () => { let component: SankeyChartMinHeightTestComponent; let fixture: ComponentFixture; let element: HTMLElement; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ AccessibilityModule, CommonModule, TooltipModule, ResizeModule, BrowserAnimationsModule, ColorServiceModule.forRoot(colorSets.keppel), SankeyChartMinHeightTestComponent, SankeyChartComponent, SankeyNodeDirective, ], }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(SankeyChartMinHeightTestComponent); component = fixture.componentInstance; element = fixture.nativeElement; fixture.detectChanges(); }); it('should size nodes with minHeight = 0', () => { expect(component.getNodeHeight(element, 'node0')).toBe(0, 'node0'); expect(component.getNodeHeight(element, 'node1')).toBe(7, 'node1'); expect(component.getNodeHeight(element, 'node2')).toBe(31, 'node2'); expect(component.getNodeHeight(element, 'node3')).toBeWithinRange(77, 78, 'node3'); expect(component.getNodeHeight(element, 'node4')).toBeWithinRange(170, 171, 'node4'); expect(component.getNodeHeight(element, 'node5')).toBe(358, 'node5'); expect(component.getNodeHeight(element, 'node6')).toBe(720, 'node6'); }); it('should size nodes with minHeight = 30', () => { component.minHeight = 30; fixture.detectChanges(); expect(component.getNodeHeight(element, 'node0')).toBe(28, 'node0'); expect(component.getNodeHeight(element, 'node1')).toBe(28, 'node1'); expect(component.getNodeHeight(element, 'node2')).toBe(28, 'node2'); expect(component.getNodeHeight(element, 'node3')).toBe(71, 'node3'); expect(component.getNodeHeight(element, 'node4')).toBe(157, 'node4'); expect(component.getNodeHeight(element, 'node5')).toBe(328, 'node5'); expect(component.getNodeHeight(element, 'node6')).toBe(661, 'node6'); }); it('should size nodes with minHeight = 100', () => { component.minHeight = 100; fixture.detectChanges(); expect(component.getNodeHeight(element, 'node0')).toBe(98, 'node0'); expect(component.getNodeHeight(element, 'node1')).toBe(98, 'node1'); expect(component.getNodeHeight(element, 'node2')).toBe(98, 'node2'); expect(component.getNodeHeight(element, 'node3')).toBe(98, 'node3'); expect(component.getNodeHeight(element, 'node4')).toBe(98, 'node4'); expect(component.getNodeHeight(element, 'node5')).toBe(150, 'node5'); expect(component.getNodeHeight(element, 'node6')).toBe(303, 'node6'); }); it('should size nodes with minHeight = 108', () => { component.minHeight = 108; fixture.detectChanges(); expect(component.getNodeHeight(element, 'node0')).toBe(106, 'node0'); expect(component.getNodeHeight(element, 'node1')).toBe(106, 'node1'); expect(component.getNodeHeight(element, 'node2')).toBe(106, 'node2'); expect(component.getNodeHeight(element, 'node3')).toBe(106, 'node3'); expect(component.getNodeHeight(element, 'node4')).toBe(106, 'node4'); expect(component.getNodeHeight(element, 'node5')).toBe(110, 'node5'); expect(component.getNodeHeight(element, 'node6')).toBe(223, 'node6'); }); it('should fail to size nodes with minHeight = 109, and fall back to minHeight = 0', () => { component.minHeight = 109; fixture.detectChanges(); expect(component.getNodeHeight(element, 'node0')).toBe(0, 'node0'); expect(component.getNodeHeight(element, 'node1')).toBe(7, 'node1'); expect(component.getNodeHeight(element, 'node2')).toBe(31, 'node2'); expect(component.getNodeHeight(element, 'node3')).toBe(77, 'node3'); expect(component.getNodeHeight(element, 'node4')).toBe(171, 'node4'); expect(component.getNodeHeight(element, 'node5')).toBe(358, 'node5'); expect(component.getNodeHeight(element, 'node6')).toBe(720, 'node6'); }); });