///
/// Copyright 2015-2026 Micro Focus or one of its affiliates.
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
import { Component, inject } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import {
FormBuilder,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { By } from '@angular/platform-browser';
import { NumberPickerComponent, StepDirection } from './number-picker.component';
import { NumberPickerModule } from './number-picker.module';
/** Number picker example using form group */
@Component({
selector: 'app-number-picker-form',
template: `
`,
imports: [NumberPickerModule, ReactiveFormsModule],
})
export class NumberPickerTestFormGroupComponent {
form: FormGroup;
disabled = false;
min = -10;
max = 10;
step: number | ((value: number, direction: StepDirection) => number) = 1;
placeholder: string;
constructor() {
const formBuilder = inject(FormBuilder);
this.form = formBuilder.group({
integer: [
{
value: 0,
disabled: false,
},
Validators.compose([Validators.required, Validators.min(-10), Validators.max(10)]),
],
integer2: [
{
value: 0,
disabled: true,
},
Validators.compose([Validators.required, Validators.min(-10), Validators.max(10)]),
],
});
}
}
describe('Number Picker Component - FormGroup', () => {
let component: NumberPickerTestFormGroupComponent;
let fixture: ComponentFixture;
let nativeElement: HTMLElement;
let numberPickers: NodeListOf;
let numberPicker1: HTMLInputElement;
let numberPicker2: HTMLInputElement;
let input1: HTMLInputElement;
let input2: HTMLInputElement;
async function clickElement(element: HTMLDivElement) {
element.click();
fixture.detectChanges();
await fixture.whenStable();
}
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NumberPickerModule, ReactiveFormsModule, NumberPickerTestFormGroupComponent],
}).compileComponents();
fixture = TestBed.createComponent(NumberPickerTestFormGroupComponent);
component = fixture.componentInstance;
nativeElement = fixture.nativeElement;
numberPickers = nativeElement.querySelectorAll('ux-number-picker');
numberPicker1 = numberPickers.item(0);
numberPicker2 = numberPickers.item(1);
input1 = numberPicker1.querySelector('input');
input2 = numberPicker2.querySelector('input');
fixture.detectChanges();
await fixture.whenStable();
});
it('should initialise correctly', () => {
expect(component).toBeTruthy();
expect(component.form.controls.integer.value).toBe(0);
expect(component.form.controls.integer2.value).toBe(0);
});
it('should allow a number to be entered', async () => {
component.form.controls.integer.setValue(6);
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('6');
});
it('should not display ux-number-picker-invalid class when value above max value and disabled', async () => {
component.form.controls.integer2.setValue(20);
fixture.detectChanges();
await fixture.whenStable();
expect(input2.value).toBe('20');
expect(numberPicker2.classList.contains('ux-number-picker-invalid')).toBe(false);
});
it('should display ng-invalid class when value above max value', async () => {
component.form.controls.integer.setValue(20);
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('20');
expect(numberPicker1.classList.contains('ng-invalid')).toBe(true);
});
it('should display ng-invalid class when value below min value', async () => {
component.form.controls.integer.setValue(-20);
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('-20');
expect(numberPicker1.classList.contains('ng-invalid')).toBe(true);
});
it('should not display ng-invalid class when value above max value and disabled', async () => {
component.form.controls.integer2.setValue(20);
fixture.detectChanges();
await fixture.whenStable();
expect(input2.value).toBe('20');
expect(numberPicker2.classList.contains('ng-invalid')).toBe(false);
});
it('should not display ux-number-picker-invalid when max value increased to match input value', async () => {
component.form.controls.integer.setValue(15);
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('15');
expect(numberPicker1.classList.contains('ux-number-picker-invalid')).toBe(true);
component.max = 15;
fixture.detectChanges();
component.form.controls.integer.setValidators([
Validators.required,
Validators.min(-10),
Validators.max(15),
]);
component.form.controls.integer.updateValueAndValidity();
fixture.detectChanges();
await fixture.whenStable();
expect(numberPicker1.classList.contains('ux-number-picker-invalid')).toBe(false);
});
it('should not display ux-number-picker-invalid when min value decreased to match input value', async () => {
component.form.controls.integer.setValue(-15);
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('-15');
expect(numberPicker1.classList.contains('ux-number-picker-invalid')).toBe(true);
component.min = -15;
fixture.detectChanges();
component.form.controls.integer.setValidators([
Validators.required,
Validators.min(-15),
Validators.max(10),
]);
component.form.controls.integer.updateValueAndValidity();
fixture.detectChanges();
await fixture.whenStable();
expect(numberPicker1.classList.contains('ux-number-picker-invalid')).toBe(false);
});
it('should apply a placeholder', () => {
component.placeholder = 'Placeholder Text';
fixture.detectChanges();
const inputElement = fixture.nativeElement.querySelector('input') as HTMLInputElement;
expect(inputElement.placeholder).toBe('Placeholder Text');
});
it('should not apply a placeholder if unset', () => {
const inputElement = fixture.nativeElement.querySelector('input') as HTMLInputElement;
const attribute = inputElement.getAttribute('placeholder');
expect(attribute).toBeNull();
});
describe('on increment/decrement', () => {
let input1IncrementBtn: HTMLDivElement;
let input1DecrementBtn: HTMLDivElement;
let input2IncrementBtn: HTMLDivElement;
let input2DecrementBtn: HTMLDivElement;
beforeEach(() => {
input1IncrementBtn = numberPicker1.querySelector('.number-picker-control-up');
input1DecrementBtn = numberPicker1.querySelector('.number-picker-control-down');
input2IncrementBtn = numberPicker2.querySelector('.number-picker-control-up');
input2DecrementBtn = numberPicker2.querySelector('.number-picker-control-down');
});
it('should work with whole numbers', async () => {
component.form.controls.integer.setValue(7);
fixture.detectChanges();
await fixture.whenStable();
// 1st increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(8);
// 2nd increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(9);
// 3rd increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(10);
// 4th increment (tries to go outside max bounds)
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(10);
// 1st decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(9);
// 2nd decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(8);
// 3rd decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(7);
});
it('should work with decimal numbers without precision issues', async () => {
component.step = 0.3;
component.form.controls.integer.setValue(4);
fixture.detectChanges();
await fixture.whenStable();
// 1st increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(4.3);
// 2nd increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(4.6);
// 3rd increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(4.9);
// 4th increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(5.2);
// 1st decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(4.9);
// 2nd decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(4.6);
// 3rd decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(4.3);
});
it('should work with large whole numbers', async () => {
component.step = 1000000;
component.max = 1000000000;
component.form.controls.integer.setValue(997000000);
fixture.detectChanges();
await fixture.whenStable();
// 1st increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(998000000);
// 2nd increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(999000000);
// 3rd increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(1000000000);
// 1st decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(999000000);
// 2nd decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(998000000);
// 3rd decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(997000000);
});
it('should work with large decimal numbers', async () => {
component.step = 0.1;
component.max = 100000000;
component.form.controls.integer.setValue(99999999.1);
fixture.detectChanges();
await fixture.whenStable();
// 1st increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(99999999.2);
// 2nd increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(99999999.3);
// 3rd increment
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(99999999.4);
// 1st decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(99999999.3);
// 2nd decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(99999999.2);
// 3rd decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(99999999.1);
// 4rd decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(99999999);
// 5th decrement
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(99999998.9);
});
it('should work when using a function as the step input', async () => {
component.form.controls.integer.setValue(5);
component.step = (value: number, direction: StepDirection) => {
if (value === 5 && direction === StepDirection.Increment) {
return 2;
} else {
return 1;
}
};
fixture.detectChanges();
await fixture.whenStable();
await clickElement(input1IncrementBtn);
expect(component.form.controls.integer.value).toBe(7, 'step function should increment by 2');
await clickElement(input1DecrementBtn);
expect(component.form.controls.integer.value).toBe(6, 'step function should decrement by 1');
});
});
describe('on scroll', () => {
it('should not change the value if the picker has not been focused', async () => {
component.form.controls.integer.setValue(0);
fixture.detectChanges();
await fixture.whenStable();
input1.dispatchEvent(new WheelEvent('wheel', { deltaY: 5 }));
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('0');
input1.dispatchEvent(new WheelEvent('wheel', { deltaY: -5 }));
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('0');
});
it('should change the value if the picker is focused', async () => {
component.form.controls.integer.setValue(0);
fixture.detectChanges();
await fixture.whenStable();
const numberPicker = fixture.debugElement.query(By.directive(NumberPickerComponent));
numberPicker.triggerEventHandler('focusin');
fixture.detectChanges();
await fixture.whenStable();
input1.dispatchEvent(new WheelEvent('wheel', { deltaY: 5 }));
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('-1');
input1.dispatchEvent(new WheelEvent('wheel', { deltaY: -5 }));
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('0');
});
it('should stop changing the value if the picker is blured', async () => {
component.form.controls.integer.setValue(0);
fixture.detectChanges();
await fixture.whenStable();
const numberPicker = fixture.debugElement.query(By.directive(NumberPickerComponent));
numberPicker.triggerEventHandler('focusin');
fixture.detectChanges();
await fixture.whenStable();
input1.dispatchEvent(new WheelEvent('wheel', { deltaY: 5 }));
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('-1');
numberPicker.triggerEventHandler('focusout');
fixture.detectChanges();
await fixture.whenStable();
input1.dispatchEvent(new WheelEvent('wheel', { deltaY: -5 }));
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('-1');
});
});
});
@Component({
selector: 'app-number-picker-ngmodel',
template: `
`,
imports: [NumberPickerModule, FormsModule],
})
export class NumberPickerTestNgModelComponent {
value = 0;
disabled = false;
min = -10;
max = 10;
}
describe('Number Picker Component - ngModel', () => {
let component: NumberPickerTestNgModelComponent;
let fixture: ComponentFixture;
let nativeElement: HTMLElement;
let numberPicker: HTMLElement;
let input: HTMLInputElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NumberPickerModule, FormsModule, NumberPickerTestNgModelComponent],
}).compileComponents();
fixture = TestBed.createComponent(NumberPickerTestNgModelComponent);
component = fixture.componentInstance;
nativeElement = fixture.nativeElement;
numberPicker = nativeElement.querySelector('ux-number-picker');
input = numberPicker.querySelector('input');
fixture.detectChanges();
await fixture.whenStable();
});
it('should initialise correctly', () => {
expect(component).toBeTruthy();
expect(component.value).toBe(0);
});
it('should allow a number to be entered', async () => {
component.value = 9;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('9');
});
it('should display ux-number-picker-invalid class when value above max value', async () => {
component.value = 90;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('90');
fixture.detectChanges();
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(true);
});
it('should not display ux-number-picker-invalid class when max value increased to match input value', async () => {
component.value = 15;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('15');
fixture.detectChanges();
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(true);
component.max = 15;
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(false);
});
it('should not display ux-number-picker-invalid class when min value decreased to match input value', async () => {
component.value = -15;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('-15');
fixture.detectChanges();
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(true);
component.min = -15;
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(false);
});
it('should display ux-number-picker-invalid class when value below min value', async () => {
component.value = -20;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('-20');
fixture.detectChanges();
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(true);
});
it('should not display ux-number-picker-invalid class when value above max value and disabled', async () => {
component.disabled = true;
fixture.detectChanges();
component.value = 20;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('20');
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(false);
});
it('should have aria-valuemin as the min value', () => {
expect(input.getAttribute('aria-valuemin')).toBe('-10');
});
it('should have aria-valuemax as the max value', () => {
expect(input.getAttribute('aria-valuemax')).toBe('10');
});
it('should have an aria-label when provided as an input', () => {
expect(input.getAttribute('aria-label')).toBe('Example label');
});
});
@Component({
selector: 'app-number-picker-value',
template: `
`,
imports: [NumberPickerModule, FormsModule],
})
export class NumberPickerTestValueComponent {
required: boolean = false;
value = 0;
disabled = false;
min = -10;
max = 10;
// eslint-disable-next-line @typescript-eslint/no-empty-function
onValueChange(value: number): void {}
// eslint-disable-next-line @typescript-eslint/no-empty-function
onNgModelChange(value: number): void {}
}
describe('Number Picker Component - value', () => {
let component: NumberPickerTestValueComponent;
let fixture: ComponentFixture;
let nativeElement: HTMLElement;
let numberPicker: HTMLElement;
let input: HTMLInputElement;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [NumberPickerModule, FormsModule, NumberPickerTestValueComponent],
}).compileComponents();
fixture = TestBed.createComponent(NumberPickerTestValueComponent);
component = fixture.componentInstance;
nativeElement = fixture.nativeElement;
numberPicker = nativeElement.querySelector('ux-number-picker');
input = numberPicker.querySelector('input');
fixture.detectChanges();
await fixture.whenStable();
});
it('should initialise correctly', () => {
expect(component).toBeTruthy();
expect(component.value).toBe(0);
});
it('should allow a number to be entered', async () => {
component.value = 9;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('9');
});
it('should display ux-number-picker-invalid class when value above max value', async () => {
component.value = 78;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('78');
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(true);
});
it('should display ux-number-picker-invalid class when value below min value', async () => {
component.value = -20;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('-20');
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(true);
});
it('should not display ux-number-picker-invalid class when max value increased to match input value', async () => {
component.value = 15;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('15');
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(true);
component.max = 15;
fixture.detectChanges();
await fixture.whenStable();
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(false);
});
it('should not display ux-number-picker-invalid class when min value decreased to match input value', async () => {
component.value = -15;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('-15');
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(true);
component.min = -15;
fixture.detectChanges();
await fixture.whenStable();
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(false);
});
it('should not display ux-number-picker-invalid class when value above max value and disabled', async () => {
component.disabled = true;
component.value = 20;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('20');
expect(numberPicker.classList.contains('ux-number-picker-invalid')).toBe(false);
});
it('should not display ng-invalid class when value above max value', async () => {
component.value = 20;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('20');
expect(numberPicker.classList.contains('ng-invalid')).toBe(false);
});
it('should not display ng-invalid class when value below min value', async () => {
component.value = -20;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('-20');
expect(numberPicker.classList.contains('ng-invalid')).toBe(false);
});
it('should not display ng-invalid class when value above max value and disabled', async () => {
component.disabled = true;
component.value = 20;
fixture.detectChanges();
await fixture.whenStable();
expect(input.value).toBe('20');
expect(numberPicker.classList.contains('ng-invalid')).toBe(false);
});
it('should call the event emitter once per change', () => {
const controlUp = nativeElement.querySelector('.number-picker-control-up');
spyOn(component, 'onValueChange');
controlUp.click();
expect(component.onValueChange).toHaveBeenCalledWith(1);
expect(component.onValueChange).toHaveBeenCalledTimes(1);
});
it('should not emit valueChange when the value input changes', async () => {
spyOn(component, 'onValueChange');
component.value = 1;
fixture.detectChanges();
await fixture.whenStable();
const controlUp = nativeElement.querySelector('input').value;
expect(controlUp).toEqual('1');
expect(component.onValueChange).not.toHaveBeenCalled();
});
it('should emit valueChange event when a value is entered', async () => {
const inputElement = getInput();
spyOn(component, 'onValueChange');
spyOn(component, 'onNgModelChange');
inputElement.value = '7';
input.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
expect(component.onValueChange).toHaveBeenCalledWith(7);
expect(component.onValueChange).toHaveBeenCalledTimes(1);
expect(component.onNgModelChange).toHaveBeenCalledWith(7);
expect(component.onNgModelChange).toHaveBeenCalledTimes(1);
});
it('should add a required attribute to the input when required is true', () => {
component.required = true;
fixture.detectChanges();
const inputElementEmpty = nativeElement.querySelector('input.form-control');
const attributeRequired = inputElementEmpty.hasAttribute('required');
expect(attributeRequired).toBe(true);
});
function getInput(): HTMLInputElement | null {
return nativeElement.querySelector('input.form-control');
}
});
@Component({
selector: 'app-number-picker-form',
template: `
`,
imports: [NumberPickerModule, ReactiveFormsModule],
})
export class NumberPickerTestReadonlyComponent {
form: FormGroup;
constructor() {
const formBuilder = inject(FormBuilder);
this.form = formBuilder.group({
readonly: [5, Validators.required],
});
}
}
describe('Number Picker Component - Readonly', () => {
let component: NumberPickerTestReadonlyComponent;
let fixture: ComponentFixture;
let nativeElement: HTMLElement;
let numberPickers: NodeListOf;
let numberPicker1: HTMLInputElement;
let input1: HTMLInputElement;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NumberPickerModule, ReactiveFormsModule, NumberPickerTestReadonlyComponent],
}).compileComponents();
fixture = TestBed.createComponent(NumberPickerTestReadonlyComponent);
component = fixture.componentInstance;
nativeElement = fixture.nativeElement;
numberPickers = nativeElement.querySelectorAll('ux-number-picker');
numberPicker1 = numberPickers.item(0);
input1 = numberPicker1.querySelector('input');
fixture.detectChanges();
await fixture.whenStable();
});
it('should initialise correctly', () => {
expect(component).toBeTruthy();
expect(component.form.controls.readonly.value).toBe(5);
});
describe('on scroll', () => {
it('should not change the value when scrolling in reaodnly mode', async () => {
await input1.focus();
fixture.detectChanges();
await fixture.whenStable();
input1.dispatchEvent(new WheelEvent('wheel', { deltaY: 5 }));
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('5');
input1.dispatchEvent(new WheelEvent('wheel', { deltaY: -5 }));
fixture.detectChanges();
await fixture.whenStable();
expect(input1.value).toBe('5');
});
});
it('should not have aria-valuemin as the min value is undefined', () => {
expect(input1.hasAttribute('aria-valuemin')).toBe(false);
});
it('should not have aria-valuemax as the max value is undefined', () => {
expect(input1.hasAttribute('aria-valuemax')).toBe(false);
});
});