From 3e00a17deea026fdbc7420e9db7681025869e227 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 3 Dec 2025 15:10:08 +0300 Subject: [PATCH 01/29] SED-4412 step-icon tests --- .../step-icon/step-icon.component.test.ts | 51 +++++++++++++++++++ .../services/icon-provider.service.test.ts | 28 ++++++++++ 2 files changed, 79 insertions(+) create mode 100644 projects/step-core/src/lib/modules/step-icons/components/step-icon/step-icon.component.test.ts create mode 100644 projects/step-core/src/lib/modules/step-icons/services/icon-provider.service.test.ts diff --git a/projects/step-core/src/lib/modules/step-icons/components/step-icon/step-icon.component.test.ts b/projects/step-core/src/lib/modules/step-icons/components/step-icon/step-icon.component.test.ts new file mode 100644 index 0000000000..4295f0945f --- /dev/null +++ b/projects/step-core/src/lib/modules/step-icons/components/step-icon/step-icon.component.test.ts @@ -0,0 +1,51 @@ +import { Component, input, signal } from '@angular/core'; +import { StepIconsModule } from '../../step-icons.module'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { allIcons } from '../../icons'; + +@Component({ + selector: 'step-icon-test', + template: ` + @if (iconName(); as name) { + + } + `, + imports: [StepIconsModule], +}) +class IconTestComponent { + readonly iconName = signal(undefined); +} + +describe('Icon Component', () => { + let component: IconTestComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [IconTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(IconTestComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('Rendering', async () => { + let iconElement = fixture.nativeElement.querySelector('step-icon'); + expect(iconElement).toBeFalsy(); + + component.iconName.set('pie-chart'); + fixture.detectChanges(); + + iconElement = fixture.nativeElement.querySelector('step-icon'); + expect(iconElement).toBeTruthy(); + expect(iconElement.innerHTML).toEqual(allIcons.PieChart); + + component.iconName.set('plus-circle'); + fixture.detectChanges(); + + iconElement = fixture.nativeElement.querySelector('step-icon'); + expect(iconElement).toBeTruthy(); + expect(iconElement.innerHTML).toEqual(allIcons.PlusCircle); + }); +}); diff --git a/projects/step-core/src/lib/modules/step-icons/services/icon-provider.service.test.ts b/projects/step-core/src/lib/modules/step-icons/services/icon-provider.service.test.ts new file mode 100644 index 0000000000..8930a71e60 --- /dev/null +++ b/projects/step-core/src/lib/modules/step-icons/services/icon-provider.service.test.ts @@ -0,0 +1,28 @@ +import { IconProviderService } from './icon-provider.service'; +import { TestBed } from '@angular/core/testing'; +import { allIcons } from '../icons'; + +describe('IconProviderService', () => { + let iconProviderService: IconProviderService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [IconProviderService], + }).compileComponents(); + iconProviderService = TestBed.inject(IconProviderService); + }); + + it('Get icon success', () => { + expect(iconProviderService.getIcon('pie-chart')).toBe(allIcons.PieChart); + expect(iconProviderService.getIcon('plusCircle')).toBe(allIcons.PlusCircle); + }); + + it('Get icon failed', () => { + const spyWarn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + expect(iconProviderService.getIcon('')).toBe(''); + expect(spyWarn).not.toHaveBeenCalled(); + + expect(iconProviderService.getIcon('aaBBcc')).toBe(''); + expect(spyWarn).toHaveBeenCalled(); + }); +}); From dd8f0a1d8275f073214191844b88ed303a4e8b85 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 3 Dec 2025 15:10:37 +0300 Subject: [PATCH 02/29] SED-4412 step-tabs tests --- .../components/tabs/tabs.component.test.ts | 346 ++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 projects/step-core/src/lib/modules/tabs/components/tabs/tabs.component.test.ts diff --git a/projects/step-core/src/lib/modules/tabs/components/tabs/tabs.component.test.ts b/projects/step-core/src/lib/modules/tabs/components/tabs/tabs.component.test.ts new file mode 100644 index 0000000000..f87940b43e --- /dev/null +++ b/projects/step-core/src/lib/modules/tabs/components/tabs/tabs.component.test.ts @@ -0,0 +1,346 @@ +import { Component, model, signal } from '@angular/core'; +import { Tab } from '../../shared/tab'; +import { TAB_EXPORTS, TabsComponent } from '../../index'; +import { provideRouter, Router, RouterModule, withHashLocation } from '@angular/router'; +import { ComponentFixture, fakeAsync, flushMicrotasks, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { MatTabLink } from '@angular/material/tabs'; + +const TABS: Tab[] = [ + { id: 'first', label: 'First', link: '/first' }, + { id: 'second', label: 'Second', link: '/second' }, + { id: 'third', label: 'Third', link: '/third' }, +]; + +@Component({ + selector: 'step-tab-one', + template: `
TAB 1
`, +}) +class TabOneComponent {} + +@Component({ + selector: 'step-tab-two', + template: `
TAB 2
`, +}) +class TabTwoComponent {} + +@Component({ + selector: 'step-tab-three', + template: `
TAB 3
`, +}) +class TabThreeComponent {} + +abstract class TestTabsBase { + readonly selectedTab = model('first'); + readonly tabMode = signal<'buttons' | 'tabs'>('buttons'); + readonly isShrink = signal(false); + protected readonly TABS = TABS; +} + +@Component({ + selector: 'step-test-tabs-inline-children', + imports: [TAB_EXPORTS, TabOneComponent, TabTwoComponent, TabThreeComponent], + template: ` + + @switch (selectedTab()) { + @case ('first') { + + } + @case ('second') { + + } + @case ('third') { + + } + } + `, +}) +class TestTabsInlineChildrenComponent extends TestTabsBase {} + +@Component({ + selector: 'step-test-tabs-router-children', + imports: [TAB_EXPORTS, RouterModule], + template: ` + + + + Input Template: {{ tab.label }} + + `, +}) +class TestTabsRouterChildrenComponent extends TestTabsBase {} + +@Component({ + selector: 'step-test-tabs-router-children-with-template-directive', + imports: [TAB_EXPORTS, RouterModule], + template: ` + + Directive Template: {{ tab.label }} + + + `, +}) +class TestTabsRouterChildrenWithTemplateDirectiveComponent extends TestTabsBase {} + +describe('Tabs', () => { + const checkTabSelection = (fixture: ComponentFixture, activeTabIndex: number) => { + const tabs = fixture.debugElement.queryAll(By.directive(MatTabLink)); + tabs.forEach((tab, i) => { + if (i === activeTabIndex) { + expect(tab.componentInstance.active).toBeTruthy(); + } else { + expect(tab.componentInstance.active).toBeFalsy(); + } + }); + + const tabContent = fixture.nativeElement.querySelector('.tab-content'); + expect(tabContent).toBeTruthy(); + expect(tabContent.textContent).toEqual(`TAB ${activeTabIndex + 1}`); + }; + + describe('Inline tabs content', () => { + let component: TestTabsInlineChildrenComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestTabsInlineChildrenComponent], + }).compileComponents(); + fixture = TestBed.createComponent(TestTabsInlineChildrenComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('Initial view', () => { + const tabs = fixture.debugElement.queryAll(By.directive(MatTabLink)); + expect(tabs.length).toBe(TABS.length); + tabs.forEach((tab, i) => { + expect(tab.nativeElement.textContent.trim()).toEqual(TABS[i].label); + }); + }); + + it('Tabs switch by click', async () => { + expect(component.selectedTab()).toBe('first'); + checkTabSelection(fixture, 0); + + let tabElement = fixture.debugElement.query(By.css('[data-step-testid="tab-selector-second"]')); + expect(tabElement).toBeTruthy(); + tabElement.triggerEventHandler('click', null); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.selectedTab()).toBe('second'); + checkTabSelection(fixture, 1); + + tabElement = fixture.debugElement.query(By.css('[data-step-testid="tab-selector-third"]')); + expect(tabElement).toBeTruthy(); + tabElement.triggerEventHandler('click', null); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(component.selectedTab()).toBe('third'); + checkTabSelection(fixture, 2); + + tabElement = fixture.debugElement.query(By.css('[data-step-testid="tab-selector-first"]')); + expect(tabElement).toBeTruthy(); + tabElement.triggerEventHandler('click', null); + + fixture.detectChanges(); + await fixture.whenStable(); + + checkTabSelection(fixture, 0); + }); + + it('Tab switch by model change', async () => { + checkTabSelection(fixture, 0); + + component.selectedTab.set('second'); + fixture.detectChanges(); + await fixture.whenStable(); + + checkTabSelection(fixture, 1); + + component.selectedTab.set('third'); + fixture.detectChanges(); + await fixture.whenStable(); + + checkTabSelection(fixture, 2); + + component.selectedTab.set('first'); + fixture.detectChanges(); + await fixture.whenStable(); + + checkTabSelection(fixture, 0); + }); + + it('Class changes based on component inputs', async () => { + const tabs = fixture.debugElement.query(By.directive(TabsComponent)); + expect(tabs.nativeElement.classList.contains('tab-mode-buttons')).toBeTruthy(); + expect(tabs.nativeElement.classList.contains('tab-mode-tabs')).toBeFalsy(); + expect(tabs.nativeElement.classList.contains('shrink')).toBeFalsy(); + + component.tabMode.set('tabs'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(tabs.nativeElement.classList.contains('tab-mode-buttons')).toBeFalsy(); + expect(tabs.nativeElement.classList.contains('tab-mode-tabs')).toBeTruthy(); + expect(tabs.nativeElement.classList.contains('shrink')).toBeFalsy(); + + component.isShrink.set(true); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(tabs.nativeElement.classList.contains('tab-mode-buttons')).toBeFalsy(); + expect(tabs.nativeElement.classList.contains('tab-mode-tabs')).toBeTruthy(); + expect(tabs.nativeElement.classList.contains('shrink')).toBeTruthy(); + }); + }); + + describe('Router tabs', () => { + let fixture: ComponentFixture; + let router: Router; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestTabsRouterChildrenComponent], + providers: [ + provideRouter( + [ + { path: '', redirectTo: TABS[0].id, pathMatch: 'full' }, + { path: TABS[0].id, component: TabOneComponent }, + { path: TABS[1].id, component: TabTwoComponent }, + { path: TABS[2].id, component: TabThreeComponent }, + ], + withHashLocation(), + ), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestTabsRouterChildrenComponent); + router = TestBed.inject(Router); + router.initialNavigation(); + + fixture.detectChanges(); + }); + + it('Initial view', () => { + const tabs = fixture.debugElement.queryAll(By.directive(MatTabLink)); + expect(tabs.length).toBe(TABS.length); + tabs.forEach((tab, i) => { + expect(tab.nativeElement.textContent.trim()).toEqual(`Input Template: ${TABS[i].label}`); + }); + }); + + // RouterLink and RouterLinkActive contains logic, which relies on observable chains + // It's hard to achieve in unit tests, when these chains are over and doesn't flush any new result + // To test it `fakeAsync` is used. It allows to control async task's execution manually. + + it('Tab switch by click', fakeAsync(() => { + flushMicrotasks(); + fixture.detectChanges(); + + expect(router.url).toEqual('/first'); + + checkTabSelection(fixture, 0); + + let tabElement = fixture.debugElement.query(By.css('[data-step-testid="tab-selector-second"]')); + expect(tabElement).toBeTruthy(); + tabElement.triggerEventHandler('click', { button: 0 }); + + fixture.detectChanges(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(router.url).toEqual('/second'); + checkTabSelection(fixture, 1); + + tabElement = fixture.debugElement.query(By.css('[data-step-testid="tab-selector-third"]')); + expect(tabElement).toBeTruthy(); + tabElement.triggerEventHandler('click', { button: 0 }); + + fixture.detectChanges(); + flushMicrotasks(); + fixture.detectChanges(); + + expect(router.url).toEqual('/third'); + checkTabSelection(fixture, 2); + + tabElement = fixture.debugElement.query(By.css('[data-step-testid="tab-selector-first"]')); + expect(tabElement).toBeTruthy(); + tabElement.triggerEventHandler('click', { button: 0 }); + + fixture.detectChanges(); + flushMicrotasks(); + fixture.detectChanges(); + + checkTabSelection(fixture, 0); + })); + + it('Tab switch by router change', fakeAsync(() => { + flushMicrotasks(); + fixture.detectChanges(); + + checkTabSelection(fixture, 0); + + router.navigateByUrl('/second'); + fixture.detectChanges(); + flushMicrotasks(); + fixture.detectChanges(); + + checkTabSelection(fixture, 1); + + router.navigateByUrl('/third'); + fixture.detectChanges(); + flushMicrotasks(); + fixture.detectChanges(); + + checkTabSelection(fixture, 2); + + router.navigateByUrl('/first'); + fixture.detectChanges(); + flushMicrotasks(); + fixture.detectChanges(); + + checkTabSelection(fixture, 0); + })); + }); + + describe('Tabs with template directive', () => { + let fixture: ComponentFixture; + let router: Router; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestTabsRouterChildrenWithTemplateDirectiveComponent], + providers: [ + provideRouter( + [ + { path: '', redirectTo: TABS[0].id, pathMatch: 'full' }, + { path: TABS[0].id, component: TabOneComponent }, + { path: TABS[1].id, component: TabTwoComponent }, + { path: TABS[2].id, component: TabThreeComponent }, + ], + withHashLocation(), + ), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestTabsRouterChildrenWithTemplateDirectiveComponent); + router = TestBed.inject(Router); + router.initialNavigation(); + + fixture.detectChanges(); + }); + + it('Initial view', () => { + const tabs = fixture.debugElement.queryAll(By.directive(MatTabLink)); + expect(tabs.length).toBe(TABS.length); + tabs.forEach((tab, i) => { + expect(tab.nativeElement.textContent.trim()).toEqual(`Directive Template: ${TABS[i].label}`); + }); + }); + }); +}); From 0e13014a0ad68e4714d995cbb67c4aacd86c2679 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 3 Dec 2025 16:05:14 +0300 Subject: [PATCH 03/29] SED-4412 Trace viewer tests --- .../trace-viewer.component.test.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 projects/step-core/src/lib/modules/attachments/components/trace-viewer/trace-viewer.component.test.ts diff --git a/projects/step-core/src/lib/modules/attachments/components/trace-viewer/trace-viewer.component.test.ts b/projects/step-core/src/lib/modules/attachments/components/trace-viewer/trace-viewer.component.test.ts new file mode 100644 index 0000000000..f1081af6cc --- /dev/null +++ b/projects/step-core/src/lib/modules/attachments/components/trace-viewer/trace-viewer.component.test.ts @@ -0,0 +1,56 @@ +import { TraceViewerComponent } from './trace-viewer.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { APP_HOST } from '../../../../client/_common'; +import { By } from '@angular/platform-browser'; +import { ComponentRef } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; + +describe('Trace Viewer', () => { + let componentRef: ComponentRef; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TraceViewerComponent], + providers: [ + { + provide: APP_HOST, + useValue: 'https://step.ch', + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TraceViewerComponent); + componentRef = fixture.componentRef; + fixture.detectChanges(); + }); + + it('Initial view', async () => { + const iframe = fixture.debugElement.query(By.css('iframe')); + expect(iframe.nativeElement.src).toEqual('https://step.ch/trace-viewer/'); + }); + + it('Set report url', async () => { + componentRef.setInput('reportUrl', 'https://step.ch/reports/1'); + fixture.detectChanges(); + await fixture.whenStable(); + const iframe = fixture.debugElement.query(By.css('iframe')); + expect(iframe.nativeElement.src).toEqual('https://step.ch/trace-viewer/?trace=https://step.ch/reports/1'); + }); + + it('Oper url', async () => { + componentRef.setInput('reportUrl', 'https://step.ch/reports/1'); + fixture.detectChanges(); + await fixture.whenStable(); + + const doc = TestBed.inject(DOCUMENT); + const spyWindowOpen = jest.spyOn(doc.defaultView!, 'open').mockImplementation(() => {}); + + expect(spyWindowOpen).not.toHaveBeenCalled(); + componentRef.instance.openInSeparateTab(); + expect(spyWindowOpen).toHaveBeenCalledWith( + 'https://step.ch/trace-viewer/?trace=https://step.ch/reports/1', + '_blank', + ); + }); +}); From 604704ae11af98d3d7442628c9bc251c8f3f9c0b Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 10 Dec 2025 16:25:48 +0300 Subject: [PATCH 04/29] SED-4412 Errors-list and form field tests --- .../errors-list/errors-list.component.test.ts | 92 ++++ .../form-field/form-field.component.test.ts | 521 ++++++++++++++++++ .../types/form-control-warnings-extension.ts | 2 +- 3 files changed, 614 insertions(+), 1 deletion(-) create mode 100644 projects/step-core/src/lib/modules/basics/components/errors-list/errors-list.component.test.ts create mode 100644 projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.test.ts diff --git a/projects/step-core/src/lib/modules/basics/components/errors-list/errors-list.component.test.ts b/projects/step-core/src/lib/modules/basics/components/errors-list/errors-list.component.test.ts new file mode 100644 index 0000000000..02b92ff50c --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/errors-list/errors-list.component.test.ts @@ -0,0 +1,92 @@ +import { ErrorsListComponent } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentRef } from '@angular/core'; + +describe('ErrorListComponent', () => { + let fixture: ComponentFixture; + let component: ErrorsListComponent; + let componentRef: ComponentRef; + + const getTexts = () => { + const result: string[] = []; + const divTexts = fixture.nativeElement.querySelectorAll('div'); + divTexts?.forEach?.((item: HTMLHtmlElement) => result.push(item.textContent ?? '')); + return result; + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ErrorsListComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ErrorsListComponent); + component = fixture.componentInstance; + componentRef = fixture.componentRef; + fixture.detectChanges(); + }); + + it('Error list without dictionary', async () => { + componentRef.setInput('errors', { + foo: 'errFoo', + bar: 'errBar', + bazz: 'errBazz', + }); + + fixture.detectChanges(); + await fixture.whenStable(); + + let divTexts = getTexts(); + expect(divTexts.length).toBe(3); + expect(divTexts).toEqual(['errFoo', 'errBar', 'errBazz']); + + componentRef.setInput('errors', { + aaa: 'errAaa', + bbb: 43, + ccc: 'errCcc', + }); + + fixture.detectChanges(); + await fixture.whenStable(); + + divTexts = getTexts(); + expect(divTexts.length).toBe(2); + expect(divTexts).toEqual(['errAaa', 'errCcc']); + }); + + it('Error list with dictionary', async () => { + componentRef.setInput('keysDictionary', { + foo: 'This is error foo', + bazz: 'This is error bazz', + aaa: 'This is error aaa', + bbb: 'This is error bbb', + ddd: 'This is error ddd', + }); + + componentRef.setInput('errors', { + foo: 'errFoo', + bar: 'errBar', + bazz: 'errBazz', + }); + + fixture.detectChanges(); + await fixture.whenStable(); + + let divTexts = getTexts(); + expect(divTexts.length).toBe(3); + expect(divTexts).toEqual(['This is error foo', 'errBar', 'This is error bazz']); + + componentRef.setInput('errors', { + aaa: 'errAaa', + bbb: 43, + ccc: 'errCcc', + ddd: 'errDdd', + }); + + fixture.detectChanges(); + await fixture.whenStable(); + + divTexts = getTexts(); + expect(divTexts.length).toBe(4); + expect(divTexts).toEqual(['This is error aaa', 'This is error bbb', 'errCcc', 'This is error ddd']); + }); +}); diff --git a/projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.test.ts b/projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.test.ts new file mode 100644 index 0000000000..7206645e33 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/form-field/form-field.component.test.ts @@ -0,0 +1,521 @@ +import { Component, DestroyRef, inject, input, model, OnInit } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; +import { + AlignLabelAddon, + decorateWithWarnings, + ErrorsListComponent, + FormFieldComponent, + getControlWarningsContainer, + StepBasicsModule, + StepIconComponent, +} from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { firstValueFrom, timer } from 'rxjs'; + +class TestComponentBase { + protected readonly _fb = inject(FormBuilder); + + readonly form = this._fb.group({ + testField: this._fb.control('', Validators.required), + }); +} + +@Component({ + selector: 'step-no-label', + imports: [StepBasicsModule], + template: ` +
+ + + +
+ `, +}) +class NoLabelComponent extends TestComponentBase {} + +@Component({ + selector: 'step-with-label', + imports: [StepBasicsModule], + template: ` +
+ + Test field + + +
+ `, +}) +class WithLabelComponent extends TestComponentBase { + readonly showRequired = input(undefined); +} + +@Component({ + selector: 'step-with-label-addon', + imports: [StepBasicsModule], + template: ` +
+ + + + + + +
+ `, +}) +class WithLabelAddonComponent extends TestComponentBase {} + +@Component({ + selector: 'step-with-label-and-addon', + imports: [StepBasicsModule], + template: ` +
+ + Test field + + + + + +
+ `, +}) +class WithLabelAndAddonComponent extends TestComponentBase { + readonly alignLabelAddon = input('separate'); +} + +@Component({ + selector: 'step-with-description', + imports: [StepBasicsModule], + template: ` +
+ + Test field description + + +
+ `, +}) +class WithDescriptionComponent extends TestComponentBase {} + +@Component({ + selector: 'step-with-hint', + imports: [StepBasicsModule], + template: ` +
+ + + Field's hint + +
+ `, +}) +class WithHintComponent extends TestComponentBase {} + +@Component({ + selector: 'step-with-errors', + imports: [StepBasicsModule], + template: ` +
+ + + + + + +
+ `, +}) +class WithErrorsComponent extends TestComponentBase { + protected readonly errorsDictionary: Record = { + required: 'This field is required', + }; + + protected readonly ctrlTestField = this.form.controls.testField; +} + +@Component({ + selector: 'step-with-explicit-control', + imports: [StepBasicsModule], + template: ` +
+ + + +
+ `, +}) +class WithExplicitControlComponent extends TestComponentBase { + readonly value = model(''); +} + +@Component({ + selector: 'step-with-warnings', + imports: [StepBasicsModule], + template: ` +
+ + + @if ((ctrlTestField | controlWarnings)?.(); as warnings) { + + + + } + +
+ `, +}) +class WithWarningsComponent extends TestComponentBase implements OnInit { + private _destroyRef = inject(DestroyRef); + + readonly ctrlTestField = this.form.controls.testField; + + ngOnInit(): void { + decorateWithWarnings(this.ctrlTestField, this._destroyRef); + } +} + +describe('FormFieldComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + NoLabelComponent, + WithLabelComponent, + WithLabelAddonComponent, + WithLabelAndAddonComponent, + WithDescriptionComponent, + WithHintComponent, + WithErrorsComponent, + WithExplicitControlComponent, + WithWarningsComponent, + ], + }).compileComponents(); + }); + + describe('Label visibility and settings', () => { + it('No Label', async () => { + const fixture = TestBed.createComponent(NoLabelComponent); + fixture.detectChanges(); + await fixture.whenStable(); + const labelContainer = fixture.debugElement.query(By.css('.label-container')); + expect(labelContainer).toBeTruthy(); + expect(labelContainer.classes['without-children']).toBeTruthy(); + + const label = labelContainer.query(By.css('label')); + expect(label.childNodes.length).toBe(0); + expect(label.nativeElement.textContent).toBe(''); + + const labelAddon = labelContainer.query(By.css('.label-addon')); + expect(labelAddon.childNodes.length).toBe(0); + expect(labelAddon.nativeElement.textContent).toBe(''); + }); + + it('Label and required marker', async () => { + const fixture = TestBed.createComponent(WithLabelComponent); + fixture.detectChanges(); + await fixture.whenStable(); + const labelContainer = fixture.debugElement.query(By.css('.label-container')); + expect(labelContainer).toBeTruthy(); + expect(labelContainer.classes['without-children']).toBeFalsy(); + expect(labelContainer.classes['show-required-marker']).toBeFalsy(); + + const label = labelContainer.query(By.css('label')); + expect(label.childNodes.length).toBe(1); + expect(label.nativeElement.textContent).toBe('Test field'); + + const labelAddon = labelContainer.query(By.css('.label-addon')); + expect(labelAddon.childNodes.length).toBe(0); + expect(labelAddon.nativeElement.textContent).toBe(''); + + fixture.componentRef.setInput('showRequired', true); + fixture.detectChanges(); + await fixture.whenStable(); + expect(labelContainer.classes['show-required-marker']).toBeTruthy(); + }); + + it('With label addon', async () => { + const fixture = TestBed.createComponent(WithLabelAddonComponent); + fixture.detectChanges(); + await fixture.whenStable(); + const labelContainer = fixture.debugElement.query(By.css('.label-container')); + expect(labelContainer).toBeTruthy(); + expect(labelContainer.classes['without-children']).toBeFalsy(); + + const label = labelContainer.query(By.css('label')); + expect(label.childNodes.length).toBe(0); + expect(label.nativeElement.textContent).toBe(''); + + const labelAddon = labelContainer.query(By.css('.label-addon')); + expect(labelAddon.childNodes.length).toBe(1); + const icon = labelAddon.query(By.directive(StepIconComponent)); + expect(icon).toBeTruthy(); + }); + + it('With label and addon', async () => { + const fixture = TestBed.createComponent(WithLabelAndAddonComponent); + fixture.detectChanges(); + await fixture.whenStable(); + const labelContainer = fixture.debugElement.query(By.css('.label-container')); + expect(labelContainer).toBeTruthy(); + expect(labelContainer.classes['without-children']).toBeFalsy(); + + const label = labelContainer.query(By.css('label')); + expect(label.childNodes.length).toBe(1); + expect(label.nativeElement.textContent).toBe('Test field'); + + const labelAddon = labelContainer.query(By.css('.label-addon')); + expect(labelAddon.childNodes.length).toBe(1); + const icon = labelAddon.query(By.directive(StepIconComponent)); + expect(icon).toBeTruthy(); + }); + + it('Align label addon', async () => { + const fixture = TestBed.createComponent(WithLabelAndAddonComponent); + fixture.detectChanges(); + await fixture.whenStable(); + const labelContainer = fixture.debugElement.query(By.css('.label-container')); + expect(labelContainer).toBeTruthy(); + expect(labelContainer.classes['align-left']).toBeFalsy(); + expect(labelContainer.classes['align-fill']).toBeFalsy(); + + fixture.componentRef.setInput('alignLabelAddon', 'near'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(labelContainer.classes['align-left']).toBeTruthy(); + expect(labelContainer.classes['align-fill']).toBeFalsy(); + + fixture.componentRef.setInput('alignLabelAddon', 'fill'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(labelContainer.classes['align-left']).toBeFalsy(); + expect(labelContainer.classes['align-fill']).toBeTruthy(); + + fixture.componentRef.setInput('alignLabelAddon', 'separate'); + fixture.detectChanges(); + await fixture.whenStable(); + expect(labelContainer.classes['align-left']).toBeFalsy(); + expect(labelContainer.classes['align-fill']).toBeFalsy(); + }); + }); + + describe('Description and hint', () => { + it('Description', async () => { + const fxtNoDescription = TestBed.createComponent(NoLabelComponent); + fxtNoDescription.detectChanges(); + await fxtNoDescription.whenStable(); + + let description = fxtNoDescription.debugElement + .query(By.directive(FormFieldComponent)) + .query(By.css('step-description')); + + expect(description).toBeFalsy(); + + const fxtDescription = TestBed.createComponent(WithDescriptionComponent); + fxtDescription.detectChanges(); + await fxtDescription.whenStable(); + + description = fxtDescription.debugElement + .query(By.directive(FormFieldComponent)) + .query(By.css('step-description')); + + expect(description).toBeTruthy(); + expect(description.nativeElement.textContent).toBe('Test field description'); + }); + + it('Field hint', async () => { + const fxtNoHint = TestBed.createComponent(NoLabelComponent); + fxtNoHint.detectChanges(); + await fxtNoHint.whenStable(); + + let hintContainer = fxtNoHint.debugElement.query(By.css('.form-field-hint')); + expect(hintContainer).toBeTruthy(); + expect(hintContainer.children.length).toBe(0); + expect(hintContainer.nativeElement.textContent).toBe(''); + + const fxtHint = TestBed.createComponent(WithHintComponent); + fxtHint.detectChanges(); + await fxtHint.whenStable(); + + hintContainer = fxtHint.debugElement.query(By.css('.form-field-hint')); + expect(hintContainer).toBeTruthy(); + expect(hintContainer.children.length).toBe(1); + expect(hintContainer.nativeElement.textContent).toBe(`Field's hint`); + }); + }); + + describe('Validation and errors', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + fixture = TestBed.createComponent(WithErrorsComponent); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Validation classes', async () => { + const formField = fixture.debugElement.query(By.directive(FormFieldComponent)); + expect(formField.classes['ng-invalid']).toBeTruthy(); + expect(formField.classes['ng-touched']).toBeFalsy(); + + const input = formField.query(By.css('input')); + input.triggerEventHandler('blur'); + + fixture.detectChanges(); + await fixture.whenStable(); + expect(formField.classes['ng-invalid']).toBeTruthy(); + expect(formField.classes['ng-touched']).toBeTruthy(); + + (input.nativeElement as HTMLInputElement).value = 'foo bar bazz'; + input.triggerEventHandler('input', { target: input.nativeElement }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(formField.classes['ng-invalid']).toBeFalsy(); + expect(formField.classes['ng-touched']).toBeTruthy(); + }); + + it('Errors', async () => { + const fxtNoDisplayedErrors = TestBed.createComponent(NoLabelComponent); + fxtNoDisplayedErrors.detectChanges(); + await fxtNoDisplayedErrors.whenStable(); + + let errorContainer = fxtNoDisplayedErrors.debugElement.query(By.css('.form-field-error')); + expect(errorContainer).toBeTruthy(); + expect(errorContainer.children.length).toBe(0); + + errorContainer = fixture.debugElement.query(By.css('.form-field-error')); + expect(errorContainer).toBeTruthy(); + expect(errorContainer.children.length).toBe(1); + + const errors = errorContainer.query(By.directive(ErrorsListComponent)); + expect(errors).toBeTruthy(); + expect(errors.children.length).toBe(1); + expect(errors.nativeElement.textContent).toBe('This field is required'); + + const formField = fixture.debugElement.query(By.directive(FormFieldComponent)); + const input = formField.query(By.css('input')); + (input.nativeElement as HTMLInputElement).value = 'foo bar bazz'; + input.triggerEventHandler('input', { target: input.nativeElement }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(errors.children.length).toBe(0); + expect(errors.nativeElement.textContent).toBe(''); + }); + + it('Explicit control', async () => { + const fxtExplicitControl = TestBed.createComponent(WithExplicitControlComponent); + fxtExplicitControl.detectChanges(); + await fxtExplicitControl.whenStable(); + + const formField = fxtExplicitControl.debugElement.query(By.directive(FormFieldComponent)); + expect(formField.classes['ng-invalid']).toBeTruthy(); + expect(fxtExplicitControl.componentInstance.value()).toBe(''); + + const input = formField.query(By.css('input')); + (input.nativeElement as HTMLInputElement).value = 'foo bar bazz'; + input.triggerEventHandler('input', { target: input.nativeElement }); + fxtExplicitControl.detectChanges(); + await fxtExplicitControl.whenStable(); + + expect(formField.classes['ng-invalid']).toBeFalsy(); + expect(fxtExplicitControl.componentInstance.value()).toBe('foo bar bazz'); + }); + }); + + describe('Warnings', () => { + let fixture: ComponentFixture; + let component: WithWarningsComponent; + + beforeEach(async () => { + fixture = TestBed.createComponent(WithWarningsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Warning container', async () => { + const fxtNonWarning = TestBed.createComponent(NoLabelComponent); + fxtNonWarning.detectChanges(); + await fxtNonWarning.whenStable(); + + let warningContainer = fxtNonWarning.debugElement + .query(By.css('.form-field-warning')) + .query(By.directive(ErrorsListComponent)); + + expect(warningContainer).toBeFalsy(); + + warningContainer = fixture.debugElement + .query(By.css('.form-field-warning')) + .query(By.directive(ErrorsListComponent)); + + expect(warningContainer).toBeTruthy(); + }); + + it('Not persistent warnings', async () => { + let formField = fixture.debugElement.query(By.directive(FormFieldComponent)); + + const warningContainer = fixture.debugElement + .query(By.css('.form-field-warning')) + .query(By.directive(ErrorsListComponent)); + + const input = formField.query(By.css('input')); + + expect(formField.classes['step-has-warnings']).toBeFalsy(); + expect(warningContainer.nativeElement.textContent).toBe(''); + + getControlWarningsContainer(component.ctrlTestField)!.setWarning('warn', 'This field is important!'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(formField.classes['step-has-warnings']).toBeTruthy(); + expect(warningContainer.nativeElement.textContent).toBe('This field is important!'); + + (input.nativeElement as HTMLInputElement).value = 'foo bar bazz'; + input.triggerEventHandler('input', { target: input.nativeElement }); + await firstValueFrom(timer(500)); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(formField.classes['step-has-warnings']).toBeFalsy(); + expect(warningContainer.nativeElement.textContent).toBe(''); + }); + + it('Persistent warnings', async () => { + let formField = fixture.debugElement.query(By.directive(FormFieldComponent)); + + const warningContainer = fixture.debugElement + .query(By.css('.form-field-warning')) + .query(By.directive(ErrorsListComponent)); + + const input = formField.query(By.css('input')); + + expect(formField.classes['step-has-warnings']).toBeFalsy(); + expect(warningContainer.nativeElement.textContent).toBe(''); + + getControlWarningsContainer(component.ctrlTestField)!.setWarning('warn', 'This field is important!', true); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(formField.classes['step-has-warnings']).toBeTruthy(); + expect(warningContainer.nativeElement.textContent).toBe('This field is important!'); + + (input.nativeElement as HTMLInputElement).value = 'foo bar bazz'; + input.triggerEventHandler('input', { target: input.nativeElement }); + await firstValueFrom(timer(500)); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(formField.classes['step-has-warnings']).toBeTruthy(); + expect(warningContainer.nativeElement.textContent).toBe('This field is important!'); + + getControlWarningsContainer(component.ctrlTestField)!.clearAll(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(formField.classes['step-has-warnings']).toBeFalsy(); + expect(warningContainer.nativeElement.textContent).toBe(''); + }); + }); +}); diff --git a/projects/step-core/src/lib/modules/basics/types/form-control-warnings-extension.ts b/projects/step-core/src/lib/modules/basics/types/form-control-warnings-extension.ts index 0b3d22198c..ca72eaeb62 100644 --- a/projects/step-core/src/lib/modules/basics/types/form-control-warnings-extension.ts +++ b/projects/step-core/src/lib/modules/basics/types/form-control-warnings-extension.ts @@ -63,7 +63,7 @@ export class WarningContainer { export const decorateWithWarnings = ( control: T, - destroyRefOrTerminator: DestroyRef | Subject, + destroyRefOrTerminator: DestroyRef | Subject | Subject, ): T => { const container = new WarningContainer(); From 2f0149afd6d01507a4fac7659891287fef950a62 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 10 Dec 2025 17:54:41 +0300 Subject: [PATCH 05/29] SED-4412 Progress bar --- .../progress-bar.component.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 projects/step-core/src/lib/modules/basics/components/progress-bar/progress-bar.component.test.ts diff --git a/projects/step-core/src/lib/modules/basics/components/progress-bar/progress-bar.component.test.ts b/projects/step-core/src/lib/modules/basics/components/progress-bar/progress-bar.component.test.ts new file mode 100644 index 0000000000..26df4e5346 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/progress-bar/progress-bar.component.test.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ProgressBarComponent, StepBasicsModule } from '@exense/step-core'; +import { MatProgressBar } from '@angular/material/progress-bar'; +import { By } from '@angular/platform-browser'; + +describe('ProgressBarComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StepBasicsModule], + }).compileComponents(); + + fixture = TestBed.createComponent(ProgressBarComponent); + fixture.componentRef.setInput('progress', 0); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Progress change', async () => { + const progressPar = fixture.debugElement.query(By.directive(MatProgressBar)); + const percentageContainer = fixture.debugElement.query(By.css('.progress-bar-percentage')); + expect((progressPar.componentInstance as MatProgressBar).value).toBe(0); + expect(percentageContainer.nativeElement.textContent).toBe('0%'); + + fixture.componentRef.setInput('progress', 15); + fixture.detectChanges(); + await fixture.whenStable(); + + expect((progressPar.componentInstance as MatProgressBar).value).toBe(15); + expect(percentageContainer.nativeElement.textContent).toBe('15%'); + + fixture.componentRef.setInput('progress', 100); + fixture.detectChanges(); + await fixture.whenStable(); + + expect((progressPar.componentInstance as MatProgressBar).value).toBe(100); + expect(percentageContainer.nativeElement.textContent).toBe('100%'); + }); +}); From 6bc63c03a018055d54e6da5a3d66f9d10090d802 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 10 Dec 2025 18:56:44 +0300 Subject: [PATCH 06/29] SED-4412 Key value component --- .../key-value/key-value.component.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 projects/step-core/src/lib/modules/json-viewer/components/key-value/key-value.component.test.ts diff --git a/projects/step-core/src/lib/modules/json-viewer/components/key-value/key-value.component.test.ts b/projects/step-core/src/lib/modules/json-viewer/components/key-value/key-value.component.test.ts new file mode 100644 index 0000000000..71dc4b63bf --- /dev/null +++ b/projects/step-core/src/lib/modules/json-viewer/components/key-value/key-value.component.test.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { JsonViewerModule, KeyValueComponent } from '@exense/step-core'; + +describe('KeyValueComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [JsonViewerModule], + }).compileComponents(); + + fixture = TestBed.createComponent(KeyValueComponent); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Rendering', async () => { + expect(fixture.debugElement.children.length).toBe(0); + fixture.componentRef.setInput('json', { + foo: 'FOO', + bar: 'BAR', + baz: 'BAZ', + subItem: { item_1: 1, item_2: 2, item_3: 3 }, + }); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.debugElement.children.length).toBe(4); + expect(fixture.debugElement.children[0].nativeElement.textContent).toBe('foo = FOO'); + expect(fixture.debugElement.children[1].nativeElement.textContent).toBe('bar = BAR'); + expect(fixture.debugElement.children[2].nativeElement.textContent).toBe('baz = BAZ'); + expect(fixture.debugElement.children[3].nativeElement.textContent).toBe( + 'subItem = {"item_1":1,"item_2":2,"item_3":3}', + ); + fixture.componentRef.setInput('json', { + abc: 'ABC', + def: 'DEF', + }); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.debugElement.children.length).toBe(2); + expect(fixture.debugElement.children[0].nativeElement.textContent).toBe('abc = ABC'); + expect(fixture.debugElement.children[1].nativeElement.textContent).toBe('def = DEF'); + }); +}); From 0b4c43a985f497a5c827962a79a085001296ab95 Mon Sep 17 00:00:00 2001 From: dvladir Date: Thu, 11 Dec 2025 12:45:19 +0300 Subject: [PATCH 07/29] SED-4412 key-value-inline tests --- .../key-value-inline.component.test.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 projects/step-core/src/lib/modules/json-viewer/components/key-value-inline/key-value-inline.component.test.ts diff --git a/projects/step-core/src/lib/modules/json-viewer/components/key-value-inline/key-value-inline.component.test.ts b/projects/step-core/src/lib/modules/json-viewer/components/key-value-inline/key-value-inline.component.test.ts new file mode 100644 index 0000000000..298c611528 --- /dev/null +++ b/projects/step-core/src/lib/modules/json-viewer/components/key-value-inline/key-value-inline.component.test.ts @@ -0,0 +1,59 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DynamicValue, JsonViewerModule, KeyValueInlineComponent } from '@exense/step-core'; +import { By } from '@angular/platform-browser'; + +describe('KeyValueInlineComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [JsonViewerModule], + }).compileComponents(); + + fixture = TestBed.createComponent(KeyValueInlineComponent); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Ordinary rendering', async () => { + const element = fixture.debugElement.query(By.css('.key-value-item')); + expect(element.nativeElement.textContent).toBe(''); + + fixture.componentRef.setInput('json', { + foo: 'FOO', + bar: 'BAR', + baz: 'BAZ', + subItem: { item_1: 1, item_2: 2, item_3: 3 }, + }); + + fixture.detectChanges(true); + await fixture.whenStable(); + + expect(element.nativeElement.textContent.trim()).toEqual( + 'foo = FOO ¦ bar = BAR ¦ baz = BAZ ¦ subItem = {"item_1":1,"item_2":2,"item_3":3}', + ); + + fixture.componentRef.setInput('maxChars', 10); + + fixture.detectChanges(true); + await fixture.whenStable(); + + expect(element.nativeElement.textContent.trim()).toEqual('foo = FOO ...'); + }); + + it('Dynamic value rendering', async () => { + const element = fixture.debugElement.query(By.css('.key-value-item')); + expect(element.nativeElement.textContent).toBe(''); + + fixture.componentRef.setInput('json', { + foo: 'FOO', + bar: { value: 'BAR', dynamic: false, expression: undefined } as DynamicValue, + baz: { value: undefined, dynamic: true, expression: 'BAZ' } as DynamicValue, + }); + + fixture.detectChanges(true); + await fixture.whenStable(); + + expect(element.nativeElement.textContent.trim()).toEqual('foo = FOO ¦ bar = BAR ¦ baz = BAZ'); + }); +}); From bea17572c04cf1e768128ed2375035e85a0b0fb4 Mon Sep 17 00:00:00 2001 From: dvladir Date: Thu, 11 Dec 2025 14:00:34 +0300 Subject: [PATCH 08/29] SED-4412 Pretty print tests --- .../key-value-inline.component.test.ts | 6 +-- .../pretty-print-inline.component.test.ts | 53 +++++++++++++++++++ .../pretty-print.component.test.ts | 45 ++++++++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 projects/step-core/src/lib/modules/json-viewer/components/pretty-print-inline/pretty-print-inline.component.test.ts create mode 100644 projects/step-core/src/lib/modules/json-viewer/components/pretty-print/pretty-print.component.test.ts diff --git a/projects/step-core/src/lib/modules/json-viewer/components/key-value-inline/key-value-inline.component.test.ts b/projects/step-core/src/lib/modules/json-viewer/components/key-value-inline/key-value-inline.component.test.ts index 298c611528..35c98e6ebd 100644 --- a/projects/step-core/src/lib/modules/json-viewer/components/key-value-inline/key-value-inline.component.test.ts +++ b/projects/step-core/src/lib/modules/json-viewer/components/key-value-inline/key-value-inline.component.test.ts @@ -26,7 +26,7 @@ describe('KeyValueInlineComponent', () => { subItem: { item_1: 1, item_2: 2, item_3: 3 }, }); - fixture.detectChanges(true); + fixture.detectChanges(); await fixture.whenStable(); expect(element.nativeElement.textContent.trim()).toEqual( @@ -35,7 +35,7 @@ describe('KeyValueInlineComponent', () => { fixture.componentRef.setInput('maxChars', 10); - fixture.detectChanges(true); + fixture.detectChanges(); await fixture.whenStable(); expect(element.nativeElement.textContent.trim()).toEqual('foo = FOO ...'); @@ -51,7 +51,7 @@ describe('KeyValueInlineComponent', () => { baz: { value: undefined, dynamic: true, expression: 'BAZ' } as DynamicValue, }); - fixture.detectChanges(true); + fixture.detectChanges(); await fixture.whenStable(); expect(element.nativeElement.textContent.trim()).toEqual('foo = FOO ¦ bar = BAR ¦ baz = BAZ'); diff --git a/projects/step-core/src/lib/modules/json-viewer/components/pretty-print-inline/pretty-print-inline.component.test.ts b/projects/step-core/src/lib/modules/json-viewer/components/pretty-print-inline/pretty-print-inline.component.test.ts new file mode 100644 index 0000000000..0947994892 --- /dev/null +++ b/projects/step-core/src/lib/modules/json-viewer/components/pretty-print-inline/pretty-print-inline.component.test.ts @@ -0,0 +1,53 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { JsonViewerModule, PrettyPrintInlineComponent } from '@exense/step-core'; + +describe('PrettyPrintInlineComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [JsonViewerModule], + }).compileComponents(); + + fixture = TestBed.createComponent(PrettyPrintInlineComponent); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Rendering', async () => { + expect(fixture.debugElement.nativeElement.textContent).toBe('\n'); + + fixture.componentRef.setInput('json', { + foo: 'FOO', + bar: 'BAR', + baz: 'BAZ', + subItem: { item_1: 1, item_2: 2, item_3: 3 }, + }); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.debugElement.nativeElement.textContent).toBe( + `{ + "foo": "FOO", + "bar": "BAR", + "baz": "BAZ", + "subItem": { + "item_1": 1, + "item_2": 2, + "item_3": 3 + } +`, + ); + + fixture.componentRef.setInput('maxChars', 10); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.debugElement.nativeElement.textContent).toBe( + `{ + "foo":... +`, + ); + }); +}); diff --git a/projects/step-core/src/lib/modules/json-viewer/components/pretty-print/pretty-print.component.test.ts b/projects/step-core/src/lib/modules/json-viewer/components/pretty-print/pretty-print.component.test.ts new file mode 100644 index 0000000000..1c8abf8c0c --- /dev/null +++ b/projects/step-core/src/lib/modules/json-viewer/components/pretty-print/pretty-print.component.test.ts @@ -0,0 +1,45 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { JsonViewerModule, PrettyPrintComponent } from '@exense/step-core'; +import { By } from '@angular/platform-browser'; + +describe('PrettyPrintComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [JsonViewerModule], + }).compileComponents(); + + fixture = TestBed.createComponent(PrettyPrintComponent); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Rendering', async () => { + const pre = fixture.debugElement.query(By.css('pre')); + expect(pre.nativeElement.textContent).toBe(''); + + fixture.componentRef.setInput('json', { + foo: 'FOO', + bar: 'BAR', + baz: 'BAZ', + subItem: { item_1: 1, item_2: 2, item_3: 3 }, + }); + + fixture.detectChanges(); + await fixture.whenStable(); + + expect(pre.nativeElement.textContent).toBe( + `{ + "foo": "FOO", + "bar": "BAR", + "baz": "BAZ", + "subItem": { + "item_1": 1, + "item_2": 2, + "item_3": 3 + } +}`, + ); + }); +}); From c83aea8a29525c6eb4a51c08cd330bf0bd5e1b6f Mon Sep 17 00:00:00 2001 From: dvladir Date: Fri, 12 Dec 2025 16:12:49 +0300 Subject: [PATCH 09/29] SED-4412 select tests --- jest.config.js | 1 + package.json | 3 +- .../select/select.component.test.ts | 251 ++++++++++++++++++ 3 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 projects/step-core/src/lib/modules/basics/components/select/select.component.test.ts diff --git a/jest.config.js b/jest.config.js index 36684b1622..7a27f29be1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -10,4 +10,5 @@ module.exports = { ], moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '' }), modulePathIgnorePatterns: ['/dist/'], + testEnvironment: '@happy-dom/jest-environment', }; diff --git a/package.json b/package.json index fac3c3a26a..6971432aba 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,8 @@ "typescript": "5.5.4", "jest": "29.5.0", "@types/jest": "29.5.14", - "jest-preset-angular": "14.6.2" + "jest-preset-angular": "14.6.2", + "@happy-dom/jest-environment": "19.0.2" }, "engines": { "node": ">=18.19.0" diff --git a/projects/step-core/src/lib/modules/basics/components/select/select.component.test.ts b/projects/step-core/src/lib/modules/basics/components/select/select.component.test.ts new file mode 100644 index 0000000000..00e7f0b1b7 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/select/select.component.test.ts @@ -0,0 +1,251 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArrayItemLabelValueExtractor, StepBasicsModule } from '@exense/step-core'; +import { ChangeDetectorRef, Component, input, model } from '@angular/core'; +import { KeyValue } from '@angular/common'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { By } from '@angular/platform-browser'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatSelectHarness } from '@angular/material/select/testing'; + +@Component({ + selector: 'step-test-select', + imports: [StepBasicsModule], + template: ` + + `, +}) +class TestSelectComponent { + readonly selectModel = model(undefined); + readonly useSearch = input(false); + readonly multiple = input(false); + readonly items = input(undefined); + readonly emptyPlaceholder = input(''); + readonly extractor = input | undefined>(undefined); + readonly useClear = input(false); + readonly clearLabel = input('clear'); + readonly clearValue = input(undefined); +} + +const updateSearchValue = (fixture: ComponentFixture, searchValue: string) => { + const seInput = fixture.debugElement.parent!.query(By.css('.mat-select-search-inner-row > .mat-select-search-input')); + seInput.nativeElement.value = searchValue; + seInput.triggerEventHandler('input', { target: seInput.nativeElement }); +}; + +describe('SelectComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + let changeDetectorRef: ChangeDetectorRef; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestSelectComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TestSelectComponent); + changeDetectorRef = fixture.debugElement.injector.get(ChangeDetectorRef); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + await fixture.whenStable(); + }); + + it('Selection', async () => { + const select = await loader.getHarness(MatSelectHarness); + await select.open(); + let options = await select.getOptions(); + + expect(options.length).toBe(0); + await select.close(); + + fixture.componentRef.setInput('items', [ + { key: 1, value: 'One' }, + { key: 2, value: 'Two' }, + { key: 3, value: 'Three' }, + ] as KeyValue[]); + + fixture.detectChanges(); + await fixture.whenStable(); + + await select.open(); + + options = await select.getOptions(); + + expect(options.length).toBe(3); + + expect(fixture.componentInstance.selectModel()).toBe(undefined); + + await options[0].click(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.componentInstance.selectModel()).toBe(1); + + await options[1].click(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.componentInstance.selectModel()).toBe(2); + + await options[2].click(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.componentInstance.selectModel()).toBe(3); + }); + + it('Clear', async () => { + const SELECTOR_CLEAR_OPTION = { selector: '.step-select-clear-value' }; + const SELECTOR_ITEM_OPTION = { selector: ':not(.step-select-clear-value)' }; + + const select = await loader.getHarness(MatSelectHarness); + await select.open(); + let clearOptions = await select.getOptions(SELECTOR_CLEAR_OPTION); + let itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + + expect(clearOptions.length).toBe(0); + expect(itemOptions.length).toBe(0); + + await select.close(); + fixture.componentRef.setInput('useClear', true); + fixture.detectChanges(); + await fixture.whenStable(); + + await select.open(); + clearOptions = await select.getOptions(SELECTOR_CLEAR_OPTION); + itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + + expect(clearOptions.length).toBe(1); + expect(itemOptions.length).toBe(0); + await select.close(); + + fixture.componentRef.setInput('items', [ + { key: 1, value: 'One' }, + { key: 2, value: 'Two' }, + { key: 3, value: 'Three' }, + ] as KeyValue[]); + + fixture.detectChanges(); + await fixture.whenStable(); + + await select.open(); + clearOptions = await select.getOptions(SELECTOR_CLEAR_OPTION); + itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + + expect(clearOptions.length).toBe(1); + expect(itemOptions.length).toBe(3); + + await itemOptions[0].click(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.componentInstance.selectModel()).toBe(1); + + await select.clickOptions(SELECTOR_CLEAR_OPTION); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentInstance.selectModel()).toBe(undefined); + + await itemOptions[1].click(); + fixture.componentRef.setInput('clearValue', null); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.componentInstance.selectModel()).toBe(2); + + await select.clickOptions(SELECTOR_CLEAR_OPTION); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentInstance.selectModel()).toBe(null); + + await select.open(); + let clearOptionText = await select.getOptions(SELECTOR_CLEAR_OPTION).then((options) => options[0].getText()); + expect(clearOptionText).toBe('clear'); + + fixture.componentRef.setInput('clearLabel', 'Erase value'); + fixture.detectChanges(); + await fixture.whenStable(); + await select.open(); + clearOptionText = await select.getOptions(SELECTOR_CLEAR_OPTION).then((options) => options[0].getText()); + expect(clearOptionText).toBe('Erase value'); + }); + + it('Search', async () => { + const SELECTOR_SEARCH_OPTION = { selector: '.contains-mat-select-search' }; + const SELECTOR_ITEM_OPTION = { selector: ':not(.contains-mat-select-search)' }; + + const select = await loader.getHarness(MatSelectHarness); + await select.open(); + + let searchOptions = await select.getOptions(SELECTOR_SEARCH_OPTION); + let itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + expect(searchOptions.length).toBe(0); + expect(itemOptions.length).toBe(0); + await select.close(); + + fixture.componentRef.setInput('useSearch', true); + await select.open(); + + searchOptions = await select.getOptions(SELECTOR_SEARCH_OPTION); + itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + expect(searchOptions.length).toBe(1); + expect(itemOptions.length).toBe(0); + + fixture.componentRef.setInput('items', [ + { key: 'aaa_aaa', value: 'AAA AAA' }, + { key: 'aaa_bbb', value: 'AAA BBB' }, + { key: 'bbb_ccc', value: 'BBB CCC' }, + ] as KeyValue[]); + + await select.open(); + searchOptions = await select.getOptions(SELECTOR_SEARCH_OPTION); + itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + expect(searchOptions.length).toBe(1); + expect(itemOptions.length).toBe(3); + + const getOptionTexts = async () => { + const options = await select.getOptions(SELECTOR_ITEM_OPTION); + const result = [] as string[]; + for (const opt of options) { + const text = await opt.getText(); + result.push(text); + } + return result; + }; + + let texts = await getOptionTexts(); + expect(texts).toEqual(['AAA AAA', 'AAA BBB', 'BBB CCC']); + + updateSearchValue(fixture, 'aaa'); + fixture.detectChanges(); + await fixture.whenStable(); + + texts = await getOptionTexts(); + expect(texts).toEqual(['AAA AAA', 'AAA BBB']); + + updateSearchValue(fixture, 'bbb'); + fixture.detectChanges(); + await fixture.whenStable(); + texts = await getOptionTexts(); + expect(texts).toEqual(['AAA BBB', 'BBB CCC']); + + updateSearchValue(fixture, ''); + fixture.detectChanges(); + await fixture.whenStable(); + texts = await getOptionTexts(); + expect(texts).toEqual(['AAA AAA', 'AAA BBB', 'BBB CCC']); + /* + searchOptions[0].host() + .then((node) => node.getAttribute('class')) + .then(console.log); + const hasInput = await searchOptions[0].hasHarness(MatInputHarness); + console.log(hasInput); +*/ + }); +}); From a991f524e3962acbdb35705518ad4d91ce49fa3b Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 30 Dec 2025 13:31:45 +0300 Subject: [PATCH 10/29] SED-4412 Select component tests --- .../select/select.component.test.ts | 476 ++++++++++-------- tsconfig.spec.json | 2 +- 2 files changed, 266 insertions(+), 212 deletions(-) diff --git a/projects/step-core/src/lib/modules/basics/components/select/select.component.test.ts b/projects/step-core/src/lib/modules/basics/components/select/select.component.test.ts index 00e7f0b1b7..4cb4c29c85 100644 --- a/projects/step-core/src/lib/modules/basics/components/select/select.component.test.ts +++ b/projects/step-core/src/lib/modules/basics/components/select/select.component.test.ts @@ -1,12 +1,25 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ArrayItemLabelValueExtractor, StepBasicsModule } from '@exense/step-core'; -import { ChangeDetectorRef, Component, input, model } from '@angular/core'; +import { Component, input, model } from '@angular/core'; import { KeyValue } from '@angular/common'; import { HarnessLoader } from '@angular/cdk/testing'; import { By } from '@angular/platform-browser'; import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; import { MatSelectHarness } from '@angular/material/select/testing'; +@Component({ template: '' }) +abstract class BaseTestSelectComponent { + readonly selectModel = model(undefined); + readonly useSearch = input(false); + readonly multiple = input(false); + readonly items = input(undefined); + readonly emptyPlaceholder = input(''); + readonly extractor = input | undefined>(undefined); + readonly useClear = input(false); + readonly clearLabel = input('clear'); + readonly clearValue = input(undefined); +} + @Component({ selector: 'step-test-select', imports: [StepBasicsModule], @@ -24,228 +37,269 @@ import { MatSelectHarness } from '@angular/material/select/testing'; /> `, }) -class TestSelectComponent { - readonly selectModel = model(undefined); - readonly useSearch = input(false); - readonly multiple = input(false); - readonly items = input(undefined); - readonly emptyPlaceholder = input(''); - readonly extractor = input | undefined>(undefined); - readonly useClear = input(false); - readonly clearLabel = input('clear'); - readonly clearValue = input(undefined); -} +class TestSelectComponent extends BaseTestSelectComponent {} + +@Component({ + selector: 'step-test-select-with-extra-options', + imports: [StepBasicsModule], + template: ` + + + ONE + TWO + THREE + + + `, +}) +class TestSelectWithExtraOptionsComponent extends BaseTestSelectComponent {} -const updateSearchValue = (fixture: ComponentFixture, searchValue: string) => { +const updateSearchValue = (fixture: ComponentFixture, searchValue: string) => { const seInput = fixture.debugElement.parent!.query(By.css('.mat-select-search-inner-row > .mat-select-search-input')); seInput.nativeElement.value = searchValue; seInput.triggerEventHandler('input', { target: seInput.nativeElement }); }; -describe('SelectComponent', () => { - let fixture: ComponentFixture; - let loader: HarnessLoader; - let changeDetectorRef: ChangeDetectorRef; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TestSelectComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(TestSelectComponent); - changeDetectorRef = fixture.debugElement.injector.get(ChangeDetectorRef); - fixture.detectChanges(); - loader = TestbedHarnessEnvironment.loader(fixture); - await fixture.whenStable(); - }); - - it('Selection', async () => { - const select = await loader.getHarness(MatSelectHarness); - await select.open(); - let options = await select.getOptions(); - - expect(options.length).toBe(0); - await select.close(); - - fixture.componentRef.setInput('items', [ - { key: 1, value: 'One' }, - { key: 2, value: 'Two' }, - { key: 3, value: 'Three' }, - ] as KeyValue[]); - - fixture.detectChanges(); - await fixture.whenStable(); - - await select.open(); - - options = await select.getOptions(); - - expect(options.length).toBe(3); - - expect(fixture.componentInstance.selectModel()).toBe(undefined); - - await options[0].click(); - fixture.detectChanges(); - await fixture.whenStable(); - expect(fixture.componentInstance.selectModel()).toBe(1); - - await options[1].click(); - fixture.detectChanges(); - await fixture.whenStable(); - expect(fixture.componentInstance.selectModel()).toBe(2); - - await options[2].click(); - fixture.detectChanges(); - await fixture.whenStable(); - expect(fixture.componentInstance.selectModel()).toBe(3); - }); - - it('Clear', async () => { - const SELECTOR_CLEAR_OPTION = { selector: '.step-select-clear-value' }; - const SELECTOR_ITEM_OPTION = { selector: ':not(.step-select-clear-value)' }; - - const select = await loader.getHarness(MatSelectHarness); - await select.open(); - let clearOptions = await select.getOptions(SELECTOR_CLEAR_OPTION); - let itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); - - expect(clearOptions.length).toBe(0); - expect(itemOptions.length).toBe(0); - - await select.close(); - fixture.componentRef.setInput('useClear', true); - fixture.detectChanges(); - await fixture.whenStable(); - - await select.open(); - clearOptions = await select.getOptions(SELECTOR_CLEAR_OPTION); - itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); - - expect(clearOptions.length).toBe(1); - expect(itemOptions.length).toBe(0); - await select.close(); - - fixture.componentRef.setInput('items', [ - { key: 1, value: 'One' }, - { key: 2, value: 'Two' }, - { key: 3, value: 'Three' }, - ] as KeyValue[]); - - fixture.detectChanges(); - await fixture.whenStable(); - - await select.open(); - clearOptions = await select.getOptions(SELECTOR_CLEAR_OPTION); - itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); - - expect(clearOptions.length).toBe(1); - expect(itemOptions.length).toBe(3); - - await itemOptions[0].click(); - fixture.detectChanges(); - await fixture.whenStable(); - expect(fixture.componentInstance.selectModel()).toBe(1); - - await select.clickOptions(SELECTOR_CLEAR_OPTION); - fixture.detectChanges(); - await fixture.whenStable(); - - expect(fixture.componentInstance.selectModel()).toBe(undefined); +const getOptionTexts = async (select: MatSelectHarness, optionsSelector?: { selector: string }) => { + const options = await select.getOptions(optionsSelector); + const result = [] as string[]; + for (const opt of options) { + const text = await opt.getText(); + result.push(text); + } + return result; +}; - await itemOptions[1].click(); - fixture.componentRef.setInput('clearValue', null); - fixture.detectChanges(); - await fixture.whenStable(); - expect(fixture.componentInstance.selectModel()).toBe(2); +describe('SelectComponent', () => { + describe('Common Functionality', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestSelectComponent], + }).compileComponents(); - await select.clickOptions(SELECTOR_CLEAR_OPTION); - fixture.detectChanges(); - await fixture.whenStable(); + fixture = TestBed.createComponent(TestSelectComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + await fixture.whenStable(); + }); - expect(fixture.componentInstance.selectModel()).toBe(null); + it('Selection', async () => { + const select = await loader.getHarness(MatSelectHarness); + await select.open(); + let options = await select.getOptions(); - await select.open(); - let clearOptionText = await select.getOptions(SELECTOR_CLEAR_OPTION).then((options) => options[0].getText()); - expect(clearOptionText).toBe('clear'); + expect(options.length).toBe(0); + await select.close(); - fixture.componentRef.setInput('clearLabel', 'Erase value'); - fixture.detectChanges(); - await fixture.whenStable(); - await select.open(); - clearOptionText = await select.getOptions(SELECTOR_CLEAR_OPTION).then((options) => options[0].getText()); - expect(clearOptionText).toBe('Erase value'); + fixture.componentRef.setInput('items', [ + { key: 1, value: 'One' }, + { key: 2, value: 'Two' }, + { key: 3, value: 'Three' }, + ] as KeyValue[]); + + fixture.detectChanges(); + await fixture.whenStable(); + + await select.open(); + + options = await select.getOptions(); + + expect(options.length).toBe(3); + + expect(fixture.componentInstance.selectModel()).toBe(undefined); + + await options[0].click(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.componentInstance.selectModel()).toBe(1); + + await options[1].click(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.componentInstance.selectModel()).toBe(2); + + await options[2].click(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.componentInstance.selectModel()).toBe(3); + }); + + it('Clear', async () => { + const SELECTOR_CLEAR_OPTION = { selector: '.step-select-clear-value' }; + const SELECTOR_ITEM_OPTION = { selector: ':not(.step-select-clear-value)' }; + + const select = await loader.getHarness(MatSelectHarness); + await select.open(); + let clearOptions = await select.getOptions(SELECTOR_CLEAR_OPTION); + let itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + + expect(clearOptions.length).toBe(0); + expect(itemOptions.length).toBe(0); + + await select.close(); + fixture.componentRef.setInput('useClear', true); + fixture.detectChanges(); + await fixture.whenStable(); + + await select.open(); + clearOptions = await select.getOptions(SELECTOR_CLEAR_OPTION); + itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + + expect(clearOptions.length).toBe(1); + expect(itemOptions.length).toBe(0); + await select.close(); + + fixture.componentRef.setInput('items', [ + { key: 1, value: 'One' }, + { key: 2, value: 'Two' }, + { key: 3, value: 'Three' }, + ] as KeyValue[]); + + fixture.detectChanges(); + await fixture.whenStable(); + + await select.open(); + clearOptions = await select.getOptions(SELECTOR_CLEAR_OPTION); + itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + + expect(clearOptions.length).toBe(1); + expect(itemOptions.length).toBe(3); + + await itemOptions[0].click(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.componentInstance.selectModel()).toBe(1); + + await select.clickOptions(SELECTOR_CLEAR_OPTION); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentInstance.selectModel()).toBe(undefined); + + await itemOptions[1].click(); + fixture.componentRef.setInput('clearValue', null); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.componentInstance.selectModel()).toBe(2); + + await select.clickOptions(SELECTOR_CLEAR_OPTION); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentInstance.selectModel()).toBe(null); + + await select.open(); + let clearOptionText = await select.getOptions(SELECTOR_CLEAR_OPTION).then((options) => options[0].getText()); + expect(clearOptionText).toBe('clear'); + + fixture.componentRef.setInput('clearLabel', 'Erase value'); + fixture.detectChanges(); + await fixture.whenStable(); + await select.open(); + clearOptionText = await select.getOptions(SELECTOR_CLEAR_OPTION).then((options) => options[0].getText()); + expect(clearOptionText).toBe('Erase value'); + }); + + it('Search', async () => { + const SELECTOR_SEARCH_OPTION = { selector: '.contains-mat-select-search' }; + const SELECTOR_ITEM_OPTION = { selector: ':not(.contains-mat-select-search)' }; + + const select = await loader.getHarness(MatSelectHarness); + await select.open(); + + let searchOptions = await select.getOptions(SELECTOR_SEARCH_OPTION); + let itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + expect(searchOptions.length).toBe(0); + expect(itemOptions.length).toBe(0); + await select.close(); + + fixture.componentRef.setInput('useSearch', true); + await select.open(); + + searchOptions = await select.getOptions(SELECTOR_SEARCH_OPTION); + itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + expect(searchOptions.length).toBe(1); + expect(itemOptions.length).toBe(0); + + fixture.componentRef.setInput('items', [ + { key: 'aaa_aaa', value: 'AAA AAA' }, + { key: 'aaa_bbb', value: 'AAA BBB' }, + { key: 'bbb_ccc', value: 'BBB CCC' }, + ] as KeyValue[]); + + await select.open(); + searchOptions = await select.getOptions(SELECTOR_SEARCH_OPTION); + itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); + expect(searchOptions.length).toBe(1); + expect(itemOptions.length).toBe(3); + + let texts = await getOptionTexts(select, SELECTOR_ITEM_OPTION); + expect(texts).toEqual(['AAA AAA', 'AAA BBB', 'BBB CCC']); + + updateSearchValue(fixture, 'aaa'); + fixture.detectChanges(); + await fixture.whenStable(); + + texts = await getOptionTexts(select, SELECTOR_ITEM_OPTION); + expect(texts).toEqual(['AAA AAA', 'AAA BBB']); + + updateSearchValue(fixture, 'bbb'); + fixture.detectChanges(); + await fixture.whenStable(); + texts = await getOptionTexts(select, SELECTOR_ITEM_OPTION); + expect(texts).toEqual(['AAA BBB', 'BBB CCC']); + + updateSearchValue(fixture, ''); + fixture.detectChanges(); + await fixture.whenStable(); + texts = await getOptionTexts(select, SELECTOR_ITEM_OPTION); + expect(texts).toEqual(['AAA AAA', 'AAA BBB', 'BBB CCC']); + }); }); - it('Search', async () => { - const SELECTOR_SEARCH_OPTION = { selector: '.contains-mat-select-search' }; - const SELECTOR_ITEM_OPTION = { selector: ':not(.contains-mat-select-search)' }; - - const select = await loader.getHarness(MatSelectHarness); - await select.open(); - - let searchOptions = await select.getOptions(SELECTOR_SEARCH_OPTION); - let itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); - expect(searchOptions.length).toBe(0); - expect(itemOptions.length).toBe(0); - await select.close(); - - fixture.componentRef.setInput('useSearch', true); - await select.open(); - - searchOptions = await select.getOptions(SELECTOR_SEARCH_OPTION); - itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); - expect(searchOptions.length).toBe(1); - expect(itemOptions.length).toBe(0); - - fixture.componentRef.setInput('items', [ - { key: 'aaa_aaa', value: 'AAA AAA' }, - { key: 'aaa_bbb', value: 'AAA BBB' }, - { key: 'bbb_ccc', value: 'BBB CCC' }, - ] as KeyValue[]); - - await select.open(); - searchOptions = await select.getOptions(SELECTOR_SEARCH_OPTION); - itemOptions = await select.getOptions(SELECTOR_ITEM_OPTION); - expect(searchOptions.length).toBe(1); - expect(itemOptions.length).toBe(3); - - const getOptionTexts = async () => { - const options = await select.getOptions(SELECTOR_ITEM_OPTION); - const result = [] as string[]; - for (const opt of options) { - const text = await opt.getText(); - result.push(text); - } - return result; - }; - - let texts = await getOptionTexts(); - expect(texts).toEqual(['AAA AAA', 'AAA BBB', 'BBB CCC']); - - updateSearchValue(fixture, 'aaa'); - fixture.detectChanges(); - await fixture.whenStable(); - - texts = await getOptionTexts(); - expect(texts).toEqual(['AAA AAA', 'AAA BBB']); - - updateSearchValue(fixture, 'bbb'); - fixture.detectChanges(); - await fixture.whenStable(); - texts = await getOptionTexts(); - expect(texts).toEqual(['AAA BBB', 'BBB CCC']); - - updateSearchValue(fixture, ''); - fixture.detectChanges(); - await fixture.whenStable(); - texts = await getOptionTexts(); - expect(texts).toEqual(['AAA AAA', 'AAA BBB', 'BBB CCC']); - /* - searchOptions[0].host() - .then((node) => node.getAttribute('class')) - .then(console.log); - const hasInput = await searchOptions[0].hasHarness(MatInputHarness); - console.log(hasInput); -*/ + describe('Extra options', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestSelectWithExtraOptionsComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TestSelectWithExtraOptionsComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + await fixture.whenStable(); + }); + + it('Extra options', async () => { + const select = await loader.getHarness(MatSelectHarness); + await select.open(); + let optionsText = await getOptionTexts(select); + expect(optionsText).toEqual(['ONE', 'TWO', 'THREE']); + + fixture.componentRef.setInput('items', [ + { key: 'aaa_aaa', value: 'AAA AAA' }, + { key: 'aaa_bbb', value: 'AAA BBB' }, + { key: 'bbb_ccc', value: 'BBB CCC' }, + ] as KeyValue[]); + + await select.open(); + optionsText = await getOptionTexts(select); + expect(optionsText).toEqual(['ONE', 'TWO', 'THREE', 'AAA AAA', 'AAA BBB', 'BBB CCC']); + }); }); }); diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 837d62bcfe..02d28b0b52 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "out-tsc/test", "types": ["jest", "node"], - "target": "ES2022", + "target": "es2016", "module": "ES2022", "lib": ["ES2022", "dom"], "skipLibCheck": true, From ae2021f8ccc948b3bbc1342db69153e97c908879 Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 30 Dec 2025 14:21:49 +0300 Subject: [PATCH 11/29] SED-4412 Artefact Inline Details header tests --- ...ct-inline-details-header.component.test.ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 projects/step-core/src/lib/modules/artefacts-common/components/artefact-inline-details-header/artefact-inline-details-header.component.test.ts diff --git a/projects/step-core/src/lib/modules/artefacts-common/components/artefact-inline-details-header/artefact-inline-details-header.component.test.ts b/projects/step-core/src/lib/modules/artefacts-common/components/artefact-inline-details-header/artefact-inline-details-header.component.test.ts new file mode 100644 index 0000000000..29c5e718b7 --- /dev/null +++ b/projects/step-core/src/lib/modules/artefacts-common/components/artefact-inline-details-header/artefact-inline-details-header.component.test.ts @@ -0,0 +1,43 @@ +import { Component, signal } from '@angular/core'; +import { ArtefactInlineDetailsHeaderComponent } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +@Component({ + selector: 'step-artefact-inline-details-header-test', + imports: [ArtefactInlineDetailsHeaderComponent], + template: ` + + +
FOO
+
+ `, +}) +class ArtefactInlineDetailsHeaderTestComponent { + readonly isVisible = signal(false); +} + +describe('ArtefactInlineDetailsHeader', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ imports: [ArtefactInlineDetailsHeaderTestComponent] }).compileComponents(); + + fixture = TestBed.createComponent(ArtefactInlineDetailsHeaderTestComponent); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Content visibility', async () => { + let content = fixture.debugElement.query(By.css('.testDiv')); + expect(content).toBeNull(); + + fixture.componentInstance.isVisible.set(true); + fixture.detectChanges(); + await fixture.whenStable(); + + content = fixture.debugElement.query(By.css('.testDiv')); + expect(content).not.toBeNull(); + expect((content.nativeElement as HTMLElement).textContent).toBe('FOO'); + }); +}); From 1a2a031f0c3a634489fab1579fe0bd3c8e23844f Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 30 Dec 2025 15:38:36 +0300 Subject: [PATCH 12/29] SED-4412 Streaming Attachment Indicator component --- ...ing-attachment-indicator.component.test.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 projects/step-core/src/lib/modules/attachments/components/streaming-attachment-indicator/streaming-attachment-indicator.component.test.ts diff --git a/projects/step-core/src/lib/modules/attachments/components/streaming-attachment-indicator/streaming-attachment-indicator.component.test.ts b/projects/step-core/src/lib/modules/attachments/components/streaming-attachment-indicator/streaming-attachment-indicator.component.test.ts new file mode 100644 index 0000000000..429e375c05 --- /dev/null +++ b/projects/step-core/src/lib/modules/attachments/components/streaming-attachment-indicator/streaming-attachment-indicator.component.test.ts @@ -0,0 +1,78 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + AttachmentMeta, + AugmentedResourcesService, + StreamingAttachmentIndicatorComponent, + StreamingAttachmentMeta, +} from '@exense/step-core'; +import { v4 } from 'uuid'; +import { By } from '@angular/platform-browser'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { AugmentedStreamingResourcesService } from '../../../../client/augmented/services/augmented-streaming-resources.service'; + +const SIMPLE_ATTACHMENT: AttachmentMeta = { + name: 'test.jpg', + mimeType: 'image/jpeg', + type: 'step.attachment.AttachmentMeta', + id: v4(), +}; + +const STREAMING_ATTACHMENT: StreamingAttachmentMeta = { + name: 'test.txt', + mimeType: 'plain/text', + type: 'step.attachments.StreamingAttachmentMeta', + id: v4(), + currentNumberOfLines: 10, + status: 'INITIATED', +}; + +const IN_PROGRESS_STREAMING_ATTACHMENT: StreamingAttachmentMeta = { + ...STREAMING_ATTACHMENT, + status: 'IN_PROGRESS', +}; + +describe('StreamingAttachmentIndicatorComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StreamingAttachmentIndicatorComponent], + providers: [ + { + provide: AugmentedResourcesService, + useValue: null, + }, + { + provide: AugmentedStreamingResourcesService, + useValue: null, + }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(StreamingAttachmentIndicatorComponent); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Check indicator visibility', async () => { + let indicator = fixture.debugElement.query(By.directive(MatProgressSpinner)); + expect(indicator).toBeNull(); + + fixture.componentRef.setInput('attachment', SIMPLE_ATTACHMENT); + fixture.detectChanges(); + await fixture.whenStable(); + indicator = fixture.debugElement.query(By.directive(MatProgressSpinner)); + expect(indicator).toBeNull(); + + fixture.componentRef.setInput('attachment', STREAMING_ATTACHMENT); + fixture.detectChanges(); + await fixture.whenStable(); + indicator = fixture.debugElement.query(By.directive(MatProgressSpinner)); + expect(indicator).toBeNull(); + + fixture.componentRef.setInput('attachment', IN_PROGRESS_STREAMING_ATTACHMENT); + fixture.detectChanges(); + await fixture.whenStable(); + indicator = fixture.debugElement.query(By.directive(MatProgressSpinner)); + expect(indicator).not.toBeNull(); + }); +}); From bae5a1282c10554ac8740343284759402460055d Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 30 Dec 2025 17:07:48 +0300 Subject: [PATCH 13/29] SED-4412 AutoShrinkItemValueComponent tests --- .../auto-shrink-item-value.component.test.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 projects/step-core/src/lib/modules/auto-shrink-list/components/auto-shrink-item-value/auto-shrink-item-value.component.test.ts diff --git a/projects/step-core/src/lib/modules/auto-shrink-list/components/auto-shrink-item-value/auto-shrink-item-value.component.test.ts b/projects/step-core/src/lib/modules/auto-shrink-list/components/auto-shrink-item-value/auto-shrink-item-value.component.test.ts new file mode 100644 index 0000000000..f40e2391ed --- /dev/null +++ b/projects/step-core/src/lib/modules/auto-shrink-list/components/auto-shrink-item-value/auto-shrink-item-value.component.test.ts @@ -0,0 +1,74 @@ +import { Component, Directive, signal } from '@angular/core'; +import { KeyValue } from '@angular/common'; +import { AutoShrinkItemValueComponent } from '@exense/step-core'; +import { TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +@Directive() +abstract class AutoShrinkItemValueBaseComponent { + readonly item = signal>({ key: '', value: '' }); +} + +@Component({ + selector: 'step-auto-shrink-item-value-without-template-test', + imports: [AutoShrinkItemValueComponent], + template: ` + @if (item(); as item) { + + } + `, +}) +class AutoShrinkItemValueWithoutTemplateComponent extends AutoShrinkItemValueBaseComponent {} + +@Component({ + selector: 'step-auto-shrink-item-value-with-template-test', + imports: [AutoShrinkItemValueComponent], + template: ` + @if (item(); as item) { + + } + +
EMPTY
+
+ `, +}) +class AutoShrinkItemValueWithTemplateComponent extends AutoShrinkItemValueBaseComponent {} + +describe('AutoShrinkItemValueComponent', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AutoShrinkItemValueWithoutTemplateComponent, AutoShrinkItemValueWithTemplateComponent], + }).compileComponents(); + }); + + it('Without empty template', async () => { + let fixture = TestBed.createComponent(AutoShrinkItemValueWithoutTemplateComponent); + fixture.detectChanges(); + await fixture.whenStable(); + + expect((fixture.nativeElement as HTMLElement).innerText.trim()).toBe(''); + fixture.componentInstance.item.set({ key: 'foo', value: 'FOO' }); + fixture.detectChanges(); + await fixture.whenStable(); + + expect((fixture.nativeElement as HTMLElement).innerText.trim()).toBe('FOO'); + }); + + it('With empty template', async () => { + let fixture = TestBed.createComponent(AutoShrinkItemValueWithTemplateComponent); + fixture.detectChanges(); + await fixture.whenStable(); + + let emptyContainer = fixture.debugElement.query(By.css('.test')); + expect(emptyContainer).not.toBeNull(); + expect((emptyContainer.nativeElement as HTMLElement).innerText).toBe('EMPTY'); + + fixture.componentInstance.item.set({ key: 'foo', value: 'FOO' }); + fixture.detectChanges(); + await fixture.whenStable(); + + emptyContainer = fixture.debugElement.query(By.css('.test')); + expect(emptyContainer).toBeNull(); + expect((fixture.nativeElement as HTMLElement).innerText.trim()).toBe('FOO'); + }); +}); From 4aa73d97d2648c5bb83de8e9720254cfdd1679d5 Mon Sep 17 00:00:00 2001 From: dvladir Date: Thu, 15 Jan 2026 15:38:51 +0300 Subject: [PATCH 14/29] SED-4412 Cron based components --- .../every-day-editor.component.test.ts | 91 +++++++++++++++ .../every-week-day-editor.component.test.ts | 82 +++++++++++++ .../hours-editor/hours-editor.component.html | 6 +- .../hours-editor.component.test.ts | 82 +++++++++++++ .../minutes-editor.component.html | 4 +- .../minutes-editor.component.test.ts | 73 ++++++++++++ .../monthly-day-editor.component.test.ts | 100 ++++++++++++++++ .../monthly-week-editor.component.test.ts | 109 ++++++++++++++++++ .../src/lib/modules/cron/cron.module.ts | 10 +- 9 files changed, 551 insertions(+), 6 deletions(-) create mode 100644 projects/step-core/src/lib/modules/cron/components/every-day-editor/every-day-editor.component.test.ts create mode 100644 projects/step-core/src/lib/modules/cron/components/every-week-day-editor/every-week-day-editor.component.test.ts create mode 100644 projects/step-core/src/lib/modules/cron/components/hours-editor/hours-editor.component.test.ts create mode 100644 projects/step-core/src/lib/modules/cron/components/minutes-editor/minutes-editor.component.test.ts create mode 100644 projects/step-core/src/lib/modules/cron/components/monthly-day-editor/monthly-day-editor.component.test.ts create mode 100644 projects/step-core/src/lib/modules/cron/components/monthly-week-editor/monthly-week-editor.component.test.ts diff --git a/projects/step-core/src/lib/modules/cron/components/every-day-editor/every-day-editor.component.test.ts b/projects/step-core/src/lib/modules/cron/components/every-day-editor/every-day-editor.component.test.ts new file mode 100644 index 0000000000..f08d6dd7d4 --- /dev/null +++ b/projects/step-core/src/lib/modules/cron/components/every-day-editor/every-day-editor.component.test.ts @@ -0,0 +1,91 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { CronModule } from '@exense/step-core'; +import { Component, signal } from '@angular/core'; +import { MatSelectHarness } from '@angular/material/select/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; + +@Component({ + selector: 'step-every-day-editor-test', + imports: [CronModule], + template: ` `, +}) +class EveryDayEditorComponentTestComponent { + readonly isActive = signal(true); + readonly expression = signal(''); +} + +describe('EveryDayEditorComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EveryDayEditorComponentTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(EveryDayEditorComponentTestComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + await fixture.whenStable(); + }); + + it('Selection', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(4); + + const [day, hour, minute, second] = selects; + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 1/1 * ? *'); + + await day.open(); + let options = await day.getOptions(); + expect(options.length).toBe(31); + await options[19].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 1/20 * ? *'); + + await hour.open(); + options = await hour.getOptions(); + expect(options.length).toBe(24); + await options[16].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 16 1/20 * ? *'); + + await minute.open(); + options = await minute.getOptions(); + expect(options.length).toBe(60); + await options[35].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 35 16 1/20 * ? *'); + + await second.open(); + options = await second.getOptions(); + expect(options.length).toBe(60); + await options[11].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('11 35 16 1/20 * ? *'); + }); + + it('Activity', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(4); + + let areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeFalsy(); + + fixture.componentInstance.isActive.set(false); + fixture.detectChanges(); + await fixture.whenStable(); + + areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeTruthy(); + }); +}); diff --git a/projects/step-core/src/lib/modules/cron/components/every-week-day-editor/every-week-day-editor.component.test.ts b/projects/step-core/src/lib/modules/cron/components/every-week-day-editor/every-week-day-editor.component.test.ts new file mode 100644 index 0000000000..754eda294e --- /dev/null +++ b/projects/step-core/src/lib/modules/cron/components/every-week-day-editor/every-week-day-editor.component.test.ts @@ -0,0 +1,82 @@ +import { Component, signal } from '@angular/core'; +import { CronModule } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatSelectHarness } from '@angular/material/select/testing'; + +@Component({ + selector: 'step-every-week-day-editor-test', + imports: [CronModule], + template: ` `, +}) +class EveryWeekDayEditorTestComponent { + readonly isActive = signal(true); + readonly expression = signal(''); +} + +describe('EveryWeekDayEditorComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EveryWeekDayEditorTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(EveryWeekDayEditorTestComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + await fixture.whenStable(); + }); + + it('Selection', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(3); + + const [hour, minute, second] = selects; + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 ? * MON-FRI *'); + + await hour.open(); + let options = await hour.getOptions(); + expect(options.length).toBe(24); + await options[16].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 16 ? * MON-FRI *'); + + await minute.open(); + options = await minute.getOptions(); + expect(options.length).toBe(60); + await options[35].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 35 16 ? * MON-FRI *'); + + await second.open(); + options = await second.getOptions(); + expect(options.length).toBe(60); + await options[11].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('11 35 16 ? * MON-FRI *'); + }); + + it('Activity', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(3); + + let areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeFalsy(); + + fixture.componentInstance.isActive.set(false); + fixture.detectChanges(); + await fixture.whenStable(); + + areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeTruthy(); + }); +}); diff --git a/projects/step-core/src/lib/modules/cron/components/hours-editor/hours-editor.component.html b/projects/step-core/src/lib/modules/cron/components/hours-editor/hours-editor.component.html index 39e4d66cc2..74b534052e 100644 --- a/projects/step-core/src/lib/modules/cron/components/hours-editor/hours-editor.component.html +++ b/projects/step-core/src/lib/modules/cron/components/hours-editor/hours-editor.component.html @@ -1,15 +1,15 @@

Every

- + {{ h.value }} hour(s) on minute - + 00 {{ m.value }} and second - + {{ s.value }}
diff --git a/projects/step-core/src/lib/modules/cron/components/hours-editor/hours-editor.component.test.ts b/projects/step-core/src/lib/modules/cron/components/hours-editor/hours-editor.component.test.ts new file mode 100644 index 0000000000..866a607f2a --- /dev/null +++ b/projects/step-core/src/lib/modules/cron/components/hours-editor/hours-editor.component.test.ts @@ -0,0 +1,82 @@ +import { Component, signal } from '@angular/core'; +import { CronModule } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatSelectHarness } from '@angular/material/select/testing'; + +@Component({ + selector: 'step-hours-editor-test', + imports: [CronModule], + template: ` `, +}) +class HoursEditorTestComponent { + readonly isActive = signal(true); + readonly expression = signal(''); +} + +describe('HoursEditorComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HoursEditorTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(HoursEditorTestComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + await fixture.whenStable(); + }); + + it('Selection', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(3); + + const [hour, minute, second] = selects; + expect(fixture.componentRef.instance.expression()).toBe('0 0 0/1 1/1 * ? *'); + + await hour.open(); + let options = await hour.getOptions(); + expect(options.length).toBe(23); + await options[16].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 0/17 1/1 * ? *'); + + await minute.open(); + options = await minute.getOptions(); + expect(options.length).toBe(60); + await options[35].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 35 0/17 1/1 * ? *'); + + await second.open(); + options = await second.getOptions(); + expect(options.length).toBe(60); + await options[11].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('11 35 0/17 1/1 * ? *'); + }); + + it('Activity', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(3); + + let areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeFalsy(); + + fixture.componentInstance.isActive.set(false); + fixture.detectChanges(); + await fixture.whenStable(); + + areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeTruthy(); + }); +}); diff --git a/projects/step-core/src/lib/modules/cron/components/minutes-editor/minutes-editor.component.html b/projects/step-core/src/lib/modules/cron/components/minutes-editor/minutes-editor.component.html index 9ba9afbe71..dac6b3f10c 100644 --- a/projects/step-core/src/lib/modules/cron/components/minutes-editor/minutes-editor.component.html +++ b/projects/step-core/src/lib/modules/cron/components/minutes-editor/minutes-editor.component.html @@ -1,10 +1,10 @@

Every

- + {{ m.value }} minutes(s) on second - + {{ s.value }}
diff --git a/projects/step-core/src/lib/modules/cron/components/minutes-editor/minutes-editor.component.test.ts b/projects/step-core/src/lib/modules/cron/components/minutes-editor/minutes-editor.component.test.ts new file mode 100644 index 0000000000..783f34ebd2 --- /dev/null +++ b/projects/step-core/src/lib/modules/cron/components/minutes-editor/minutes-editor.component.test.ts @@ -0,0 +1,73 @@ +import { Component, signal } from '@angular/core'; +import { CronModule } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatSelectHarness } from '@angular/material/select/testing'; + +@Component({ + selector: 'step-minutes-editor-test', + imports: [CronModule], + template: ` `, +}) +class MinutesEditorTestComponent { + readonly isActive = signal(true); + readonly expression = signal(''); +} + +describe('MinutesEditorComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MinutesEditorTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MinutesEditorTestComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + await fixture.whenStable(); + }); + + it('Selection', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(2); + + const [minute, second] = selects; + expect(fixture.componentRef.instance.expression()).toBe('0 0/1 * 1/1 * ? *'); + + await minute.open(); + let options = await minute.getOptions(); + expect(options.length).toBe(59); + await options[34].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0/35 * 1/1 * ? *'); + + await second.open(); + options = await second.getOptions(); + expect(options.length).toBe(60); + await options[11].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('11 0/35 * 1/1 * ? *'); + }); + + it('Activity', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(2); + + let areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeFalsy(); + + fixture.componentInstance.isActive.set(false); + fixture.detectChanges(); + await fixture.whenStable(); + + areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeTruthy(); + }); +}); diff --git a/projects/step-core/src/lib/modules/cron/components/monthly-day-editor/monthly-day-editor.component.test.ts b/projects/step-core/src/lib/modules/cron/components/monthly-day-editor/monthly-day-editor.component.test.ts new file mode 100644 index 0000000000..1331d407d0 --- /dev/null +++ b/projects/step-core/src/lib/modules/cron/components/monthly-day-editor/monthly-day-editor.component.test.ts @@ -0,0 +1,100 @@ +import { Component, signal } from '@angular/core'; +import { CronModule } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatSelectHarness } from '@angular/material/select/testing'; + +@Component({ + selector: 'step-monthly-day-editor-test', + imports: [CronModule], + template: ` `, +}) +class MonthlyDayEditorTestComponent { + readonly isActive = signal(true); + readonly expression = signal(''); +} + +describe('MonthlyDayEditor', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MonthlyDayEditorTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MonthlyDayEditorTestComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + await fixture.whenStable(); + }); + + it('Selection', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(5); + + const [day, month, hour, minute, second] = selects; + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 1W 1/1 ? *'); + + await day.open(); + let options = await day.getOptions(); + expect(options.length).toBe(34); + await options[19].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 19 1/1 ? *'); + + await month.open(); + options = await month.getOptions(); + expect(options.length).toBe(12); + await options[2].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 19 1/3 ? *'); + + await hour.open(); + options = await hour.getOptions(); + expect(options.length).toBe(24); + await options[16].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 16 19 1/3 ? *'); + + await minute.open(); + options = await minute.getOptions(); + expect(options.length).toBe(60); + await options[35].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 35 16 19 1/3 ? *'); + + await second.open(); + options = await second.getOptions(); + expect(options.length).toBe(60); + await options[11].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('11 35 16 19 1/3 ? *'); + }); + + it('Activity', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(5); + + let areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeFalsy(); + + fixture.componentInstance.isActive.set(false); + fixture.detectChanges(); + await fixture.whenStable(); + + areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeTruthy(); + }); +}); diff --git a/projects/step-core/src/lib/modules/cron/components/monthly-week-editor/monthly-week-editor.component.test.ts b/projects/step-core/src/lib/modules/cron/components/monthly-week-editor/monthly-week-editor.component.test.ts new file mode 100644 index 0000000000..f4d269f427 --- /dev/null +++ b/projects/step-core/src/lib/modules/cron/components/monthly-week-editor/monthly-week-editor.component.test.ts @@ -0,0 +1,109 @@ +import { Component, signal } from '@angular/core'; +import { CronModule } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatSelectHarness } from '@angular/material/select/testing'; + +@Component({ + selector: 'step-monthly-week-editor-test', + imports: [CronModule], + template: ` `, +}) +class MonthlyWeekEditorTestComponent { + readonly isActive = signal(true); + readonly expression = signal(''); +} + +describe('MonthlyWeekEditorComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MonthlyWeekEditorTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(MonthlyWeekEditorTestComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + await fixture.whenStable(); + }); + + it('Selection', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(6); + + const [dayNum, weekDay, month, hour, minute, second] = selects; + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 ? 1/1 MON#1 *'); + + await dayNum.open(); + let options = await dayNum.getOptions(); + expect(options.length).toBe(6); + await options[5].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 ? 1/1 MONL *'); + + await weekDay.open(); + options = await weekDay.getOptions(); + expect(options.length).toBe(7); + await options[3].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 ? 1/1 THUL *'); + + await month.open(); + options = await month.getOptions(); + expect(options.length).toBe(12); + await options[3].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 ? 1/4 THUL *'); + + await hour.open(); + options = await hour.getOptions(); + expect(options.length).toBe(24); + await options[16].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 16 ? 1/4 THUL *'); + + await minute.open(); + options = await minute.getOptions(); + expect(options.length).toBe(60); + await options[35].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 35 16 ? 1/4 THUL *'); + + await second.open(); + options = await second.getOptions(); + expect(options.length).toBe(61); + await options[11].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('10 35 16 ? 1/4 THUL *'); + }); + + it('Activity', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(6); + + let areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeFalsy(); + + fixture.componentInstance.isActive.set(false); + fixture.detectChanges(); + await fixture.whenStable(); + + areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeTruthy(); + }); +}); diff --git a/projects/step-core/src/lib/modules/cron/cron.module.ts b/projects/step-core/src/lib/modules/cron/cron.module.ts index a0d1a98521..3057e521e8 100644 --- a/projects/step-core/src/lib/modules/cron/cron.module.ts +++ b/projects/step-core/src/lib/modules/cron/cron.module.ts @@ -56,7 +56,15 @@ import { TAB_EXPORTS } from '../tabs'; DatePickerModule, TAB_EXPORTS, ], - exports: [ValidateCronDirective], + exports: [ + ValidateCronDirective, + EveryDayEditorComponent, + EveryWeekDayEditorComponent, + HoursEditorComponent, + MinutesEditorComponent, + MonthlyDayEditorComponent, + MonthlyWeekEditorComponent, + ], }) export class CronModule {} From c8b959b1b20f3ce4591b6f0e6bb68efb187c10ea Mon Sep 17 00:00:00 2001 From: dvladir Date: Thu, 15 Jan 2026 19:24:04 +0300 Subject: [PATCH 15/29] SED-4412 Cron unit tests --- .../day-of-week-selector.component.test.ts | 92 +++++++++++++++ .../yearly-day-editor.component.test.ts | 99 ++++++++++++++++ .../yearly-week-editor.component.test.ts | 109 ++++++++++++++++++ .../src/lib/modules/cron/cron.module.ts | 3 + 4 files changed, 303 insertions(+) create mode 100644 projects/step-core/src/lib/modules/cron/components/week-selector/day-of-week-selector.component.test.ts create mode 100644 projects/step-core/src/lib/modules/cron/components/yearly-day-editor/yearly-day-editor.component.test.ts create mode 100644 projects/step-core/src/lib/modules/cron/components/yearly-week-editor/yearly-week-editor.component.test.ts diff --git a/projects/step-core/src/lib/modules/cron/components/week-selector/day-of-week-selector.component.test.ts b/projects/step-core/src/lib/modules/cron/components/week-selector/day-of-week-selector.component.test.ts new file mode 100644 index 0000000000..3e738bc928 --- /dev/null +++ b/projects/step-core/src/lib/modules/cron/components/week-selector/day-of-week-selector.component.test.ts @@ -0,0 +1,92 @@ +import { Component, model, signal } from '@angular/core'; +import { CronModule } from '@exense/step-core'; +import { FormsModule } from '@angular/forms'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatCheckboxHarness } from '@angular/material/checkbox/testing'; + +@Component({ + selector: 'step-day-of-week-selector-test', + imports: [CronModule, FormsModule], + template: ` `, +}) +export class DayOfWeekSelectorTestComponent { + readonly weeks = model(undefined); + readonly isDisabled = signal(false); +} + +describe('DayOfWeekSelectorComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DayOfWeekSelectorTestComponent], + }).compileComponents(); + fixture = TestBed.createComponent(DayOfWeekSelectorTestComponent); + fixture.detectChanges(); + await fixture.whenStable(); + loader = TestbedHarnessEnvironment.loader(fixture); + }); + + it('Model changes', async () => { + const checkboxes = await loader.getAllHarnesses(MatCheckboxHarness); + expect(checkboxes.length).toBe(7); + expect(fixture.componentInstance.weeks()).toBeUndefined(); + + await checkboxes[0].check(); + expect(fixture.componentInstance.weeks()).toEqual(['MON']); + + await checkboxes[1].check(); + expect(fixture.componentInstance.weeks()).toEqual(['MON', 'TUE']); + + await checkboxes[2].check(); + expect(fixture.componentInstance.weeks()).toEqual(['MON', 'TUE', 'WED']); + + await checkboxes[3].check(); + expect(fixture.componentInstance.weeks()).toEqual(['MON', 'TUE', 'WED', 'THU']); + + await checkboxes[4].check(); + expect(fixture.componentInstance.weeks()).toEqual(['MON', 'TUE', 'WED', 'THU', 'FRI']); + + await checkboxes[5].check(); + expect(fixture.componentInstance.weeks()).toEqual(['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']); + + await checkboxes[6].check(); + expect(fixture.componentInstance.weeks()).toEqual(['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']); + + await checkboxes[0].uncheck(); + expect(fixture.componentInstance.weeks()).toEqual(['TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']); + + await checkboxes[1].uncheck(); + expect(fixture.componentInstance.weeks()).toEqual(['WED', 'THU', 'FRI', 'SAT', 'SUN']); + + await checkboxes[2].uncheck(); + expect(fixture.componentInstance.weeks()).toEqual(['THU', 'FRI', 'SAT', 'SUN']); + + await checkboxes[3].uncheck(); + expect(fixture.componentInstance.weeks()).toEqual(['FRI', 'SAT', 'SUN']); + + await checkboxes[4].uncheck(); + expect(fixture.componentInstance.weeks()).toEqual(['SAT', 'SUN']); + + await checkboxes[5].uncheck(); + expect(fixture.componentInstance.weeks()).toEqual(['SUN']); + + await checkboxes[6].uncheck(); + expect(fixture.componentInstance.weeks()).toEqual([]); + }); + + it('Enable disable', async () => { + const checkboxes = await loader.getAllHarnesses(MatCheckboxHarness); + expect(checkboxes.length).toBe(7); + + let areAllDisabled = await Promise.all(checkboxes.map((el) => el.isDisabled())); + expect(areAllDisabled.every((x) => !x)).toBeTruthy(); + + fixture.componentInstance.isDisabled.set(true); + areAllDisabled = await Promise.all(checkboxes.map((el) => el.isDisabled())); + expect(areAllDisabled.every((x) => x)).toBeTruthy(); + }); +}); diff --git a/projects/step-core/src/lib/modules/cron/components/yearly-day-editor/yearly-day-editor.component.test.ts b/projects/step-core/src/lib/modules/cron/components/yearly-day-editor/yearly-day-editor.component.test.ts new file mode 100644 index 0000000000..bd3028f6ea --- /dev/null +++ b/projects/step-core/src/lib/modules/cron/components/yearly-day-editor/yearly-day-editor.component.test.ts @@ -0,0 +1,99 @@ +import { Component, signal } from '@angular/core'; +import { CronModule } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatSelectHarness } from '@angular/material/select/testing'; + +@Component({ + selector: 'step-yearly-day-editor-test', + imports: [CronModule], + template: ` `, +}) +class YearlyDayEditorTestComponent { + readonly isActive = signal(true); + readonly expression = signal(''); +} + +describe('YearlyDayEditor', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [YearlyDayEditorTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(YearlyDayEditorTestComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + await fixture.whenStable(); + }); + + it('Selection', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(5); + + const [month, day, hour, minute, second] = selects; + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 1W 1 ? *'); + + await month.open(); + let options = await month.getOptions(); + expect(options.length).toBe(12); + await options[2].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 1W 3 ? *'); + + await day.open(); + options = await day.getOptions(); + expect(options.length).toBe(34); + await options[19].click(); + fixture.detectChanges(); + await fixture.whenStable(); + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 19 3 ? *'); + + await hour.open(); + options = await hour.getOptions(); + expect(options.length).toBe(24); + await options[16].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 16 19 3 ? *'); + + await minute.open(); + options = await minute.getOptions(); + expect(options.length).toBe(60); + await options[35].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 35 16 19 3 ? *'); + + await second.open(); + options = await second.getOptions(); + expect(options.length).toBe(60); + await options[11].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('11 35 16 19 3 ? *'); + }); + + it('Activity', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(5); + + let areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeFalsy(); + + fixture.componentInstance.isActive.set(false); + fixture.detectChanges(); + await fixture.whenStable(); + + areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeTruthy(); + }); +}); diff --git a/projects/step-core/src/lib/modules/cron/components/yearly-week-editor/yearly-week-editor.component.test.ts b/projects/step-core/src/lib/modules/cron/components/yearly-week-editor/yearly-week-editor.component.test.ts new file mode 100644 index 0000000000..bca0f6defb --- /dev/null +++ b/projects/step-core/src/lib/modules/cron/components/yearly-week-editor/yearly-week-editor.component.test.ts @@ -0,0 +1,109 @@ +import { Component, signal } from '@angular/core'; +import { CronModule } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatSelectHarness } from '@angular/material/select/testing'; + +@Component({ + selector: 'step-yearly-week-editor-test', + imports: [CronModule], + template: ` `, +}) +class YearlyWeekEditorTestComponent { + readonly isActive = signal(true); + readonly expression = signal(''); +} + +describe('YearlyWeekEditorComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [YearlyWeekEditorTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(YearlyWeekEditorTestComponent); + fixture.detectChanges(); + loader = TestbedHarnessEnvironment.loader(fixture); + await fixture.whenStable(); + }); + + it('Selection', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(6); + + const [dayNum, weekDay, month, hour, minute, second] = selects; + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 ? 1 MON#1 *'); + + await dayNum.open(); + let options = await dayNum.getOptions(); + expect(options.length).toBe(6); + await options[5].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 ? 1 MONL *'); + + await weekDay.open(); + options = await weekDay.getOptions(); + expect(options.length).toBe(7); + await options[3].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 ? 1 THUL *'); + + await month.open(); + options = await month.getOptions(); + expect(options.length).toBe(12); + await options[3].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 0 ? 4 THUL *'); + + await hour.open(); + options = await hour.getOptions(); + expect(options.length).toBe(24); + await options[16].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 0 16 ? 4 THUL *'); + + await minute.open(); + options = await minute.getOptions(); + expect(options.length).toBe(60); + await options[35].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('0 35 16 ? 4 THUL *'); + + await second.open(); + options = await second.getOptions(); + expect(options.length).toBe(60); + await options[11].click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentRef.instance.expression()).toBe('11 35 16 ? 4 THUL *'); + }); + + it('Activity', async () => { + const selects = await loader.getAllHarnesses(MatSelectHarness); + expect(selects.length).toBe(6); + + let areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeFalsy(); + + fixture.componentInstance.isActive.set(false); + fixture.detectChanges(); + await fixture.whenStable(); + + areDisabled = await Promise.all(selects.map((select) => select.isDisabled())); + expect(areDisabled.every((x) => x)).toBeTruthy(); + }); +}); diff --git a/projects/step-core/src/lib/modules/cron/cron.module.ts b/projects/step-core/src/lib/modules/cron/cron.module.ts index 3057e521e8..5b5d036c9b 100644 --- a/projects/step-core/src/lib/modules/cron/cron.module.ts +++ b/projects/step-core/src/lib/modules/cron/cron.module.ts @@ -64,6 +64,9 @@ import { TAB_EXPORTS } from '../tabs'; MinutesEditorComponent, MonthlyDayEditorComponent, MonthlyWeekEditorComponent, + DayOfWeekSelectorComponent, + YearlyWeekEditorComponent, + YearlyDayEditorComponent, ], }) export class CronModule {} From f8e44855119c38a6d7362fdd15a754f78c2108f0 Mon Sep 17 00:00:00 2001 From: dvladir Date: Fri, 16 Jan 2026 16:58:32 +0300 Subject: [PATCH 16/29] SED-4412 Popover component tests --- .../components/popover/popover-harness.ts | 44 +++++++ .../popover/popover.component.test.ts | 111 ++++++++++++++++++ 2 files changed, 155 insertions(+) create mode 100644 projects/step-core/src/lib/modules/basics/components/popover/popover-harness.ts create mode 100644 projects/step-core/src/lib/modules/basics/components/popover/popover.component.test.ts diff --git a/projects/step-core/src/lib/modules/basics/components/popover/popover-harness.ts b/projects/step-core/src/lib/modules/basics/components/popover/popover-harness.ts new file mode 100644 index 0000000000..262b47b459 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/popover/popover-harness.ts @@ -0,0 +1,44 @@ +import { ComponentHarness, TestElement } from '@angular/cdk/testing'; + +export class PopoverHarness extends ComponentHarness { + static hostSelector = 'step-popover'; + + private documentRootLocator = this.documentRootLocatorFactory(); + + async getTriggerContent(selector: string): Promise { + return this.locatorFor(`.trigger-wrapper ${selector}`)(); + } + + getPopoverContent(selector: string): Promise { + return this.documentRootLocator.locatorForAll(`step-popover-content ${selector}`)(); + } + + async isPopoverOpened(): Promise { + const content = await this.documentRootLocator.locatorForAll(`step-popover-content`)(); + return content.length > 0; + } + + async mouseEnter(): Promise { + const triggerWrapper = await this.getTriggerWrapper(); + await triggerWrapper.hover(); + } + + async mouseLeave(): Promise { + const triggerWrapper = await this.getTriggerWrapper(); + await triggerWrapper.mouseAway(); + } + + async click(): Promise { + const triggerWrapper = await this.getTriggerWrapper(); + await triggerWrapper.click(); + } + + async backdropClick(): Promise { + const backdrop = await this.documentRootLocator.locatorFor('.cdk-overlay-backdrop')(); + await backdrop.click(); + } + + private async getTriggerWrapper(): Promise { + return await this.locatorFor('.trigger-wrapper')(); + } +} diff --git a/projects/step-core/src/lib/modules/basics/components/popover/popover.component.test.ts b/projects/step-core/src/lib/modules/basics/components/popover/popover.component.test.ts new file mode 100644 index 0000000000..bfae1b2837 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/popover/popover.component.test.ts @@ -0,0 +1,111 @@ +import { Component, signal } from '@angular/core'; +import { PopoverMode, StepBasicsModule, StepIconsModule } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { PopoverHarness } from './popover-harness'; + +@Component({ + selector: 'step-popover-test', + imports: [StepBasicsModule, StepIconsModule], + template: ` + + +
Test test test
+
+
`, +}) +class PopoverTestComponent { + readonly popoverMode = signal(PopoverMode.HOVER); +} + +describe('PopoverComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PopoverTestComponent], + }).compileComponents(); + fixture = TestBed.createComponent(PopoverTestComponent); + loader = TestbedHarnessEnvironment.loader(fixture); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Hover mode and popover content', async () => { + const popover = await loader.getHarness(PopoverHarness); + const popoverIcon = await popover.getTriggerContent('step-icon'); + const iconName = await popoverIcon.getAttribute('name'); + expect(iconName).toBe('help-circle'); + + let contentItems = await popover.getPopoverContent('.popover-content'); + expect(contentItems.length).toBe(0); + + await popover.click(); + contentItems = await popover.getPopoverContent('.popover-content'); + expect(contentItems.length).toBe(0); + + await popover.mouseEnter(); + contentItems = await popover.getPopoverContent('.popover-content'); + expect(contentItems.length).toBe(1); + const content = await contentItems[0].text(); + expect(content).toEqual('Test test test'); + + await popover.mouseLeave(); + contentItems = await popover.getPopoverContent('.popover-content'); + expect(contentItems.length).toBe(0); + }); + + it('Click mode', async () => { + fixture.componentInstance.popoverMode.set(PopoverMode.CLICK); + const popover = await loader.getHarness(PopoverHarness); + + let isOpened = await popover.isPopoverOpened(); + expect(isOpened).toBeFalsy(); + + await popover.mouseEnter(); + isOpened = await popover.isPopoverOpened(); + expect(isOpened).toBeFalsy(); + + await popover.click(); + isOpened = await popover.isPopoverOpened(); + expect(isOpened).toBeTruthy(); + + await popover.mouseLeave(); + isOpened = await popover.isPopoverOpened(); + expect(isOpened).toBeTruthy(); + + await popover.backdropClick(); + isOpened = await popover.isPopoverOpened(); + expect(isOpened).toBeFalsy(); + }); + + it('Both mode', async () => { + fixture.componentInstance.popoverMode.set(PopoverMode.BOTH); + const popover = await loader.getHarness(PopoverHarness); + + let isOpened = await popover.isPopoverOpened(); + expect(isOpened).toBeFalsy(); + + await popover.mouseEnter(); + isOpened = await popover.isPopoverOpened(); + expect(isOpened).toBeTruthy(); + + await popover.mouseLeave(); + isOpened = await popover.isPopoverOpened(); + expect(isOpened).toBeFalsy(); + + await popover.mouseEnter(); + isOpened = await popover.isPopoverOpened(); + expect(isOpened).toBeTruthy(); + await popover.click(); + await popover.mouseLeave(); + isOpened = await popover.isPopoverOpened(); + expect(isOpened).toBeTruthy(); + + await popover.backdropClick(); + isOpened = await popover.isPopoverOpened(); + expect(isOpened).toBeFalsy(); + }); +}); From 5461995e103c59e3fb7d40d08cf7275e7c54b1f3 Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 20 Jan 2026 11:56:49 +0300 Subject: [PATCH 17/29] SED-4412 Custom item renderer --- .../precet-editor.component.test.ts | 0 .../custom-item-render.component.test.ts | 111 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 projects/step-core/src/lib/modules/cron/components/preset-editor/precet-editor.component.test.ts create mode 100644 projects/step-core/src/lib/modules/custom-registeries/components/custom-item-render/custom-item-render.component.test.ts diff --git a/projects/step-core/src/lib/modules/cron/components/preset-editor/precet-editor.component.test.ts b/projects/step-core/src/lib/modules/cron/components/preset-editor/precet-editor.component.test.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/projects/step-core/src/lib/modules/custom-registeries/components/custom-item-render/custom-item-render.component.test.ts b/projects/step-core/src/lib/modules/custom-registeries/components/custom-item-render/custom-item-render.component.test.ts new file mode 100644 index 0000000000..0a519dab3d --- /dev/null +++ b/projects/step-core/src/lib/modules/custom-registeries/components/custom-item-render/custom-item-render.component.test.ts @@ -0,0 +1,111 @@ +import { CustomComponent, CustomItemRenderComponent } from '@exense/step-core'; +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +interface TestContext { + foo?: string; + bar?: string; +} + +class ContextChangeHandler { + protected constructor() {} + + static readonly instance = new ContextChangeHandler(); + + contextChange(previousContext?: TestContext, currentContext?: TestContext): void {} +} + +@Component({ + selector: 'step-test-component', + template: ` +
{{ context?.foo }}
+
{{ context?.bar }}
+ `, +}) +class TestComponent implements CustomComponent { + context?: TestContext; + + contextChange(previousContext?: TestContext, currentContext?: TestContext): void { + ContextChangeHandler.instance.contextChange(previousContext, currentContext); + } +} + +describe('CustomItemRenderComponent', () => { + let fixture: ComponentFixture; + let contextChangeSpy: jest.SpyInstance; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestComponent], + declarations: [CustomItemRenderComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CustomItemRenderComponent); + contextChangeSpy = jest.spyOn(ContextChangeHandler.instance, 'contextChange'); + contextChangeSpy.mockReset(); + + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Rendering', async () => { + let elementFoo = fixture.debugElement.query(By.css('#foo')); + let elementBar = fixture.debugElement.query(By.css('#bar')); + expect(elementFoo).toBeFalsy(); + expect(elementBar).toBeFalsy(); + + fixture.componentRef.setInput('component', TestComponent); + fixture.detectChanges(); + + elementFoo = fixture.debugElement.query(By.css('#foo')); + elementBar = fixture.debugElement.query(By.css('#bar')); + expect(elementFoo).toBeTruthy(); + expect(elementFoo.nativeElement.innerHTML).toBe(''); + expect(elementBar).toBeTruthy(); + expect(elementBar.nativeElement.innerHTML).toBe(''); + + fixture.componentRef.setInput('context', { foo: 'FOO', bar: 'BAR' } as TestContext); + fixture.detectChanges(); + + expect(elementFoo.nativeElement.innerHTML).toBe('FOO'); + expect(elementBar.nativeElement.innerHTML).toBe('BAR'); + + fixture.componentRef.setInput('context', { foo: 'aaa', bar: 'bbb' } as TestContext); + fixture.detectChanges(); + + expect(elementFoo.nativeElement.innerHTML).toBe('aaa'); + expect(elementBar.nativeElement.innerHTML).toBe('bbb'); + + fixture.componentRef.setInput('context', undefined); + fixture.detectChanges(); + + expect(elementFoo.nativeElement.innerHTML).toBe(''); + expect(elementBar.nativeElement.innerHTML).toBe(''); + }); + + it('Context change handler', () => { + fixture.componentRef.setInput('component', TestComponent); + fixture.detectChanges(); + + expect(contextChangeSpy).not.toHaveBeenCalled(); + const context1: TestContext = { foo: 'FOO', bar: 'BAR' }; + const context2: TestContext = { foo: 'aaa', bar: 'bbb' }; + + fixture.componentRef.setInput('context', context1); + fixture.detectChanges(); + + expect(contextChangeSpy).toHaveBeenCalledWith(undefined, context1); + + contextChangeSpy.mockReset(); + expect(contextChangeSpy).not.toHaveBeenCalled(); + fixture.componentRef.setInput('context', context1); + fixture.detectChanges(); + expect(contextChangeSpy).not.toHaveBeenCalled(); + + fixture.componentRef.setInput('context', context2); + fixture.detectChanges(); + + expect(contextChangeSpy).toHaveBeenCalledWith(context1, context2); + }); +}); From 41d5ea1ac0a65af8351bb675923f2dddb478770e Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 20 Jan 2026 12:53:47 +0300 Subject: [PATCH 18/29] SED-4412 Relative Time Picker --- .../relative-time-picker.component.test.ts | 90 +++++++++++++++++++ .../modules/date-picker/date-picker.module.ts | 2 +- 2 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 projects/step-core/src/lib/modules/date-picker/components/relative-time-picker/relative-time-picker.component.test.ts diff --git a/projects/step-core/src/lib/modules/date-picker/components/relative-time-picker/relative-time-picker.component.test.ts b/projects/step-core/src/lib/modules/date-picker/components/relative-time-picker/relative-time-picker.component.test.ts new file mode 100644 index 0000000000..e8d4e97b48 --- /dev/null +++ b/projects/step-core/src/lib/modules/date-picker/components/relative-time-picker/relative-time-picker.component.test.ts @@ -0,0 +1,90 @@ +import { Component } from '@angular/core'; +import { DatePickerModule, DateRange, TimeOption } from '@exense/step-core'; +import { DateTime } from 'luxon'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; + +const start = DateTime.fromISO('2021-06-21'); +const end = start.plus({ day: 1 }); + +const MILLISECONDS_RANGE = 10_000; + +const TIME_OPTIONS: TimeOption[] = [ + { label: 'Defined range', value: { start, end } }, + { label: 'Relative range', value: { isRelative: true, msFromNow: MILLISECONDS_RANGE } }, +]; + +@Component({ + selector: 'step-relative-time-picker-test', + imports: [DatePickerModule], + template: ``, +}) +export class RelativeTimePickerTestComponent { + readonly timeOptions = TIME_OPTIONS; + + handleRangeChange(range: DateRange): void {} + + handleRelativeOptionChange(timeOption?: TimeOption): void {} +} + +describe('RelativeTimePicker', () => { + let fixture: ComponentFixture; + let spyHandleRangeChange: jest.SpyInstance; + let spyHandleRelativeOptionChange: jest.SpyInstance; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RelativeTimePickerTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(RelativeTimePickerTestComponent); + spyHandleRangeChange = jest.spyOn(fixture.componentInstance, 'handleRangeChange'); + spyHandleRangeChange.mockReset(); + spyHandleRelativeOptionChange = jest.spyOn(fixture.componentInstance, 'handleRelativeOptionChange'); + spyHandleRelativeOptionChange.mockReset(); + + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Rendering', () => { + const divs = fixture.debugElement.queryAll(By.css('section > div')).map((el) => el.nativeElement as HTMLDivElement); + + expect(divs.length).toBe(TIME_OPTIONS.length); + divs.forEach((div, i) => { + expect(div.innerText).toBe(TIME_OPTIONS[i].label); + }); + }); + + it('Range change', () => { + expect(spyHandleRangeChange).not.toHaveBeenCalled(); + expect(spyHandleRelativeOptionChange).not.toHaveBeenCalled(); + + const div = fixture.debugElement.queryAll(By.css('section > div'))[0]; + div.triggerEventHandler('click'); + + expect(spyHandleRangeChange).toHaveBeenCalledWith(TIME_OPTIONS[0].value); + expect(spyHandleRelativeOptionChange).toHaveBeenCalledWith(undefined); + }); + + it('Relative range change', () => { + const expectedRange: DateRange = { + end, + start: end.set({ millisecond: end.millisecond - MILLISECONDS_RANGE }), + }; + jest.spyOn(DateTime, 'now').mockReturnValueOnce(expectedRange.end as DateTime); + + expect(spyHandleRangeChange).not.toHaveBeenCalled(); + expect(spyHandleRelativeOptionChange).not.toHaveBeenCalled(); + + const div = fixture.debugElement.queryAll(By.css('section > div'))[1]; + div.triggerEventHandler('click'); + + expect(spyHandleRangeChange).toHaveBeenCalledWith(expectedRange); + expect(spyHandleRelativeOptionChange).toHaveBeenCalledWith(TIME_OPTIONS[1]); + }); +}); diff --git a/projects/step-core/src/lib/modules/date-picker/date-picker.module.ts b/projects/step-core/src/lib/modules/date-picker/date-picker.module.ts index 2dbbe10ee9..2f37dc4c5a 100644 --- a/projects/step-core/src/lib/modules/date-picker/date-picker.module.ts +++ b/projects/step-core/src/lib/modules/date-picker/date-picker.module.ts @@ -21,7 +21,7 @@ import { RelativeTimePickerComponent } from './components/relative-time-picker/r RelativeTimePickerComponent, ], imports: [StepBasicsModule], - exports: [DatePickerComponent, DatePickerDirective, DateRangePickerDirective], + exports: [DatePickerComponent, DatePickerDirective, DateRangePickerDirective, RelativeTimePickerComponent], }) export class DatePickerModule {} From 3b4ee1612aa621ad1e58829ad145cc0276656727 Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 20 Jan 2026 15:57:35 +0300 Subject: [PATCH 19/29] SED-4412 Array Input Component tests --- .../array-input/array-input.component.test.ts | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts diff --git a/projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts b/projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts new file mode 100644 index 0000000000..5981eecb19 --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts @@ -0,0 +1,157 @@ +import { KeyValue } from '@angular/common'; +import { Component, inject, model, signal } from '@angular/core'; +import { ArrayItemLabelValueDefaultExtractorService, StepBasicsModule } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing'; +import { MatChipGridHarness, MatChipRemoveHarness } from '@angular/material/chips/testing'; + +const ITEMS: KeyValue[] = [ + { key: 'item_1', value: 'Item 1 aaa' }, + { key: 'item_2', value: 'Item 2 bbb' }, + { key: 'item_3', value: 'Item 3 ccc' }, + { key: 'item_4', value: 'Item 4 aabb' }, + { key: 'item_5', value: 'Item 5 aacc' }, + { key: 'item_6', value: 'Item 6 bbaa' }, + { key: 'item_7', value: 'Item 7 bbcc' }, + { key: 'item_8', value: 'Item 8 ccaa' }, + { key: 'item_9', value: 'Item 9 ccbb' }, + { key: 'item_10', value: 'Item 10 ccc' }, +]; + +@Component({ + selector: 'step-array-input-test', + imports: [StepBasicsModule], + template: ` + + `, +}) +class ArrayInputTestComponent { + protected readonly _extractor = inject(ArrayItemLabelValueDefaultExtractorService); + readonly ITEMS = ITEMS; + readonly data = model([]); + readonly isDisabled = signal(false); +} + +const getOptionsLabels = async (autocomplete: MatAutocompleteHarness): Promise => { + const options = await autocomplete.getOptions(); + return Promise.all(options.map((option) => option.getText())); +}; + +const getChipLabels = async (chipGrid: MatChipGridHarness): Promise => { + const chipItems = await chipGrid.getRows(); + return Promise.all(chipItems.map((item) => item.getText())); +}; + +describe('ArrayInputComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ArrayInputTestComponent], + }).compileComponents(); + fixture = TestBed.createComponent(ArrayInputTestComponent); + loader = TestbedHarnessEnvironment.loader(fixture); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Render options', async () => { + const autoComplete = await loader.getHarness(MatAutocompleteHarness); + await autoComplete.focus(); + + let labels = await getOptionsLabels(autoComplete); + expect(labels).toEqual(ITEMS.map((item) => item.value)); + + await autoComplete.clear(); + await autoComplete.enterText('aa'); + labels = await getOptionsLabels(autoComplete); + expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('aa'))); + + await autoComplete.clear(); + await autoComplete.enterText('bb'); + labels = await getOptionsLabels(autoComplete); + expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('bb'))); + + await autoComplete.clear(); + await autoComplete.enterText('cc'); + labels = await getOptionsLabels(autoComplete); + expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('cc'))); + }); + + it('Selection changes', async () => { + const autoComplete = await loader.getHarness(MatAutocompleteHarness); + const chipGrid = await loader.getHarness(MatChipGridHarness); + let chipItems = await chipGrid.getRows(); + + expect(chipItems.length).toBe(0); + expect(fixture.componentInstance.data()).toEqual([]); + + let selection = ITEMS.filter((item, i) => (i + 1) % 2 === 0); + for (const item of selection) { + await autoComplete.focus(); + await autoComplete.selectOption({ text: item.value }); + await autoComplete.blur(); + } + expect(fixture.componentInstance.data()).toEqual(selection.map((item) => item.key)); + let chipTexts = await getChipLabels(chipGrid); + expect(chipTexts).toEqual(selection.map((item) => item.value)); + + chipItems = await chipGrid.getRows(); + const lastSelectedItem = chipItems[chipItems.length - 1]; + const lastRemove = await lastSelectedItem.getRemoveButton(); + await lastRemove.click(); + + selection.splice(selection.length - 1, 1); + expect(fixture.componentInstance.data()).toEqual(selection.map((item) => item.key)); + chipTexts = await getChipLabels(chipGrid); + expect(chipTexts).toEqual(selection.map((item) => item.value)); + + selection = ITEMS.slice(0, 3); + fixture.componentInstance.data.set(selection.map((item) => item.key)); + fixture.detectChanges(); + await fixture.whenStable(); + + chipTexts = await getChipLabels(chipGrid); + expect(chipTexts).toEqual(selection.map((item) => item.value)); + + const notSelected = ITEMS.slice(3); + await autoComplete.focus(); + const optionLabels = await getOptionsLabels(autoComplete); + expect(optionLabels).toEqual(notSelected.map((item) => item.value)); + }); + + it('Disable control', async () => { + const autoComplete = await loader.getHarness(MatAutocompleteHarness); + const chipGrid = await loader.getHarness(MatChipGridHarness); + + fixture.componentInstance.data.set([ITEMS[0].key]); + fixture.detectChanges(); + await fixture.whenStable(); + + let isAutoCompleteDisabled = await autoComplete.isDisabled(); + expect(isAutoCompleteDisabled).toBeFalsy(); + + let chipItems = await chipGrid.getRows(); + let hasRemoveButton = await chipItems[0].hasHarness(MatChipRemoveHarness); + expect(hasRemoveButton).toBeTruthy(); + + fixture.componentInstance.isDisabled.set(true); + fixture.detectChanges(); + await fixture.whenStable(); + + isAutoCompleteDisabled = await autoComplete.isDisabled(); + expect(isAutoCompleteDisabled).toBeTruthy(); + + chipItems = await chipGrid.getRows(); + hasRemoveButton = await chipItems[0].hasHarness(MatChipRemoveHarness); + expect(hasRemoveButton).toBeFalsy(); + }); +}); From 7b8e3091f323048711b8c0c36846f5f363e04ea6 Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 20 Jan 2026 17:28:03 +0300 Subject: [PATCH 20/29] SED-4412 Autocomplete input tests --- .../autocomplete-input.component.test.ts | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 projects/step-core/src/lib/modules/basics/components/autocomplete-input/autocomplete-input.component.test.ts diff --git a/projects/step-core/src/lib/modules/basics/components/autocomplete-input/autocomplete-input.component.test.ts b/projects/step-core/src/lib/modules/basics/components/autocomplete-input/autocomplete-input.component.test.ts new file mode 100644 index 0000000000..623dd6d88f --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/autocomplete-input/autocomplete-input.component.test.ts @@ -0,0 +1,115 @@ +import { KeyValue } from '@angular/common'; +import { Component, inject, model, signal } from '@angular/core'; +import { ArrayItemLabelValueDefaultExtractorService, StepBasicsModule } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing'; + +const ITEMS: KeyValue[] = [ + { key: 'item_1', value: 'Item 1 aaa' }, + { key: 'item_2', value: 'Item 2 bbb' }, + { key: 'item_3', value: 'Item 3 ccc' }, + { key: 'item_4', value: 'Item 4 aabb' }, + { key: 'item_5', value: 'Item 5 aacc' }, + { key: 'item_6', value: 'Item 6 bbaa' }, + { key: 'item_7', value: 'Item 7 bbcc' }, + { key: 'item_8', value: 'Item 8 ccaa' }, + { key: 'item_9', value: 'Item 9 ccbb' }, + { key: 'item_10', value: 'Item 10 ccc' }, +]; + +@Component({ + selector: 'step-test-autocomplete-input', + imports: [StepBasicsModule], + template: ``, +}) +class AutocompleteInputTestComponent { + protected readonly ITEMS = ITEMS; + protected readonly _extractor = inject(ArrayItemLabelValueDefaultExtractorService); + readonly data = model(''); + readonly isDisabled = signal(false); +} + +const getOptionsLabels = async (autocomplete: MatAutocompleteHarness): Promise => { + const options = await autocomplete.getOptions(); + return Promise.all(options.map((option) => option.getText())); +}; + +describe('AutocompleteInputComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AutocompleteInputTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AutocompleteInputTestComponent); + loader = TestbedHarnessEnvironment.loader(fixture); + + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Render options', async () => { + const autoComplete = await loader.getHarness(MatAutocompleteHarness); + await autoComplete.focus(); + + let labels = await getOptionsLabels(autoComplete); + expect(labels).toEqual(ITEMS.map((item) => item.value)); + + await autoComplete.clear(); + await autoComplete.enterText('aa'); + labels = await getOptionsLabels(autoComplete); + expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('aa'))); + + await autoComplete.clear(); + await autoComplete.enterText('bb'); + labels = await getOptionsLabels(autoComplete); + expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('bb'))); + + await autoComplete.clear(); + await autoComplete.enterText('cc'); + labels = await getOptionsLabels(autoComplete); + expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('cc'))); + }); + + it('Value change', async () => { + const autoComplete = await loader.getHarness(MatAutocompleteHarness); + expect(fixture.componentInstance.data()).toBe(''); + + await autoComplete.clear(); + await autoComplete.focus(); + let options = await autoComplete.getOptions(); + await options[0].click(); + await autoComplete.blur(); + expect(fixture.componentInstance.data()).toBe(ITEMS[0].value); + + await autoComplete.clear(); + await autoComplete.focus(); + options = await autoComplete.getOptions(); + await options[1].click(); + await autoComplete.blur(); + expect(fixture.componentInstance.data()).toBe(ITEMS[1].value); + }); + + it('Disabled', async () => { + const autoComplete = await loader.getHarness(MatAutocompleteHarness); + + let isAutoCompleteDisabled = await autoComplete.isDisabled(); + expect(isAutoCompleteDisabled).toBeFalsy(); + + fixture.componentInstance.isDisabled.set(true); + fixture.detectChanges(); + await fixture.whenStable(); + + isAutoCompleteDisabled = await autoComplete.isDisabled(); + expect(isAutoCompleteDisabled).toBeTruthy(); + }); +}); From d0938b75a31d91725bc9accd3006459efc66223e Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 20 Jan 2026 17:55:31 +0300 Subject: [PATCH 21/29] SED-4412 Preset editor test --- .../precet-editor.component.test.ts | 0 .../preset-editor.component.html | 2 +- .../preset-editor.component.test.ts | 58 +++++++++++++++++++ .../src/lib/modules/cron/cron.module.ts | 1 + 4 files changed, 60 insertions(+), 1 deletion(-) delete mode 100644 projects/step-core/src/lib/modules/cron/components/preset-editor/precet-editor.component.test.ts create mode 100644 projects/step-core/src/lib/modules/cron/components/preset-editor/preset-editor.component.test.ts diff --git a/projects/step-core/src/lib/modules/cron/components/preset-editor/precet-editor.component.test.ts b/projects/step-core/src/lib/modules/cron/components/preset-editor/precet-editor.component.test.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/projects/step-core/src/lib/modules/cron/components/preset-editor/preset-editor.component.html b/projects/step-core/src/lib/modules/cron/components/preset-editor/preset-editor.component.html index 7312e74478..04d0366a9e 100644 --- a/projects/step-core/src/lib/modules/cron/components/preset-editor/preset-editor.component.html +++ b/projects/step-core/src/lib/modules/cron/components/preset-editor/preset-editor.component.html @@ -1,4 +1,4 @@ - + {{ item.value }} diff --git a/projects/step-core/src/lib/modules/cron/components/preset-editor/preset-editor.component.test.ts b/projects/step-core/src/lib/modules/cron/components/preset-editor/preset-editor.component.test.ts new file mode 100644 index 0000000000..e0be9273e2 --- /dev/null +++ b/projects/step-core/src/lib/modules/cron/components/preset-editor/preset-editor.component.test.ts @@ -0,0 +1,58 @@ +import { Component, signal } from '@angular/core'; +import { CRON_PRESETS, CronModule } from '@exense/step-core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HarnessLoader } from '@angular/cdk/testing'; +import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import { MatRadioButtonHarness } from '@angular/material/radio/testing'; + +@Component({ + selector: 'step-preset-editor-test', + imports: [CronModule], + template: ``, +}) +class PresetEditorTestComponent { + readonly isActive = signal(true); + readonly expression = signal(''); +} + +describe('PresetEditorComponent', () => { + let fixture: ComponentFixture; + let loader: HarnessLoader; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PresetEditorTestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PresetEditorTestComponent); + loader = TestbedHarnessEnvironment.loader(fixture); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Selection', async () => { + const presets = TestBed.inject(CRON_PRESETS); + const radioButtons = await loader.getAllHarnesses(MatRadioButtonHarness); + expect(radioButtons.length).toBe(presets.length); + + for (let i = 0; i < radioButtons.length; i++) { + const radioButton = radioButtons[i]; + await radioButton.check(); + expect(fixture.componentInstance.expression()).toBe(presets[i].key); + } + }); + + it('Activity', async () => { + const radioButtons = await loader.getAllHarnesses(MatRadioButtonHarness); + + let areDisabled = await Promise.all(radioButtons.map((radio) => radio.isDisabled())); + expect(areDisabled.every((x) => x)).toBeFalsy(); + + fixture.componentInstance.isActive.set(false); + fixture.detectChanges(); + await fixture.whenStable(); + + areDisabled = await Promise.all(radioButtons.map((radio) => radio.isDisabled())); + expect(areDisabled.every((x) => x)).toBeTruthy(); + }); +}); diff --git a/projects/step-core/src/lib/modules/cron/cron.module.ts b/projects/step-core/src/lib/modules/cron/cron.module.ts index 5b5d036c9b..19bf10be6b 100644 --- a/projects/step-core/src/lib/modules/cron/cron.module.ts +++ b/projects/step-core/src/lib/modules/cron/cron.module.ts @@ -67,6 +67,7 @@ import { TAB_EXPORTS } from '../tabs'; DayOfWeekSelectorComponent, YearlyWeekEditorComponent, YearlyDayEditorComponent, + PresetEditorComponent, ], }) export class CronModule {} From cc50e0e95cb3d3674831be7405800e249a6ee5d7 Mon Sep 17 00:00:00 2001 From: dvladir Date: Tue, 20 Jan 2026 19:05:19 +0300 Subject: [PATCH 22/29] SED-4412 Editable actions component test --- .../editable-actions.component.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 projects/step-core/src/lib/modules/editable-labels/components/editable-actions/editable-actions.component.test.ts diff --git a/projects/step-core/src/lib/modules/editable-labels/components/editable-actions/editable-actions.component.test.ts b/projects/step-core/src/lib/modules/editable-labels/components/editable-actions/editable-actions.component.test.ts new file mode 100644 index 0000000000..3cd573cc7f --- /dev/null +++ b/projects/step-core/src/lib/modules/editable-labels/components/editable-actions/editable-actions.component.test.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { EditableActionsComponent } from './editable-actions.component'; +import { By } from '@angular/platform-browser'; + +describe('EditableActionsComponent', () => { + let fixture: ComponentFixture; + let spyEmitApply: jest.SpyInstance; + let spyEmitCancel: jest.SpyInstance; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EditableActionsComponent], + }).compileComponents(); + fixture = TestBed.createComponent(EditableActionsComponent); + + spyEmitApply = jest.spyOn(fixture.componentInstance.apply, 'emit'); + spyEmitCancel = jest.spyOn(fixture.componentInstance.cancel, 'emit'); + + spyEmitApply.mockReset(); + spyEmitCancel.mockReset(); + + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Emit events', () => { + const [applyButton, cancelButton] = fixture.debugElement.queryAll(By.css('button')); + + expect(spyEmitApply).not.toHaveBeenCalled(); + applyButton.triggerEventHandler('click', null); + expect(spyEmitApply).toHaveBeenCalled(); + + expect(spyEmitCancel).not.toHaveBeenCalled(); + cancelButton.triggerEventHandler('click', null); + expect(spyEmitCancel).toHaveBeenCalled(); + }); +}); From 55dfb98b5a9a9b07d23a7036830cddc5902698e7 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 21 Jan 2026 13:38:45 +0300 Subject: [PATCH 23/29] SED-4412 Add Field Button component --- .../add-field-button.component.test.ts | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 projects/step-core/src/lib/modules/json-forms/components/add-field-button/add-field-button.component.test.ts diff --git a/projects/step-core/src/lib/modules/json-forms/components/add-field-button/add-field-button.component.test.ts b/projects/step-core/src/lib/modules/json-forms/components/add-field-button/add-field-button.component.test.ts new file mode 100644 index 0000000000..f8ac738dea --- /dev/null +++ b/projects/step-core/src/lib/modules/json-forms/components/add-field-button/add-field-button.component.test.ts @@ -0,0 +1,104 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AddFieldButtonComponent } from './add-field-button.component'; +import { By } from '@angular/platform-browser'; + +const eventLike = { + stopPropagation() {}, +}; + +const FIELDS = ['field_A', 'field_B', 'field_C']; + +describe('AddFieldButtonComponent', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AddFieldButtonComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AddFieldButtonComponent) as ComponentFixture>; + fixture.detectChanges(); + await fixture.whenStable(); + }); + + const getButtons = () => { + const addField = fixture.debugElement.query(By.css('button.add-additional')); + const possibleFields = fixture.debugElement.queryAll(By.css('.possible-fields-container > button')); + return { addField, possibleFields }; + }; + + it('No possible fields', async () => { + const emitAddFiled = jest.spyOn(fixture.componentInstance.addField, 'emit'); + let btn = getButtons(); + expect(btn.addField).toBeTruthy(); + expect(btn.possibleFields.length).toBe(0); + + btn.addField.triggerEventHandler('click', eventLike); + fixture.detectChanges(); + await fixture.whenStable(); + + btn = getButtons(); + expect(btn.addField).toBeTruthy(); + expect(btn.possibleFields.length).toBe(0); + + expect(emitAddFiled).toHaveBeenCalledWith(undefined); + }); + + it('With possible fields', async () => { + fixture.componentRef.setInput('possibleFields', FIELDS); + fixture.detectChanges(); + await fixture.whenStable(); + + const emitAddFiled = jest.spyOn(fixture.componentInstance.addField, 'emit'); + let btn = getButtons(); + expect(btn.addField).toBeTruthy(); + expect(btn.possibleFields.length).toBe(0); + + btn.addField.triggerEventHandler('click', eventLike); + fixture.detectChanges(); + await fixture.whenStable(); + + btn = getButtons(); + expect(btn.addField).toBeFalsy(); + expect(btn.possibleFields.length).toBe(FIELDS.length + 1); + expect(emitAddFiled).not.toHaveBeenCalled(); + + for (let i = 0; i < FIELDS.length; i++) { + btn.possibleFields[i].triggerEventHandler('click', eventLike); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(emitAddFiled).toHaveBeenCalledWith(FIELDS[i]); + emitAddFiled.mockReset(); + + btn = getButtons(); + expect(btn.addField).toBeTruthy(); + expect(btn.possibleFields.length).toBe(0); + + btn.addField.triggerEventHandler('click', eventLike); + fixture.detectChanges(); + await fixture.whenStable(); + btn = getButtons(); + expect(btn.addField).toBeFalsy(); + expect(btn.possibleFields.length).toBe(FIELDS.length + 1); + } + btn.possibleFields[btn.possibleFields.length - 1].triggerEventHandler('click', eventLike); + expect(emitAddFiled).toHaveBeenCalledWith(undefined); + }); + + it('Child mode', async () => { + let first = fixture.debugElement.children[0]; + let last = fixture.debugElement.children[fixture.debugElement.children.length - 1]; + expect(first.name).toBe('hr'); + expect(last.name).toBe('hr'); + + fixture.componentRef.setInput('isChildMode', true); + fixture.detectChanges(); + await fixture.whenStable(); + + first = fixture.debugElement.children[0]; + last = fixture.debugElement.children[fixture.debugElement.children.length - 1]; + expect(first.name).not.toBe('hr'); + expect(last.name).not.toBe('hr'); + }); +}); From 515754fd1e67dae8c4dd3e77041977869146503b Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 21 Jan 2026 14:43:16 +0300 Subject: [PATCH 24/29] SED-4412 Resource label component tests --- .../src/lib/client/step-client-module.ts | 15 ++++ .../resource-label.component.test.ts | 78 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 projects/step-core/src/lib/modules/basics/components/resource-label/resource-label.component.test.ts diff --git a/projects/step-core/src/lib/client/step-client-module.ts b/projects/step-core/src/lib/client/step-client-module.ts index 576ee6f792..fb76c99aa7 100644 --- a/projects/step-core/src/lib/client/step-client-module.ts +++ b/projects/step-core/src/lib/client/step-client-module.ts @@ -11,6 +11,7 @@ import { import { BaseHttpRequest } from './generated/core/BaseHttpRequest'; import { OPEN_API_CONFIG_PROVIDER } from './generated/open-api-config.provider'; import { lazyLoadedMainInterceptor } from './augmented/interceptros/lazy-loaded.interceptor'; +import { TableRemoteDataSourceFactoryService } from './table'; export * from './generated/core/BaseHttpRequest'; export { ApiError } from './generated/core/ApiError'; @@ -31,3 +32,17 @@ export const provideStepApi = (...features: HttpFeature[]) => useExisting: StepHttpRequestService, }, ]); + +export const provideTestStepApi = () => + makeEnvironmentProviders([ + provideHttpClient(withInterceptorsFromDi()), + OPEN_API_CONFIG_PROVIDER, + { + provide: BaseHttpRequest, + useExisting: StepHttpRequestService, + }, + { + provide: TableRemoteDataSourceFactoryService, + useValue: null, + }, + ]); diff --git a/projects/step-core/src/lib/modules/basics/components/resource-label/resource-label.component.test.ts b/projects/step-core/src/lib/modules/basics/components/resource-label/resource-label.component.test.ts new file mode 100644 index 0000000000..bbacc5956e --- /dev/null +++ b/projects/step-core/src/lib/modules/basics/components/resource-label/resource-label.component.test.ts @@ -0,0 +1,78 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ResourceLabelComponent } from './resource-label.component'; +import { AugmentedResourcesService, provideTestStepApi, Resource } from '../../../../client/step-client-module'; +import { StepBasicsModule } from '../../step-basics.module'; +import { By } from '@angular/platform-browser'; +import { of } from 'rxjs'; + +describe('ResourceLabelComponent', () => { + let fixture: ComponentFixture; + let api: AugmentedResourcesService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StepBasicsModule], + providers: [provideTestStepApi()], + }).compileComponents(); + fixture = TestBed.createComponent(ResourceLabelComponent); + api = TestBed.inject(AugmentedResourcesService); + jest.spyOn(api, 'overrideInterceptor').mockImplementation(() => api); + }); + + it('File path/name from model', async () => { + const FILE_NAME = 'picture.png'; + const FILE_PATH = `/tmp/something/resources/${FILE_NAME}`; + + let span = fixture.debugElement.query(By.css('span')); + expect(span).toBeFalsy(); + + fixture.componentRef.setInput('stModel', FILE_PATH); + fixture.detectChanges(); + await fixture.whenStable(); + + span = fixture.debugElement.query(By.css('span')); + expect(span.nativeElement.innerHTML).toBe(FILE_PATH); + + fixture.componentRef.setInput('stFormat', 'filename'); + fixture.detectChanges(); + await fixture.whenStable(); + + span = fixture.debugElement.query(By.css('span')); + expect(span.nativeElement.innerHTML).toBe(FILE_NAME); + }); + + it('Existed resource name', async () => { + const resource: Resource = { + resourceName: 'some_resource_name.txt', + }; + const getResource = jest.spyOn(api, 'getResource').mockImplementation(() => of(resource)); + + let span = fixture.debugElement.query(By.css('span')); + expect(span).toBeFalsy(); + + expect(getResource).not.toHaveBeenCalled(); + fixture.componentRef.setInput('stModel', 'resource:some_resource:123'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getResource).toHaveBeenCalledWith('some_resource'); + span = fixture.debugElement.query(By.css('span')); + expect(span.nativeElement.innerHTML).toBe(resource.resourceName); + }); + + it('Not existed resource', async () => { + const getResource = jest.spyOn(api, 'getResource').mockImplementation(() => of(undefined as any as Resource)); + + let span = fixture.debugElement.query(By.css('span')); + expect(span).toBeFalsy(); + + expect(getResource).not.toHaveBeenCalled(); + fixture.componentRef.setInput('stModel', 'resource:some_resource:123'); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(getResource).toHaveBeenCalledWith('some_resource'); + span = fixture.debugElement.query(By.css('span')); + expect(span.nativeElement.innerHTML).toBe(`Error: the referenced resource doesn't exist anymore.`); + }); +}); From e514a566a39589dc974b63887685aa25cc1e8eb5 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 21 Jan 2026 16:12:06 +0300 Subject: [PATCH 25/29] SED-4412 Automation package info tests --- .../automation-package-info.component.html | 4 +- .../automation-package-info.component.test.ts | 62 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 projects/step-core/src/lib/modules/automation-package-common/components/automation-package-info/automation-package-info.component.test.ts diff --git a/projects/step-core/src/lib/modules/automation-package-common/components/automation-package-info/automation-package-info.component.html b/projects/step-core/src/lib/modules/automation-package-common/components/automation-package-info/automation-package-info.component.html index cf4611251e..97eae128da 100644 --- a/projects/step-core/src/lib/modules/automation-package-common/components/automation-package-info/automation-package-info.component.html +++ b/projects/step-core/src/lib/modules/automation-package-common/components/automation-package-info/automation-package-info.component.html @@ -1,3 +1 @@ -
- {{(automationPackage$ | async)?.attributes?.['name'] ?? ''}} -
+
{{ (automationPackage$ | async)?.attributes?.['name'] ?? '' }}
diff --git a/projects/step-core/src/lib/modules/automation-package-common/components/automation-package-info/automation-package-info.component.test.ts b/projects/step-core/src/lib/modules/automation-package-common/components/automation-package-info/automation-package-info.component.test.ts new file mode 100644 index 0000000000..fb59721620 --- /dev/null +++ b/projects/step-core/src/lib/modules/automation-package-common/components/automation-package-info/automation-package-info.component.test.ts @@ -0,0 +1,62 @@ +import { ComponentFixture, fakeAsync, flush, flushMicrotasks, TestBed, tick } from '@angular/core/testing'; +import { AutomationPackageInfoComponent } from './automation-package-info.component'; +import { AutomationPackageCommonModule } from '../../automation-package-common.module'; +import { AugmentedAutomationPackagesService, AutomationPackage, provideTestStepApi } from '@exense/step-core'; +import { AutomationPackageChildEntity } from '../../types/automation-package-child-entity'; +import { v4 } from 'uuid'; +import { of } from 'rxjs'; +import { By } from '@angular/platform-browser'; + +interface ChildEntity extends AutomationPackageChildEntity {} + +const PACKAGE_ID = v4(); + +const PACKAGE_CHILD: ChildEntity = { + customFields: { + automationPackageId: PACKAGE_ID, + }, +}; + +const AUTOMATION_PACKAGE: AutomationPackage = { + id: PACKAGE_ID, + attributes: { + name: 'TEST PACKAGE', + }, +}; + +describe('AutomationPackageInfoComponent', () => { + let fixture: ComponentFixture>; + let api: AugmentedAutomationPackagesService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AutomationPackageCommonModule], + providers: [provideTestStepApi()], + }).compileComponents(); + fixture = TestBed.createComponent(AutomationPackageInfoComponent); + api = TestBed.inject(AugmentedAutomationPackagesService); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('Load package', fakeAsync(() => { + const spySearchByIds = jest.spyOn(api, 'searchByIDs').mockImplementation((ids) => of([AUTOMATION_PACKAGE])); + + let div = fixture.debugElement.query(By.css('div')); + expect(div.nativeElement.innerHTML).toBe(''); + expect(spySearchByIds).not.toHaveBeenCalled(); + + fixture.componentRef.setInput('context', PACKAGE_CHILD); + fixture.detectChanges(); + // When automationPackage is loaded inside AutomationPackageInfoComponent, + // bulkRequest pipe operator is used. In current case it is initialized with default parameters. + // Default parameters have these values: startDue - 500 and intervalDuration - 1500 + // That is why it's required to await 2000ms (500 + 1500) before checking the result. + tick(2000); + fixture.detectChanges(); + expect(spySearchByIds).toHaveBeenCalledWith([PACKAGE_ID]); + + div = fixture.debugElement.query(By.css('div')); + expect(div.nativeElement.innerHTML).toBe(AUTOMATION_PACKAGE.attributes!['name']); + })); +}); From d8526fa845a04086dc38992125b07014d1d451d1 Mon Sep 17 00:00:00 2001 From: dvladir Date: Wed, 21 Jan 2026 17:47:16 +0300 Subject: [PATCH 26/29] SED-4412 Fix exports --- package-lock.json | 40 +++++++++++++++++++ .../array-input/array-input.component.test.ts | 2 + .../src/lib/modules/cron/cron.module.ts | 10 +++++ .../modules/date-picker/date-picker.module.ts | 1 + 4 files changed, 53 insertions(+) diff --git a/package-lock.json b/package-lock.json index e8c0c09320..edbc0a3cb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "@angular-eslint/template-parser": "19.7.1", "@angular/cli": "19.2.14", "@angular/compiler-cli": "19.2.14", + "@happy-dom/jest-environment": "19.0.2", "@schematics/angular": "20.0.1", "@types/jest": "29.5.14", "@types/luxon": "3.4.2", @@ -3715,6 +3716,25 @@ "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-4.5.8.tgz", "integrity": "sha512-CnD7zLItIzt86q4Sj3kZUiLcBk1dSk81qcqgMGaZe7SQ1P8hFNxhMl5AZthK1zrDM5m74VVhaOpuMGIL4gagaA==" }, + "node_modules/@happy-dom/jest-environment": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@happy-dom/jest-environment/-/jest-environment-19.0.2.tgz", + "integrity": "sha512-dRX5Xuiwevif8mPQK9EYDxub/Nz6JvPKzIwNv4cIDz8+dwUrAKxJzLmfsKeKImjLPad0zpo+6orUfgadoJCwFQ==", + "dev": true, + "dependencies": { + "happy-dom": "^19.0.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@jest/environment": ">=25.0.0", + "@jest/fake-timers": ">=25.0.0", + "@jest/types": ">=25.0.0", + "jest-mock": ">=25.0.0", + "jest-util": ">=25.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -7600,6 +7620,12 @@ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "dev": true }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -12407,6 +12433,20 @@ "node": ">=0.10.0" } }, + "node_modules/happy-dom": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-19.0.2.tgz", + "integrity": "sha512-831CLbgDyjRbd2lApHZFsBDe56onuFcjsCBPodzWpzedTpeDr8CGZjs7iEIdNW1DVwSFRecfwzLpVyGBPamwGA==", + "dev": true, + "dependencies": { + "@types/node": "^20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", diff --git a/projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts b/projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts index 5981eecb19..5af31a144b 100644 --- a/projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts +++ b/projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts @@ -64,6 +64,8 @@ describe('ArrayInputComponent', () => { }); it('Render options', async () => { + fixture.detectChanges(); + await fixture.whenStable(); const autoComplete = await loader.getHarness(MatAutocompleteHarness); await autoComplete.focus(); diff --git a/projects/step-core/src/lib/modules/cron/cron.module.ts b/projects/step-core/src/lib/modules/cron/cron.module.ts index 19bf10be6b..adde08dec6 100644 --- a/projects/step-core/src/lib/modules/cron/cron.module.ts +++ b/projects/step-core/src/lib/modules/cron/cron.module.ts @@ -77,3 +77,13 @@ export * from './components/cron-editor/cron-editor.component'; export * from './directives/validate-cron.directive'; export * from './types/cron-validator'; export * from './types/cron-editor-tab.enum'; +export * from './components/minutes-editor/minutes-editor.component'; +export * from './components/hours-editor/hours-editor.component'; +export * from './components/every-day-editor/every-day-editor.component'; +export * from './components/every-week-day-editor/every-week-day-editor.component'; +export * from './components/monthly-day-editor/monthly-day-editor.component'; +export * from './components/monthly-week-editor/monthly-week-editor.component'; +export * from './components/yearly-day-editor/yearly-day-editor.component'; +export * from './components/yearly-week-editor/yearly-week-editor.component'; +export * from './components/preset-editor/preset-editor.component'; +export * from './components/week-selector/day-of-week-selector.component'; diff --git a/projects/step-core/src/lib/modules/date-picker/date-picker.module.ts b/projects/step-core/src/lib/modules/date-picker/date-picker.module.ts index 2f37dc4c5a..bd842adbff 100644 --- a/projects/step-core/src/lib/modules/date-picker/date-picker.module.ts +++ b/projects/step-core/src/lib/modules/date-picker/date-picker.module.ts @@ -40,3 +40,4 @@ export { STEP_DATE_FORMAT_CONFIG, STEP_DATE_TIME_FORMAT_PROVIDERS, } from './injectables/step-date-format-config.providers'; +export * from './components/relative-time-picker/relative-time-picker.component'; From db00eaeae2064679ea65e02de450f3863366ac8d Mon Sep 17 00:00:00 2001 From: Tim Rasim Date: Thu, 5 Feb 2026 17:08:41 +0100 Subject: [PATCH 27/29] Apply suggestion from @neogucky --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 30c1e4ec18..0b6b66c788 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "jest": "29.5.0", "@types/jest": "29.5.14", "jest-preset-angular": "14.6.2", - "@happy-dom/jest-environment": "19.0.2" + "@happy-dom/jest-environment": "19.0.2", "ts-jest": "29.4.4" }, "engines": { From 4b62926995613d02d3274b010af469d511ed2831 Mon Sep 17 00:00:00 2001 From: Tim Rasim <2033832+neogucky@users.noreply.github.com> Date: Thu, 5 Feb 2026 17:53:42 +0100 Subject: [PATCH 28/29] SED-4412 Added some timeouts --- .../array-input/array-input.component.test.ts | 8 ++++++++ .../autocomplete-input.component.test.ts | 8 ++++++++ .../components/popover/popover.component.test.ts | 10 ++++++++++ .../resource-label/resource-label.component.test.ts | 5 +++-- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts b/projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts index 5af31a144b..1acd63af5c 100644 --- a/projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts +++ b/projects/step-core/src/lib/modules/basics/components/array-input/array-input.component.test.ts @@ -63,6 +63,11 @@ describe('ArrayInputComponent', () => { await fixture.whenStable(); }); + const wait = async (ms: number) => { + await new Promise((resolve) => setTimeout(resolve, ms)); + await fixture.whenStable(); + }; + it('Render options', async () => { fixture.detectChanges(); await fixture.whenStable(); @@ -74,16 +79,19 @@ describe('ArrayInputComponent', () => { await autoComplete.clear(); await autoComplete.enterText('aa'); + await wait(350); labels = await getOptionsLabels(autoComplete); expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('aa'))); await autoComplete.clear(); await autoComplete.enterText('bb'); + await wait(350); labels = await getOptionsLabels(autoComplete); expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('bb'))); await autoComplete.clear(); await autoComplete.enterText('cc'); + await wait(350); labels = await getOptionsLabels(autoComplete); expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('cc'))); }); diff --git a/projects/step-core/src/lib/modules/basics/components/autocomplete-input/autocomplete-input.component.test.ts b/projects/step-core/src/lib/modules/basics/components/autocomplete-input/autocomplete-input.component.test.ts index 623dd6d88f..30ef10340b 100644 --- a/projects/step-core/src/lib/modules/basics/components/autocomplete-input/autocomplete-input.component.test.ts +++ b/projects/step-core/src/lib/modules/basics/components/autocomplete-input/autocomplete-input.component.test.ts @@ -57,6 +57,11 @@ describe('AutocompleteInputComponent', () => { await fixture.whenStable(); }); + const wait = async (ms: number) => { + await new Promise((resolve) => setTimeout(resolve, ms)); + await fixture.whenStable(); + }; + it('Render options', async () => { const autoComplete = await loader.getHarness(MatAutocompleteHarness); await autoComplete.focus(); @@ -66,16 +71,19 @@ describe('AutocompleteInputComponent', () => { await autoComplete.clear(); await autoComplete.enterText('aa'); + await wait(350); labels = await getOptionsLabels(autoComplete); expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('aa'))); await autoComplete.clear(); await autoComplete.enterText('bb'); + await wait(350); labels = await getOptionsLabels(autoComplete); expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('bb'))); await autoComplete.clear(); await autoComplete.enterText('cc'); + await wait(350); labels = await getOptionsLabels(autoComplete); expect(labels).toEqual(ITEMS.map((item) => item.value).filter((item) => item.includes('cc'))); }); diff --git a/projects/step-core/src/lib/modules/basics/components/popover/popover.component.test.ts b/projects/step-core/src/lib/modules/basics/components/popover/popover.component.test.ts index bfae1b2837..33b7827cb5 100644 --- a/projects/step-core/src/lib/modules/basics/components/popover/popover.component.test.ts +++ b/projects/step-core/src/lib/modules/basics/components/popover/popover.component.test.ts @@ -33,6 +33,11 @@ describe('PopoverComponent', () => { await fixture.whenStable(); }); + const wait = async (ms: number) => { + await new Promise((resolve) => setTimeout(resolve, ms)); + await fixture.whenStable(); + }; + it('Hover mode and popover content', async () => { const popover = await loader.getHarness(PopoverHarness); const popoverIcon = await popover.getTriggerContent('step-icon'); @@ -47,12 +52,14 @@ describe('PopoverComponent', () => { expect(contentItems.length).toBe(0); await popover.mouseEnter(); + await wait(350); contentItems = await popover.getPopoverContent('.popover-content'); expect(contentItems.length).toBe(1); const content = await contentItems[0].text(); expect(content).toEqual('Test test test'); await popover.mouseLeave(); + await wait(450); contentItems = await popover.getPopoverContent('.popover-content'); expect(contentItems.length).toBe(0); }); @@ -89,14 +96,17 @@ describe('PopoverComponent', () => { expect(isOpened).toBeFalsy(); await popover.mouseEnter(); + await wait(350); isOpened = await popover.isPopoverOpened(); expect(isOpened).toBeTruthy(); await popover.mouseLeave(); + await wait(450); isOpened = await popover.isPopoverOpened(); expect(isOpened).toBeFalsy(); await popover.mouseEnter(); + await wait(350); isOpened = await popover.isPopoverOpened(); expect(isOpened).toBeTruthy(); await popover.click(); diff --git a/projects/step-core/src/lib/modules/basics/components/resource-label/resource-label.component.test.ts b/projects/step-core/src/lib/modules/basics/components/resource-label/resource-label.component.test.ts index bbacc5956e..33ab1e9511 100644 --- a/projects/step-core/src/lib/modules/basics/components/resource-label/resource-label.component.test.ts +++ b/projects/step-core/src/lib/modules/basics/components/resource-label/resource-label.component.test.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ResourceLabelComponent } from './resource-label.component'; import { AugmentedResourcesService, provideTestStepApi, Resource } from '../../../../client/step-client-module'; -import { StepBasicsModule } from '../../step-basics.module'; +import { CommonModule } from '@angular/common'; import { By } from '@angular/platform-browser'; import { of } from 'rxjs'; @@ -11,7 +11,8 @@ describe('ResourceLabelComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [StepBasicsModule], + imports: [CommonModule], + declarations: [ResourceLabelComponent], providers: [provideTestStepApi()], }).compileComponents(); fixture = TestBed.createComponent(ResourceLabelComponent); From e7e401f4571d2bcab807e08bf9425e18b6fce94b Mon Sep 17 00:00:00 2001 From: Tim Rasim <2033832+neogucky@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:24:19 +0100 Subject: [PATCH 29/29] SED-4412 solved E2E execution issue --- .../trace-viewer.component.test.ts | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/projects/step-core/src/lib/modules/attachments/components/trace-viewer/trace-viewer.component.test.ts b/projects/step-core/src/lib/modules/attachments/components/trace-viewer/trace-viewer.component.test.ts index f1081af6cc..728da1ab80 100644 --- a/projects/step-core/src/lib/modules/attachments/components/trace-viewer/trace-viewer.component.test.ts +++ b/projects/step-core/src/lib/modules/attachments/components/trace-viewer/trace-viewer.component.test.ts @@ -1,13 +1,11 @@ import { TraceViewerComponent } from './trace-viewer.component'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { APP_HOST } from '../../../../client/_common'; -import { By } from '@angular/platform-browser'; import { ComponentRef } from '@angular/core'; import { DOCUMENT } from '@angular/common'; describe('Trace Viewer', () => { let componentRef: ComponentRef; - let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -20,28 +18,22 @@ describe('Trace Viewer', () => { ], }).compileComponents(); - fixture = TestBed.createComponent(TraceViewerComponent); - componentRef = fixture.componentRef; - fixture.detectChanges(); + componentRef = TestBed.createComponent(TraceViewerComponent).componentRef; }); it('Initial view', async () => { - const iframe = fixture.debugElement.query(By.css('iframe')); - expect(iframe.nativeElement.src).toEqual('https://step.ch/trace-viewer/'); + const url = (componentRef.instance as any).traceViewerUrl(); + expect(url).toEqual('https://step.ch/trace-viewer/'); }); it('Set report url', async () => { componentRef.setInput('reportUrl', 'https://step.ch/reports/1'); - fixture.detectChanges(); - await fixture.whenStable(); - const iframe = fixture.debugElement.query(By.css('iframe')); - expect(iframe.nativeElement.src).toEqual('https://step.ch/trace-viewer/?trace=https://step.ch/reports/1'); + const url = (componentRef.instance as any).traceViewerUrl(); + expect(url).toEqual('https://step.ch/trace-viewer/?trace=https://step.ch/reports/1'); }); it('Oper url', async () => { componentRef.setInput('reportUrl', 'https://step.ch/reports/1'); - fixture.detectChanges(); - await fixture.whenStable(); const doc = TestBed.inject(DOCUMENT); const spyWindowOpen = jest.spyOn(doc.defaultView!, 'open').mockImplementation(() => {});