@jdlien/validator 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1922 @@
1
+ import Validator, { ValidationErrorEvent, ValidationSuccessEvent } from '../src/Validator'
2
+ import * as utils from '../src/validator-utils'
3
+ import { ValidatorOptions } from '../src/types'
4
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
5
+
6
+ describe('Validator', () => {
7
+ let form: HTMLFormElement
8
+ let formControl: HTMLInputElement
9
+ let errorEl: HTMLDivElement
10
+ let options: ValidatorOptions
11
+ let validator: Validator
12
+ let valid: boolean
13
+
14
+ beforeEach(() => {
15
+ form = document.createElement('form')
16
+ form.id = 'test-form'
17
+ document.body.appendChild(form)
18
+
19
+ formControl = document.createElement('input')
20
+ formControl.type = 'text'
21
+ formControl.name = 'test-input'
22
+ formControl.id = 'test-input'
23
+ form.appendChild(formControl)
24
+
25
+ errorEl = document.createElement('div')
26
+ errorEl.id = 'test-input-error'
27
+ errorEl.classList.add('hidden')
28
+ form.appendChild(errorEl)
29
+
30
+ options = {}
31
+ validator = new Validator(form)
32
+ })
33
+
34
+ afterEach(() => {
35
+ document.body.removeChild(form)
36
+ })
37
+
38
+ describe('constructor', () => {
39
+ it('throws an error if no form is passed', () => {
40
+ expect(() => {
41
+ // @ts-ignore
42
+ new Validator()
43
+ }).toThrowError('Validator requires a form to be passed as the first argument.')
44
+ })
45
+
46
+ it('should create a new Validator object', () => {
47
+ expect(validator).toBeTruthy()
48
+ })
49
+
50
+ it('should have a form property', () => {
51
+ expect(validator.form).toBeTruthy()
52
+ })
53
+
54
+ it('throws an error if the form argument is not an instance of HTMLFormElement', () => {
55
+ // @ts-ignore
56
+ expect(() => new Validator(document.createElement('div'))).toThrowError(
57
+ 'form argument must be an instance of HTMLFormElement'
58
+ )
59
+ })
60
+
61
+ it('sets the preventSubmit property to true if the form has data-prevent-submit attribute', () => {
62
+ form.dataset.preventSubmit = ''
63
+ const validator = new Validator(form)
64
+ expect(validator.preventSubmit).toBeTruthy()
65
+ })
66
+
67
+ it('overrides the preventSubmit property if set in the options argument', () => {
68
+ form.dataset.preventSubmit = ''
69
+ options.preventSubmit = false
70
+ const validator = new Validator(form, options)
71
+ expect(validator.preventSubmit).toBeFalsy()
72
+ })
73
+
74
+ it('merges the messages option with the default messages', () => {
75
+ options.messages = { ERROR_MAIN: 'Custom error message' }
76
+ const validator = new Validator(form, options)
77
+ expect(validator.messages.ERROR_MAIN).toBe('Custom error message')
78
+ expect(validator.messages.ERROR_REQUIRED).toBe('This field is required.')
79
+ })
80
+
81
+ it('sets the debug property to the value in the options argument', () => {
82
+ options.debug = true
83
+ const validator = new Validator(form, options)
84
+ expect(validator.debug).toBeTruthy()
85
+ })
86
+
87
+ it('sets the autoInit property to the value in the options argument', () => {
88
+ options.autoInit = false
89
+ const validator = new Validator(form, options)
90
+ expect(validator.autoInit).toBeFalsy()
91
+ })
92
+
93
+ it('sets the hiddenClasses property to the value in the options argument', () => {
94
+ options.hiddenClasses = 'custom-hidden-class'
95
+ const validator = new Validator(form, options)
96
+ expect(validator.hiddenClasses).toBe('custom-hidden-class')
97
+ })
98
+
99
+ it('sets the errorMainClasses property to the value in the options argument', () => {
100
+ options.errorMainClasses = 'custom-error-main-class'
101
+ const validator = new Validator(form, options)
102
+ expect(validator.errorMainClasses).toBe('custom-error-main-class')
103
+ })
104
+
105
+ it('sets the errorInputClasses property to the value in the options argument', () => {
106
+ options.errorInputClasses = 'custom-error-input-class'
107
+ const validator = new Validator(form, options)
108
+ expect(validator.errorInputClasses).toBe('custom-error-input-class')
109
+ })
110
+ }) // constructor
111
+
112
+ describe('Validator observer', () => {
113
+ it('observer should update inputs on input addition', async () => {
114
+ let inputCount = validator.inputs.length
115
+ // Add a new input to the form
116
+ let input = document.createElement('input')
117
+ input.type = 'text'
118
+ input.name = 'testInput'
119
+ input.value = 'test value'
120
+ form.appendChild(input)
121
+
122
+ // The observer should have been triggered, adding a new input to the inputs array
123
+ await new Promise((resolve) => setTimeout(resolve, 0))
124
+
125
+ if (!form.lastChild) throw new Error('lastChild is null')
126
+
127
+ expect(validator.inputs.length).toEqual(inputCount + 1)
128
+ form.removeChild(form.lastChild)
129
+ await new Promise((resolve) => setTimeout(resolve, 0))
130
+ })
131
+ }) // Validator observer
132
+
133
+ describe('init', () => {
134
+ it('should set the inputs property to an array of form controls', () => {
135
+ let input2 = document.createElement('input')
136
+ input2.type = 'email'
137
+ form.appendChild(input2)
138
+
139
+ validator.init()
140
+ expect(validator.inputs).toEqual([formControl, input2])
141
+ })
142
+
143
+ it('should set the novalidate attribute on the form', () => {
144
+ validator.init()
145
+ expect(form.getAttribute('novalidate')).toEqual('novalidate')
146
+ })
147
+
148
+ it('should add and remove event listeners with the correct arguments', () => {
149
+ const form = document.createElement('form')
150
+ const validator = new Validator(form)
151
+
152
+ const addEventListenerSpy = vi.spyOn(form, 'addEventListener')
153
+ const removeEventListenerSpy = vi.spyOn(form, 'removeEventListener')
154
+
155
+ validator.addEventListeners()
156
+ validator.removeEventListeners()
157
+
158
+ expect(addEventListenerSpy).toHaveBeenCalledTimes(5)
159
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
160
+ 'submit',
161
+ (validator as any).submitHandlerRef
162
+ )
163
+
164
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
165
+ 'input',
166
+ (validator as any).inputInputHandlerRef
167
+ )
168
+
169
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
170
+ 'change',
171
+ (validator as any).inputChangeHandlerRef
172
+ )
173
+
174
+ expect(addEventListenerSpy).toHaveBeenCalledWith(
175
+ 'keydown',
176
+ (validator as any).inputKeydownHandlerRef
177
+ )
178
+
179
+ expect(removeEventListenerSpy).toHaveBeenCalledTimes(5)
180
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
181
+ 'submit',
182
+ (validator as any).submitHandlerRef
183
+ )
184
+
185
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
186
+ 'input',
187
+ (validator as any).inputInputHandlerRef
188
+ )
189
+
190
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
191
+ 'change',
192
+ (validator as any).inputChangeHandlerRef
193
+ )
194
+
195
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
196
+ 'keydown',
197
+ (validator as any).inputKeydownHandlerRef
198
+ )
199
+
200
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('remove', validator.destroy)
201
+ })
202
+ }) // init
203
+
204
+ describe('getErrorEl', () => {
205
+ it('returns the error element for the input', () => {
206
+ const errorEl1 = (validator as any).getErrorEl(formControl)
207
+ expect(errorEl1).toBeTruthy()
208
+ expect(errorEl1.id).toBe('test-input-error')
209
+ })
210
+
211
+ it('returns error element by id if the input does not have a name', () => {
212
+ const formControl2 = document.createElement('input')
213
+ formControl2.type = 'text'
214
+ formControl2.id = 'form-control-2'
215
+ form.appendChild(formControl2)
216
+
217
+ const errorDiv2 = document.createElement('div')
218
+ errorDiv2.id = 'form-control-2-error'
219
+ form.appendChild(errorDiv2)
220
+
221
+ const errorEl2 = (validator as any).getErrorEl(formControl2)
222
+ expect(errorEl2).toBeTruthy()
223
+ expect(errorEl2.id).toBe('form-control-2-error')
224
+ })
225
+
226
+ it('returns null if the input does not have an error element', () => {
227
+ const formControl3 = document.createElement('input')
228
+ formControl3.type = 'text'
229
+ formControl3.name = 'form-control-3'
230
+ formControl3.id = 'form-control-3'
231
+ form.appendChild(formControl3)
232
+
233
+ const errorEl2 = (validator as any).getErrorEl(formControl3)
234
+ expect(errorEl2).toBeNull()
235
+ })
236
+ })
237
+
238
+ describe('addErrorMain', () => {
239
+ type MessageDictionary = { [key: string]: string }
240
+ let messages: MessageDictionary
241
+ let errorMainClasses: string
242
+
243
+ beforeEach(() => {
244
+ messages = { ERROR_MAIN: 'Error: main' }
245
+ errorMainClasses = 'error-main'
246
+ validator = new Validator(form, { messages, errorMainClasses })
247
+ })
248
+
249
+ it('creates an element with id "form-error-main"', () => {
250
+ ;(validator as any).addErrorMain()
251
+ const errorEl = form.querySelector('#form-error-main')
252
+ expect(errorEl).toBeTruthy()
253
+ const classes = errorMainClasses.split(' ')
254
+ if (errorEl) expect(errorEl.classList.contains.apply(errorEl.classList, classes)).toBeTruthy()
255
+ })
256
+
257
+ it('sets the innerHTML of the element to message if message is provided', () => {
258
+ const message = 'Hello, world!'
259
+ ;(validator as any).addErrorMain(message)
260
+ const errorEl = document.querySelector('#form-error-main')
261
+ if (errorEl) expect(errorEl.innerHTML).toBe(message)
262
+ })
263
+
264
+ it('sets the innerHTML of the element to messages.ERROR_MAIN if message is not provided', () => {
265
+ ;(validator as any).addErrorMain()
266
+ const errorEl = document.querySelector('#form-error-main')
267
+ if (errorEl) expect(errorEl.innerHTML).toBe(messages.ERROR_MAIN)
268
+ })
269
+
270
+ it('adds the element to the form', () => {
271
+ ;(validator as any).addErrorMain()
272
+ const errorEl = document.querySelector('#form-error-main')
273
+ if (errorEl) expect(errorEl.parentNode).toBe(form)
274
+ })
275
+ }) // addErrorMain
276
+
277
+ describe('addInputError', () => {
278
+ it('adds an error to the inputErrors array for the given form control', () => {
279
+ ;(validator as any).addInputError(formControl, 'invalid username')
280
+ expect(validator.inputErrors[formControl.name]).toContain('invalid username')
281
+ })
282
+
283
+ it('uses the id of the form control if no name is provided', () => {
284
+ formControl.name = ''
285
+ ;(validator as any).addInputError(formControl, 'invalid username')
286
+ expect(validator.inputErrors[formControl.id]).toContain('invalid username')
287
+ })
288
+
289
+ it('adds the error message if one is provided', () => {
290
+ ;(validator as any).addInputError(formControl, 'test message')
291
+ expect(validator.inputErrors[formControl.name]).toContain('test message')
292
+ })
293
+
294
+ it('adds the errorDefault message if no message is provided', () => {
295
+ formControl.dataset.errorDefault = 'default error message'
296
+ ;(validator as any).addInputError(formControl)
297
+ expect(validator.inputErrors[formControl.name]).toContain('default error message')
298
+ })
299
+
300
+ it('adds the generic error message if no message or errorDefault is provided', () => {
301
+ validator.messages.ERROR_GENERIC = 'generic error message'
302
+ ;(validator as any).addInputError(formControl)
303
+ expect(validator.inputErrors[formControl.name]).toContain('generic error message')
304
+ })
305
+
306
+ it('logs a debugging message if debug is enabled', () => {
307
+ validator.debug = true
308
+ const mockLog = vi.fn()
309
+ console.log = mockLog
310
+ ;(validator as any).addInputError(formControl, 'invalid username')
311
+ expect(mockLog).toHaveBeenCalledWith('Invalid value for test-input: invalid username')
312
+ })
313
+
314
+ it('does not show the same error multiple times for a set of radio buttons', () => {
315
+ const radio1 = document.createElement('input')
316
+ radio1.type = 'radio'
317
+ radio1.name = 'radio-group'
318
+ radio1.value = '1'
319
+ radio1.id = 'radio1'
320
+ form.appendChild(radio1)
321
+
322
+ const radio2 = document.createElement('input')
323
+ radio2.type = 'radio'
324
+ radio2.name = 'radio-group'
325
+ radio2.value = '2'
326
+ radio2.id = 'radio2'
327
+ form.appendChild(radio2)
328
+ ;(validator as any).addInputError(radio1, 'invalid radio')
329
+ ;(validator as any).addInputError(radio2, 'invalid radio')
330
+
331
+ expect(validator.inputErrors['radio-group'].length).toBe(1)
332
+ })
333
+
334
+ it('shows multiple different error messages simultaneously', () => {
335
+ ;(validator as any).addInputError(formControl, 'error message 1')
336
+ ;(validator as any).addInputError(formControl, 'error message 2')
337
+
338
+ expect(validator.inputErrors[formControl.name].length).toBe(2)
339
+ })
340
+ }) // end addInputError
341
+
342
+ describe('showInputErrors', () => {
343
+ it('shows an error message', () => {
344
+ // First add an error
345
+ ;(validator as any).addInputError(formControl)
346
+ ;(validator as any).showInputErrors(formControl)
347
+
348
+ expect(errorEl.innerHTML).toBe(validator.messages.ERROR_GENERIC)
349
+ expect(errorEl.classList.contains('hidden')).toBeFalsy()
350
+
351
+ validator.errorInputClasses.split(' ').forEach((errorClass) => {
352
+ expect(formControl.classList.contains(errorClass)).toBeTruthy()
353
+ })
354
+ })
355
+ }) // end showInputErrors
356
+
357
+ describe('showFormErrors', () => {
358
+ it('should show errors for all inputs and display main error message', async () => {
359
+ const input1 = document.createElement('input')
360
+ input1.name = 'input1'
361
+ input1.id = 'input1'
362
+ input1.required = true
363
+ input1.value = ''
364
+ form.appendChild(input1)
365
+
366
+ const input1Error = document.createElement('div')
367
+ input1Error.id = 'input1-error'
368
+ form.appendChild(input1Error)
369
+
370
+ const input2 = document.createElement('input')
371
+ input2.name = 'input2'
372
+ input2.id = 'input2'
373
+ input2.pattern = '[0-9]+'
374
+ input2.value = 'abc'
375
+ form.appendChild(input2)
376
+
377
+ const input2Error = document.createElement('div')
378
+ input2Error.id = 'input2-error'
379
+ form.appendChild(input2Error)
380
+
381
+ validator.init()
382
+ valid = await validator.validate()
383
+ ;(validator as any).showFormErrors()
384
+
385
+ const input1Errors = validator.inputErrors[input1.id]
386
+ expect(input1Errors).toHaveLength(1)
387
+ expect(input1Error?.innerHTML).toContain(validator.messages.ERROR_REQUIRED)
388
+
389
+ const input2Errors = validator.inputErrors[input2.id]
390
+ expect(input2Errors).toHaveLength(1)
391
+ expect(input2Error?.innerHTML).toContain(validator.messages.ERROR_GENERIC)
392
+
393
+ const mainError = form.querySelectorAll('#form-error-main')
394
+
395
+ mainError.forEach((error) => {
396
+ expect(error).toBeTruthy()
397
+ expect(error?.innerHTML).toContain(validator.messages.ERROR_MAIN)
398
+ // Check that this uses the default hidden classes (hidden, opacity-0)
399
+ expect(error.classList.contains('hidden')).toBeFalsy()
400
+ expect(error.classList.contains('opacity-0')).toBeFalsy()
401
+ })
402
+
403
+ // Now that we already have a mainErrorEl, check that it doesn't get added again
404
+ ;(validator as any).showFormErrors()
405
+ expect(form.querySelectorAll('#form-error-main')).toHaveLength(1)
406
+
407
+ // Check that the messages and styling are still correct on the second addition
408
+ mainError.forEach((error) => {
409
+ expect(error).toBeTruthy()
410
+ expect(error?.innerHTML).toContain(validator.messages.ERROR_MAIN)
411
+ // Check that this uses the default hidden classes (hidden, opacity-0)
412
+ expect(error.classList.contains('hidden')).toBeFalsy()
413
+ expect(error.classList.contains('opacity-0')).toBeFalsy()
414
+ })
415
+ })
416
+
417
+ it('should not display main error message if there are no input errors', async () => {
418
+ const input1 = document.createElement('input')
419
+ input1.name = 'input1'
420
+ input1.id = 'input1'
421
+ input1.required = true
422
+ input1.value = 'abc'
423
+ form.appendChild(input1)
424
+
425
+ valid = await validator.validate()
426
+ ;(validator as any).showFormErrors()
427
+
428
+ const input1Errors = validator.inputErrors[input1.id]
429
+ expect(input1Errors).toHaveLength(0)
430
+
431
+ const mainError = form.querySelector('#form-error-main')
432
+ expect(mainError).toBeFalsy()
433
+ })
434
+ }) // end showFormErrors
435
+
436
+ describe('clearInputErrors', () => {
437
+ it('clears an error message', () => {
438
+ errorEl.textContent = 'An error message!'
439
+ ;(validator as any).clearInputErrors(formControl)
440
+ expect(validator.inputErrors[formControl.name]).toEqual([])
441
+ expect(errorEl.innerHTML).toBe('')
442
+ expect(errorEl.classList.contains('hidden')).toBeTruthy()
443
+
444
+ validator.errorInputClasses.split(' ').forEach((errorClass) => {
445
+ expect(formControl.classList.contains(errorClass)).toBeFalsy()
446
+ })
447
+ })
448
+ }) // end clearInputErrors
449
+
450
+ describe('clearFormErrors', () => {
451
+ it('clears all error messages', () => {
452
+ form.id = 'clear-all-errors-form'
453
+ const formControl1 = document.createElement('input')
454
+ formControl1.type = 'text'
455
+ formControl1.id = 'test-input-1'
456
+ formControl1.name = 'test-input-1'
457
+ form.appendChild(formControl1)
458
+
459
+ const errorEl1 = document.createElement('div')
460
+ errorEl1.id = 'test-input-1-error'
461
+ errorEl1.textContent = 'An error message!'
462
+ form.appendChild(errorEl1)
463
+
464
+ const formControl2 = document.createElement('input')
465
+ formControl2.type = 'text'
466
+ formControl2.id = 'test-input-2'
467
+ formControl2.name = 'test-input-2'
468
+ form.appendChild(formControl2)
469
+
470
+ const errorEl2 = document.createElement('div')
471
+ errorEl2.id = 'test-input-2-error'
472
+ errorEl2.textContent = 'Another error message!'
473
+ form.appendChild(errorEl2)
474
+
475
+ validator.init()
476
+ ;(validator as any).addInputError(formControl1)
477
+ ;(validator as any).addInputError(formControl2)
478
+
479
+ expect(validator.inputErrors[formControl1.name]).toContain(validator.messages.ERROR_GENERIC)
480
+ expect(validator.inputErrors[formControl2.name]).toContain(validator.messages.ERROR_GENERIC)
481
+ ;(validator as any).clearFormErrors()
482
+
483
+ // validator.inputErrors should be empty
484
+ expect(Object.values(validator.inputErrors).every((i) => i.length == 0)).toBeTruthy()
485
+
486
+ // aria-invalid should not be set
487
+ expect(formControl1.getAttribute('aria-invalid')).toBeNull()
488
+ expect(formControl2.getAttribute('aria-invalid')).toBeNull()
489
+
490
+ expect(errorEl1.innerHTML).toBe('')
491
+ expect(errorEl1.classList.contains('hidden')).toBeTruthy()
492
+
493
+ expect(errorEl2.innerHTML).toBe('')
494
+ expect(errorEl2.classList.contains('hidden')).toBeTruthy()
495
+
496
+ validator.errorInputClasses.split(' ').forEach((errorClass) => {
497
+ expect(formControl1.classList.contains(errorClass)).toBeFalsy()
498
+ expect(formControl2.classList.contains(errorClass)).toBeFalsy()
499
+ })
500
+ })
501
+
502
+ it('adds hidden classes to form-error-main', async () => {
503
+ formControl.required = true
504
+ formControl.value = ''
505
+ valid = await validator.validate()
506
+ ;(validator as any).showFormErrors()
507
+
508
+ const mainError = form.querySelector('#form-error-main')
509
+ expect(mainError).toBeTruthy()
510
+ ;(validator as any).clearFormErrors()
511
+
512
+ const mainErrorClassList = mainError?.classList
513
+ expect(mainErrorClassList?.contains('hidden')).toBeTruthy()
514
+ expect(mainErrorClassList?.contains('opacity-0')).toBeTruthy()
515
+ })
516
+ }) // end clearFormErrors
517
+
518
+ describe('validateRequired', () => {
519
+ let radio1: HTMLInputElement
520
+ let radio2: HTMLInputElement
521
+ let radioError: HTMLDivElement
522
+
523
+ beforeEach(() => {
524
+ // Create a group of radio buttons
525
+ radio1 = document.createElement('input')
526
+ radio1.id = 'radio1'
527
+ radio1.type = 'radio'
528
+ radio1.name = 'test-radio'
529
+ form.appendChild(radio1)
530
+
531
+ radio2 = document.createElement('input')
532
+ radio2.id = 'radio2'
533
+ radio2.type = 'radio'
534
+ radio2.name = 'test-radio'
535
+ form.appendChild(radio2)
536
+
537
+ radioError = document.createElement('div')
538
+ radioError.id = 'test-radio-error'
539
+ radioError.classList.add('hidden')
540
+ form.appendChild(radioError)
541
+
542
+ validator.init()
543
+ })
544
+
545
+ it('returns true and shows no error if the input is not required', () => {
546
+ const result = (validator as any).validateRequired(formControl)
547
+ expect(result).toBeTruthy()
548
+ expect(errorEl.classList.contains('hidden')).toBeTruthy()
549
+ expect(errorEl.textContent).toBe('')
550
+ })
551
+
552
+ it('returns false and shows an error if the input is required and empty', () => {
553
+ formControl.required = true
554
+
555
+ const result = (validator as any).validateRequired(formControl)
556
+ expect(result).toBeFalsy()
557
+
558
+ // We show the required error message if one isn't provided
559
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_REQUIRED)
560
+ })
561
+
562
+ it('returns false and shows the specified error if the input is required and empty', () => {
563
+ formControl.required = true
564
+ let errorMessage = 'This is a custom error message'
565
+ formControl.setAttribute('data-error-default', errorMessage)
566
+
567
+ const result = (validator as any).validateRequired(formControl)
568
+ expect(result).toBeFalsy()
569
+
570
+ // We show the generic error message if one isn't provided
571
+ expect(validator.inputErrors[formControl.name]).toContain(errorMessage)
572
+ })
573
+
574
+ it('returns false and shows an error if the input is a single checkbox and not checked', () => {
575
+ formControl.type = 'checkbox'
576
+ formControl.required = true
577
+
578
+ const result = (validator as any).validateRequired(formControl)
579
+ expect(result).toBeFalsy()
580
+
581
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.CHECKED_REQUIRED)
582
+ })
583
+
584
+ it('returns false and shows an error if the input is a radio button and not checked', () => {
585
+ formControl = document.createElement('input')
586
+ formControl.type = 'radio'
587
+ formControl.id = 'test-input'
588
+ formControl.name = 'test-input'
589
+ formControl.required = true
590
+ form.appendChild(formControl)
591
+
592
+ const result = (validator as any).validateRequired(formControl)
593
+ expect(result).toBeFalsy()
594
+
595
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.OPTION_REQUIRED)
596
+ })
597
+
598
+ it('returns false and shows an error if the input is a radio group and one input not checked', () => {
599
+ radio1.required = true
600
+ radio2.required = true
601
+ const result = (validator as any).validateRequired(radio1)
602
+ expect(result).toBeFalsy()
603
+
604
+ expect(validator.inputErrors[radio1.name]).toContain(validator.messages.OPTION_REQUIRED)
605
+ })
606
+
607
+ it('returns true and shows no error for any input in group if input is radio group and an input is checked', () => {
608
+ radio1.required = true
609
+ radio1.checked = true
610
+ radio2.required = true
611
+
612
+ // Even though radio 1 is checked, validator will determine that both are valid
613
+ const result = (validator as any).validateRequired(radio1)
614
+ expect(result).toBeTruthy()
615
+
616
+ const radioError = document.querySelector(`#${radio1.name}-error`)
617
+ if (radioError) expect(radioError.classList.contains('hidden')).toBeTruthy()
618
+ if (radioError) expect(radioError.textContent).toBe('')
619
+
620
+ const result2 = (validator as any).validateRequired(radio2)
621
+ expect(result2).toBeTruthy()
622
+ if (radioError) expect(radioError.classList.contains('hidden')).toBeTruthy()
623
+ if (radioError) expect(radioError.textContent).toBe('')
624
+ })
625
+ }) // end validateRequired
626
+
627
+ describe('validate Min/Max Length', () => {
628
+ it('returns true and shows no error if the input is empty', () => {
629
+ const result = (validator as any).validateLength(formControl)
630
+ expect(result).toBeTruthy()
631
+ expect(errorEl.classList.contains('hidden')).toBeTruthy()
632
+ expect(errorEl.textContent).toBe('')
633
+
634
+ const result2 = (validator as any).validateLength(formControl)
635
+ expect(result2).toBeTruthy()
636
+ expect(validator.inputErrors[formControl.name]).toEqual([])
637
+ })
638
+
639
+ it('returns true and shows no error if the input is not empty and is within the min/max length', () => {
640
+ formControl.value = 'te'
641
+ formControl.setAttribute('data-min-length', '2')
642
+ formControl.setAttribute('data-max-length', '4')
643
+
644
+ const result = (validator as any).validateLength(formControl)
645
+ expect(result).toBeTruthy()
646
+ expect(errorEl.classList.contains('hidden')).toBeTruthy()
647
+ expect(errorEl.textContent).toBe('')
648
+
649
+ formControl.value = 'test'
650
+ const result2 = (validator as any).validateLength(formControl)
651
+ expect(result2).toBeTruthy()
652
+ expect(validator.inputErrors[formControl.name]).toEqual([])
653
+ })
654
+
655
+ it('returns false and shows an error if the input is not empty and is less than the min length', () => {
656
+ formControl.value = 'te'
657
+ formControl.setAttribute('data-min-length', '3')
658
+
659
+ const result = (validator as any).validateLength(formControl)
660
+ expect(result).toBeFalsy()
661
+ expect(validator.inputErrors[formControl.name]).toContain(
662
+ validator.messages.ERROR_MINLENGTH.replace('${val}', '3')
663
+ )
664
+ })
665
+
666
+ it('with minlength attribute returns false and shows an error if the input is not empty and is less than the min length', () => {
667
+ formControl.value = 'te'
668
+ formControl.minLength = 3
669
+
670
+ const result = (validator as any).validateLength(formControl)
671
+ expect(result).toBeFalsy()
672
+ expect(validator.inputErrors[formControl.name]).toContain(
673
+ validator.messages.ERROR_MINLENGTH.replace('${val}', '3')
674
+ )
675
+ })
676
+
677
+ it('returns false and shows an error if the input is not empty and is greater than the max length', () => {
678
+ formControl.value = 'test'
679
+ formControl.setAttribute('data-max-length', '3')
680
+
681
+ const result = (validator as any).validateLength(formControl)
682
+ expect(result).toBeFalsy()
683
+ expect(validator.inputErrors[formControl.name]).toContain(
684
+ validator.messages.ERROR_MAXLENGTH.replace('${val}', '3')
685
+ )
686
+ })
687
+
688
+ it('with minlength attribute returns false and shows an error if the input is not empty and is less than the min length', () => {
689
+ formControl.value = 'test'
690
+ formControl.maxLength = 3
691
+
692
+ const result = (validator as any).validateLength(formControl)
693
+ expect(result).toBeFalsy()
694
+ expect(validator.inputErrors[formControl.name]).toContain(
695
+ validator.messages.ERROR_MAXLENGTH.replace('${val}', '3')
696
+ )
697
+ })
698
+ }) // end validate Min/Max Length
699
+
700
+ // This version only tests the base functionality of the validateInputType method
701
+ // It doesn't test the parsing of dates, times, or colors, those will be tested in their own tests
702
+ describe('validateInputType base', () => {
703
+ it('calls the correct parse and valid methods for number', () => {
704
+ formControl.type = 'text'
705
+ formControl.dataset.type = 'number'
706
+ formControl.value = '10'
707
+
708
+ const parseSpy = vi.spyOn((validator as any).inputHandlers.number, 'parse')
709
+ const isValidSpy = vi.spyOn((validator as any).inputHandlers.number, 'isValid')
710
+
711
+ valid = (validator as any).validateInputType(formControl)
712
+
713
+ expect(parseSpy).toHaveBeenCalledWith(formControl.value, formControl.dataset.dateFormat)
714
+ expect(isValidSpy).toHaveBeenCalledWith(formControl.value)
715
+
716
+ expect(valid).toBeTruthy()
717
+ })
718
+
719
+ it('calls the correct parse and valid methods for integer', () => {
720
+ formControl.type = 'text'
721
+ formControl.dataset.type = 'integer'
722
+ formControl.value = '10'
723
+
724
+ const parseSpy = vi.spyOn((validator as any).inputHandlers.integer, 'parse')
725
+ const isValidSpy = vi.spyOn((validator as any).inputHandlers.integer, 'isValid')
726
+
727
+ valid = (validator as any).validateInputType(formControl)
728
+
729
+ expect(parseSpy).toHaveBeenCalledWith(formControl.value, formControl.dataset.dateFormat)
730
+ expect(isValidSpy).toHaveBeenCalledWith(formControl.value)
731
+
732
+ expect(valid).toBeTruthy()
733
+ })
734
+
735
+ it('calls the correct parse and valid methods for tel', () => {
736
+ formControl.type = 'text'
737
+ formControl.dataset.type = 'tel'
738
+ formControl.value = '780-700-0000'
739
+
740
+ const parseSpy = vi.spyOn((validator as any).inputHandlers.tel, 'parse')
741
+ const isValidSpy = vi.spyOn((validator as any).inputHandlers.tel, 'isValid')
742
+
743
+ valid = (validator as any).validateInputType(formControl)
744
+
745
+ expect(parseSpy).toHaveBeenCalledWith(formControl.value, formControl.dataset.dateFormat)
746
+ expect(isValidSpy).toHaveBeenCalledWith(formControl.value)
747
+
748
+ expect(valid).toBeTruthy()
749
+ })
750
+
751
+ it('calls the correct parse and valid methods for email', () => {
752
+ formControl.type = 'text'
753
+ formControl.dataset.type = 'email'
754
+ formControl.value = 'email@example.com'
755
+
756
+ const parseSpy = vi.spyOn((validator as any).inputHandlers.email, 'parse')
757
+ const isValidSpy = vi.spyOn((validator as any).inputHandlers.email, 'isValid')
758
+
759
+ valid = (validator as any).validateInputType(formControl)
760
+
761
+ expect(parseSpy).toHaveBeenCalledWith(formControl.value, formControl.dataset.dateFormat)
762
+ expect(isValidSpy).toHaveBeenCalledWith(formControl.value)
763
+
764
+ expect(valid).toBeTruthy()
765
+ })
766
+
767
+ it('calls the correct parse and valid methods for zip', () => {
768
+ formControl.type = 'text'
769
+ formControl.dataset.type = 'zip'
770
+ formControl.value = '90210'
771
+
772
+ const parseSpy = vi.spyOn((validator as any).inputHandlers.zip, 'parse')
773
+ const isValidSpy = vi.spyOn((validator as any).inputHandlers.zip, 'isValid')
774
+
775
+ valid = (validator as any).validateInputType(formControl)
776
+
777
+ expect(parseSpy).toHaveBeenCalledWith(formControl.value, formControl.dataset.dateFormat)
778
+ expect(isValidSpy).toHaveBeenCalledWith(formControl.value)
779
+ })
780
+
781
+ it('calls the correct parse and valid methods for postal', () => {
782
+ formControl.type = 'text'
783
+ formControl.dataset.type = 'postal'
784
+ formControl.value = 'T5A 0A1'
785
+
786
+ const parseSpy = vi.spyOn((validator as any).inputHandlers.postal, 'parse')
787
+ const isValidSpy = vi.spyOn((validator as any).inputHandlers.postal, 'isValid')
788
+
789
+ valid = (validator as any).validateInputType(formControl)
790
+
791
+ expect(parseSpy).toHaveBeenCalledWith(formControl.value, formControl.dataset.dateFormat)
792
+ expect(isValidSpy).toHaveBeenCalledWith(formControl.value)
793
+
794
+ expect(valid).toBeTruthy()
795
+ })
796
+
797
+ it('calls the correct parse and valid methods for url', () => {
798
+ formControl.type = 'text'
799
+ formControl.dataset.type = 'url'
800
+ formControl.value = 'https://example.com'
801
+
802
+ const parseSpy = vi.spyOn((validator as any).inputHandlers.url, 'parse')
803
+ const isValidSpy = vi.spyOn((validator as any).inputHandlers.url, 'isValid')
804
+
805
+ valid = (validator as any).validateInputType(formControl)
806
+
807
+ expect(parseSpy).toHaveBeenCalledWith(formControl.value, formControl.dataset.dateFormat)
808
+ expect(isValidSpy).toHaveBeenCalledWith(formControl.value)
809
+
810
+ expect(valid).toBeTruthy()
811
+ })
812
+
813
+ it('calls the correct parse and valid methods for date with default format', () => {
814
+ formControl.type = 'text'
815
+ formControl.dataset.type = 'date'
816
+ formControl.value = '2020-Jan-01'
817
+
818
+ const parseSpy = vi.spyOn((validator as any).inputHandlers.date, 'parse')
819
+ const isValidSpy = vi.spyOn((validator as any).inputHandlers.date, 'isValid')
820
+
821
+ valid = (validator as any).validateInputType(formControl)
822
+
823
+ expect(parseSpy).toHaveBeenCalledWith(formControl.value, formControl.dataset.dateFormat)
824
+ expect(isValidSpy).toHaveBeenCalledWith(formControl.value)
825
+
826
+ expect(valid).toBeTruthy()
827
+ })
828
+
829
+ it('calls the correct parse and valid methods for date with different date format', () => {
830
+ formControl.type = 'text'
831
+ formControl.dataset.type = 'date'
832
+ formControl.value = '2020-01-01'
833
+ formControl.dataset.dateFormat = 'YYYY-MM-DD'
834
+
835
+ const parseSpy = vi.spyOn((validator as any).inputHandlers.date, 'parse')
836
+ const isValidSpy = vi.spyOn((validator as any).inputHandlers.date, 'isValid')
837
+
838
+ valid = (validator as any).validateInputType(formControl)
839
+
840
+ expect(parseSpy).toHaveBeenCalledWith(formControl.value, formControl.dataset.dateFormat)
841
+ expect(isValidSpy).toHaveBeenCalledWith(formControl.value)
842
+
843
+ expect(valid).toBeTruthy()
844
+ })
845
+
846
+ it('calls the correct parse and valid methods for time', () => {
847
+ formControl.type = 'text'
848
+ formControl.dataset.type = 'time'
849
+ formControl.value = '20:01'
850
+ formControl.dataset.timeFormat = 'HH:mm'
851
+
852
+ const parseSpy = vi.spyOn((validator as any).inputHandlers.time, 'parse')
853
+ const isValidSpy = vi.spyOn((validator as any).inputHandlers.time, 'isValid')
854
+
855
+ valid = (validator as any).validateInputType(formControl)
856
+
857
+ expect(parseSpy).toHaveBeenCalledWith(formControl.value, formControl.dataset.timeFormat)
858
+ expect(isValidSpy).toHaveBeenCalledWith(formControl.value)
859
+
860
+ expect(valid).toBeTruthy()
861
+ })
862
+
863
+ it('returns true if there is no matching inputHandler', () => {
864
+ formControl.type = 'text'
865
+ formControl.value = 'test'
866
+
867
+ valid = (validator as any).validateInputType(formControl)
868
+
869
+ expect(valid).toBeTruthy()
870
+ })
871
+
872
+ it('returns false if the inputHandler returns false', () => {
873
+ formControl.type = 'text'
874
+ formControl.dataset.type = 'number'
875
+ formControl.value = 'test'
876
+
877
+ const isValidSpy = vi.spyOn((validator as any).inputHandlers.number, 'isValid')
878
+
879
+ isValidSpy.mockImplementation(() => false)
880
+
881
+ valid = (validator as any).validateInputType(formControl)
882
+
883
+ expect(isValidSpy).toHaveBeenCalledWith(formControl.value)
884
+ expect(valid).toBeFalsy()
885
+ })
886
+
887
+ it('does not update the value if the input is a native date input', () => {
888
+ formControl.type = 'date'
889
+ formControl.value = '2020-01-01'
890
+ formControl.dataset.dateFormat = 'YYYY-MMM-DD'
891
+
892
+ // Note that the value doesn't match the date format, so normally parse would update the value
893
+
894
+ valid = (validator as any).validateInputType(formControl)
895
+
896
+ expect(formControl.value).toBe('2020-01-01')
897
+
898
+ expect(valid).toBeTruthy()
899
+ })
900
+
901
+ it('does update the value if the input is a data-date input', () => {
902
+ formControl.type = 'text'
903
+ formControl.dataset.type = 'date'
904
+ formControl.value = '2020-01-01'
905
+ formControl.dataset.dateFormat = 'YYYY-MMM-DD'
906
+
907
+ // Note that the value doesn't match the date format, so normally parse would update the value
908
+
909
+ valid = (validator as any).validateInputType(formControl)
910
+
911
+ expect(formControl.value).toBe('2020-Jan-01')
912
+
913
+ expect(valid).toBeTruthy()
914
+ })
915
+ }) // end validateInputType base
916
+
917
+ // This version tests the functionality of each input handler type with some examples.
918
+ describe('validateInputType', () => {
919
+ let formControlColor: HTMLInputElement
920
+
921
+ // I'll not support unicode addresses for now.
922
+ const emailAddresses = ['email@example.com', 'email+tag@example.com']
923
+
924
+ const invalidEmailAddresses = ['john.doe@', 'john.doe@.com']
925
+
926
+ const times = [
927
+ ['12:00', '12:00 PM'],
928
+ ['12:00:00', '12:00 PM'],
929
+ ['1p', '1:00 PM'],
930
+ ['1 pm', '1:00 PM'],
931
+ ['132', '1:32 AM'],
932
+ ['132pm', '1:32 PM'],
933
+ ]
934
+
935
+ const invalidTimes = ['asdf']
936
+
937
+ const colors = [
938
+ { name: 'red', value: '#ff0000', rgb: 'rgb(255, 0, 0)', hsl: 'hsl(0, 100%, 50%)' },
939
+ { name: 'green', value: '#008000', rgb: 'rgb(0, 128, 0)', hsl: 'hsl(120, 100%, 25%)' },
940
+ { name: 'blue', value: '#0000ff', rgb: 'rgb(0, 0, 255)', hsl: 'hsl(240, 100%, 50%)' },
941
+ { name: 'yellow', value: '#ffff00', rgb: 'rgb(255, 255, 0)', hsl: 'hsl(60, 100%, 50%)' },
942
+ { name: 'cyan', value: '#00ffff', rgb: 'rgb(0, 255, 255)', hsl: 'hsl(180, 100%, 50%)' },
943
+ { name: 'magenta', value: '#ff00ff', rgb: 'rgb(255, 0, 255)', hsl: 'hsl(300, 100%, 50%)' },
944
+ { name: 'black', value: '#000000', rgb: 'rgba(0, 0, 0)', hsl: 'hsl(0 0% 0%)' },
945
+ { name: 'white', value: '#ffffff', rgb: 'rgb(255, 255, 255)', hsl: 'hsl(0 0% 100%)' },
946
+ ]
947
+
948
+ const invalidColors = ['asdf', '#ff000', '#ff00000', '#ff0000f', 'rgb()', 'rgb(0, 0, )']
949
+
950
+ beforeEach(() => {
951
+ formControlColor = document.createElement('input')
952
+ formControlColor.type = 'color'
953
+ formControlColor.id = 'test-input-color'
954
+ formControlColor.name = 'test-input-color'
955
+ form.appendChild(formControlColor)
956
+ validator.init()
957
+ })
958
+
959
+ it('returns true if the input type was not matched to anything', () => {
960
+ formControl.type = 'nothing'
961
+ formControl.dataset.type = 'nothing'
962
+ valid = (validator as any).validateInputType(formControl)
963
+ expect(valid).toBeTruthy()
964
+ expect(validator.inputErrors[formControl.name]).toEqual([])
965
+ })
966
+
967
+ it('should not replace the value for native date type', () => {
968
+ formControl.type = 'date'
969
+ formControl.value = '2019-01-01'
970
+ valid = (validator as any).validateInputType(formControl)
971
+ expect(valid).toBeTruthy()
972
+ expect(formControl.value).toBe('2019-01-01')
973
+ expect(validator.inputErrors[formControl.name]).toEqual([])
974
+ })
975
+
976
+ it('should replace the value for data-type=date', () => {
977
+ formControl.type = 'text'
978
+ formControl.dataset.type = 'date'
979
+ formControl.value = '2032-01-01'
980
+ valid = (validator as any).validateInputType(formControl)
981
+ expect(valid).toBeTruthy()
982
+ expect(formControl.value).toBe('2032-Jan-01')
983
+ expect(validator.inputErrors[formControl.name]).toEqual([])
984
+
985
+ // test with a different date format
986
+ formControl.dataset.dateFormat = 'MMMM D, YY'
987
+ valid = (validator as any).validateInputType(formControl)
988
+ expect(valid).toBeTruthy()
989
+ expect(formControl.value).toBe('January 1, 32')
990
+ expect(validator.inputErrors[formControl.name]).toEqual([])
991
+ })
992
+
993
+ it('should parse and validate number type correctly', () => {
994
+ formControl.setAttribute('data-type', 'number')
995
+ formControl.value = '12.5'
996
+
997
+ valid = (validator as any).validateInputType(formControl)
998
+
999
+ expect(valid).toBeTruthy()
1000
+ expect(formControl.value).toBe('12.5')
1001
+ expect(validator.inputErrors[formControl.name]).toEqual([])
1002
+
1003
+ formControl.value = '-1asdf5'
1004
+
1005
+ valid = (validator as any).validateInputType(formControl)
1006
+
1007
+ expect(valid).toBeTruthy()
1008
+ expect(formControl.value).toBe('-15')
1009
+ expect(validator.inputErrors[formControl.name]).toEqual([])
1010
+ })
1011
+
1012
+ it('should parse and validate integer type correctly', () => {
1013
+ formControl.type = 'text'
1014
+ formControl.setAttribute('data-type', 'integer')
1015
+ formControl.value = '1230098'
1016
+
1017
+ valid = (validator as any).validateInputType(formControl)
1018
+
1019
+ expect(valid).toBeTruthy()
1020
+ expect(formControl.value).toBe('1230098')
1021
+ expect(validator.inputErrors[formControl.name]).toEqual([])
1022
+
1023
+ formControl.value = '123.098'
1024
+ valid = (validator as any).validateInputType(formControl)
1025
+ // Remove non-numeric characters
1026
+ expect(formControl.value).toBe('123098')
1027
+ expect(valid).toBeTruthy()
1028
+
1029
+ // If the value contains no number it will not be parsed
1030
+ formControl.value = 'asdf'
1031
+ valid = (validator as any).validateInputType(formControl)
1032
+ // Remove non-numeric characters
1033
+ expect(formControl.value).toBe('asdf')
1034
+ expect(valid).toBeFalsy()
1035
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_INTEGER)
1036
+ })
1037
+
1038
+ it('should parse and validate tel type correctly', () => {
1039
+ formControl.type = 'tel'
1040
+ formControl.value = '923-456-7890'
1041
+
1042
+ valid = (validator as any).validateInputType(formControl)
1043
+ expect(valid).toBeTruthy()
1044
+ expect(formControl.value).toBe('923-456-7890')
1045
+ expect(validator.inputErrors[formControl.name]).toEqual([])
1046
+
1047
+ // Leading 1 should be removed
1048
+ formControl.value = '12345678900'
1049
+
1050
+ valid = (validator as any).validateInputType(formControl)
1051
+ expect(valid).toBeTruthy()
1052
+ expect(formControl.value).toBe('234-567-8900')
1053
+ expect(validator.inputErrors[formControl.name]).toEqual([])
1054
+
1055
+ // If a number isn't a phone number, it won't be parsed (other than removing a leading 1)
1056
+ formControl.value = '123456789'
1057
+
1058
+ valid = (validator as any).validateInputType(formControl)
1059
+ expect(valid).toBeFalsy()
1060
+ expect(formControl.value).toBe('23456789')
1061
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_TEL)
1062
+ })
1063
+
1064
+ it('should parse and validate email type correctly', () => {
1065
+ formControl.type = 'email'
1066
+ formControl.dataset.type = 'email'
1067
+ formControl.value = 'email@example.com'
1068
+
1069
+ valid = (validator as any).validateInputType(formControl)
1070
+
1071
+ expect(valid).toBeTruthy()
1072
+ expect(formControl.value).toBe('email@example.com')
1073
+
1074
+ formControl.value = 'email@example'
1075
+ valid = (validator as any).validateInputType(formControl)
1076
+ expect(valid).toBeFalsy()
1077
+ expect(formControl.value).toBe('email@example')
1078
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_EMAIL)
1079
+ })
1080
+
1081
+ // Test a bunch of valid but odd-looking email addresses
1082
+ emailAddresses.forEach((email) => {
1083
+ it(`should parse and validate the email "${email}" correctly`, () => {
1084
+ formControl.type = 'email'
1085
+ formControl.dataset.type = 'email'
1086
+ formControl.value = email
1087
+
1088
+ valid = (validator as any).validateInputType(formControl)
1089
+
1090
+ expect(valid).toBeTruthy()
1091
+ expect(formControl.value).toBe(email)
1092
+ })
1093
+ })
1094
+
1095
+ invalidEmailAddresses.forEach((email) => {
1096
+ it(`should fail the string "${email}" as an invalid email address`, () => {
1097
+ formControl.type = 'email'
1098
+ formControl.dataset.type = 'email'
1099
+ formControl.value = email
1100
+
1101
+ valid = (validator as any).validateInputType(formControl)
1102
+
1103
+ expect(valid).toBeFalsy()
1104
+ expect(formControl.value).toBe(email)
1105
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_EMAIL)
1106
+ })
1107
+ })
1108
+
1109
+ it('should parse and validate postal type correctly', () => {
1110
+ formControl.type = 'text'
1111
+ formControl.dataset.type = 'postal'
1112
+ formControl.value = 'T5Y3J5'
1113
+
1114
+ valid = (validator as any).validateInputType(formControl)
1115
+
1116
+ expect(valid).toBeTruthy()
1117
+ expect(formControl.value).toBe('T5Y 3J5')
1118
+
1119
+ formControl.value = '1234'
1120
+ valid = (validator as any).validateInputType(formControl)
1121
+ expect(valid).toBeFalsy()
1122
+ expect(formControl.value).toBe('123 4')
1123
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_POSTAL)
1124
+ })
1125
+
1126
+ it('should parse and validate url type correctly, adding https:// if no protocol specified', () => {
1127
+ formControl.type = 'url'
1128
+ formControl.dataset.type = 'url'
1129
+ formControl.value = 'www.example.com/'
1130
+
1131
+ valid = (validator as any).validateInputType(formControl)
1132
+
1133
+ expect(valid).toBeTruthy()
1134
+ expect(formControl.value).toBe('https://www.example.com/')
1135
+
1136
+ formControl.value = 'http://123example.com'
1137
+ valid = (validator as any).validateInputType(formControl)
1138
+ expect(valid).toBeTruthy()
1139
+ expect(formControl.value).toBe('http://123example.com')
1140
+ })
1141
+
1142
+ it('should parse and validate dates', () => {
1143
+ formControl.type = 'text'
1144
+ formControl.dataset.type = 'date'
1145
+ formControl.value = '2019-01-01'
1146
+
1147
+ valid = (validator as any).validateInputType(formControl)
1148
+
1149
+ expect(valid).toBeTruthy()
1150
+ expect(formControl.value).toBe('2019-Jan-01')
1151
+
1152
+ formControl.value = '2019-01-32'
1153
+ valid = (validator as any).validateInputType(formControl)
1154
+ expect(valid).toBeFalsy()
1155
+ expect(formControl.value).toBe('2019-01-32')
1156
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_DATE)
1157
+ })
1158
+
1159
+ it('should parse and validate dates with a custom format', () => {
1160
+ formControl.type = 'text'
1161
+ formControl.dataset.type = 'date'
1162
+ formControl.dataset.dateFormat = 'YYYY/MM/DD'
1163
+ formControl.value = '2019-Jan/13'
1164
+
1165
+ valid = (validator as any).validateInputType(formControl)
1166
+
1167
+ expect(valid).toBeTruthy()
1168
+ expect(formControl.value).toBe('2019/01/13')
1169
+
1170
+ formControl.value = '2019/Jan/32'
1171
+ valid = (validator as any).validateInputType(formControl)
1172
+ expect(valid).toBeFalsy()
1173
+ expect(formControl.value).toBe('2019/Jan/32')
1174
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_DATE)
1175
+ })
1176
+
1177
+ it('should parse and validate times', () => {
1178
+ formControl.type = 'text'
1179
+ formControl.dataset.type = 'time'
1180
+ formControl.value = '13:00'
1181
+
1182
+ valid = (validator as any).validateInputType(formControl)
1183
+
1184
+ expect(valid).toBeTruthy()
1185
+ expect(formControl.value).toBe('1:00 PM')
1186
+
1187
+ formControl.value = 'now'
1188
+ valid = (validator as any).validateInputType(formControl)
1189
+ expect(valid).toBeTruthy()
1190
+ // Get the current time in the format I want to test against
1191
+ let now = new Date()
1192
+ let hours = now.getHours()
1193
+ let minutes = now.getMinutes()
1194
+ let ampm = hours >= 12 ? 'PM' : 'AM'
1195
+ hours = hours % 12
1196
+ hours = hours ? hours : 12 // the hour '0' should be '12'
1197
+ let strTime = hours + ':' + (minutes < 10 ? '0' + minutes : minutes) + ' ' + ampm
1198
+
1199
+ expect(formControl.value).toBe(strTime)
1200
+
1201
+ formControl.value = '25:00'
1202
+ valid = (validator as any).validateInputType(formControl)
1203
+ expect(valid).toBeFalsy()
1204
+ expect(formControl.value).toBe('25:00')
1205
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_TIME)
1206
+ })
1207
+
1208
+ // Test a bunch of valid times
1209
+ times.forEach((time) => {
1210
+ it(`should parse and validate the time "${time[0]}" correctly`, () => {
1211
+ formControl.type = 'text'
1212
+ formControl.dataset.type = 'time'
1213
+ formControl.value = time[0]
1214
+
1215
+ valid = (validator as any).validateInputType(formControl)
1216
+
1217
+ expect(valid).toBeTruthy()
1218
+ // I need a new array of the times in the format I want to test against
1219
+ expect(formControl.value).toBe(time[1])
1220
+ })
1221
+ })
1222
+
1223
+ invalidTimes.forEach((time) => {
1224
+ it(`should fail the string "${time}" as an invalid time`, () => {
1225
+ formControl.type = 'text'
1226
+ formControl.dataset.type = 'time'
1227
+ formControl.value = time
1228
+
1229
+ valid = (validator as any).validateInputType(formControl)
1230
+
1231
+ expect(valid).toBeFalsy()
1232
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_TIME)
1233
+ })
1234
+ })
1235
+
1236
+ it('should parse and validate colors', () => {
1237
+ formControl.type = 'text'
1238
+ formControl.dataset.type = 'color'
1239
+ formControl.value = '#123456'
1240
+
1241
+ valid = (validator as any).validateInputType(formControl)
1242
+
1243
+ expect(valid).toBeTruthy()
1244
+ expect(formControl.value).toBe('#123456')
1245
+
1246
+ formControl.value = '#1234567'
1247
+ valid = (validator as any).validateInputType(formControl)
1248
+ expect(valid).toBeFalsy()
1249
+ expect(formControl.value).toBe('#1234567')
1250
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_COLOR)
1251
+
1252
+ formControl.value = 'transparent'
1253
+ valid = (validator as any).validateInputType(formControl)
1254
+ expect(valid).toBeTruthy()
1255
+ })
1256
+
1257
+ // Test a bunch of valid but odd-looking colors
1258
+ colors.forEach((color) => {
1259
+ it(`should parse and validate the color "${color.name}" correctly`, () => {
1260
+ formControl.type = 'text'
1261
+ formControl.dataset.type = 'color'
1262
+
1263
+ // We ignore black because we use a transparent color that isn't technically 'black'
1264
+ formControl.value = color.name
1265
+ valid = (validator as any).validateInputType(formControl)
1266
+ expect(valid).toBeTruthy()
1267
+ expect(formControl.value).toBe(color.name)
1268
+
1269
+ // Trigger an input event and check the value of the associated color input
1270
+ formControl.dispatchEvent(new Event('input', { bubbles: true }))
1271
+ expect(formControlColor.value).toBe(color.value)
1272
+
1273
+ formControl.value = color.rgb
1274
+ valid = (validator as any).validateInputType(formControl)
1275
+ expect(valid).toBeTruthy()
1276
+ expect(formControl.value).toBe(color.rgb)
1277
+
1278
+ formControl.dispatchEvent(new Event('input', { bubbles: true }))
1279
+ expect(formControlColor.value).toBe(color.value)
1280
+
1281
+ formControl.value = color.hsl
1282
+ valid = (validator as any).validateInputType(formControl)
1283
+ expect(valid).toBeTruthy()
1284
+ expect(formControl.value).toBe(color.hsl)
1285
+
1286
+ formControl.dispatchEvent(new Event('input', { bubbles: true }))
1287
+ expect(formControlColor.value).toBe(color.value)
1288
+
1289
+ formControl.value = color.value
1290
+ valid = (validator as any).validateInputType(formControl)
1291
+ expect(valid).toBeTruthy()
1292
+ expect(formControl.value).toBe(color.value)
1293
+
1294
+ formControl.dispatchEvent(new Event('input', { bubbles: true }))
1295
+ expect(formControlColor.value).toBe(color.value)
1296
+ })
1297
+ })
1298
+
1299
+ invalidColors.forEach((color) => {
1300
+ it(`should fail the string "${color}" as an invalid color`, () => {
1301
+ formControl.type = 'text'
1302
+ formControl.dataset.type = 'color'
1303
+ formControl.value = color
1304
+
1305
+ valid = (validator as any).validateInputType(formControl)
1306
+
1307
+ expect(valid).toBeFalsy()
1308
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_COLOR)
1309
+ })
1310
+ })
1311
+ }) // end validateInputType
1312
+
1313
+ describe('validateDateRange', () => {
1314
+ beforeEach(() => {
1315
+ formControl.type = 'text'
1316
+ formControl.name = 'test-date'
1317
+ formControl.id = 'test-date'
1318
+ formControl.dataset.type = 'date'
1319
+ })
1320
+
1321
+ it('should return true if no date range specified', () => {
1322
+ formControl.value = '2022-01-01'
1323
+ expect((validator as any).validateDateRange(formControl)).toBe(true)
1324
+ })
1325
+
1326
+ it('should add an error message and return false if the date is not in the past', () => {
1327
+ formControl.dataset.dateRange = 'past'
1328
+ formControl.value = '2093-01-01'
1329
+ expect((validator as any).validateDateRange(formControl)).toBe(false)
1330
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_DATE_PAST)
1331
+ })
1332
+
1333
+ it('should add an error message and return false if the date is not in the future', () => {
1334
+ formControl.dataset.dateRange = 'future'
1335
+ formControl.value = '2003-01-01'
1336
+ expect((validator as any).validateDateRange(formControl)).toBe(false)
1337
+ expect(validator.inputErrors[formControl.name]).toContain(
1338
+ validator.messages.ERROR_DATE_FUTURE
1339
+ )
1340
+ })
1341
+
1342
+ it('should return true if the date is in the past', () => {
1343
+ formControl.dataset.dateRange = 'past'
1344
+ formControl.value = '2003-01-01'
1345
+ expect((validator as any).validateDateRange(formControl)).toBe(true)
1346
+ })
1347
+
1348
+ it('should return true if the date is in the future', () => {
1349
+ formControl.dataset.dateRange = 'future'
1350
+ formControl.value = '2093-01-01'
1351
+ expect((validator as any).validateDateRange(formControl)).toBe(true)
1352
+ })
1353
+ }) // end validateDateRange
1354
+
1355
+ describe('validatePattern', () => {
1356
+ it('should return true if no pattern specified', () => {
1357
+ formControl.value = 'test'
1358
+ expect((validator as any).validatePattern(formControl)).toBe(true)
1359
+ })
1360
+
1361
+ it('should return true if the pattern matches', () => {
1362
+ formControl.dataset.pattern = '^[a-z]+$'
1363
+ formControl.value = 'test'
1364
+ expect((validator as any).validatePattern(formControl)).toBe(true)
1365
+ })
1366
+
1367
+ it('should add an error message and return false if the pattern does not match', () => {
1368
+ formControl.dataset.pattern = '^[a-z]+$'
1369
+ formControl.value = 'test123'
1370
+ expect((validator as any).validatePattern(formControl)).toBe(false)
1371
+ expect(validator.inputErrors[formControl.name]).toContain(validator.messages.ERROR_GENERIC)
1372
+ })
1373
+ }) // end validatePattern
1374
+
1375
+ // Next we test validateCustom. This will be a bit more involved as we need to test a variety of
1376
+ // different functions including some that return promises and others that do not.
1377
+ // We will also test the case where the function returns a validation result object and the case
1378
+ // where it returns a boolean.
1379
+ describe('validateCustom', () => {
1380
+ let validationFnTrue = vi.fn(() => true)
1381
+ let validationFnFalse = vi.fn(() => false)
1382
+ let validationFn = vi.fn(() => true)
1383
+ window['validationFnTrue'] = validationFnTrue
1384
+ window['validationFnFalse'] = validationFnFalse
1385
+ window['validation'] = validationFn
1386
+
1387
+ it('returns true if no validation is specified', async () => {
1388
+ const result = await (validator as any).validateCustom(formControl)
1389
+ expect(result).toBe(true)
1390
+ })
1391
+
1392
+ it('returns true if validation is true boolean', async () => {
1393
+ formControl.dataset.validation = 'validationFnTrue'
1394
+ const result = await (validator as any).validateCustom(formControl)
1395
+ expect(result).toBe(true)
1396
+ })
1397
+
1398
+ it('returns true if validation function is not found', async () => {
1399
+ formControl.dataset.validation = 'invalid'
1400
+ const result = await (validator as any).validateCustom(formControl)
1401
+ expect(result).toBe(true)
1402
+ })
1403
+
1404
+ it('returns false if validation function returns false', async () => {
1405
+ window['validationFnFalse'] = validationFnFalse
1406
+ formControl.dataset.validation = 'validationFnFalse'
1407
+ const result = await (validator as any).validateCustom(formControl)
1408
+ expect(result).toBe(false)
1409
+ })
1410
+
1411
+ it('returns true if promise resolves to object with valid:true', async () => {
1412
+ function validationPromiseFn() {
1413
+ return new Promise((resolve, reject) => {
1414
+ setTimeout(() => {
1415
+ resolve({ valid: true, messages: ['success'], error: false })
1416
+ }, 100)
1417
+ })
1418
+ }
1419
+
1420
+ window['validationPromiseFn'] = validationPromiseFn
1421
+ formControl.dataset.validation = 'validationPromiseFn'
1422
+ formControl.value = 'test'
1423
+
1424
+ const result = await (validator as any).validateCustom(formControl)
1425
+ expect(result).toBe(true)
1426
+ })
1427
+
1428
+ it('returns false if promise resolves to object with valid:false', async () => {
1429
+ function validationPromiseFn(arg: any) {
1430
+ return new Promise((resolve, reject) => {
1431
+ setTimeout(() => {
1432
+ resolve({ valid: false, messages: ['error message'] })
1433
+ }, 100)
1434
+ })
1435
+ }
1436
+
1437
+ window['validationPromiseFn'] = validationPromiseFn
1438
+ formControl.dataset.validation = 'validationPromiseFn'
1439
+ formControl.value = 'test'
1440
+ const result = await (validator as any).validateCustom(formControl)
1441
+ expect(result).toBe(false)
1442
+ expect(validator.inputErrors[formControl.name]).toContain('error message')
1443
+ })
1444
+
1445
+ it('adds an error message if error is caught', async () => {
1446
+ function validationFnReject(arg: any) {
1447
+ return new Promise((resolve, reject) => {
1448
+ setTimeout(() => {
1449
+ reject(new Error('error message'))
1450
+ })
1451
+ })
1452
+ }
1453
+
1454
+ window['validationFnReject'] = validationFnReject
1455
+ formControl.dataset.validation = 'validationFnReject'
1456
+ formControl.value = 'test'
1457
+
1458
+ const result = await (validator as any).validateCustom(formControl)
1459
+ expect(result).toBe(false)
1460
+ expect(validator.inputErrors[formControl.name]).toContain(
1461
+ validator.messages.ERROR_CUSTOM_VALIDATION
1462
+ )
1463
+ })
1464
+ }) // end validateCustom
1465
+
1466
+ describe('validateInput', () => {
1467
+ it('returns true for empty input elements', async () => {
1468
+ expect(await (validator as any).validateInput(formControl)).toBe(true)
1469
+ })
1470
+
1471
+ it('returns true if there is a length and all validation functions return true', async () => {
1472
+ formControl.value = 'test'
1473
+ const spy1 = vi.spyOn(validator as any, 'validateInputType').mockImplementation(() => true)
1474
+ const spy2 = vi.spyOn(validator as any, 'validateDateRange').mockImplementation(() => true)
1475
+ const spy3 = vi.spyOn(validator as any, 'validatePattern').mockImplementation(() => true)
1476
+ const spy4 = vi
1477
+ .spyOn(validator as any, 'validateCustom')
1478
+ .mockImplementation(() => Promise.resolve(true))
1479
+
1480
+ expect(await (validator as any).validateInput(formControl)).toBe(true)
1481
+ })
1482
+
1483
+ it('returns false if there is a length and any validation function returns false', async () => {
1484
+ formControl.value = 'test'
1485
+ const spy1 = vi.spyOn(validator as any, 'validateInputType').mockImplementation(() => false)
1486
+ const spy2 = vi.spyOn(validator as any, 'validateDateRange').mockImplementation(() => true)
1487
+ const spy3 = vi.spyOn(validator as any, 'validatePattern').mockImplementation(() => true)
1488
+ const spy4 = vi
1489
+ .spyOn(validator as any, 'validateCustom')
1490
+ .mockImplementation(() => Promise.resolve(true))
1491
+
1492
+ expect(await (validator as any).validateInput(formControl)).toBe(false)
1493
+ })
1494
+
1495
+ it('returns false if there is a length and all validation functions return false', async () => {
1496
+ formControl.value = 'test'
1497
+ const spy1 = vi.spyOn(validator as any, 'validateInputType').mockImplementation(() => false)
1498
+ const spy2 = vi.spyOn(validator as any, 'validateDateRange').mockImplementation(() => false)
1499
+ const spy3 = vi.spyOn(validator as any, 'validatePattern').mockImplementation(() => false)
1500
+ const spy4 = vi
1501
+ .spyOn(validator as any, 'validateCustom')
1502
+ .mockImplementation(() => Promise.resolve(false))
1503
+
1504
+ expect(await (validator as any).validateInput(formControl)).toBe(false)
1505
+ })
1506
+ }) // end validateInput
1507
+
1508
+ describe('validate', () => {
1509
+ it('returns false if validateRequired returns false', async () => {
1510
+ vi.spyOn(validator as any, 'validateRequired').mockImplementation(() => false)
1511
+ vi.spyOn(validator as any, 'validateLength').mockImplementation(() => true)
1512
+ vi.spyOn(validator as any, 'validateInput').mockImplementation(() => Promise.resolve(true))
1513
+
1514
+ expect(await validator.validate(new Event(''))).toBe(false)
1515
+ })
1516
+
1517
+ it('returns false if validateLength returns false', async () => {
1518
+ vi.spyOn(validator as any, 'validateRequired').mockImplementation(() => true)
1519
+ vi.spyOn(validator as any, 'validateLength').mockImplementation(() => false)
1520
+ vi.spyOn(validator as any, 'validateInput').mockImplementation(() => Promise.resolve(true))
1521
+
1522
+ expect(await validator.validate(new Event(''))).toBe(false)
1523
+ })
1524
+
1525
+ it('returns false if validateInput returns false', async () => {
1526
+ vi.spyOn(validator as any, 'validateRequired').mockImplementation(() => true)
1527
+ vi.spyOn(validator as any, 'validateLength').mockImplementation(() => true)
1528
+ vi.spyOn(validator as any, 'validateInput').mockImplementation(() => Promise.resolve(false))
1529
+
1530
+ expect(await validator.validate(new Event(''))).toBe(false)
1531
+ })
1532
+
1533
+ it('returns true if all validation functions return true', async () => {
1534
+ vi.spyOn(validator as any, 'validateRequired').mockImplementation(() => true)
1535
+ vi.spyOn(validator as any, 'validateLength').mockImplementation(() => true)
1536
+ vi.spyOn(validator as any, 'validateInput').mockImplementation(() => Promise.resolve(true))
1537
+
1538
+ expect(await validator.validate(new Event(''))).toBe(true)
1539
+ })
1540
+ }) // end validate
1541
+
1542
+ describe('submitHandler', () => {
1543
+ it('prevents form submission if the form is already submitting', () => {
1544
+ ;(validator as any).isSubmitting = true
1545
+ vi.spyOn(form, 'submit').mockImplementation(() => {})
1546
+ ;(validator as any).isSubmitting = false
1547
+ ;(validator as any).submitHandler(new Event('submit'))
1548
+ expect(form.submit).not.toHaveBeenCalled()
1549
+ })
1550
+
1551
+ it('calls clearFormErrors method before validation', () => {
1552
+ vi.spyOn(validator as any, 'clearFormErrors')
1553
+ vi.spyOn(form, 'submit').mockImplementation(() => {})
1554
+ ;(validator as any).submitHandler(new Event('submit'))
1555
+ expect((validator as any).clearFormErrors).toHaveBeenCalled()
1556
+ })
1557
+
1558
+ it('calls showFormErrors method after validation', async () => {
1559
+ vi.spyOn(validator as any, 'showFormErrors')
1560
+ vi.spyOn(validator, 'validate').mockImplementation(() => Promise.resolve(false))
1561
+ await (validator as any).submitHandler(new Event('submit'))
1562
+ expect((validator as any).showFormErrors).toHaveBeenCalled()
1563
+ })
1564
+
1565
+ it('dispatches ValidationSuccessEvent if form is valid', async () => {
1566
+ vi.spyOn(form, 'dispatchEvent')
1567
+ vi.spyOn(form, 'submit').mockImplementation(() => {})
1568
+ vi.spyOn(validator, 'validate').mockImplementation(() => Promise.resolve(true))
1569
+ await (validator as any).submitHandler(new Event('submit'))
1570
+ expect(form.dispatchEvent).toHaveBeenCalledWith(expect.any(ValidationSuccessEvent))
1571
+ })
1572
+
1573
+ it('dispatches ValidationErrorEvent if form is invalid', async () => {
1574
+ vi.spyOn(form, 'dispatchEvent')
1575
+ vi.spyOn(validator, 'validate').mockImplementation(() => Promise.resolve(false))
1576
+ await (validator as any).submitHandler(new Event('submit'))
1577
+ expect(form.dispatchEvent).toHaveBeenCalledWith(expect.any(ValidationErrorEvent))
1578
+ })
1579
+
1580
+ it('calls validationSuccessCallback if form is valid and no default is prevented', async () => {
1581
+ const validationSuccessCallback = vi.fn()
1582
+ vi.spyOn(form, 'submit').mockImplementation(() => {})
1583
+ ;(validator as any).validationSuccessCallback = validationSuccessCallback
1584
+ vi.spyOn(validator, 'validate').mockImplementation(() => Promise.resolve(true))
1585
+ await (validator as any).submitHandler(new Event('submit'))
1586
+ expect(validationSuccessCallback).toHaveBeenCalled()
1587
+ })
1588
+
1589
+ it('calls validationErrorCallback if form is invalid and no default is prevented', async () => {
1590
+ const validationErrorCallback = vi.fn()
1591
+ ;(validator as any).validationErrorCallback = validationErrorCallback
1592
+ vi.spyOn(validator, 'validate').mockImplementation(() => Promise.resolve(false))
1593
+ await (validator as any).submitHandler(new Event('submit'))
1594
+ expect(validationErrorCallback).toHaveBeenCalled()
1595
+ })
1596
+
1597
+ // If this.preventSubmit is true, the form will not be submitted
1598
+ it('does not submit the form if preventSubmit is true even when validation is successful', () => {
1599
+ validator.preventSubmit = true
1600
+ vi.spyOn(validator, 'validate').mockImplementation(() => Promise.resolve(true))
1601
+ vi.spyOn(form, 'submit').mockImplementation(() => {})
1602
+ ;(validator as any).submitHandler(new Event('submit'))
1603
+ expect(form.submit).not.toHaveBeenCalled()
1604
+ })
1605
+ }) // end submitHandler
1606
+
1607
+ describe('inputChangeHandler', () => {
1608
+ it('validates the input element when it changes', async () => {
1609
+ vi.spyOn(validator as any, 'validateInput').mockImplementation(() => Promise.resolve())
1610
+ const event = new Event('change', { bubbles: true })
1611
+ Object.defineProperty(event, 'target', { value: formControl })
1612
+ formControl.dispatchEvent(event)
1613
+
1614
+ expect((validator as any).validateInput).toHaveBeenCalledWith(formControl)
1615
+ })
1616
+
1617
+ it('clears the error messages for the input element', async () => {
1618
+ vi.spyOn(validator as any, 'clearInputErrors')
1619
+ const event = new Event('change', { bubbles: true })
1620
+ Object.defineProperty(event, 'target', { value: formControl })
1621
+ await formControl.dispatchEvent(event)
1622
+ expect((validator as any).clearInputErrors).toHaveBeenCalledWith(formControl)
1623
+ })
1624
+
1625
+ it('shows the error messages for the input element', async () => {
1626
+ vi.spyOn(validator as any, 'showInputErrors')
1627
+ const event = new Event('change', { bubbles: true })
1628
+ Object.defineProperty(event, 'target', { value: formControl })
1629
+ await formControl.dispatchEvent(event)
1630
+ expect((validator as any).showInputErrors).toHaveBeenCalledWith(formControl)
1631
+ })
1632
+ }) // end inputChangeHandler
1633
+
1634
+ describe('inputInputHandler', () => {
1635
+ it('should parse integer input values', () => {
1636
+ formControl.type = 'text'
1637
+ formControl.dataset.type = 'integer'
1638
+ formControl.value = '123.45'
1639
+
1640
+ const event = new Event('input', { bubbles: true })
1641
+ Object.defineProperty(event, 'target', { value: formControl })
1642
+
1643
+ formControl.dispatchEvent(event)
1644
+ expect(formControl.value).toEqual('12345')
1645
+
1646
+ formControl.value = '123,45'
1647
+ formControl.dispatchEvent(event)
1648
+ expect(formControl.value).toEqual('12345')
1649
+
1650
+ formControl.value = '-12345'
1651
+ formControl.dispatchEvent(event)
1652
+ expect(formControl.value).toEqual('12345')
1653
+ })
1654
+
1655
+ it('should parse non-native number input values', () => {
1656
+ formControl.type = 'text'
1657
+ formControl.dataset.type = 'number'
1658
+
1659
+ const event = new Event('input', { bubbles: true })
1660
+ Object.defineProperty(event, 'target', { value: formControl })
1661
+
1662
+ formControl.value = '123.45'
1663
+ formControl.dispatchEvent(event)
1664
+ expect(formControl.value).toEqual('123.45')
1665
+
1666
+ formControl.value = '123,45'
1667
+ formControl.dispatchEvent(event)
1668
+ expect(formControl.value).toEqual('12345')
1669
+
1670
+ formControl.value = '-12345'
1671
+ formControl.dispatchEvent(event)
1672
+ expect(formControl.value).toEqual('-12345')
1673
+
1674
+ formControl.value = '-12345a'
1675
+ formControl.dispatchEvent(event)
1676
+ expect(formControl.value).toEqual('-12345')
1677
+ })
1678
+
1679
+ it('should not parse native number input values', () => {
1680
+ formControl.type = 'number'
1681
+
1682
+ const event = new Event('input', { bubbles: true })
1683
+ Object.defineProperty(event, 'target', { value: formControl })
1684
+
1685
+ formControl.value = '123.45'
1686
+ formControl.dispatchEvent(event)
1687
+ expect(formControl.value).toEqual('123.45')
1688
+
1689
+ // This value won't be allowed and will be blanked out
1690
+ formControl.value = '123,45'
1691
+ formControl.dispatchEvent(event)
1692
+ expect(formControl.value).toEqual('')
1693
+
1694
+ formControl.value = '-123.4'
1695
+ formControl.dispatchEvent(event)
1696
+ expect(formControl.value).toEqual('-123.4')
1697
+ })
1698
+
1699
+ it('should call syncColorInput for color inputs', () => {
1700
+ const syncColorInputSpy = vi.spyOn(validator as any, 'syncColorInput')
1701
+ formControl.type = 'color'
1702
+ const event = new Event('input', { bubbles: true })
1703
+ Object.defineProperty(event, 'target', { value: formControl })
1704
+
1705
+ formControl.dispatchEvent(event)
1706
+ expect(syncColorInputSpy).toHaveBeenCalledWith(expect.any(Event))
1707
+
1708
+ formControl.type = 'text'
1709
+ formControl.dataset.type = 'color'
1710
+ formControl.dispatchEvent(event)
1711
+ expect(syncColorInputSpy).toHaveBeenCalledWith(expect.any(Event))
1712
+ })
1713
+ }) // end inputInputHandler
1714
+
1715
+ describe('syncColorInput', () => {
1716
+ let colorInput: HTMLInputElement
1717
+ let colorPicker: HTMLInputElement
1718
+ let colorLabel: HTMLLabelElement
1719
+
1720
+ beforeEach(() => {
1721
+ colorInput = document.createElement('input')
1722
+ colorInput.type = 'text'
1723
+ colorInput.id = 'test-color'
1724
+ colorInput.value = '#ff0000'
1725
+ colorInput.dataset.type = 'color'
1726
+ form.appendChild(colorInput)
1727
+
1728
+ colorPicker = document.createElement('input')
1729
+ colorPicker.type = 'color'
1730
+ colorPicker.id = 'test-color-color'
1731
+ colorPicker.value = '#ff0000'
1732
+ form.appendChild(colorPicker)
1733
+
1734
+ colorLabel = document.createElement('label')
1735
+ colorLabel.htmlFor = 'test-color-color'
1736
+ colorLabel.id = 'test-color-color-label'
1737
+ form.appendChild(colorLabel)
1738
+
1739
+ validator.init()
1740
+ })
1741
+
1742
+ it('should update the HTML color picker input and its label background when color input changes', () => {
1743
+ const event = new Event('input', { bubbles: true })
1744
+ Object.defineProperty(event, 'target', { value: colorInput })
1745
+ const colorLbl = form.querySelector(`#${colorInput.id}-color-label`)
1746
+
1747
+ colorInput.value = '#00ff00'
1748
+ colorInput.dispatchEvent(event)
1749
+ // validator.syncColorInput(event)
1750
+
1751
+ expect(colorPicker.value).toEqual('#00ff00')
1752
+
1753
+ const color = utils.parseColor(window.getComputedStyle(colorLabel).backgroundColor)
1754
+
1755
+ expect(color).toEqual('#00ff00')
1756
+ })
1757
+
1758
+ it('should update the color input and label background when the HTML color picker is changed', () => {
1759
+ const event = new Event('input', { bubbles: true })
1760
+ Object.defineProperty(event, 'target', { value: colorPicker })
1761
+
1762
+ colorPicker.value = '#0000ff'
1763
+ ;(validator as any).syncColorInput(event)
1764
+
1765
+ expect(colorInput.value).toEqual('#0000ff')
1766
+ expect(utils.parseColor(colorLabel.style.backgroundColor)).toEqual('#0000ff')
1767
+ })
1768
+
1769
+ it('should not update the HTML color picker if the color input value is not a valid color', () => {
1770
+ const event = new Event('input', { bubbles: true })
1771
+ Object.defineProperty(event, 'target', { value: colorInput })
1772
+
1773
+ colorInput.value = 'not-a-color'
1774
+ ;(validator as any).syncColorInput(event)
1775
+
1776
+ expect(colorPicker.value).toEqual('#ff0000')
1777
+ })
1778
+ }) // end syncColorInput
1779
+
1780
+ describe('inputKeydownHandler', () => {
1781
+ let event: Event
1782
+
1783
+ beforeEach(() => {
1784
+ formControl.type = 'text'
1785
+ formControl.dataset.type = 'integer'
1786
+
1787
+ event = new Event('keydown', { bubbles: true })
1788
+ })
1789
+
1790
+ it('should increment integer input value when ArrowUp key is pressed', () => {
1791
+ Object.defineProperty(event, 'target', { value: formControl })
1792
+ Object.defineProperty(event, 'key', { value: 'ArrowUp' })
1793
+
1794
+ formControl.value = '5'
1795
+ ;(validator as any).inputKeydownHandler(event)
1796
+ expect(formControl.value).toEqual('6')
1797
+
1798
+ Object.defineProperty(event, 'target', { value: formControl })
1799
+ Object.defineProperty(event, 'key', { value: 'ArrowUp' })
1800
+
1801
+ formControl.value = ''
1802
+ formControl.dispatchEvent(event)
1803
+ expect(formControl.value).toEqual('1')
1804
+ })
1805
+
1806
+ it('should decrement integer input value when ArrowDown key is pressed', () => {
1807
+ Object.defineProperty(event, 'target', { value: formControl })
1808
+ Object.defineProperty(event, 'key', { value: 'ArrowDown' })
1809
+
1810
+ formControl.value = '5'
1811
+ formControl.dispatchEvent(event)
1812
+ expect(formControl.value).toEqual('4')
1813
+
1814
+ // Test that it doesn't go below 0
1815
+ formControl.value = '0'
1816
+ formControl.dispatchEvent(event)
1817
+ expect(formControl.value).toEqual('0')
1818
+
1819
+ formControl.value = ''
1820
+ formControl.dispatchEvent(event)
1821
+ expect(formControl.value).toEqual('0')
1822
+ })
1823
+
1824
+ it('should not increment or decrement non-integer inputs on ArrowUp', () => {
1825
+ Object.defineProperty(event, 'target', { value: formControl })
1826
+ Object.defineProperty(event, 'key', { value: 'ArrowUp' })
1827
+ formControl.dataset.type = 'text'
1828
+ formControl.value = '1'
1829
+
1830
+ formControl.dispatchEvent(event)
1831
+ expect(formControl.value).toEqual('1')
1832
+ })
1833
+
1834
+ it('should not increment or decrement non-integer inputs on ArrowDown', () => {
1835
+ Object.defineProperty(event, 'target', { value: formControl })
1836
+ Object.defineProperty(event, 'key', { value: 'ArrowDown' })
1837
+ formControl.dataset.type = 'text'
1838
+ formControl.value = '1'
1839
+
1840
+ formControl.dispatchEvent(event)
1841
+ expect(formControl.value).toEqual('1')
1842
+ })
1843
+ }) // end inputKeydownHandler
1844
+
1845
+ describe('ValidationSuccessEvent', () => {
1846
+ let submitEvent: Event
1847
+ let validationSuccessEvent: ValidationSuccessEvent
1848
+
1849
+ beforeEach(() => {
1850
+ submitEvent = new Event('submit')
1851
+ validationSuccessEvent = new ValidationSuccessEvent(submitEvent)
1852
+ })
1853
+
1854
+ it('should create a new ValidationSuccessEvent', () => {
1855
+ expect(validationSuccessEvent instanceof ValidationSuccessEvent).toBe(true)
1856
+ })
1857
+
1858
+ it('should set the event type to validationSuccess', () => {
1859
+ expect(validationSuccessEvent.type).toEqual('validationSuccess')
1860
+ })
1861
+
1862
+ it('should set the submitEvent property to the provided submit event', () => {
1863
+ expect(validationSuccessEvent.submitEvent).toEqual(submitEvent)
1864
+ })
1865
+ }) // end ValidationSuccessEvent
1866
+
1867
+ describe('ValidationEvents', () => {
1868
+ let submitEvent: Event
1869
+ let validationErrorEvent: ValidationErrorEvent
1870
+
1871
+ beforeEach(() => {
1872
+ submitEvent = new Event('submit')
1873
+ validationErrorEvent = new ValidationErrorEvent(submitEvent)
1874
+ })
1875
+
1876
+ it('should create a new ValidationErrorEvent', () => {
1877
+ expect(validationErrorEvent instanceof ValidationErrorEvent).toBe(true)
1878
+ })
1879
+
1880
+ it('should set the event type to validationError', () => {
1881
+ expect(validationErrorEvent.type).toEqual('validationError')
1882
+ })
1883
+
1884
+ it('should set the submitEvent property to the provided submit event', () => {
1885
+ expect(validationErrorEvent.submitEvent).toEqual(submitEvent)
1886
+ })
1887
+ }) // end ValidationEvents
1888
+
1889
+ describe('destroy', () => {
1890
+ it('removes all event listeners', () => {
1891
+ const validator = new Validator(form)
1892
+ vi.spyOn(validator, 'removeEventListeners')
1893
+
1894
+ validator.destroy()
1895
+
1896
+ expect(validator.removeEventListeners).toHaveBeenCalled()
1897
+ })
1898
+
1899
+ it('removes the "novalidate" attribute from the form if it was not originally present', () => {
1900
+ const form = document.createElement('form')
1901
+ const validator = new Validator(form)
1902
+ vi.spyOn(validator, 'removeEventListeners')
1903
+
1904
+ validator.destroy()
1905
+
1906
+ expect(validator.removeEventListeners).toHaveBeenCalled()
1907
+ expect(form.hasAttribute('novalidate')).toBe(false)
1908
+ })
1909
+
1910
+ it('does not remove the "novalidate" attribute from the form if it was originally present', () => {
1911
+ const form = document.createElement('form')
1912
+ form.setAttribute('novalidate', '')
1913
+ const validator = new Validator(form)
1914
+ vi.spyOn(validator, 'removeEventListeners')
1915
+
1916
+ validator.destroy()
1917
+
1918
+ expect(validator.removeEventListeners).toHaveBeenCalled()
1919
+ expect(form.hasAttribute('novalidate')).toBe(true)
1920
+ })
1921
+ })
1922
+ })