@jdlien/validator 1.0.4 → 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.
- package/.prettierrc +8 -0
- package/README.md +1 -1
- package/demo-src.css +106 -0
- package/demo.html +779 -0
- package/index.ts +5 -0
- package/package.json +1 -10
- package/src/Validator.ts +628 -0
- package/src/types.d.ts +23 -0
- package/src/validator-utils.ts +656 -0
- package/tailwind.config.cjs +12 -0
- package/tests/Validator.test.ts +1922 -0
- package/tests/utils.test.ts +1048 -0
- package/tsconfig.json +19 -0
- package/vite.config.js +22 -0
- package/dist/validator.js +0 -1
|
@@ -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
|
+
})
|