/// /// 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 { Component, QueryList, ViewChild, ViewChildren } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DashboardComponent, DashboardOptions } from './dashboard.component'; import { DashboardModule } from './dashboard.module'; import { DashboardLayoutData } from './dashboard.service'; import { DashboardWidgetComponent } from './widget/dashboard-widget.component'; @Component({ selector: 'app-ux-dashboard', template: ` `, imports: [DashboardModule], }) export class DashboardComponentSpec { options: DashboardOptions = { columns: 5, padding: 5, minWidth: 100, rowHeight: 100, emptyRow: true, }; } describe('Dashboard', () => { let component: DashboardComponentSpec; let fixture: ComponentFixture; let nativeElement: HTMLElement; let widgetA: HTMLElement; let widgetB: HTMLElement; let widgetC: HTMLElement; beforeEach(() => { TestBed.configureTestingModule({ imports: [DashboardModule, DashboardComponentSpec], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(DashboardComponentSpec); component = fixture.componentInstance; nativeElement = fixture.nativeElement; widgetA = nativeElement.querySelector('#widget-A'); widgetB = nativeElement.querySelector('#widget-B'); widgetC = nativeElement.querySelector('#widget-C'); fixture.detectChanges(); }); it('should initialise correctly', () => { expect(component).toBeTruthy(); }); it('should stack widgets if columns reduced allowing only one widget per row', async () => { expect(widgetB.style.top).toBe(''); const adjustedOptions = { ...component.options, columns: 3 }; component.options = adjustedOptions; fixture.detectChanges(); await fixture.whenStable(); expect(widgetB.style.top).toBe('100px'); }); it('should increase widget padding if padding option increased', async () => { const adjustedOptions = { ...component.options, padding: 20 }; expect(widgetA.style.padding).toBe('5px'); component.options = adjustedOptions; fixture.detectChanges(); await fixture.whenStable(); expect(widgetA.style.padding).toBe('20px'); }); it('should increase widget rowHeight if rowHeight option increased', async () => { const adjustedOptions = { ...component.options, rowHeight: 400 }; expect(widgetA.style.height).toBe('100px'); component.options = adjustedOptions; fixture.detectChanges(); await fixture.whenStable(); expect(widgetA.style.height).toBe('400px'); }); it('should switch to stacked mode when minWidth increased', async () => { // setup columns option so row one has widgetA and Row two has widgetB/C const columnsSetup = { ...component.options, columns: 4 }; component.options = columnsSetup; fixture.detectChanges(); await fixture.whenStable(); expect(getPixels(widgetC.style.left)).toBeGreaterThan(0); const increasedMinWidth = { ...component.options, minWidth: 3000 }; component.options = increasedMinWidth; fixture.detectChanges(); await fixture.whenStable(); expect(widgetC.style.left).toBe('0px'); }); }); function getPixels(cssValue: string): number { return parseInt(cssValue.replace('px', '')); } @Component({ selector: 'app-ux-dashboard-layout', template: ` `, imports: [DashboardModule], }) export class DashboardWithInitialLayoutTestComponent { options: DashboardOptions = { columns: 3, padding: 10, rowHeight: 300, emptyRow: false, minWidth: 187, }; layout: ReadonlyArray = [ { id: 'run-widget', col: 0, row: 0, colSpan: 2, rowSpan: 1 }, { id: 'purpose-widget', col: 0, row: 2, colSpan: 1, rowSpan: 1 }, { id: 'host-widget', col: 0, row: 3, colSpan: 3, rowSpan: 1 }, ]; @ViewChild(DashboardComponent) dashboard: DashboardComponent; @ViewChildren(DashboardWidgetComponent) widgets: QueryList; // eslint-disable-next-line @typescript-eslint/no-empty-function onLayoutChange(): void {} refreshLayout(): void { this.dashboard.refreshLayout(); } } describe('Dashboard with initial layout', () => { let component: DashboardWithInitialLayoutTestComponent; let fixture: ComponentFixture; let nativeElement: HTMLElement; let layoutChangeSpy: jasmine.Spy; beforeEach(() => { TestBed.configureTestingModule({ imports: [DashboardModule, DashboardWithInitialLayoutTestComponent], }).compileComponents(); }); beforeEach(async () => { fixture = TestBed.createComponent(DashboardWithInitialLayoutTestComponent); component = fixture.componentInstance; nativeElement = fixture.nativeElement; layoutChangeSpy = spyOn(component, 'onLayoutChange'); fixture.detectChanges(); await fixture.whenStable(); }); it('should update the widgets correctly when an initial layout value is given', () => { // check that the underlying widgets receive the correct position and size values expect(layoutChangeSpy).not.toHaveBeenCalled(); expect(component.widgets.toArray()[0].getColumn()).toBe(0, 'widget 0 column'); expect(component.widgets.toArray()[0].getRow()).toBe(0, 'widget 0 row'); expect(component.widgets.toArray()[0].getColumnSpan()).toBe(2, 'widget 0 columnSpan'); expect(component.widgets.toArray()[0].getRowSpan()).toBe(1, 'widget 0 rowSpan'); expect(component.widgets.toArray()[1].getColumn()).toBe(0, 'widget 1 column'); expect(component.widgets.toArray()[1].getRow()).toBe(2, 'widget 1 row'); expect(component.widgets.toArray()[1].getColumnSpan()).toBe(1, 'widget 1 columnSpan'); expect(component.widgets.toArray()[1].getRowSpan()).toBe(1, 'widget 1 rowSpan'); expect(component.widgets.toArray()[2].getColumn()).toBe(0, 'widget 2 column'); expect(component.widgets.toArray()[2].getRow()).toBe(3, 'widget 2 row'); expect(component.widgets.toArray()[2].getColumnSpan()).toBe(3, 'widget 2 columnSpan'); expect(component.widgets.toArray()[2].getRowSpan()).toBe(1, 'widget 2 rowSpan'); }); it('should update the layout when a new value is passed to the input', async () => { component.layout = [ { id: 'purpose-widget', col: 0, row: 0, colSpan: 2, rowSpan: 1 }, { id: 'host-widget', col: 0, row: 2, colSpan: 1, rowSpan: 1 }, { id: 'run-widget', col: 0, row: 3, colSpan: 3, rowSpan: 1 }, ]; fixture.detectChanges(); await fixture.whenStable(); expect(layoutChangeSpy).not.toHaveBeenCalled(); expect(component.widgets.toArray()[0].getColumn()).toBe(0, 'widget 0 column'); expect(component.widgets.toArray()[0].getRow()).toBe(3, 'widget 0 row'); expect(component.widgets.toArray()[0].getColumnSpan()).toBe(3, 'widget 0 columnSpan'); expect(component.widgets.toArray()[0].getRowSpan()).toBe(1, 'widget 0 rowSpan'); expect(component.widgets.toArray()[1].getColumn()).toBe(0, 'widget 1 column'); expect(component.widgets.toArray()[1].getRow()).toBe(0, 'widget 1 row'); expect(component.widgets.toArray()[1].getColumnSpan()).toBe(2, 'widget 1 columnSpan'); expect(component.widgets.toArray()[1].getRowSpan()).toBe(1, 'widget 1 rowSpan'); expect(component.widgets.toArray()[2].getColumn()).toBe(0, 'widget 2 column'); expect(component.widgets.toArray()[2].getRow()).toBe(2, 'widget 2 row'); expect(component.widgets.toArray()[2].getColumnSpan()).toBe(1, 'widget 2 columnSpan'); expect(component.widgets.toArray()[2].getRowSpan()).toBe(1, 'widget 2 rowSpan'); }); it('should update the layout whenever `refreshLayout` is called and widgets can move up', () => { component.refreshLayout(); expect(layoutChangeSpy).toHaveBeenCalled(); expect(component.widgets.toArray()[0].getColumn()).toBe(0, 'widget 0 column'); expect(component.widgets.toArray()[0].getRow()).toBe(0, 'widget 0 row'); expect(component.widgets.toArray()[0].getColumnSpan()).toBe(2, 'widget 0 columnSpan'); expect(component.widgets.toArray()[0].getRowSpan()).toBe(1, 'widget 0 rowSpan'); expect(component.widgets.toArray()[1].getColumn()).toBe(0, 'widget 1 column'); expect(component.widgets.toArray()[1].getRow()).toBe(1, 'widget 1 row'); expect(component.widgets.toArray()[1].getColumnSpan()).toBe(1, 'widget 1 columnSpan'); expect(component.widgets.toArray()[1].getRowSpan()).toBe(1, 'widget 1 rowSpan'); expect(component.widgets.toArray()[2].getColumn()).toBe(0, 'widget 2 column'); expect(component.widgets.toArray()[2].getRow()).toBe(2, 'widget 2 row'); expect(component.widgets.toArray()[2].getColumnSpan()).toBe(3, 'widget 2 columnSpan'); expect(component.widgets.toArray()[2].getRowSpan()).toBe(1, 'widget 2 rowSpan'); }); it('should not update the layout whenever `refreshLayout` is called and widgets cannot move up', async () => { component.layout = [ { id: 'run-widget', col: 0, row: 1, colSpan: 2, rowSpan: 1 }, { id: 'purpose-widget', col: 0, row: 0, colSpan: 1, rowSpan: 1 }, { id: 'host-widget', col: 0, row: 2, colSpan: 3, rowSpan: 1 }, ]; fixture.detectChanges(); await fixture.whenStable(); component.refreshLayout(); expect(layoutChangeSpy).not.toHaveBeenCalled(); expect(component.widgets.toArray()[0].getColumn()).toBe(0, 'widget 0 column'); expect(component.widgets.toArray()[0].getRow()).toBe(1, 'widget 0 row'); expect(component.widgets.toArray()[0].getColumnSpan()).toBe(2, 'widget 0 columnSpan'); expect(component.widgets.toArray()[0].getRowSpan()).toBe(1, 'widget 0 rowSpan'); expect(component.widgets.toArray()[1].getColumn()).toBe(0, 'widget 1 column'); expect(component.widgets.toArray()[1].getRow()).toBe(0, 'widget 1 row'); expect(component.widgets.toArray()[1].getColumnSpan()).toBe(1, 'widget 1 columnSpan'); expect(component.widgets.toArray()[1].getRowSpan()).toBe(1, 'widget 1 rowSpan'); expect(component.widgets.toArray()[2].getColumn()).toBe(0, 'widget 2 column'); expect(component.widgets.toArray()[2].getRow()).toBe(2, 'widget 2 row'); expect(component.widgets.toArray()[2].getColumnSpan()).toBe(3, 'widget 2 columnSpan'); expect(component.widgets.toArray()[2].getRowSpan()).toBe(1, 'widget 2 rowSpan'); }); }); @Component({ selector: 'app-ux-dashboard-widget-layout', template: ` `, imports: [DashboardModule], }) export class DashboardWidgetLayoutTestComponent { options: DashboardOptions = { columns: 3, padding: 10, rowHeight: 300, emptyRow: false, minWidth: 187, }; @ViewChildren(DashboardWidgetComponent) widgets: QueryList; } describe('Dashboard Widgets layout', () => { let component: DashboardWidgetLayoutTestComponent; let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ imports: [DashboardModule, DashboardWidgetLayoutTestComponent], }).compileComponents(); }); beforeEach(async () => { fixture = TestBed.createComponent(DashboardWidgetLayoutTestComponent); component = fixture.componentInstance; fixture.detectChanges(); await fixture.whenStable(); }); it('should correctly position widgets when using row and col inputs', () => { const widgets = component.widgets.toArray(); expect(widgets[0].getColumn()).toBe(1, 'run-widget col'); expect(widgets[0].getRow()).toBe(1, 'run-widget row'); expect(widgets[1].getColumn()).toBe(1, 'purpose-widget col'); expect(widgets[1].getRow()).toBe(0, 'purpose-widget row'); expect(widgets[2].getColumn()).toBe(0, 'host-widget col'); expect(widgets[2].getRow()).toBe(0, 'host-widget row'); }); it('should update position when changes programmatically', () => { const widgets = component.widgets.toArray(); expect(widgets[0].getColumn()).toBe(1, 'run-widget col'); expect(widgets[0].x).not.toBe(0); widgets[0].col = 0; expect(widgets[0].getColumn()).toBe(0, 'run-widget col'); expect(widgets[0].x).toBe(0); }); }); @Component({ selector: 'app-ux-dashboard-autopositioning', template: ` @for (user of users; track user) {

