@oslokommune/punkt-elements 13.4.2 → 13.5.1
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 +35 -0
- package/dist/calendar-32W9p9uc.cjs +115 -0
- package/dist/{calendar-DevQhOup.js → calendar-CJSxvwAq.js} +353 -340
- package/dist/{card-Dtw26f7i.js → card-BDz4RWxK.js} +1 -1
- package/dist/{card-BUITGoqX.cjs → card-DBlFf1ry.cjs} +1 -1
- package/dist/{datepicker-CYOn3tRm.js → datepicker-BJKJBoy_.js} +102 -59
- package/dist/datepicker-CmTrG5GE.cjs +164 -0
- package/dist/{heading-D6jXE_Mz.js → heading-Bdh9absf.js} +22 -22
- package/dist/heading-CNycsyMj.cjs +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/pkt-calendar.cjs +1 -1
- package/dist/pkt-calendar.js +1 -1
- package/dist/pkt-card.cjs +1 -1
- package/dist/pkt-card.js +1 -1
- package/dist/pkt-datepicker.cjs +1 -1
- package/dist/pkt-datepicker.js +1 -1
- package/dist/pkt-heading.cjs +1 -1
- package/dist/pkt-heading.js +1 -1
- package/dist/pkt-index.cjs +1 -1
- package/dist/pkt-index.js +5 -5
- package/package.json +3 -3
- package/src/components/calendar/calendar.accessibility.test.ts +111 -0
- package/src/components/calendar/calendar.constraints.test.ts +110 -0
- package/src/components/calendar/calendar.core.test.ts +367 -0
- package/src/components/calendar/calendar.interaction.test.ts +139 -0
- package/src/components/calendar/calendar.selection.test.ts +273 -0
- package/src/components/calendar/calendar.ts +74 -42
- package/src/components/card/card.test.ts +19 -5
- package/src/components/consent/consent.test.ts +436 -0
- package/src/components/datepicker/datepicker.accessibility.test.ts +193 -0
- package/src/components/datepicker/datepicker.core.test.ts +322 -0
- package/src/components/datepicker/datepicker.input.test.ts +268 -0
- package/src/components/datepicker/datepicker.selection.test.ts +286 -0
- package/src/components/datepicker/datepicker.ts +121 -19
- package/src/components/datepicker/datepicker.validation.test.ts +176 -0
- package/src/components/heading/heading.test.ts +458 -0
- package/src/components/heading/heading.ts +3 -0
- package/src/components/helptext/helptext.test.ts +474 -0
- package/dist/calendar-BZe2D4Sr.cjs +0 -108
- package/dist/datepicker-B9rhz_AF.cjs +0 -154
- package/dist/heading-BRE_iFtR.cjs +0 -1
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
|
|
3
|
+
import './datepicker'
|
|
4
|
+
import '../calendar/calendar'
|
|
5
|
+
import { PktDatepicker } from './datepicker'
|
|
6
|
+
|
|
7
|
+
const waitForCustomElements = async () => {
|
|
8
|
+
await customElements.whenDefined('pkt-datepicker')
|
|
9
|
+
await customElements.whenDefined('pkt-calendar')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Helper function to create datepicker markup
|
|
13
|
+
const createDatepicker = async (datepickerProps = '') => {
|
|
14
|
+
const container = document.createElement('div')
|
|
15
|
+
container.innerHTML = `
|
|
16
|
+
<pkt-datepicker ${datepickerProps}></pkt-datepicker>
|
|
17
|
+
`
|
|
18
|
+
document.body.appendChild(container)
|
|
19
|
+
await waitForCustomElements()
|
|
20
|
+
return container
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Cleanup after each test
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
document.body.innerHTML = ''
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('PktDatepicker', () => {
|
|
29
|
+
describe('Date boundary testing', () => {
|
|
30
|
+
test('handles minimum possible dates', async () => {
|
|
31
|
+
const minDate = '1900-01-01'
|
|
32
|
+
const container = await createDatepicker(`value="${minDate}" earliest="${minDate}"`)
|
|
33
|
+
|
|
34
|
+
const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
|
|
35
|
+
await datepicker.updateComplete
|
|
36
|
+
|
|
37
|
+
expect(datepicker.value).toBe(minDate)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('handles maximum possible dates', async () => {
|
|
41
|
+
const maxDate = '2100-12-31'
|
|
42
|
+
const container = await createDatepicker(`value="${maxDate}" latest="${maxDate}"`)
|
|
43
|
+
|
|
44
|
+
const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
|
|
45
|
+
await datepicker.updateComplete
|
|
46
|
+
|
|
47
|
+
expect(datepicker.value).toBe(maxDate)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('handles year boundaries correctly', async () => {
|
|
51
|
+
const yearBoundaryDates = ['1999-12-31', '2000-01-01', '2000-12-31', '2001-01-01']
|
|
52
|
+
|
|
53
|
+
for (const date of yearBoundaryDates) {
|
|
54
|
+
const container = await createDatepicker(`value="${date}"`)
|
|
55
|
+
const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
|
|
56
|
+
await datepicker.updateComplete
|
|
57
|
+
|
|
58
|
+
expect(datepicker.value).toBe(date)
|
|
59
|
+
container.remove()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('handles month boundaries correctly', async () => {
|
|
64
|
+
const monthBoundaryDates = [
|
|
65
|
+
'2024-01-31',
|
|
66
|
+
'2024-02-01',
|
|
67
|
+
'2024-02-28',
|
|
68
|
+
'2024-02-29', // Leap year
|
|
69
|
+
'2024-03-01',
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
for (const date of monthBoundaryDates) {
|
|
73
|
+
const container = await createDatepicker(`value="${date}"`)
|
|
74
|
+
const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
|
|
75
|
+
await datepicker.updateComplete
|
|
76
|
+
|
|
77
|
+
expect(datepicker.value).toBe(date)
|
|
78
|
+
container.remove()
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('validates dates against earliest/latest boundaries', async () => {
|
|
83
|
+
const container = await createDatepicker('min="2024-06-10" max="2024-06-20"')
|
|
84
|
+
|
|
85
|
+
const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
|
|
86
|
+
await datepicker.updateComplete
|
|
87
|
+
|
|
88
|
+
const input = datepicker.querySelector('input') as HTMLInputElement
|
|
89
|
+
|
|
90
|
+
// Test date before min - component should filter it out
|
|
91
|
+
datepicker.value = '2024-06-05'
|
|
92
|
+
await datepicker.updateComplete
|
|
93
|
+
|
|
94
|
+
// Component should reject invalid date (filter it out)
|
|
95
|
+
expect(datepicker.value).toBe('')
|
|
96
|
+
expect(input.value).toBe('')
|
|
97
|
+
|
|
98
|
+
// Test date after max - component should filter it out
|
|
99
|
+
datepicker.value = '2024-06-25'
|
|
100
|
+
await datepicker.updateComplete
|
|
101
|
+
|
|
102
|
+
// Component should reject invalid date (filter it out)
|
|
103
|
+
expect(datepicker.value).toBe('')
|
|
104
|
+
expect(input.value).toBe('')
|
|
105
|
+
|
|
106
|
+
// Test valid date - should be accepted
|
|
107
|
+
datepicker.value = '2024-06-15'
|
|
108
|
+
await datepicker.updateComplete
|
|
109
|
+
|
|
110
|
+
// Valid date should be accepted
|
|
111
|
+
expect(datepicker.value).toBe('2024-06-15')
|
|
112
|
+
expect(input.value).toBe('2024-06-15')
|
|
113
|
+
expect(input.validity.valid).toBe(true)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('Error handling and edge cases', () => {
|
|
118
|
+
test('handles invalid date formats gracefully', async () => {
|
|
119
|
+
const container = await createDatepicker('value="not-a-date"')
|
|
120
|
+
|
|
121
|
+
const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
|
|
122
|
+
await datepicker.updateComplete
|
|
123
|
+
|
|
124
|
+
// Should not crash
|
|
125
|
+
expect(datepicker).toBeInTheDocument()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('handles empty values correctly', async () => {
|
|
129
|
+
const container = await createDatepicker('value=""')
|
|
130
|
+
|
|
131
|
+
const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
|
|
132
|
+
await datepicker.updateComplete
|
|
133
|
+
|
|
134
|
+
expect(datepicker.value).toBe('')
|
|
135
|
+
expect(datepicker.hasError).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('handles malformed multiple values', async () => {
|
|
139
|
+
const malformedDates = '2024-06-15,,2024-06-20,,,'
|
|
140
|
+
const container = await createDatepicker(`value="${malformedDates}" multiple`)
|
|
141
|
+
|
|
142
|
+
const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
|
|
143
|
+
await datepicker.updateComplete
|
|
144
|
+
|
|
145
|
+
// Should filter out empty values
|
|
146
|
+
const tags = datepicker.querySelectorAll('pkt-tag')
|
|
147
|
+
expect(tags.length).toBe(2) // Only valid dates
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('handles very long value strings', async () => {
|
|
151
|
+
// Create a string with many dates
|
|
152
|
+
const longValueArray = Array.from({ length: 100 }, (_, i) => `2024-06-${(i % 30) + 1}`)
|
|
153
|
+
const longValue = longValueArray.join(',')
|
|
154
|
+
const container = await createDatepicker(`value="${longValue}" multiple maxlength="50"`)
|
|
155
|
+
|
|
156
|
+
const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
|
|
157
|
+
await datepicker.updateComplete
|
|
158
|
+
|
|
159
|
+
// Should handle gracefully - maxlength may or may not be enforced at render time
|
|
160
|
+
const tags = datepicker.querySelectorAll('pkt-tag')
|
|
161
|
+
expect(tags.length).toBeGreaterThan(0) // Should show some tags
|
|
162
|
+
expect(datepicker).toBeInTheDocument() // Should not crash
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('handles conflicting properties gracefully', async () => {
|
|
166
|
+
// Both multiple and range shouldn't typically be used together
|
|
167
|
+
const container = await createDatepicker('multiple range value="2024-06-15,2024-06-20"')
|
|
168
|
+
|
|
169
|
+
const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
|
|
170
|
+
await datepicker.updateComplete
|
|
171
|
+
|
|
172
|
+
// Should still render without errors
|
|
173
|
+
expect(datepicker).toBeInTheDocument()
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
})
|
|
@@ -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
|
}
|