@oslokommune/punkt-elements 13.5.5 → 13.5.9
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/CHANGELOG.md +34 -0
- package/package.json +9 -9
- package/src/components/alert/alert.test.ts +3 -2
- package/src/components/button/button.test.ts +6 -5
- package/src/components/card/card.test.ts +5 -4
- package/src/components/combobox/combobox.test.ts +2 -1
- package/src/components/consent/consent.test.ts +22 -21
- package/src/components/heading/heading.test.ts +2 -1
- package/src/components/icon/icon.test.ts +3 -2
- package/src/components/link/link.test.ts +2 -1
- package/src/components/messagebox/messagebox.test.ts +3 -2
- package/src/components/radiobutton/radiobutton.test.ts +3 -2
- package/src/components/select/select.test.ts +3 -2
- package/src/components/tag/tag.test.ts +212 -0
- package/src/components/textarea/textarea.test.ts +289 -0
- package/src/components/textinput/textinput.test.ts +421 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import { axe, toHaveNoViolations } from 'jest-axe'
|
|
3
|
+
import { fireEvent } from '@testing-library/dom'
|
|
4
|
+
import { createElementTest, BaseTestConfig } from '../../tests/test-framework'
|
|
5
|
+
import { CustomElementFor } from '../../tests/component-registry'
|
|
6
|
+
import './textinput'
|
|
7
|
+
|
|
8
|
+
export interface TextinputTestConfig extends BaseTestConfig {
|
|
9
|
+
// From PktTextinput specific properties
|
|
10
|
+
type?: string
|
|
11
|
+
value?: string
|
|
12
|
+
autocomplete?: string | null
|
|
13
|
+
iconNameRight?: string | null
|
|
14
|
+
prefix?: string | null
|
|
15
|
+
suffix?: string | null
|
|
16
|
+
size?: number | null
|
|
17
|
+
omitSearchIcon?: boolean
|
|
18
|
+
|
|
19
|
+
// From PktInputElement base class (commonly used ones)
|
|
20
|
+
id?: string
|
|
21
|
+
label?: string
|
|
22
|
+
name?: string
|
|
23
|
+
disabled?: boolean
|
|
24
|
+
readonly?: boolean
|
|
25
|
+
required?: boolean
|
|
26
|
+
placeholder?: string | null
|
|
27
|
+
maxlength?: number | null
|
|
28
|
+
minlength?: number | null
|
|
29
|
+
hasError?: boolean
|
|
30
|
+
errorMessage?: string
|
|
31
|
+
helptext?: string
|
|
32
|
+
fullwidth?: boolean
|
|
33
|
+
counter?: boolean
|
|
34
|
+
inline?: boolean
|
|
35
|
+
ariaLabelledby?: string | null
|
|
36
|
+
ariaDescribedBy?: string | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Use shared framework
|
|
40
|
+
export const createTextinputTest = async (config: TextinputTestConfig = {}) => {
|
|
41
|
+
const { container, element } = await createElementTest<
|
|
42
|
+
CustomElementFor<'pkt-textinput'>,
|
|
43
|
+
TextinputTestConfig
|
|
44
|
+
>('pkt-textinput', config)
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
container,
|
|
48
|
+
textinput: element,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
expect.extend(toHaveNoViolations)
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
document.body.innerHTML = ''
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('PktTextinput', () => {
|
|
59
|
+
describe('Basic Rendering', () => {
|
|
60
|
+
test('renders without errors', async () => {
|
|
61
|
+
const { textinput } = await createTextinputTest()
|
|
62
|
+
expect(textinput).toBeInTheDocument()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('renders with default properties', async () => {
|
|
66
|
+
const { textinput } = await createTextinputTest()
|
|
67
|
+
expect(textinput.type).toBe('text')
|
|
68
|
+
expect(textinput.value).toBe('')
|
|
69
|
+
expect(textinput.autocomplete).toBe(null) // Property defaults to null, template sets 'off'
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('renders input element', async () => {
|
|
73
|
+
const { textinput } = await createTextinputTest()
|
|
74
|
+
const inputElement = textinput.querySelector('input')
|
|
75
|
+
expect(inputElement).toBeInTheDocument()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('Input Types', () => {
|
|
80
|
+
test('renders text input by default', async () => {
|
|
81
|
+
const { textinput } = await createTextinputTest()
|
|
82
|
+
const inputElement = textinput.querySelector('input')
|
|
83
|
+
expect(inputElement?.getAttribute('type')).toBe('text')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('renders email input type', async () => {
|
|
87
|
+
const { textinput } = await createTextinputTest({ type: 'email' })
|
|
88
|
+
const inputElement = textinput.querySelector('input')
|
|
89
|
+
expect(inputElement?.getAttribute('type')).toBe('email')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('renders password input type', async () => {
|
|
93
|
+
const { textinput } = await createTextinputTest({ type: 'password' })
|
|
94
|
+
const inputElement = textinput.querySelector('input')
|
|
95
|
+
expect(inputElement?.getAttribute('type')).toBe('password')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('renders tel input type', async () => {
|
|
99
|
+
const { textinput } = await createTextinputTest({ type: 'tel' })
|
|
100
|
+
const inputElement = textinput.querySelector('input')
|
|
101
|
+
expect(inputElement?.getAttribute('type')).toBe('tel')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('renders url input type', async () => {
|
|
105
|
+
const { textinput } = await createTextinputTest({ type: 'url' })
|
|
106
|
+
const inputElement = textinput.querySelector('input')
|
|
107
|
+
expect(inputElement?.getAttribute('type')).toBe('url')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('renders search input type', async () => {
|
|
111
|
+
const { textinput } = await createTextinputTest({ type: 'search' })
|
|
112
|
+
const inputElement = textinput.querySelector('input')
|
|
113
|
+
expect(inputElement?.getAttribute('type')).toBe('search')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('Properties and Attributes', () => {
|
|
118
|
+
test('sets value correctly', async () => {
|
|
119
|
+
const value = 'Test input value'
|
|
120
|
+
const { textinput } = await createTextinputTest({ value })
|
|
121
|
+
|
|
122
|
+
expect(textinput.value).toBe(value)
|
|
123
|
+
const inputElement = textinput.querySelector('input') as HTMLInputElement
|
|
124
|
+
expect(inputElement.value).toBe(value)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('sets autocomplete correctly', async () => {
|
|
128
|
+
const { textinput } = await createTextinputTest({ autocomplete: 'email' })
|
|
129
|
+
|
|
130
|
+
expect(textinput.autocomplete).toBe('email')
|
|
131
|
+
const inputElement = textinput.querySelector('input')
|
|
132
|
+
expect(inputElement?.getAttribute('autocomplete')).toBe('email')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('handles disabled state', async () => {
|
|
136
|
+
const { textinput } = await createTextinputTest({ disabled: true })
|
|
137
|
+
|
|
138
|
+
const inputElement = textinput.querySelector('input')
|
|
139
|
+
expect(inputElement?.hasAttribute('disabled')).toBe(true)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('handles readonly state', async () => {
|
|
143
|
+
const { textinput } = await createTextinputTest({ readonly: true })
|
|
144
|
+
|
|
145
|
+
const inputElement = textinput.querySelector('input')
|
|
146
|
+
expect(inputElement?.hasAttribute('readonly')).toBe(true)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('handles required state', async () => {
|
|
150
|
+
const { textinput } = await createTextinputTest({ required: true })
|
|
151
|
+
|
|
152
|
+
const inputElement = textinput.querySelector('input')
|
|
153
|
+
expect(inputElement?.hasAttribute('required')).toBe(false) // Not set as attribute on input
|
|
154
|
+
|
|
155
|
+
const inputWrapper = textinput.querySelector('pkt-input-wrapper')
|
|
156
|
+
expect(inputWrapper?.hasAttribute('required')).toBe(true) // But passed to wrapper
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('Icons', () => {
|
|
161
|
+
test('renders right icon', async () => {
|
|
162
|
+
const { textinput } = await createTextinputTest({
|
|
163
|
+
iconNameRight: 'search',
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
expect(textinput.iconNameRight).toBe('search')
|
|
167
|
+
|
|
168
|
+
const icon = textinput.querySelector('pkt-icon')
|
|
169
|
+
expect(icon).toBeInTheDocument()
|
|
170
|
+
expect(icon?.getAttribute('name')).toBe('search')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('renders search icon for search type by default', async () => {
|
|
174
|
+
const { textinput } = await createTextinputTest({ type: 'search' })
|
|
175
|
+
|
|
176
|
+
const icon = textinput.querySelector('pkt-icon')
|
|
177
|
+
expect(icon).toBeInTheDocument()
|
|
178
|
+
expect(icon?.getAttribute('name')).toBe('magnifying-glass-big')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('can omit search icon for search type', async () => {
|
|
182
|
+
const { textinput } = await createTextinputTest({
|
|
183
|
+
type: 'search',
|
|
184
|
+
omitSearchIcon: true,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
const icon = textinput.querySelector('pkt-icon')
|
|
188
|
+
expect(icon).not.toBeInTheDocument()
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe('Prefix and Suffix', () => {
|
|
193
|
+
test('renders prefix text', async () => {
|
|
194
|
+
const { textinput } = await createTextinputTest({ prefix: 'https://' })
|
|
195
|
+
|
|
196
|
+
expect(textinput.prefix).toBe('https://')
|
|
197
|
+
const prefixElement = textinput.querySelector('.pkt-input-prefix')
|
|
198
|
+
expect(prefixElement).toBeInTheDocument()
|
|
199
|
+
expect(prefixElement?.textContent).toBe('https://')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('renders suffix text', async () => {
|
|
203
|
+
const { textinput } = await createTextinputTest({ suffix: '.com' })
|
|
204
|
+
|
|
205
|
+
expect(textinput.suffix).toBe('.com')
|
|
206
|
+
const suffixElement = textinput.querySelector('.pkt-input-suffix')
|
|
207
|
+
expect(suffixElement).toBeInTheDocument()
|
|
208
|
+
expect(suffixElement?.textContent?.trim()).toBe('.com')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('renders both prefix and suffix', async () => {
|
|
212
|
+
const { textinput } = await createTextinputTest({
|
|
213
|
+
prefix: '$',
|
|
214
|
+
suffix: 'USD',
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
const prefixElement = textinput.querySelector('.pkt-input-prefix')
|
|
218
|
+
const suffixElement = textinput.querySelector('.pkt-input-suffix')
|
|
219
|
+
|
|
220
|
+
expect(prefixElement?.textContent).toBe('$')
|
|
221
|
+
expect(suffixElement?.textContent?.trim()).toBe('USD')
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe('Input Wrapper Integration', () => {
|
|
226
|
+
test('displays label correctly', async () => {
|
|
227
|
+
const { textinput } = await createTextinputTest({ label: 'Email Address' })
|
|
228
|
+
|
|
229
|
+
const inputWrapper = textinput.querySelector('pkt-input-wrapper')
|
|
230
|
+
expect(inputWrapper?.getAttribute('label')).toBe('Email Address')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('displays helptext correctly', async () => {
|
|
234
|
+
const { textinput } = await createTextinputTest({ helptext: 'Enter a valid email' })
|
|
235
|
+
|
|
236
|
+
// helptext is passed as a property, not attribute to input-wrapper
|
|
237
|
+
expect(textinput.helptext).toBe('Enter a valid email')
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('handles error state', async () => {
|
|
241
|
+
const { textinput } = await createTextinputTest({
|
|
242
|
+
hasError: true,
|
|
243
|
+
errorMessage: 'Email is required',
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
expect(textinput.hasError).toBe(true)
|
|
247
|
+
expect(textinput.errorMessage).toBe('Email is required')
|
|
248
|
+
|
|
249
|
+
const inputWrapper = textinput.querySelector('pkt-input-wrapper')
|
|
250
|
+
expect(inputWrapper?.hasAttribute('hasError')).toBe(true)
|
|
251
|
+
|
|
252
|
+
const inputElement = textinput.querySelector('input')
|
|
253
|
+
expect(inputElement?.getAttribute('aria-invalid')).toBe('true')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('handles fullwidth styling', async () => {
|
|
257
|
+
const { textinput } = await createTextinputTest({ fullwidth: true })
|
|
258
|
+
|
|
259
|
+
const inputElement = textinput.querySelector('input')
|
|
260
|
+
expect(inputElement?.className).toContain('pkt-input--fullwidth')
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe('Character Counter', () => {
|
|
265
|
+
test('shows counter when enabled', async () => {
|
|
266
|
+
const { textinput } = await createTextinputTest({
|
|
267
|
+
counter: true,
|
|
268
|
+
maxlength: 50,
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
const inputWrapper = textinput.querySelector('pkt-input-wrapper')
|
|
272
|
+
expect(inputWrapper?.hasAttribute('counter')).toBe(true)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('updates counter on value change', async () => {
|
|
276
|
+
const { textinput } = await createTextinputTest({
|
|
277
|
+
counter: true,
|
|
278
|
+
maxlength: 50,
|
|
279
|
+
value: 'Hello',
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
expect(textinput.counterCurrent).toBe(5)
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
describe('User Interaction', () => {
|
|
287
|
+
test('updates value on user input', async () => {
|
|
288
|
+
const { textinput } = await createTextinputTest()
|
|
289
|
+
const inputElement = textinput.querySelector('input') as HTMLInputElement
|
|
290
|
+
|
|
291
|
+
fireEvent.input(inputElement, { target: { value: 'new value' } })
|
|
292
|
+
await textinput.updateComplete
|
|
293
|
+
|
|
294
|
+
expect(textinput.value).toBe('new value')
|
|
295
|
+
expect(textinput.touched).toBe(true)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('handles focus and blur events', async () => {
|
|
299
|
+
const { textinput } = await createTextinputTest()
|
|
300
|
+
const inputElement = textinput.querySelector('input') as HTMLInputElement
|
|
301
|
+
|
|
302
|
+
// Focus and input to trigger touched state
|
|
303
|
+
fireEvent.focus(inputElement)
|
|
304
|
+
fireEvent.input(inputElement, { target: { value: 'test input' } })
|
|
305
|
+
await textinput.updateComplete
|
|
306
|
+
fireEvent.blur(inputElement)
|
|
307
|
+
await textinput.updateComplete
|
|
308
|
+
|
|
309
|
+
// Test that input with value change sets touched state
|
|
310
|
+
expect(textinput.touched).toBe(true)
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('Validation', () => {
|
|
315
|
+
test('respects maxlength constraint', async () => {
|
|
316
|
+
const { textinput } = await createTextinputTest({ maxlength: 20 })
|
|
317
|
+
|
|
318
|
+
const inputElement = textinput.querySelector('input')
|
|
319
|
+
expect(inputElement?.getAttribute('maxlength')).toBe('20')
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
test('respects minlength constraint', async () => {
|
|
323
|
+
const { textinput } = await createTextinputTest({ minlength: 3 })
|
|
324
|
+
|
|
325
|
+
const inputElement = textinput.querySelector('input')
|
|
326
|
+
expect(inputElement?.getAttribute('minlength')).toBe('3')
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
describe('Accessibility', () => {
|
|
331
|
+
test('passes through accessibility attributes', async () => {
|
|
332
|
+
const { textinput } = await createTextinputTest({
|
|
333
|
+
ariaLabelledby: 'external-label',
|
|
334
|
+
ariaDescribedBy: 'external-description',
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
const inputElement = textinput.querySelector('input')
|
|
338
|
+
expect(inputElement?.getAttribute('aria-labelledby')).toBe('external-label')
|
|
339
|
+
|
|
340
|
+
// ariaDescribedBy is passed as property to input-wrapper
|
|
341
|
+
expect(textinput.ariaDescribedBy).toBe('external-description')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
test('textinput is accessible', async () => {
|
|
345
|
+
const { textinput } = await createTextinputTest({
|
|
346
|
+
label: 'Email',
|
|
347
|
+
type: 'email',
|
|
348
|
+
helptext: 'Enter your email address',
|
|
349
|
+
required: true,
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
const results = await axe(textinput)
|
|
353
|
+
expect(results).toHaveNoViolations()
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe('Complex Configuration', () => {
|
|
358
|
+
test('renders email input with all features', async () => {
|
|
359
|
+
const config: TextinputTestConfig = {
|
|
360
|
+
type: 'email',
|
|
361
|
+
label: 'Email Address',
|
|
362
|
+
value: 'user@example.com',
|
|
363
|
+
placeholder: 'Enter your email...',
|
|
364
|
+
iconNameRight: 'mail',
|
|
365
|
+
maxlength: 100,
|
|
366
|
+
counter: true,
|
|
367
|
+
required: true,
|
|
368
|
+
autocomplete: 'email',
|
|
369
|
+
helptext: 'We will never share your email',
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const { textinput } = await createTextinputTest(config)
|
|
373
|
+
|
|
374
|
+
expect(textinput.type).toBe(config.type)
|
|
375
|
+
expect(textinput.value).toBe(config.value)
|
|
376
|
+
expect(textinput.iconNameRight).toBe(config.iconNameRight)
|
|
377
|
+
expect(textinput.maxlength).toBe(config.maxlength)
|
|
378
|
+
expect(textinput.required).toBe(config.required)
|
|
379
|
+
expect(textinput.autocomplete).toBe(config.autocomplete)
|
|
380
|
+
|
|
381
|
+
const inputWrapper = textinput.querySelector('pkt-input-wrapper')
|
|
382
|
+
expect(inputWrapper?.getAttribute('label')).toBe(config.label)
|
|
383
|
+
expect(textinput.helptext).toBe(config.helptext) // Property, not attribute
|
|
384
|
+
expect(inputWrapper?.hasAttribute('counter')).toBe(true)
|
|
385
|
+
|
|
386
|
+
const inputElement = textinput.querySelector('input')
|
|
387
|
+
expect(inputElement?.getAttribute('type')).toBe(config.type)
|
|
388
|
+
expect(inputElement?.getAttribute('placeholder')).toBe(config.placeholder)
|
|
389
|
+
expect(inputElement?.getAttribute('maxlength')).toBe(String(config.maxlength))
|
|
390
|
+
expect(inputElement?.getAttribute('autocomplete')).toBe(config.autocomplete)
|
|
391
|
+
|
|
392
|
+
const icon = textinput.querySelector('pkt-icon')
|
|
393
|
+
expect(icon?.getAttribute('name')).toBe(config.iconNameRight)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
test('renders URL input with prefix and suffix', async () => {
|
|
397
|
+
const config: TextinputTestConfig = {
|
|
398
|
+
type: 'url',
|
|
399
|
+
label: 'Website URL',
|
|
400
|
+
prefix: 'https://',
|
|
401
|
+
suffix: '.com',
|
|
402
|
+
placeholder: 'example',
|
|
403
|
+
fullwidth: true,
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const { textinput } = await createTextinputTest(config)
|
|
407
|
+
|
|
408
|
+
expect(textinput.prefix).toBe(config.prefix)
|
|
409
|
+
expect(textinput.suffix).toBe(config.suffix)
|
|
410
|
+
|
|
411
|
+
const prefixElement = textinput.querySelector('.pkt-input-prefix')
|
|
412
|
+
const suffixElement = textinput.querySelector('.pkt-input-suffix')
|
|
413
|
+
|
|
414
|
+
expect(prefixElement?.textContent).toBe(config.prefix)
|
|
415
|
+
expect(suffixElement?.textContent?.trim()).toBe(config.suffix)
|
|
416
|
+
|
|
417
|
+
const inputElement = textinput.querySelector('input')
|
|
418
|
+
expect(inputElement?.className).toContain('pkt-input--fullwidth')
|
|
419
|
+
})
|
|
420
|
+
})
|
|
421
|
+
})
|