{{ user }}

}
`, imports: [DashboardModule], }) export class DashboardAutoPositioningTestComponent { options: DashboardOptions = { columns: 2, padding: 10, rowHeight: 220, emptyRow: false, minWidth: 187, }; users: string[] = ['User 1', 'User 2', 'User 3']; @ViewChild('dashboard') dashboard: DashboardComponent; @ViewChildren(DashboardWidgetComponent) widgets: QueryList; } describe('Dashboard Auto Positioning', () => { let component: DashboardAutoPositioningTestComponent; let fixture: ComponentFixture; let nativeElement: HTMLElement; beforeEach(() => { TestBed.configureTestingModule({ imports: [DashboardModule, DashboardAutoPositioningTestComponent], }).compileComponents(); }); beforeEach(async () => { fixture = TestBed.createComponent(DashboardAutoPositioningTestComponent); component = fixture.componentInstance; nativeElement = fixture.nativeElement; fixture.detectChanges(); await fixture.whenStable(); }); it('should not reposition widget when autoPositioning=false', () => { let widgets = component.widgets.toArray(); expect(widgets[0].getColumn()).toBe(0, 'User 1 - initial col'); expect(widgets[0].getRow()).toBe(0, 'User 1 - initial row'); expect(widgets[1].getColumn()).toBe(1, 'User 2 - initial col'); expect(widgets[1].getRow()).toBe(0, 'User 2 - initial row'); expect(widgets[2].getColumn()).toBe(0, 'User 3 - initial col'); expect(widgets[2].getRow()).toBe(1, 'User 3 - initial row'); // remove first widget component.users.splice(0, 1); fixture.detectChanges(); component.dashboard.refreshLayout(); widgets = component.widgets.toArray(); expect(widgets[0].getColumn()).toBe(1, 'User 2 col - not affected'); expect(widgets[0].getRow()).toBe(0, 'User 2 row - not affected'); expect(widgets[1].getColumn()).toBe(0, 'User 3 col - should not be repositioned'); expect(widgets[1].getRow()).toBe(1, 'User 3 row - should not be repositioned'); }); });