@@ -2,8 +2,9 @@ import type { NumberFieldRootProps } from './NumberFieldRoot.vue'
22import userEvent from '@testing-library/user-event'
33import { fireEvent , render } from '@testing-library/vue'
44import { mount } from '@vue/test-utils'
5- import { beforeEach , describe , expect , it } from 'vitest'
5+ import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
66import { axe } from 'vitest-axe'
7+ import { nextTick } from 'vue'
78import { useKbd } from '@/shared'
89import { handleSubmit } from '@/test'
910import NumberField from './story/_NumberField.vue'
@@ -21,6 +22,28 @@ function setup(props?: NumberFieldRootProps) {
2122}
2223
2324const kbd = useKbd ( )
25+
26+ async function dispatchPointerEvent (
27+ target : EventTarget ,
28+ type : 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel' ,
29+ init : MouseEventInit & { pointerId ?: number , pointerType ?: string } = { } ,
30+ ) {
31+ await nextTick ( )
32+ const event = new MouseEvent ( type , {
33+ bubbles : true ,
34+ cancelable : true ,
35+ button : 0 ,
36+ ...init ,
37+ } )
38+ Object . defineProperties ( event , {
39+ pointerId : { value : init . pointerId ?? 1 } ,
40+ pointerType : { value : init . pointerType ?? 'touch' } ,
41+ } )
42+ target . dispatchEvent ( event )
43+ await nextTick ( )
44+ return event
45+ }
46+
2447describe ( 'numberField' , ( ) => {
2548 beforeEach ( ( ) => {
2649 // @ts -expect-error aXe throwing error complaining getComputedStyle
@@ -30,6 +53,10 @@ describe('numberField', () => {
3053 document . body . innerHTML = ''
3154 } )
3255
56+ afterEach ( ( ) => {
57+ vi . useRealTimers ( )
58+ } )
59+
3360 it ( 'should pass axe accessibility tests' , async ( ) => {
3461 const { root } = setup ( )
3562 expect ( await axe ( root ) ) . toHaveNoViolations ( )
@@ -91,6 +118,119 @@ describe('numberField', () => {
91118 expect ( input . value ) . toBe ( '0' )
92119 } )
93120
121+ describe ( 'with pointer hold controls' , ( ) => {
122+ it ( 'should preserve immediate mouse activation' , async ( ) => {
123+ const { input, increment } = setup ( { defaultValue : 0 } )
124+
125+ const pointerDown = await dispatchPointerEvent ( increment , 'pointerdown' , { pointerType : 'mouse' } )
126+ expect ( pointerDown . defaultPrevented ) . toBe ( true )
127+ expect ( input . value ) . toBe ( '1' )
128+
129+ await dispatchPointerEvent ( window , 'pointerup' , { pointerType : 'mouse' } )
130+ expect ( input . value ) . toBe ( '1' )
131+ } )
132+
133+ it ( 'should activate a touch tap on release' , async ( ) => {
134+ const { input, increment } = setup ( { defaultValue : 0 } )
135+
136+ const pointerDown = await dispatchPointerEvent ( increment , 'pointerdown' )
137+ expect ( pointerDown . defaultPrevented ) . toBe ( true )
138+ expect ( input . value ) . toBe ( '0' )
139+
140+ await dispatchPointerEvent ( window , 'pointerup' )
141+ expect ( input . value ) . toBe ( '1' )
142+ } )
143+
144+ it ( 'should ignore a touch press while disabled' , async ( ) => {
145+ vi . useFakeTimers ( )
146+ const { input, increment } = setup ( { defaultValue : 0 , disabled : true } )
147+
148+ await dispatchPointerEvent ( increment , 'pointerdown' )
149+ expect ( increment ) . not . toHaveAttribute ( 'data-pressed' )
150+
151+ await vi . advanceTimersByTimeAsync ( 500 )
152+ await dispatchPointerEvent ( window , 'pointerup' )
153+ expect ( input . value ) . toBe ( '0' )
154+ } )
155+
156+ it ( 'should cancel a touch press when movement exceeds the tolerance' , async ( ) => {
157+ vi . useFakeTimers ( )
158+ const { input, increment } = setup ( { defaultValue : 0 } )
159+
160+ await dispatchPointerEvent ( increment , 'pointerdown' , { clientX : 10 , clientY : 10 } )
161+ await dispatchPointerEvent ( window , 'pointermove' , { clientX : 21 , clientY : 10 } )
162+ await vi . advanceTimersByTimeAsync ( 500 )
163+ await dispatchPointerEvent ( window , 'pointerup' , { clientX : 21 , clientY : 10 } )
164+
165+ expect ( input . value ) . toBe ( '0' )
166+ } )
167+
168+ it ( 'should repeat a stationary touch press after the hold delay' , async ( ) => {
169+ vi . useFakeTimers ( )
170+ const { input, increment } = setup ( { defaultValue : 0 } )
171+
172+ await dispatchPointerEvent ( increment , 'pointerdown' )
173+ await vi . advanceTimersByTimeAsync ( 399 )
174+ expect ( input . value ) . toBe ( '0' )
175+
176+ await vi . advanceTimersByTimeAsync ( 1 )
177+ await nextTick ( )
178+ expect ( input . value ) . toBe ( '1' )
179+
180+ await vi . advanceTimersByTimeAsync ( 60 )
181+ await nextTick ( )
182+ expect ( input . value ) . toBe ( '2' )
183+
184+ await dispatchPointerEvent ( window , 'pointerup' )
185+ await vi . advanceTimersByTimeAsync ( 60 )
186+ expect ( input . value ) . toBe ( '2' )
187+ } )
188+
189+ it ( 'should stop an active touch hold when movement exceeds the tolerance' , async ( ) => {
190+ vi . useFakeTimers ( )
191+ const { input, increment } = setup ( { defaultValue : 0 } )
192+
193+ await dispatchPointerEvent ( increment , 'pointerdown' , { clientX : 10 , clientY : 10 } )
194+ await vi . advanceTimersByTimeAsync ( 400 )
195+ await nextTick ( )
196+ expect ( input . value ) . toBe ( '1' )
197+
198+ await dispatchPointerEvent ( window , 'pointermove' , { clientX : 21 , clientY : 10 } )
199+ await vi . advanceTimersByTimeAsync ( 120 )
200+ expect ( input . value ) . toBe ( '1' )
201+ } )
202+
203+ it ( 'should tolerate minor movement after a touch hold starts' , async ( ) => {
204+ vi . useFakeTimers ( )
205+ const { input, increment } = setup ( { defaultValue : 0 } )
206+
207+ await dispatchPointerEvent ( increment , 'pointerdown' , { clientX : 10 , clientY : 10 } )
208+ await vi . advanceTimersByTimeAsync ( 400 )
209+ await nextTick ( )
210+ expect ( input . value ) . toBe ( '1' )
211+
212+ await dispatchPointerEvent ( window , 'pointermove' , { clientX : 15 , clientY : 10 } )
213+ await vi . advanceTimersByTimeAsync ( 60 )
214+ await nextTick ( )
215+ expect ( input . value ) . toBe ( '2' )
216+
217+ await dispatchPointerEvent ( window , 'pointerup' , { clientX : 15 , clientY : 10 } )
218+ await vi . advanceTimersByTimeAsync ( 60 )
219+ expect ( input . value ) . toBe ( '2' )
220+ } )
221+
222+ it ( 'should cancel a touch press on pointercancel' , async ( ) => {
223+ vi . useFakeTimers ( )
224+ const { input, increment } = setup ( { defaultValue : 0 } )
225+
226+ await dispatchPointerEvent ( increment , 'pointerdown' )
227+ await dispatchPointerEvent ( window , 'pointercancel' )
228+ await vi . advanceTimersByTimeAsync ( 500 )
229+
230+ expect ( input . value ) . toBe ( '0' )
231+ } )
232+ } )
233+
94234 it ( 'should increase and decrease based on keyboard navigation on input' , async ( ) => {
95235 const { input } = setup ( { defaultValue : 0 , min : 0 , max : 10 } )
96236
0 commit comments