@oslokommune/punkt-elements 13.5.0 → 13.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,458 @@
1
+ import '@testing-library/jest-dom'
2
+ import { axe, toHaveNoViolations } from 'jest-axe'
3
+
4
+ expect.extend(toHaveNoViolations)
5
+
6
+ import './heading'
7
+ import { PktHeading, TPktHeadingLevel, TPktHeadingSize } from './heading'
8
+
9
+ const waitForCustomElements = async () => {
10
+ await customElements.whenDefined('pkt-heading')
11
+ }
12
+
13
+ // Helper function to create heading element
14
+ const createHeading = async (headingProps = '', content = 'Test Heading') => {
15
+ const container = document.createElement('div')
16
+ container.innerHTML = `
17
+ <pkt-heading ${headingProps}>${content}</pkt-heading>
18
+ `
19
+ document.body.appendChild(container)
20
+ await waitForCustomElements()
21
+ return container
22
+ }
23
+
24
+ // Cleanup after each test
25
+ afterEach(() => {
26
+ document.body.innerHTML = ''
27
+ })
28
+
29
+ describe('PktHeading', () => {
30
+ describe('Rendering and basic functionality', () => {
31
+ test('renders without errors', async () => {
32
+ const container = await createHeading()
33
+ const heading = container.querySelector('pkt-heading') as PktHeading
34
+
35
+ expect(heading).toBeInTheDocument()
36
+ expect(heading.shadowRoot).toBeTruthy()
37
+ })
38
+
39
+ test('renders with default properties', async () => {
40
+ const container = await createHeading()
41
+ const heading = container.querySelector('pkt-heading') as PktHeading
42
+ await heading.updateComplete
43
+
44
+ expect(heading.size).toBe('medium')
45
+ expect(heading.level).toBe(2)
46
+ expect(heading.visuallyHidden).toBe(false)
47
+ expect(heading.align).toBe('start')
48
+ })
49
+
50
+ test('renders content in shadow DOM slot', async () => {
51
+ const container = await createHeading('', 'Custom Heading Text')
52
+ const heading = container.querySelector('pkt-heading') as PktHeading
53
+ await heading.updateComplete
54
+
55
+ const slot = heading.shadowRoot?.querySelector('slot')
56
+ expect(slot).toBeInTheDocument()
57
+ expect(heading.textContent).toContain('Custom Heading Text')
58
+ })
59
+ })
60
+
61
+ describe('Properties and attributes', () => {
62
+ test('applies default properties correctly', async () => {
63
+ const container = await createHeading()
64
+ const heading = container.querySelector('pkt-heading') as PktHeading
65
+ await heading.updateComplete
66
+
67
+ expect(heading.getAttribute('size')).toBe('medium')
68
+ expect(heading.getAttribute('level')).toBe('2')
69
+ expect(heading.getAttribute('visually-hidden')).toBe(null)
70
+ expect(heading.getAttribute('align')).toBe('start')
71
+ })
72
+
73
+ test('sets size property correctly', async () => {
74
+ const sizes: TPktHeadingSize[] = ['xsmall', 'small', 'medium', 'large', 'xlarge']
75
+
76
+ for (const size of sizes) {
77
+ const container = await createHeading(`size="${size}"`)
78
+ const heading = container.querySelector('pkt-heading') as PktHeading
79
+ await heading.updateComplete
80
+
81
+ expect(heading.size).toBe(size)
82
+ expect(heading.getAttribute('size')).toBe(size)
83
+ document.body.innerHTML = ''
84
+ }
85
+ })
86
+
87
+ test('sets level property correctly', async () => {
88
+ const levels: TPktHeadingLevel[] = [1, 2, 3, 4, 5, 6]
89
+
90
+ for (const level of levels) {
91
+ const container = await createHeading(`level="${level}"`)
92
+ const heading = container.querySelector('pkt-heading') as PktHeading
93
+ await heading.updateComplete
94
+
95
+ expect(heading.level).toBe(level)
96
+ expect(heading.getAttribute('level')).toBe(String(level))
97
+ document.body.innerHTML = ''
98
+ }
99
+ })
100
+
101
+ test('sets visuallyHidden property correctly', async () => {
102
+ const container = await createHeading('visuallyHidden="true"')
103
+ const heading = container.querySelector('pkt-heading') as PktHeading
104
+ await heading.updateComplete
105
+
106
+ expect(heading.visuallyHidden).toBe(true)
107
+ expect(heading.hasAttribute('visuallyHidden')).toBe(true)
108
+ })
109
+
110
+ test('sets align property correctly', async () => {
111
+ const alignments = ['start', 'center', 'end'] as const
112
+
113
+ for (const align of alignments) {
114
+ const container = await createHeading(`align="${align}"`)
115
+ const heading = container.querySelector('pkt-heading') as PktHeading
116
+ await heading.updateComplete
117
+
118
+ expect(heading.align).toBe(align)
119
+ expect(heading.getAttribute('align')).toBe(align)
120
+ document.body.innerHTML = ''
121
+ }
122
+ })
123
+ })
124
+
125
+ describe('CSS classes and styling', () => {
126
+ test('applies default CSS classes', async () => {
127
+ const container = await createHeading()
128
+ const heading = container.querySelector('pkt-heading') as PktHeading
129
+ await heading.updateComplete
130
+
131
+ expect(heading.classList.contains('pkt-heading')).toBe(true)
132
+ expect(heading.classList.contains('pkt-heading--medium')).toBe(true)
133
+ expect(heading.classList.contains('pkt-heading--start')).toBe(true)
134
+ })
135
+
136
+ test('applies size-specific CSS classes', async () => {
137
+ const sizes: TPktHeadingSize[] = ['xsmall', 'small', 'medium', 'large', 'xlarge']
138
+
139
+ for (const size of sizes) {
140
+ const container = await createHeading(`size="${size}"`)
141
+ const heading = container.querySelector('pkt-heading') as PktHeading
142
+ await heading.updateComplete
143
+
144
+ expect(heading.classList.contains(`pkt-heading--${size}`)).toBe(true)
145
+ document.body.innerHTML = ''
146
+ }
147
+ })
148
+
149
+ test('applies visually hidden class when enabled', async () => {
150
+ const container = await createHeading('visuallyHidden="true"')
151
+ const heading = container.querySelector('pkt-heading') as PktHeading
152
+ await heading.updateComplete
153
+
154
+ expect(heading.classList.contains('pkt-sr-only')).toBe(true)
155
+ })
156
+
157
+ test('applies alignment-specific CSS classes', async () => {
158
+ const alignments = ['start', 'center', 'end'] as const
159
+
160
+ for (const align of alignments) {
161
+ const container = await createHeading(`align="${align}"`)
162
+ const heading = container.querySelector('pkt-heading') as PktHeading
163
+ await heading.updateComplete
164
+
165
+ expect(heading.classList.contains(`pkt-heading--${align}`)).toBe(true)
166
+ document.body.innerHTML = ''
167
+ }
168
+ })
169
+
170
+ test('removes old classes when properties change', async () => {
171
+ const container = await createHeading('size="small" align="center"')
172
+ const heading = container.querySelector('pkt-heading') as PktHeading
173
+ await heading.updateComplete
174
+
175
+ expect(heading.classList.contains('pkt-heading--small')).toBe(true)
176
+ expect(heading.classList.contains('pkt-heading--center')).toBe(true)
177
+
178
+ // Change properties
179
+ heading.size = 'large'
180
+ heading.align = 'end'
181
+ await heading.updateComplete
182
+
183
+ expect(heading.classList.contains('pkt-heading--small')).toBe(false)
184
+ expect(heading.classList.contains('pkt-heading--center')).toBe(false)
185
+ expect(heading.classList.contains('pkt-heading--large')).toBe(true)
186
+ expect(heading.classList.contains('pkt-heading--end')).toBe(true)
187
+ })
188
+ })
189
+
190
+ describe('ARIA and accessibility attributes', () => {
191
+ test('sets role and aria-level attributes on connection', async () => {
192
+ const container = await createHeading('level="3"')
193
+ const heading = container.querySelector('pkt-heading') as PktHeading
194
+ await heading.updateComplete
195
+
196
+ expect(heading.getAttribute('role')).toBe('heading')
197
+ expect(heading.getAttribute('aria-level')).toBe('3')
198
+ })
199
+
200
+ test('updates aria-level when level property changes', async () => {
201
+ const container = await createHeading('level="2"')
202
+ const heading = container.querySelector('pkt-heading') as PktHeading
203
+ await heading.updateComplete
204
+
205
+ expect(heading.getAttribute('aria-level')).toBe('2')
206
+
207
+ heading.level = 4
208
+ await heading.updateComplete
209
+
210
+ expect(heading.getAttribute('aria-level')).toBe('4')
211
+ })
212
+
213
+ test('updates aria-level when level attribute changes', async () => {
214
+ const container = await createHeading('level="1"')
215
+ const heading = container.querySelector('pkt-heading') as PktHeading
216
+ await heading.updateComplete
217
+
218
+ expect(heading.getAttribute('aria-level')).toBe('1')
219
+
220
+ heading.setAttribute('level', '5')
221
+ await heading.updateComplete
222
+
223
+ expect(heading.getAttribute('aria-level')).toBe('5')
224
+ expect(heading.level).toBe(5)
225
+ })
226
+ })
227
+
228
+ describe('Level validation', () => {
229
+ test('accepts valid heading levels (1-6)', async () => {
230
+ const validLevels: TPktHeadingLevel[] = [1, 2, 3, 4, 5, 6]
231
+
232
+ for (const level of validLevels) {
233
+ const container = await createHeading(`level="${level}"`)
234
+ const heading = container.querySelector('pkt-heading') as PktHeading
235
+ await heading.updateComplete
236
+
237
+ expect(heading.level).toBe(level)
238
+ expect(heading.getAttribute('aria-level')).toBe(String(level))
239
+ document.body.innerHTML = ''
240
+ }
241
+ })
242
+
243
+ test('handles invalid levels gracefully', async () => {
244
+ const consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
245
+
246
+ const container = await createHeading()
247
+ const heading = container.querySelector('pkt-heading') as PktHeading
248
+ await heading.updateComplete
249
+
250
+ // Test invalid level via property
251
+ heading.level = 0 as TPktHeadingLevel
252
+ await heading.updateComplete
253
+
254
+ expect(consoleSpy).toHaveBeenCalledWith('Invalid heading level: 0. Must be between 1 and 6.')
255
+
256
+ heading.level = 7 as TPktHeadingLevel
257
+ await heading.updateComplete
258
+
259
+ expect(consoleSpy).toHaveBeenCalledWith('Invalid heading level: 7. Must be between 1 and 6.')
260
+
261
+ consoleSpy.mockRestore()
262
+ })
263
+ })
264
+
265
+ describe('Property updates and lifecycle', () => {
266
+ test('updates classes when size property changes', async () => {
267
+ const container = await createHeading('size="small"')
268
+ const heading = container.querySelector('pkt-heading') as PktHeading
269
+ await heading.updateComplete
270
+
271
+ expect(heading.classList.contains('pkt-heading--small')).toBe(true)
272
+
273
+ heading.size = 'xlarge'
274
+ await heading.updateComplete
275
+
276
+ expect(heading.classList.contains('pkt-heading--small')).toBe(false)
277
+ expect(heading.classList.contains('pkt-heading--xlarge')).toBe(true)
278
+ })
279
+
280
+ test('updates classes when visuallyHidden property changes', async () => {
281
+ const container = await createHeading()
282
+ const heading = container.querySelector('pkt-heading') as PktHeading
283
+ await heading.updateComplete
284
+
285
+ expect(heading.classList.contains('pkt-sr-only')).toBe(false)
286
+
287
+ heading.visuallyHidden = true
288
+ await heading.updateComplete
289
+
290
+ expect(heading.classList.contains('pkt-sr-only')).toBe(true)
291
+ })
292
+
293
+ test('updates classes when align property changes', async () => {
294
+ const container = await createHeading('align="start"')
295
+ const heading = container.querySelector('pkt-heading') as PktHeading
296
+ await heading.updateComplete
297
+
298
+ expect(heading.classList.contains('pkt-heading--start')).toBe(true)
299
+
300
+ heading.align = 'center'
301
+ await heading.updateComplete
302
+
303
+ expect(heading.classList.contains('pkt-heading--start')).toBe(false)
304
+ expect(heading.classList.contains('pkt-heading--center')).toBe(true)
305
+ })
306
+ })
307
+
308
+ describe('Content rendering', () => {
309
+ test('renders simple text content', async () => {
310
+ const container = await createHeading('', 'Simple Heading')
311
+ const heading = container.querySelector('pkt-heading') as PktHeading
312
+
313
+ expect(heading.textContent).toContain('Simple Heading')
314
+ })
315
+
316
+ test('renders HTML content safely', async () => {
317
+ const container = await createHeading('', '<strong>Bold</strong> heading')
318
+ const heading = container.querySelector('pkt-heading') as PktHeading
319
+
320
+ expect(heading.innerHTML).toContain('<strong>Bold</strong>')
321
+ expect(heading.innerHTML).toContain('heading')
322
+ expect(heading.querySelector('strong')).toBeInTheDocument()
323
+ })
324
+
325
+ test('renders multiple child elements', async () => {
326
+ const container = document.createElement('div')
327
+ container.innerHTML = `
328
+ <pkt-heading>
329
+ <span>Part 1</span>
330
+ <em>Part 2</em>
331
+ </pkt-heading>
332
+ `
333
+ document.body.appendChild(container)
334
+ await waitForCustomElements()
335
+
336
+ const heading = container.querySelector('pkt-heading') as PktHeading
337
+ await heading.updateComplete
338
+
339
+ expect(heading.querySelector('span')).toBeInTheDocument()
340
+ expect(heading.querySelector('em')).toBeInTheDocument()
341
+ expect(heading.textContent).toContain('Part 1')
342
+ expect(heading.textContent).toContain('Part 2')
343
+ })
344
+ })
345
+
346
+ describe('Accessibility', () => {
347
+ test('basic heading is accessible', async () => {
348
+ const container = await createHeading('', 'Accessible Heading')
349
+ const heading = container.querySelector('pkt-heading') as PktHeading
350
+ await heading.updateComplete
351
+
352
+ const results = await axe(heading)
353
+ expect(results).toHaveNoViolations()
354
+ })
355
+
356
+ test('heading with different levels is accessible', async () => {
357
+ const levels: TPktHeadingLevel[] = [1, 2, 3, 4, 5, 6]
358
+
359
+ for (const level of levels) {
360
+ const container = await createHeading(`level="${level}"`, `Level ${level} Heading`)
361
+ const heading = container.querySelector('pkt-heading') as PktHeading
362
+ await heading.updateComplete
363
+
364
+ const results = await axe(heading)
365
+ expect(results).toHaveNoViolations()
366
+ document.body.innerHTML = ''
367
+ }
368
+ })
369
+
370
+ test('visually hidden heading is accessible', async () => {
371
+ const container = await createHeading('visually-hidden="true"', 'Hidden Heading')
372
+ const heading = container.querySelector('pkt-heading') as PktHeading
373
+ await heading.updateComplete
374
+
375
+ const results = await axe(heading)
376
+ expect(results).toHaveNoViolations()
377
+ })
378
+
379
+ test('heading with different alignments is accessible', async () => {
380
+ const alignments = ['start', 'center', 'end'] as const
381
+
382
+ for (const align of alignments) {
383
+ const container = await createHeading(`align="${align}"`, `${align} aligned heading`)
384
+ const heading = container.querySelector('pkt-heading') as PktHeading
385
+ await heading.updateComplete
386
+
387
+ const results = await axe(heading)
388
+ expect(results).toHaveNoViolations()
389
+ document.body.innerHTML = ''
390
+ }
391
+ })
392
+
393
+ test('complex heading content is accessible', async () => {
394
+ const container = document.createElement('div')
395
+ container.innerHTML = `
396
+ <pkt-heading level="1" size="large" align="center">
397
+ <span>Main</span> <em>Title</em> with <strong>emphasis</strong>
398
+ </pkt-heading>
399
+ `
400
+ document.body.appendChild(container)
401
+ await waitForCustomElements()
402
+
403
+ const heading = container.querySelector('pkt-heading') as PktHeading
404
+ await heading.updateComplete
405
+
406
+ const results = await axe(heading)
407
+ expect(results).toHaveNoViolations()
408
+ })
409
+ })
410
+
411
+ describe('Integration scenarios', () => {
412
+ test('works correctly with multiple headings', async () => {
413
+ const container = document.createElement('div')
414
+ container.innerHTML = `
415
+ <pkt-heading level="1" size="xlarge">Main Title</pkt-heading>
416
+ <pkt-heading level="2" size="large">Subtitle</pkt-heading>
417
+ <pkt-heading level="3" size="medium">Section</pkt-heading>
418
+ `
419
+ document.body.appendChild(container)
420
+ await waitForCustomElements()
421
+
422
+ const headings = container.querySelectorAll('pkt-heading') as NodeListOf<PktHeading>
423
+ await Promise.all([...headings].map((h) => h.updateComplete))
424
+
425
+ expect(headings[0].level).toBe(1)
426
+ expect(headings[0].size).toBe('xlarge')
427
+ expect(headings[1].level).toBe(2)
428
+ expect(headings[1].size).toBe('large')
429
+ expect(headings[2].level).toBe(3)
430
+ expect(headings[2].size).toBe('medium')
431
+ })
432
+
433
+ test('maintains independence between multiple instances', async () => {
434
+ const container = document.createElement('div')
435
+ container.innerHTML = `
436
+ <pkt-heading id="h1" level="1" size="large">Heading 1</pkt-heading>
437
+ <pkt-heading id="h2" level="2" size="small">Heading 2</pkt-heading>
438
+ `
439
+ document.body.appendChild(container)
440
+ await waitForCustomElements()
441
+
442
+ const heading1 = container.querySelector('#h1') as PktHeading
443
+ const heading2 = container.querySelector('#h2') as PktHeading
444
+ await Promise.all([heading1.updateComplete, heading2.updateComplete])
445
+
446
+ // Change properties on first heading
447
+ heading1.size = 'xlarge'
448
+ heading1.align = 'center'
449
+ await heading1.updateComplete
450
+
451
+ // Verify second heading is unaffected
452
+ expect(heading2.size).toBe('small')
453
+ expect(heading2.align).toBe('start')
454
+ expect(heading1.size).toBe('xlarge')
455
+ expect(heading1.align).toBe('center')
456
+ })
457
+ })
458
+ })
@@ -31,6 +31,9 @@ export class PktHeading extends PktShadowElement<IPktHeading> implements IPktHea
31
31
  if (name === 'level' && value) {
32
32
  this.setLevel(Number(value) as TPktHeadingLevel)
33
33
  }
34
+ if (name === 'visuallyHidden') {
35
+ this.visuallyHidden = value !== null && value !== 'false'
36
+ }
34
37
  if (name === 'size' || name === 'visuallyHidden' || name === 'align') {
35
38
  this.updateHostClasses()
36
39
  }