@oslokommune/punkt-elements 13.4.2 → 13.5.0

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,268 @@
1
+ import '@testing-library/jest-dom'
2
+ import { fireEvent } from '@testing-library/dom'
3
+
4
+ import './datepicker'
5
+ import '../calendar/calendar'
6
+ import { PktDatepicker } from './datepicker'
7
+
8
+ const waitForCustomElements = async () => {
9
+ await customElements.whenDefined('pkt-datepicker')
10
+ await customElements.whenDefined('pkt-calendar')
11
+ }
12
+
13
+ // Helper function to create datepicker markup
14
+ const createDatepicker = async (datepickerProps = '') => {
15
+ const container = document.createElement('div')
16
+ container.innerHTML = `
17
+ <pkt-datepicker ${datepickerProps}></pkt-datepicker>
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('PktDatepicker', () => {
30
+ describe('Date input validation and formatting', () => {
31
+ test('validates date input format', async () => {
32
+ const container = await createDatepicker('label="Test"')
33
+
34
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
35
+ await datepicker.updateComplete
36
+
37
+ const input = datepicker.querySelector('input') as HTMLInputElement
38
+
39
+ // Test invalid date input - HTML5 date inputs will reject invalid formats
40
+ // For date inputs, we test boundary validation instead
41
+ fireEvent.change(input, { target: { value: '2024-02-30' } }) // Invalid date (Feb 30th)
42
+ fireEvent.blur(input)
43
+ await datepicker.updateComplete
44
+
45
+ // Should show validation error or handle gracefully
46
+ // For HTML5 date inputs, this might not trigger hasError, so we test that it doesn't crash
47
+ expect(datepicker).toBeInTheDocument()
48
+ })
49
+
50
+ test('formats dates according to dateformat property', async () => {
51
+ const container = await createDatepicker('dateformat="yyyy-MM-dd" value="2024-06-15"')
52
+
53
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
54
+ await datepicker.updateComplete
55
+
56
+ const input = datepicker.querySelector('input') as HTMLInputElement
57
+ expect(input.value).toBe('2024-06-15')
58
+ })
59
+
60
+ test('handles different date formats', async () => {
61
+ const testCases = [
62
+ { format: 'dd.MM.yyyy', expected: /\d{2}\.\d{2}\.\d{4}/ },
63
+ { format: 'MM/dd/yyyy', expected: /\d{2}\/\d{2}\/\d{4}/ },
64
+ { format: 'yyyy-MM-dd', expected: /\d{4}-\d{2}-\d{2}/ },
65
+ ]
66
+
67
+ for (const testCase of testCases) {
68
+ const container = await createDatepicker(
69
+ `dateformat="${testCase.format}" value="2024-06-15" multiple`,
70
+ )
71
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
72
+ await datepicker.updateComplete
73
+
74
+ // Multiple mode input should be empty (for new input)
75
+ const input = datepicker.querySelector('input') as HTMLInputElement
76
+ expect(input.value).toBe('')
77
+
78
+ // Selected dates should show in tags with custom format
79
+ const tag = datepicker.querySelector('pkt-tag time')
80
+ expect(tag).toBeInTheDocument()
81
+ if (tag) {
82
+ expect(tag.textContent).toMatch(testCase.expected)
83
+ }
84
+
85
+ // Cleanup
86
+ container.remove()
87
+ }
88
+ })
89
+
90
+ test('handles leap year dates correctly', async () => {
91
+ const leapYearDate = '2024-02-29'
92
+ const container = await createDatepicker(`value="${leapYearDate}"`)
93
+
94
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
95
+ await datepicker.updateComplete
96
+
97
+ expect(datepicker.value).toBe(leapYearDate)
98
+ expect(datepicker.hasError).toBe(false)
99
+ })
100
+
101
+ test('validates February 29 in non-leap years', async () => {
102
+ const invalidLeapDate = '2023-02-29'
103
+ const container = await createDatepicker(`value="${invalidLeapDate}"`)
104
+
105
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
106
+ await datepicker.updateComplete
107
+
108
+ // Should handle gracefully or show error
109
+ expect(datepicker).toBeInTheDocument()
110
+ })
111
+
112
+ test('handles edge case dates', async () => {
113
+ const edgeDates = ['1900-01-01', '2000-01-01', '2100-12-31', '1999-12-31']
114
+
115
+ for (const date of edgeDates) {
116
+ const container = await createDatepicker(`value="${date}"`)
117
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
118
+ await datepicker.updateComplete
119
+
120
+ expect(datepicker.value).toBe(date)
121
+ container.remove()
122
+ }
123
+ })
124
+ })
125
+
126
+ describe('Input field behavior', () => {
127
+ test('allows manual date entry', async () => {
128
+ const container = await createDatepicker('dateformat="yyyy-MM-dd"')
129
+
130
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
131
+ await datepicker.updateComplete
132
+
133
+ const input = datepicker.querySelector('input') as HTMLInputElement
134
+
135
+ fireEvent.change(input, { target: { value: '2024-06-15' } })
136
+ fireEvent.blur(input)
137
+ await datepicker.updateComplete
138
+
139
+ expect(datepicker.value).toBe('2024-06-15')
140
+ })
141
+
142
+ test('validates manual date entry', async () => {
143
+ const container = await createDatepicker()
144
+
145
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
146
+ await datepicker.updateComplete
147
+
148
+ const input = datepicker.querySelector('input') as HTMLInputElement
149
+
150
+ // HTML5 date inputs reject invalid formats, so we test that the component handles this gracefully
151
+ fireEvent.change(input, { target: { value: 'invalid-date' } })
152
+ fireEvent.blur(input)
153
+ await datepicker.updateComplete
154
+
155
+ // HTML5 date input will reject invalid dates, component should handle gracefully
156
+ expect(datepicker).toBeInTheDocument()
157
+ expect(input.value).toBe('') // Invalid dates become empty
158
+ })
159
+
160
+ test('shows placeholder text correctly', async () => {
161
+ const placeholderText = 'Select a date'
162
+ const container = await createDatepicker(`placeholder="${placeholderText}"`)
163
+
164
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
165
+ await datepicker.updateComplete
166
+
167
+ const input = datepicker.querySelector('input') as HTMLInputElement
168
+ expect(input.placeholder).toBe(placeholderText)
169
+ })
170
+
171
+ test('shows help text correctly', async () => {
172
+ const helpText = 'Choose your preferred date'
173
+ const container = await createDatepicker(`helptext="${helpText}"`)
174
+
175
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
176
+ await datepicker.updateComplete
177
+
178
+ const helpTextElement = datepicker.querySelector('pkt-helptext')
179
+ expect(helpTextElement?.textContent).toContain(helpText)
180
+ })
181
+
182
+ test('handles readonly state correctly', async () => {
183
+ const container = await createDatepicker('readonly')
184
+
185
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
186
+ await datepicker.updateComplete
187
+
188
+ const input = datepicker.querySelector('input') as HTMLInputElement
189
+ expect(input.readOnly).toBe(true)
190
+
191
+ // Calendar should still be accessible
192
+ const calendarButton = datepicker.querySelector('button[type="button"]') as HTMLButtonElement
193
+ expect(calendarButton.disabled).toBe(false)
194
+ })
195
+ })
196
+
197
+ describe('Keyboard navigation and interaction', () => {
198
+ test('opens calendar with Enter key on calendar button', async () => {
199
+ const container = await createDatepicker()
200
+
201
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
202
+ await datepicker.updateComplete
203
+
204
+ const calendarButton = datepicker.querySelector('button[type="button"]') as HTMLElement
205
+ if (calendarButton) {
206
+ calendarButton.focus()
207
+
208
+ fireEvent.keyDown(calendarButton, { key: 'Enter' })
209
+ await datepicker.updateComplete
210
+
211
+ expect(datepicker.calendarOpen).toBe(true)
212
+ }
213
+ })
214
+
215
+ test('opens calendar with Space key on calendar button', async () => {
216
+ const container = await createDatepicker()
217
+
218
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
219
+ await datepicker.updateComplete
220
+
221
+ const calendarButton = datepicker.querySelector('button[type="button"]') as HTMLElement
222
+ if (calendarButton) {
223
+ calendarButton.focus()
224
+
225
+ fireEvent.keyDown(calendarButton, { key: ' ' })
226
+ await datepicker.updateComplete
227
+
228
+ expect(datepicker.calendarOpen).toBe(true)
229
+ }
230
+ })
231
+
232
+ test('closes calendar with Escape key', async () => {
233
+ const container = await createDatepicker()
234
+
235
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
236
+ await datepicker.updateComplete
237
+
238
+ // Open calendar
239
+ const calendarButton = datepicker.querySelector('button[type="button"]')
240
+ fireEvent.click(calendarButton!)
241
+ await datepicker.updateComplete
242
+
243
+ // Press Escape
244
+ fireEvent.keyDown(datepicker, { key: 'Escape' })
245
+ await datepicker.updateComplete
246
+
247
+ expect(datepicker.calendarOpen).toBe(false)
248
+ })
249
+
250
+ test('navigates between tags with arrow keys in multiple mode', async () => {
251
+ const multipleDates = '2024-06-15,2024-06-20,2024-06-25'
252
+ const container = await createDatepicker(`value="${multipleDates}" multiple`)
253
+
254
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
255
+ await datepicker.updateComplete
256
+
257
+ const tags = datepicker.querySelectorAll('pkt-tag')
258
+ const firstTag = tags[0] as HTMLElement
259
+
260
+ firstTag.focus()
261
+ fireEvent.keyDown(firstTag, { key: 'ArrowRight' })
262
+ await datepicker.updateComplete
263
+
264
+ // Focus should move to next tag
265
+ expect(document.activeElement).toBeTruthy()
266
+ })
267
+ })
268
+ })
@@ -0,0 +1,286 @@
1
+ import '@testing-library/jest-dom'
2
+ import { fireEvent } from '@testing-library/dom'
3
+
4
+ import './datepicker'
5
+ import '../calendar/calendar'
6
+ import { PktDatepicker } from './datepicker'
7
+
8
+ const waitForCustomElements = async () => {
9
+ await customElements.whenDefined('pkt-datepicker')
10
+ await customElements.whenDefined('pkt-calendar')
11
+ }
12
+
13
+ // Helper function to create datepicker markup
14
+ const createDatepicker = async (datepickerProps = '') => {
15
+ const container = document.createElement('div')
16
+ container.innerHTML = `
17
+ <pkt-datepicker ${datepickerProps}></pkt-datepicker>
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('PktDatepicker', () => {
30
+ describe('Multiple date selection', () => {
31
+ test('displays multiple selected dates as tags', async () => {
32
+ const multipleDates = '2024-06-15,2024-06-20,2024-06-25'
33
+ const container = await createDatepicker(`value="${multipleDates}" multiple`)
34
+
35
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
36
+ await datepicker.updateComplete
37
+
38
+ const tags = datepicker.querySelectorAll('pkt-tag')
39
+ expect(tags.length).toBe(3)
40
+ })
41
+
42
+ test('allows adding dates through calendar in multiple mode', async () => {
43
+ const container = await createDatepicker('multiple')
44
+
45
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
46
+ await datepicker.updateComplete
47
+
48
+ // Open calendar
49
+ const calendarButton = datepicker.querySelector('button[type="button"]')
50
+ fireEvent.click(calendarButton!)
51
+ await datepicker.updateComplete
52
+
53
+ // Select a date
54
+ const availableDate = datepicker.querySelector('[data-date]:not([data-disabled="disabled"])')
55
+ if (availableDate) {
56
+ fireEvent.click(availableDate)
57
+ await datepicker.updateComplete
58
+
59
+ // Should add tag
60
+ const tags = datepicker.querySelectorAll('pkt-tag')
61
+ expect(tags.length).toBe(1)
62
+ }
63
+ })
64
+
65
+ test('removes dates when clicking tag close button', async () => {
66
+ const multipleDates = '2024-06-15,2024-06-20'
67
+ const container = await createDatepicker(`value="${multipleDates}" multiple`)
68
+
69
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
70
+ await datepicker.updateComplete
71
+
72
+ const closeButtons = datepicker.querySelectorAll('pkt-tag .pkt-tag__close-btn')
73
+ expect(closeButtons.length).toBe(2)
74
+
75
+ // Click first close button
76
+ fireEvent.click(closeButtons[0])
77
+ await datepicker.updateComplete
78
+
79
+ const remainingTags = datepicker.querySelectorAll('pkt-tag')
80
+ expect(remainingTags.length).toBe(1)
81
+ })
82
+
83
+ test('respects maxlength in multiple mode', async () => {
84
+ const container = await createDatepicker('multiple maxlength="2"')
85
+
86
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
87
+ await datepicker.updateComplete
88
+
89
+ // Open calendar and try to select more than maxlength dates
90
+ const calendarButton = datepicker.querySelector('button[type="button"]')
91
+ fireEvent.click(calendarButton!)
92
+ await datepicker.updateComplete
93
+
94
+ const availableDates = datepicker.querySelectorAll(
95
+ '.pkt-calendar__date:not(.pkt-calendar__date--disabled)',
96
+ )
97
+
98
+ // Select 3 dates
99
+ fireEvent.click(availableDates[0])
100
+ await datepicker.updateComplete
101
+ fireEvent.click(availableDates[1])
102
+ await datepicker.updateComplete
103
+ fireEvent.click(availableDates[2])
104
+ await datepicker.updateComplete
105
+
106
+ // Should only have maxlength tags
107
+ const tags = datepicker.querySelectorAll('pkt-tag')
108
+ expect(tags.length).toBeLessThanOrEqual(2)
109
+ })
110
+
111
+ test('sorts multiple dates chronologically', async () => {
112
+ const unsortedDates = '2024-06-25,2024-06-15,2024-06-20'
113
+ const container = await createDatepicker(`value="${unsortedDates}" multiple`)
114
+
115
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
116
+ await datepicker.updateComplete
117
+
118
+ const tags = datepicker.querySelectorAll('pkt-tag')
119
+ const tagTexts = Array.from(tags).map((tag) => tag.textContent?.trim())
120
+
121
+ // Should be sorted chronologically
122
+ expect(tagTexts[0]).toContain('15')
123
+ expect(tagTexts[1]).toContain('20')
124
+ expect(tagTexts[2]).toContain('25')
125
+ })
126
+ })
127
+
128
+ describe('Range selection', () => {
129
+ test('displays range labels when showRangeLabels is true', async () => {
130
+ const rangeValue = '2024-06-15,2024-06-20'
131
+ const container = await createDatepicker(`value="${rangeValue}" range showRangeLabels`)
132
+
133
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
134
+ await datepicker.updateComplete
135
+
136
+ expect(datepicker.showRangeLabels).toBe(true)
137
+
138
+ const rangeLabels = datepicker.querySelectorAll('.pkt-input-prefix')
139
+ expect(rangeLabels.length).toBeGreaterThan(0)
140
+ })
141
+
142
+ test('populates both input fields when initialized with range value', async () => {
143
+ const rangeValue = '2024-06-15,2024-06-20'
144
+ const container = await createDatepicker(`value="${rangeValue}" range`)
145
+
146
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
147
+ await datepicker.updateComplete
148
+
149
+ const inputs = datepicker.querySelectorAll('input')
150
+ expect(inputs.length).toBe(2)
151
+
152
+ // Check that both input fields are populated
153
+ expect(inputs[0].value).toBe('2024-06-15')
154
+ expect(inputs[1].value).toBe('2024-06-20')
155
+
156
+ // Check internal state
157
+ expect(datepicker._value).toEqual(['2024-06-15', '2024-06-20'])
158
+ expect(datepicker.value).toBe('2024-06-15,2024-06-20')
159
+ })
160
+
161
+ test('dispatches value-change event with array for range and multiple datepickers', async () => {
162
+ // Test range datepicker
163
+ const rangeContainer = await createDatepicker('range')
164
+ const rangeDatepicker = rangeContainer.querySelector('pkt-datepicker') as PktDatepicker
165
+ await rangeDatepicker.updateComplete
166
+
167
+ let valueChangeEvent: CustomEvent | null = null
168
+ rangeDatepicker.addEventListener('value-change', (e: Event) => {
169
+ valueChangeEvent = e as CustomEvent
170
+ })
171
+
172
+ // Set a range value programmatically
173
+ rangeDatepicker.value = '2024-06-15,2024-06-20'
174
+ await rangeDatepicker.updateComplete
175
+
176
+ expect(valueChangeEvent).toBeTruthy()
177
+ expect(valueChangeEvent!.detail).toEqual(['2024-06-15', '2024-06-20'])
178
+
179
+ // Test multiple datepicker
180
+ const multipleContainer = await createDatepicker('multiple')
181
+ const multipleDatepicker = multipleContainer.querySelector('pkt-datepicker') as PktDatepicker
182
+ await multipleDatepicker.updateComplete
183
+
184
+ valueChangeEvent = null
185
+ multipleDatepicker.addEventListener('value-change', (e: Event) => {
186
+ valueChangeEvent = e as CustomEvent
187
+ })
188
+
189
+ // Set multiple values programmatically
190
+ multipleDatepicker.value = '2024-06-15,2024-06-20,2024-06-25'
191
+ await multipleDatepicker.updateComplete
192
+
193
+ expect(valueChangeEvent).toBeTruthy()
194
+ expect(valueChangeEvent!.detail).toEqual(['2024-06-15', '2024-06-20', '2024-06-25'])
195
+
196
+ // Test single datepicker
197
+ const singleContainer = await createDatepicker('')
198
+ const singleDatepicker = singleContainer.querySelector('pkt-datepicker') as PktDatepicker
199
+ await singleDatepicker.updateComplete
200
+
201
+ valueChangeEvent = null
202
+ singleDatepicker.addEventListener('value-change', (e: Event) => {
203
+ valueChangeEvent = e as CustomEvent
204
+ })
205
+
206
+ // Set single value programmatically
207
+ singleDatepicker.value = '2024-06-15'
208
+ await singleDatepicker.updateComplete
209
+
210
+ expect(valueChangeEvent).toBeTruthy()
211
+ expect(valueChangeEvent!.detail).toBe('2024-06-15')
212
+ })
213
+
214
+ test('handles range selection through calendar', async () => {
215
+ const container = await createDatepicker('range')
216
+
217
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
218
+ await datepicker.updateComplete
219
+
220
+ // Open calendar
221
+ const calendarButton = datepicker.querySelector('button[type="button"]')
222
+ fireEvent.click(calendarButton!)
223
+ await datepicker.updateComplete
224
+
225
+ const availableDates = datepicker.querySelectorAll(
226
+ '[data-date]:not([data-disabled="disabled"])',
227
+ )
228
+
229
+ // Select start date
230
+ if (availableDates.length > 5) {
231
+ fireEvent.click(availableDates[5])
232
+ await datepicker.updateComplete
233
+
234
+ // Select end date
235
+ if (availableDates.length > 10) {
236
+ fireEvent.click(availableDates[10])
237
+ await datepicker.updateComplete
238
+
239
+ // Should have range value
240
+ expect(datepicker.value).toContain(',')
241
+ const values = (datepicker.value as string).split(',')
242
+ expect(values.length).toBe(2)
243
+ }
244
+ }
245
+ })
246
+
247
+ test('validates range order', async () => {
248
+ const invalidRange = '2024-06-20,2024-06-15' // End before start
249
+ const container = await createDatepicker(`value="${invalidRange}" range`)
250
+
251
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
252
+ await datepicker.updateComplete
253
+
254
+ // Should handle invalid range gracefully
255
+ expect(datepicker).toBeInTheDocument()
256
+ })
257
+
258
+ test('shows range preview on hover', async () => {
259
+ const container = await createDatepicker('range')
260
+
261
+ const datepicker = container.querySelector('pkt-datepicker') as PktDatepicker
262
+ await datepicker.updateComplete
263
+
264
+ // Open calendar
265
+ const calendarButton = datepicker.querySelector('button[type="button"]')
266
+ fireEvent.click(calendarButton!)
267
+ await datepicker.updateComplete
268
+
269
+ const availableDates = datepicker.querySelectorAll(
270
+ '.pkt-calendar__date:not(.pkt-calendar__date--disabled)',
271
+ )
272
+
273
+ // Select start date
274
+ fireEvent.click(availableDates[5])
275
+ await datepicker.updateComplete
276
+
277
+ // Hover over potential end date
278
+ fireEvent.mouseOver(availableDates[10])
279
+ await datepicker.updateComplete
280
+
281
+ // Should show hover preview
282
+ const hoverRanges = datepicker.querySelectorAll('.pkt-calendar__date--in-range-hover')
283
+ expect(hoverRanges.length).toBeGreaterThan(0)
284
+ })
285
+ })
286
+ })