lion/packages/ui/components/input-stepper/test/lion-input-stepper.test.js
manuel cf4a8fd1b0
feat(input-stepper): align value with step size (#2511)
* feat(input-stepper): add alignToStep property to align value with step size

* Create silver-eyes-count.md

* refactor: make alignTosTep property behavior as default behavior and remove property attribute

* test(input-stepper): handle min value as Infinity and update tests for decrement/increment behavior

---------

Co-authored-by: Manuel Servais <manuel.servais@ing.com>
2025-05-14 16:46:00 +02:00

477 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, fixture as _fixture, nextFrame } from '@open-wc/testing';
import { html } from 'lit/static-html.js';
import sinon from 'sinon';
import { formatNumber } from '@lion/ui/localize-no-side-effects.js';
import '@lion/ui/define/lion-input-stepper.js';
/**
* @typedef {import('../src/LionInputStepper.js').LionInputStepper} LionInputStepper
* @typedef {import('lit').TemplateResult} TemplateResult
*/
const fixture = /** @type {(arg: TemplateResult|string) => Promise<LionInputStepper>} */ (_fixture);
const defaultInputStepper = html`
<lion-input-stepper name="year" label="Years"></lion-input-stepper>
`;
const inputStepperWithAttrs = html`<lion-input-stepper
step="10"
min="100"
max="200"
></lion-input-stepper>`;
describe('<lion-input-stepper>', () => {
describe('Stepper', () => {
it('has a type text', async () => {
const el = await fixture(defaultInputStepper);
expect(el._inputNode.type).to.equal('text');
});
it('has a default min and max of Infinity', async () => {
const el = await fixture(defaultInputStepper);
expect(el.getAttribute('min')).to.equal('Infinity');
expect(el.getAttribute('max')).to.equal('Infinity');
});
it('has a default step of 1', async () => {
const el = await fixture(defaultInputStepper);
expect(el.getAttribute('step')).to.equal('1');
});
it('can set a step with which the value increases', async () => {
const el = await fixture(defaultInputStepper);
el.step = 10;
await el.updateComplete;
expect(el.value).to.equal('');
expect(el.getAttribute('step')).to.equal('10');
const incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
expect(el.value).to.equal('10');
});
});
describe('Formatter', () => {
it('uses formatNumber for formatting', async () => {
const el = await fixture(defaultInputStepper);
expect(el.formatter).to.equal(formatNumber);
});
it('formatNumber uses locale provided in formatOptions', async () => {
let el = await fixture(html`
<lion-input-stepper
.formatOptions="${{ locale: 'en-GB' }}"
.modelValue="${1234.56}"
></lion-input-stepper>
`);
expect(el.formattedValue).to.equal('1,234.56');
el = await fixture(html`
<lion-input-stepper
.formatOptions="${{ locale: 'nl-NL' }}"
.modelValue="${1234.56}"
></lion-input-stepper>
`);
expect(el.formattedValue).to.equal('1.234,56');
});
it('supports overriding decimalSeparator in formatOptions', async () => {
const el = await fixture(
html`<lion-input-stepper
.formatOptions="${{ locale: 'nl-NL', decimalSeparator: '.' }}"
.modelValue="${12.34}"
></lion-input-stepper>`,
);
expect(el.formattedValue).to.equal('12.34');
});
it('supports overriding groupSeparator in formatOptions', async () => {
const el = await fixture(
html`<lion-input-stepper
.formatOptions="${{ locale: 'nl-NL', groupSeparator: ',', decimalSeparator: '.' }}"
.modelValue="${1234.56}"
></lion-input-stepper>`,
);
expect(el.formattedValue).to.equal('1,234.56');
});
});
describe('User interaction', () => {
it('should increment the value to 1 on [ArrowUp]', async () => {
const el = await fixture(defaultInputStepper);
expect(el.value).to.equal('');
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
await el.updateComplete;
expect(el.value).to.equal('1');
});
it('should increment the value to minValue on [ArrowUp] if value is below min', async () => {
const el = await fixture(inputStepperWithAttrs);
expect(el.value).to.equal('');
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
await el.updateComplete;
expect(el.value).to.equal('100');
});
it('should decrement the value to -1 on [ArrowDown]', async () => {
const el = await fixture(defaultInputStepper);
expect(el.value).to.equal('');
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
await el.updateComplete;
expect(el.value).to.equal('1');
});
it('should increment the value to minValue on [ArrowDown] if value is below min', async () => {
const el = await fixture(inputStepperWithAttrs);
el.modelValue = 600;
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' }));
await el.updateComplete;
expect(el.value).to.equal('200');
});
it('should increment the value to 1 on + button click', async () => {
const el = await fixture(defaultInputStepper);
expect(el.value).to.equal('');
const incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
expect(el.value).to.equal('1');
});
it('should decrement the value to -1 on - button click', async () => {
const el = await fixture(defaultInputStepper);
expect(el.value).to.equal('');
const decrementButton = el.querySelector('[slot=prefix]');
decrementButton?.dispatchEvent(new Event('click'));
expect(el.value).to.equal('1');
});
it('fires one "user-input-changed" event on + button click', async () => {
let counter = 0;
const el = await fixture(html`
<lion-input-stepper
name="year"
label="Years"
@user-input-changed="${() => {
counter += 1;
}}"
>
</lion-input-stepper>
`);
const incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
expect(counter).to.equal(1);
});
it('fires one "user-input-changed" event on - button click', async () => {
let counter = 0;
const el = await fixture(html`
<lion-input-stepper
name="year"
label="Years"
@user-input-changed="${() => {
counter += 1;
}}"
>
</lion-input-stepper>
`);
const decrementButton = el.querySelector('[slot=prefix]');
decrementButton?.dispatchEvent(new Event('click'));
expect(counter).to.equal(1);
});
it('fires a leave event ("blur") on button clicks', async () => {
const blurSpy = sinon.spy();
const el = await fixture(html`
<lion-input-stepper @blur=${blurSpy} name="year" label="Years"></lion-input-stepper>
`);
expect(el.value).to.equal('');
const decrementButton = el.querySelector('[slot=prefix]');
decrementButton?.dispatchEvent(new Event('focus'));
decrementButton?.dispatchEvent(new Event('click'));
decrementButton?.dispatchEvent(new Event('blur'));
expect(el.value).to.equal('1');
expect(blurSpy.calledOnce).to.be.true;
expect(el.touched).to.be.true;
el.touched = false;
const incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('focus'));
incrementButton?.dispatchEvent(new Event('click'));
incrementButton?.dispatchEvent(new Event('blur'));
expect(el.value).to.equal('0');
expect(blurSpy.calledTwice).to.be.true;
expect(el.touched).to.be.true;
});
it('should update min and max attributes when min and max property change', async () => {
const el = await fixture(inputStepperWithAttrs);
el.min = 100;
el.max = 200;
await nextFrame();
expect(el._inputNode.min).to.equal(el.min.toString());
expect(el._inputNode.max).to.equal(el.max.toString());
});
it('should remove the disabled attribute of the decrement button when the min property changes to below the modelvalue', async () => {
const el = await fixture(inputStepperWithAttrs);
const decrementButton = el.querySelector('[slot=prefix]');
el.modelValue = 100;
await nextFrame();
expect(decrementButton?.getAttribute('disabled')).to.equal('true');
el.min = 99;
await nextFrame();
expect(decrementButton?.getAttribute('disabled')).to.equal(null);
});
it('should add the disabled attribute of the decrement button when the min property changes to the modelvalue', async () => {
const el = await fixture(inputStepperWithAttrs);
const decrementButton = el.querySelector('[slot=prefix]');
el.modelValue = 101;
await nextFrame();
expect(decrementButton?.getAttribute('disabled')).to.equal(null);
el.min = 101;
await nextFrame();
expect(decrementButton?.getAttribute('disabled')).to.equal('true');
});
it('should remove the disabled attribute of the increment button when the max property changes to above the modelvalue', async () => {
const el = await fixture(inputStepperWithAttrs);
const incrementButton = el.querySelector('[slot=suffix]');
el.modelValue = 200;
await nextFrame();
expect(incrementButton?.getAttribute('disabled')).to.equal('true');
el.max = 201;
await nextFrame();
expect(incrementButton?.getAttribute('disabled')).to.equal(null);
});
it('should add the disabled attribute of the increment button when the max property changes to the modelvalue', async () => {
const el = await fixture(inputStepperWithAttrs);
const incrementButton = el.querySelector('[slot=suffix]');
el.modelValue = 199;
await nextFrame();
expect(incrementButton?.getAttribute('disabled')).to.equal(null);
el.max = 199;
await nextFrame();
expect(incrementButton?.getAttribute('disabled')).to.equal('true');
});
it('should react to changes in the modelValue by adjusting the disabled state of the button', async () => {
const el = await fixture(inputStepperWithAttrs);
const incrementButton = el.querySelector('[slot=suffix]');
el.modelValue = 199;
await nextFrame();
expect(incrementButton?.getAttribute('disabled')).to.equal(null);
el.modelValue = 200;
await nextFrame();
expect(incrementButton?.getAttribute('disabled')).to.equal('true');
});
describe('It align to steps', () => {
it('aligns the value to the nearest step when incrementing', async () => {
let el = await fixture(
html`<lion-input-stepper step="10" min="0" max="100" value="55"></lion-input-stepper>`,
);
let incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
await el.updateComplete;
expect(el.modelValue).to.equal(60, 'Fail + : (0 > 100 by 10; val 55)');
// min 1
el = await fixture(
html`<lion-input-stepper step="10" min="1" max="100" value="55"></lion-input-stepper>`,
);
incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
await el.updateComplete;
expect(el.modelValue).to.equal(61, 'Fail + : (1 > 100 by 10; val 55)');
// min 34
el = await fixture(
html`<lion-input-stepper step="10" min="34" max="100" value="55"></lion-input-stepper>`,
);
incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
await el.updateComplete;
expect(el.modelValue).to.equal(64, 'Fail + : (34 > 100 by 10; val 55)');
// min -23
el = await fixture(
html`<lion-input-stepper step="10" min="-23" max="100" value="55"></lion-input-stepper>`,
);
incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
await el.updateComplete;
expect(el.modelValue).to.equal(57, 'Fail + : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7 > ... > 57
// min -23
el = await fixture(
html`<lion-input-stepper step="10" min="-23" max="100" value="-9"></lion-input-stepper>`,
);
incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
await el.updateComplete;
expect(el.modelValue).to.equal(-3, 'Fail + : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7
});
it('aligns the value to the nearest step when decrementing', async () => {
let el = await fixture(
html`<lion-input-stepper step="10" min="0" max="100" value="55"></lion-input-stepper>`,
);
let decrementButton = el.querySelector('[slot=prefix]');
decrementButton?.dispatchEvent(new Event('click'));
await el.updateComplete;
expect(el.modelValue).to.equal(50, 'Fail - : (0 > 100 by 10; val 55)');
// min 1
el = await fixture(
html`<lion-input-stepper step="10" min="1" max="100" value="55"></lion-input-stepper>`,
);
decrementButton = el.querySelector('[slot=prefix]');
decrementButton?.dispatchEvent(new Event('click'));
await el.updateComplete;
expect(el.modelValue).to.equal(51, 'Fail - : (1 > 100 by 10; val 55)');
// min 34
el = await fixture(
html`<lion-input-stepper step="10" min="34" max="100" value="55"></lion-input-stepper>`,
);
decrementButton = el.querySelector('[slot=prefix]');
decrementButton?.dispatchEvent(new Event('click'));
await el.updateComplete;
expect(el.modelValue).to.equal(54, 'Fail - : (34 > 100 by 10; val 55)');
// min -23
el = await fixture(
html`<lion-input-stepper step="10" min="-23" max="100" value="55"></lion-input-stepper>`,
);
decrementButton = el.querySelector('[slot=prefix]');
decrementButton?.dispatchEvent(new Event('click'));
await el.updateComplete;
expect(el.modelValue).to.equal(47, 'Fail - : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7 > ... > 47
// min -23
el = await fixture(
html`<lion-input-stepper step="10" min="-23" max="100" value="-9"></lion-input-stepper>`,
);
decrementButton = el.querySelector('[slot=prefix]');
decrementButton?.dispatchEvent(new Event('click'));
await el.updateComplete;
expect(el.modelValue).to.equal(-13, 'Fail - : (-23 > 100 by 10; val 55)'); // -23 > -13 > -3 > 7
});
});
});
describe('Accessibility', () => {
it('is a11y AXE accessible', async () => {
const el = await fixture(defaultInputStepper);
await expect(el).to.be.accessible();
});
it('is accessible when disabled', async () => {
const el = await fixture(`<lion-input-stepper label="rsvp" disabled></lion-input-stepper>`);
await expect(el).to.be.accessible();
});
it('has role="spinbutton"', async () => {
const el = await fixture(defaultInputStepper);
expect(el._inputNode.hasAttribute('role')).to.be.true;
expect(el._inputNode.getAttribute('role')).to.equal('spinbutton');
});
it('updates aria-valuenow when stepper is changed', async () => {
const el = await fixture(defaultInputStepper);
el.modelValue = 1;
await el.updateComplete;
expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.true;
expect(el._inputNode.getAttribute('aria-valuenow')).to.equal('1');
el.modelValue = '';
await el.updateComplete;
expect(el._inputNode.hasAttribute('aria-valuenow')).to.be.false;
});
it('updates aria-valuetext when stepper is changed', async () => {
// VoiceOver announces percentages once the valuemin or valuemax are used.
// This can be fixed by setting valuetext to the same value as valuenow
// https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-valuenow
const el = await fixture(defaultInputStepper);
el.modelValue = 1;
await el.updateComplete;
expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('1');
el.modelValue = '';
await el.updateComplete;
expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.false;
});
it('can give aria-valuetext to override default value as a human-readable text alternative', async () => {
const values = {
1: 'first',
2: 'second',
3: 'third',
};
const el = await fixture(html`
<lion-input-stepper min="1" max="3" .valueTextMapping="${values}"></lion-input-stepper>
`);
el.modelValue = 1;
await el.updateComplete;
expect(el._inputNode.hasAttribute('aria-valuetext')).to.be.true;
expect(el._inputNode.getAttribute('aria-valuetext')).to.equal('first');
});
it('updates aria-valuemin when stepper is changed', async () => {
const el = await fixture(inputStepperWithAttrs);
const incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('100');
el.min = 0;
await el.updateComplete;
expect(el._inputNode.hasAttribute('aria-valuemin')).to.be.true;
expect(el._inputNode.getAttribute('aria-valuemin')).to.equal('0');
});
it('updates aria-valuemax when stepper is changed', async () => {
const el = await fixture(inputStepperWithAttrs);
const incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('click'));
expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('200');
el.max = 1000;
await el.updateComplete;
expect(el._inputNode.hasAttribute('aria-valuemax')).to.be.true;
expect(el._inputNode.getAttribute('aria-valuemax')).to.equal('1000');
});
it('when decrease button gets focus, it sets aria-live to input-stepper__value', async () => {
const el = await fixture(inputStepperWithAttrs);
const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
const decrementButton = el.querySelector('[slot=prefix]');
decrementButton?.dispatchEvent(new Event('focus'));
expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
decrementButton?.dispatchEvent(new Event('blur'));
expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
});
it('when increase button gets focus, it sets aria-live to input-stepper__value', async () => {
const el = await fixture(inputStepperWithAttrs);
const stepperValue = el.shadowRoot?.querySelector('.input-stepper__value');
const incrementButton = el.querySelector('[slot=suffix]');
incrementButton?.dispatchEvent(new Event('focus'));
expect(stepperValue?.hasAttribute('aria-live')).to.be.true;
expect(stepperValue?.getAttribute('aria-live')).to.equal('assertive');
incrementButton?.dispatchEvent(new Event('blur'));
expect(stepperValue?.hasAttribute('aria-live')).to.be.false;
});
});
});