import { describe, expect, it } from 'vitest' import { mount } from '@vue/test-utils' import NumberInput from '../../components/NumberInput.vue' function lastEmitted(wrapper: ReturnType) { return wrapper.emitted('update:modelValue')?.at(-1)?.[0] } describe('NumberInput', () => { it('should never render a range input, with or without min/max', () => { for (const props of [{}, { min: 0 }, { max: 10 }, { min: 0, max: 10 }]) { const wrapper = mount(NumberInput, { props: { modelValue: 5, ...props } }) expect(wrapper.find('input[type="range"]').exists()).toBe(false) } }) it('should draw the range fill only when both min and max are set', () => { expect(mount(NumberInput, { props: { modelValue: 5, min: 0 } }).find('.mint-number-input__rule').exists()).toBe(false) const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } }) expect(wrapper.find('.mint-number-input__rule').exists()).toBe(true) expect(wrapper.find('.mint-number-input__scrub').attributes('style')).toContain('--mint-number-input-fill: 50%') }) it('should expose the scrub area as a slider when min and max are set', () => { const scrub = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } }).find('.mint-number-input__scrub') expect(scrub.attributes('role')).toBe('slider') expect(scrub.attributes('aria-valuenow')).toBe('5') expect(scrub.attributes('tabindex')).toBe('0') }) it('should always render both steppers', () => { const wrapper = mount(NumberInput, { props: { modelValue: 5 } }) expect(wrapper.findAll('.mint-number-input__stepper')).toHaveLength(2) }) it('should disable the steppers at min and max', () => { const atMin = mount(NumberInput, { props: { modelValue: 0, min: 0, max: 10 } }) expect(atMin.find('.mint-number-input__stepper--down').attributes('disabled')).toBeDefined() const atMax = mount(NumberInput, { props: { modelValue: 10, min: 0, max: 10 } }) expect(atMax.find('.mint-number-input__stepper--up').attributes('disabled')).toBeDefined() }) it('should emit the step-snapped value when dragging at 25% of the scrub width', async () => { const wrapper = mount(NumberInput, { props: { modelValue: 0, min: 0, max: 50, step: 0.5 } }) const scrub = wrapper.find('.mint-number-input__scrub') scrub.element.getBoundingClientRect = () => ({ left: 100, width: 200 } as DOMRect) await scrub.trigger('pointerdown', { clientX: 150, pointerId: 1 }) expect(lastEmitted(wrapper)).toBe(12.5) }) it('should leave clicks and small pointer movements on the number available for typing', async () => { const wrapper = mount(NumberInput, { props: { modelValue: 0, min: 0, max: 50 } }) await wrapper.find('input').trigger('pointerdown', { clientX: 150, pointerId: 1 }) await wrapper.find('input').trigger('pointermove', { clientX: 152, pointerId: 1 }) await wrapper.find('input').trigger('pointerup', { pointerId: 1 }) expect(wrapper.emitted('update:modelValue')).toBeUndefined() await wrapper.find('input').setValue('12') expect(lastEmitted(wrapper)).toBe(12) }) it('should drag the number relative to its starting value and stop on release', async () => { const wrapper = mount(NumberInput, { props: { modelValue: 10, min: 0, max: 50, step: 0.5 } }) const input = wrapper.find('input') wrapper.find('.mint-number-input__scrub').element.getBoundingClientRect = () => ({ left: 100, width: 200 } as DOMRect) await input.trigger('pointerdown', { clientX: 120, pointerId: 1 }) await input.trigger('pointermove', { clientX: 170, pointerId: 1 }) expect(lastEmitted(wrapper)).toBe(22.5) await input.trigger('pointermove', { clientX: 500, pointerId: 1 }) expect(lastEmitted(wrapper)).toBe(50) await input.trigger('pointerup', { pointerId: 1 }) const count = wrapper.emitted('update:modelValue')?.length await input.trigger('pointermove', { clientX: 100, pointerId: 1 }) expect(wrapper.emitted('update:modelValue')).toHaveLength(count!) }) it('should drag numbers without a full range in step increments', async () => { const wrapper = mount(NumberInput, { props: { modelValue: 2.5, min: 0, step: 1 } }) const input = wrapper.find('input') await input.trigger('pointerdown', { clientX: 100, pointerId: 1 }) await input.trigger('pointermove', { clientX: 116, pointerId: 1 }) expect(lastEmitted(wrapper)).toBe(4.5) await input.trigger('pointermove', { clientX: 60, pointerId: 1 }) expect(lastEmitted(wrapper)).toBe(0) await input.trigger('pointercancel', { pointerId: 1 }) const count = wrapper.emitted('update:modelValue')?.length await input.trigger('pointermove', { clientX: 120, pointerId: 1 }) expect(wrapper.emitted('update:modelValue')).toHaveLength(count!) }) it('should ignore disabled inputs and non-primary buttons when dragging', async () => { for (const props of [{ disabled: true }, { disabled: false }]) { const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10, ...props } }) const input = wrapper.find('input') await input.trigger('pointerdown', { clientX: 100, pointerId: 1, button: props.disabled ? 0 : 2 }) await input.trigger('pointermove', { clientX: 150, pointerId: 1 }) expect(wrapper.emitted('update:modelValue')).toBeUndefined() wrapper.unmount() } }) it('should step with the arrow keys', async () => { const wrapper = mount(NumberInput, { props: { modelValue: 5, step: 2 } }) await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' }) expect(lastEmitted(wrapper)).toBe(7) await wrapper.find('input').trigger('keydown', { key: 'ArrowDown' }) expect(lastEmitted(wrapper)).toBe(3) }) it('should step by ten steps with PageUp and PageDown', async () => { const wrapper = mount(NumberInput, { props: { modelValue: 50, step: 1, min: 0, max: 100 } }) await wrapper.find('input').trigger('keydown', { key: 'PageUp' }) expect(lastEmitted(wrapper)).toBe(60) await wrapper.find('input').trigger('keydown', { key: 'PageDown' }) expect(lastEmitted(wrapper)).toBe(40) }) it('should jump to min and max with Home and End on the slider surface', async () => { const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } }) const scrub = wrapper.find('.mint-number-input__scrub') await scrub.trigger('keydown', { key: 'Home' }) expect(lastEmitted(wrapper)).toBe(0) await scrub.trigger('keydown', { key: 'End' }) expect(lastEmitted(wrapper)).toBe(10) }) it('should leave Home and End to the caret inside the text input', async () => { const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } }) await wrapper.find('input').trigger('keydown', { key: 'Home' }) expect(wrapper.emitted('update:modelValue')).toBeUndefined() }) it('should emit once per arrow press in slider mode', async () => { const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } }) await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' }) expect(wrapper.emitted('update:modelValue')).toHaveLength(1) }) it('should keep a fractional value precision when stepping by an integer', async () => { const wrapper = mount(NumberInput, { props: { modelValue: 2.5, step: 1 } }) await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' }) expect(lastEmitted(wrapper)).toBe(3.5) }) it('should trim float noise when stepping by a decimal', async () => { const wrapper = mount(NumberInput, { props: { modelValue: 0.1, step: 0.2 } }) await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' }) expect(lastEmitted(wrapper)).toBe(0.3) }) })