10
10
* governing permissions and limitations under the License.
11
11
*/
12
12
13
+ import { act , fireEvent , pointerMap , render } from '@react-spectrum/test-utils-internal' ;
13
14
import { Button , ButtonContext , ProgressBar , Text } from '../' ;
14
- import { fireEvent , pointerMap , render } from '@react-spectrum/test-utils-internal' ;
15
15
import React , { useState } from 'react' ;
16
16
import userEvent from '@testing-library/user-event' ;
17
17
@@ -21,6 +21,10 @@ describe('Button', () => {
21
21
user = userEvent . setup ( { delay : null , pointerMap} ) ;
22
22
jest . useFakeTimers ( ) ;
23
23
} ) ;
24
+ afterEach ( ( ) => {
25
+ // clear any live announcers from pending buttons
26
+ act ( ( ) => jest . runAllTimers ( ) ) ;
27
+ } ) ;
24
28
25
29
it ( 'should render a button with default class' , ( ) => {
26
30
let { getByRole} = render ( < Button > Test</ Button > ) ;
@@ -197,4 +201,147 @@ describe('Button', () => {
197
201
let button = getByRole ( 'button' ) ;
198
202
expect ( button ) . not . toHaveAttribute ( 'href' ) ;
199
203
} ) ;
204
+
205
+ it ( 'should prevent explicit mouse form submission when isPending' , async function ( ) {
206
+ let onSubmitSpy = jest . fn ( e => e . preventDefault ( ) ) ;
207
+ function TestComponent ( ) {
208
+ let [ pending , setPending ] = useState ( false ) ;
209
+ return (
210
+ < Button
211
+ type = "submit"
212
+ onPress = { ( ) => {
213
+ // immediately setting pending to true will remove the click handler before the form is submitted
214
+ setTimeout ( ( ) => {
215
+ setPending ( true ) ;
216
+ } , 0 ) ;
217
+ } }
218
+ isPending = { pending } >
219
+ { ( { isPending} ) => (
220
+ < >
221
+ < Text style = { { opacity : isPending ? '0' : undefined } } > Test</ Text >
222
+ < ProgressBar
223
+ aria-label = "loading"
224
+ style = { { opacity : isPending ? undefined : '0' } }
225
+ isIndeterminate >
226
+ loading
227
+ </ ProgressBar >
228
+ </ >
229
+ ) }
230
+ </ Button >
231
+ ) ;
232
+ }
233
+ let { getByRole} = render (
234
+ < form onSubmit = { onSubmitSpy } >
235
+ < TestComponent />
236
+ </ form >
237
+ ) ;
238
+ let button = getByRole ( 'button' ) ;
239
+ expect ( button ) . not . toHaveAttribute ( 'aria-disabled' ) ;
240
+
241
+ await user . click ( button ) ;
242
+ expect ( onSubmitSpy ) . toHaveBeenCalled ( ) ;
243
+ onSubmitSpy . mockClear ( ) ;
244
+
245
+ // run timer to set pending
246
+ act ( ( ) => jest . runAllTimers ( ) ) ;
247
+
248
+ await user . click ( button ) ;
249
+ expect ( onSubmitSpy ) . not . toHaveBeenCalled ( ) ;
250
+ } ) ;
251
+
252
+ it ( 'should prevent explicit keyboard form submission when isPending' , async function ( ) {
253
+ let onSubmitSpy = jest . fn ( e => e . preventDefault ( ) ) ;
254
+ function TestComponent ( ) {
255
+ let [ pending , setPending ] = useState ( false ) ;
256
+ return (
257
+ < Button
258
+ type = "submit"
259
+ onPress = { ( ) => {
260
+ // immediately setting pending to true will remove the click handler before the form is submitted
261
+ setTimeout ( ( ) => {
262
+ setPending ( true ) ;
263
+ } , 0 ) ;
264
+ } }
265
+ isPending = { pending } >
266
+ { ( { isPending} ) => (
267
+ < >
268
+ < Text style = { { opacity : isPending ? '0' : undefined } } > Test</ Text >
269
+ < ProgressBar
270
+ aria-label = "loading"
271
+ style = { { opacity : isPending ? undefined : '0' } }
272
+ isIndeterminate >
273
+ loading
274
+ </ ProgressBar >
275
+ </ >
276
+ ) }
277
+ </ Button >
278
+ ) ;
279
+ }
280
+ render (
281
+ < form onSubmit = { onSubmitSpy } >
282
+ < TestComponent />
283
+ </ form >
284
+ ) ;
285
+ await user . tab ( ) ;
286
+ await user . keyboard ( '{Enter}' ) ;
287
+ expect ( onSubmitSpy ) . toHaveBeenCalled ( ) ;
288
+ onSubmitSpy . mockClear ( ) ;
289
+ act ( ( ) => jest . runAllTimers ( ) ) ;
290
+
291
+ await user . keyboard ( '{Enter}' ) ;
292
+ expect ( onSubmitSpy ) . not . toHaveBeenCalled ( ) ;
293
+ } ) ;
294
+
295
+ // Note: two inputs are needed, otherwise https://www.w3.org/TR/2011/WD-html5-20110525/association-of-controls-and-forms.html#implicit-submission
296
+ // Implicit form submission can happen if there's only one.
297
+ it ( 'should prevent implicit form submission when isPending' , async function ( ) {
298
+ let onSubmitSpy = jest . fn ( e => e . preventDefault ( ) ) ;
299
+ function TestComponent ( props ) {
300
+ let [ pending , setPending ] = useState ( false ) ;
301
+ return (
302
+ < form
303
+ onSubmit = { ( e ) => {
304
+ // forms are submitted implicitly on keydown, so we need to wait to set pending until after to set pending
305
+ props . onSubmit ( e ) ;
306
+ } }
307
+ onKeyDown = { ( e ) => {
308
+ if ( e . key === 'Enter' ) {
309
+ // keyup could theoretically happen elsewhere if focus is moved during submission
310
+ document . body . addEventListener ( 'keyup' , ( ) => {
311
+ setPending ( true ) ;
312
+ } , { capture : true , once : true } ) ;
313
+ }
314
+ } } >
315
+ < label htmlFor = "foo" > Test</ label >
316
+ < input id = "foo" type = "text" />
317
+ < input id = "bar" type = "text" />
318
+ < Button
319
+ type = "submit"
320
+ isPending = { pending } >
321
+ { ( { isPending} ) => (
322
+ < >
323
+ < Text style = { { opacity : isPending ? '0' : undefined } } > Test</ Text >
324
+ < ProgressBar
325
+ aria-label = "loading"
326
+ style = { { opacity : isPending ? undefined : '0' } }
327
+ isIndeterminate >
328
+ loading
329
+ </ ProgressBar >
330
+ </ >
331
+ ) }
332
+ </ Button >
333
+ </ form >
334
+ ) ;
335
+ }
336
+ render (
337
+ < TestComponent onSubmit = { onSubmitSpy } />
338
+ ) ;
339
+ await user . tab ( ) ;
340
+ await user . keyboard ( '{Enter}' ) ;
341
+ expect ( onSubmitSpy ) . toHaveBeenCalled ( ) ;
342
+ onSubmitSpy . mockClear ( ) ;
343
+
344
+ await user . keyboard ( '{Enter}' ) ;
345
+ expect ( onSubmitSpy ) . not . toHaveBeenCalled ( ) ;
346
+ } ) ;
200
347
} ) ;
0 commit comments