@oslokommune/punkt-elements 13.5.1 → 13.5.3

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,368 @@
1
+ import '@testing-library/jest-dom'
2
+ import { axe, toHaveNoViolations } from 'jest-axe'
3
+ import {
4
+ createElementTest,
5
+ BaseTestConfig,
6
+ setupConsoleMocking,
7
+ restoreConsoleMocking,
8
+ } from '../../tests/test-framework'
9
+ import { CustomElementFor } from '../../tests/component-registry'
10
+ import './icon'
11
+ import { PktIcon } from './icon'
12
+
13
+ expect.extend(toHaveNoViolations)
14
+
15
+ export interface IconTestConfig extends BaseTestConfig {
16
+ name?: string
17
+ path?: string
18
+ }
19
+
20
+ // Use shared framework
21
+ export const createIconTest = async (config: IconTestConfig = {}) => {
22
+ const { container, element } = await createElementTest<
23
+ CustomElementFor<'pkt-icon'>,
24
+ IconTestConfig
25
+ >('pkt-icon', config)
26
+
27
+ return {
28
+ container,
29
+ icon: element,
30
+ }
31
+ }
32
+
33
+ // Cleanup after each test
34
+ afterEach(() => {
35
+ document.body.innerHTML = ''
36
+ // Clean up localStorage after tests
37
+ localStorage.clear()
38
+ // Reset global variables
39
+ delete (window as any).pktFetch
40
+ delete (window as any).pktIconPath
41
+ // Restore console mocking
42
+ restoreConsoleMocking()
43
+ })
44
+
45
+ // Mock fetch for icon loading
46
+ const mockFetch = jest.fn()
47
+ const mockSvgContent =
48
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="test-path"></path></svg>'
49
+
50
+ beforeEach(() => {
51
+ // Setup console mocking to suppress error logs during tests
52
+ setupConsoleMocking()
53
+
54
+ // Setup default mocks
55
+ mockFetch.mockResolvedValue({
56
+ ok: true,
57
+ text: () => Promise.resolve(mockSvgContent),
58
+ })
59
+ window.pktFetch = mockFetch
60
+ window.pktIconPath = 'https://test-cdn.example.com/icons/'
61
+ })
62
+
63
+ describe('PktIcon', () => {
64
+ describe('Rendering and basic functionality', () => {
65
+ test('renders without errors', async () => {
66
+ const { icon } = await createIconTest()
67
+
68
+ expect(icon).toBeInTheDocument()
69
+ await icon.updateComplete
70
+ expect(icon.classList.contains('pkt-icon')).toBe(true)
71
+ })
72
+
73
+ test('renders with default structure', async () => {
74
+ const { icon } = await createIconTest({
75
+ name: 'arrow-right',
76
+ })
77
+ await icon.updateComplete
78
+
79
+ expect(icon).toBeInTheDocument()
80
+ expect(icon.classList.contains('pkt-icon')).toBe(true)
81
+ })
82
+
83
+ test('renders nothing when no name is provided', async () => {
84
+ const { icon } = await createIconTest()
85
+ await icon.updateComplete
86
+
87
+ // Should render nothing meaningful when no name is provided (only Lit template comments)
88
+ expect(icon.innerHTML).not.toContain('<svg')
89
+ expect(icon.name).toBe('')
90
+ })
91
+ })
92
+
93
+ describe('Properties and attributes', () => {
94
+ test('applies default properties correctly', async () => {
95
+ const { icon } = await createIconTest()
96
+ await icon.updateComplete
97
+
98
+ expect(icon.name).toBe('')
99
+ expect(icon.path).toBe('https://test-cdn.example.com/icons/')
100
+ })
101
+
102
+ test('sets name property correctly', async () => {
103
+ const { icon } = await createIconTest({
104
+ name: 'arrow-right',
105
+ })
106
+ await icon.updateComplete
107
+
108
+ expect(icon.name).toBe('arrow-right')
109
+ expect(icon.getAttribute('name')).toBe('arrow-right')
110
+ })
111
+
112
+ test('sets path property correctly', async () => {
113
+ const customPath = 'https://custom-cdn.example.com/icons/'
114
+ const { icon } = await createIconTest({
115
+ path: customPath,
116
+ name: 'arrow-right',
117
+ })
118
+ await icon.updateComplete
119
+
120
+ expect(icon.path).toBe(customPath)
121
+ })
122
+
123
+ test('uses global pktIconPath when path not specified', async () => {
124
+ window.pktIconPath = 'https://global-cdn.example.com/icons/'
125
+ const { icon } = await createIconTest({
126
+ name: 'arrow-right',
127
+ })
128
+ await icon.updateComplete
129
+
130
+ expect(icon.path).toBe('https://global-cdn.example.com/icons/')
131
+ })
132
+ })
133
+
134
+ describe('Icon loading functionality', () => {
135
+ test('fetches icon from CDN when name is provided', async () => {
136
+ const { icon } = await createIconTest({
137
+ name: 'arrow-right',
138
+ })
139
+ await icon.updateComplete
140
+
141
+ // Allow some time for async icon loading
142
+ await new Promise((resolve) => setTimeout(resolve, 100))
143
+
144
+ expect(mockFetch).toHaveBeenCalledWith('https://test-cdn.example.com/icons/arrow-right.svg')
145
+ })
146
+
147
+ test('caches loaded icons in localStorage', async () => {
148
+ const { icon } = await createIconTest({
149
+ name: 'arrow-right',
150
+ })
151
+ await icon.updateComplete
152
+
153
+ // Wait for icon to load
154
+ await new Promise((resolve) => setTimeout(resolve, 100))
155
+
156
+ const cachedIcon = localStorage.getItem('https://test-cdn.example.com/icons/arrow-right.svg')
157
+ expect(cachedIcon).toBe(mockSvgContent)
158
+ })
159
+
160
+ test('uses cached icon when available', async () => {
161
+ // Pre-populate cache
162
+ localStorage.setItem('https://test-cdn.example.com/icons/cached-icon.svg', mockSvgContent)
163
+
164
+ const { icon } = await createIconTest({
165
+ name: 'cached-icon',
166
+ })
167
+ await icon.updateComplete
168
+
169
+ // Should not fetch since it's cached
170
+ expect(mockFetch).not.toHaveBeenCalledWith(
171
+ 'https://test-cdn.example.com/icons/cached-icon.svg',
172
+ )
173
+ })
174
+
175
+ test('handles fetch errors gracefully', async () => {
176
+ mockFetch.mockResolvedValueOnce({
177
+ ok: false,
178
+ text: () => Promise.resolve(''),
179
+ })
180
+
181
+ const { icon } = await createIconTest({
182
+ name: 'missing-icon',
183
+ })
184
+ await icon.updateComplete
185
+
186
+ // Should log error and use error SVG
187
+ expect(mockFetch).toHaveBeenCalledWith('https://test-cdn.example.com/icons/missing-icon.svg')
188
+ })
189
+ })
190
+
191
+ describe('Dynamic updates', () => {
192
+ test('updates icon when name changes', async () => {
193
+ const { icon } = await createIconTest({
194
+ name: 'arrow-right',
195
+ })
196
+ await icon.updateComplete
197
+
198
+ // Change the name
199
+ icon.name = 'arrow-left'
200
+ await icon.updateComplete
201
+
202
+ expect(icon.name).toBe('arrow-left')
203
+ expect(icon.getAttribute('name')).toBe('arrow-left')
204
+ })
205
+
206
+ test('updates icon when path changes', async () => {
207
+ const { icon } = await createIconTest({
208
+ name: 'arrow-right',
209
+ })
210
+ await icon.updateComplete
211
+
212
+ const newPath = 'https://new-cdn.example.com/icons/'
213
+ icon.path = newPath
214
+ await icon.updateComplete
215
+
216
+ expect(icon.path).toBe(newPath)
217
+ })
218
+
219
+ test('re-fetches icon when path changes', async () => {
220
+ const { icon } = await createIconTest({
221
+ name: 'arrow-right',
222
+ })
223
+ await icon.updateComplete
224
+
225
+ // Allow initial load to complete
226
+ await new Promise((resolve) => setTimeout(resolve, 100))
227
+
228
+ mockFetch.mockClear()
229
+ // Setup mock again for the new path
230
+ mockFetch.mockResolvedValue({
231
+ ok: true,
232
+ text: () => Promise.resolve(mockSvgContent),
233
+ })
234
+
235
+ const newPath = 'https://new-cdn.example.com/icons/'
236
+
237
+ // Try setting attribute to trigger attributeChangedCallback
238
+ icon.setAttribute('path', newPath)
239
+ await icon.updateComplete
240
+
241
+ // Allow some time for async icon loading
242
+ await new Promise((resolve) => setTimeout(resolve, 100))
243
+
244
+ expect(mockFetch).toHaveBeenCalledWith('https://new-cdn.example.com/icons/arrow-right.svg')
245
+ })
246
+ })
247
+
248
+ describe('CSS classes and styling', () => {
249
+ test('applies pkt-icon class', async () => {
250
+ const { icon } = await createIconTest({
251
+ name: 'arrow-right',
252
+ })
253
+ await icon.updateComplete
254
+
255
+ expect(icon.classList.contains('pkt-icon')).toBe(true)
256
+ })
257
+
258
+ test('maintains pkt-icon class after updates', async () => {
259
+ const { icon } = await createIconTest({
260
+ name: 'arrow-right',
261
+ })
262
+ await icon.updateComplete
263
+
264
+ icon.name = 'arrow-left'
265
+ await icon.updateComplete
266
+
267
+ expect(icon.classList.contains('pkt-icon')).toBe(true)
268
+ })
269
+ })
270
+
271
+ describe('Global configuration', () => {
272
+ test('uses custom pktFetch function when provided', async () => {
273
+ const customFetch = jest.fn().mockResolvedValue({
274
+ ok: true,
275
+ text: () => Promise.resolve('<svg>custom</svg>'),
276
+ })
277
+
278
+ window.pktFetch = customFetch
279
+
280
+ const { icon } = await createIconTest({
281
+ name: 'custom-icon',
282
+ })
283
+ await icon.updateComplete
284
+
285
+ // Allow some time for async icon loading
286
+ await new Promise((resolve) => setTimeout(resolve, 100))
287
+
288
+ expect(customFetch).toHaveBeenCalledWith('https://test-cdn.example.com/icons/custom-icon.svg')
289
+ })
290
+
291
+ test('falls back to error SVG when pktFetch is not available', async () => {
292
+ delete (window as any).pktFetch
293
+
294
+ const { icon } = await createIconTest({
295
+ name: 'fallback-icon',
296
+ })
297
+ await icon.updateComplete
298
+
299
+ // Allow some time for async icon loading
300
+ await new Promise((resolve) => setTimeout(resolve, 100))
301
+
302
+ // Should render error SVG in light DOM when fetch is not available
303
+ expect(icon.innerHTML).toContain('viewBox="0 0 32 32"')
304
+ })
305
+ })
306
+
307
+ describe('Accessibility', () => {
308
+ test('basic icon is accessible', async () => {
309
+ const { container } = await createIconTest({
310
+ name: 'arrow-right',
311
+ })
312
+ await new Promise((resolve) => setTimeout(resolve, 100))
313
+
314
+ const results = await axe(container)
315
+ expect(results).toHaveNoViolations()
316
+ })
317
+
318
+ test('icon with custom path is accessible', async () => {
319
+ const { container } = await createIconTest({
320
+ name: 'arrow-right',
321
+ path: 'https://custom-cdn.example.com/icons/',
322
+ })
323
+ await new Promise((resolve) => setTimeout(resolve, 100))
324
+
325
+ const results = await axe(container)
326
+ expect(results).toHaveNoViolations()
327
+ })
328
+ })
329
+
330
+ describe('Integration scenarios', () => {
331
+ test('works with multiple icons simultaneously', async () => {
332
+ const container = document.createElement('div')
333
+ container.innerHTML = `
334
+ <pkt-icon name="arrow-right"></pkt-icon>
335
+ <pkt-icon name="arrow-left"></pkt-icon>
336
+ <pkt-icon name="close"></pkt-icon>
337
+ `
338
+ document.body.appendChild(container)
339
+
340
+ // Wait for elements to be defined
341
+ await customElements.whenDefined('pkt-icon')
342
+
343
+ const icons = container.querySelectorAll('pkt-icon')
344
+ expect(icons).toHaveLength(3)
345
+
346
+ for (const icon of icons) {
347
+ await (icon as PktIcon).updateComplete
348
+ expect(icon.classList.contains('pkt-icon')).toBe(true)
349
+ }
350
+ })
351
+
352
+ test('handles rapid name changes correctly', async () => {
353
+ const { icon } = await createIconTest({
354
+ name: 'arrow-right',
355
+ })
356
+ await icon.updateComplete
357
+
358
+ // Rapidly change names
359
+ icon.name = 'arrow-left'
360
+ icon.name = 'close'
361
+ icon.name = 'menu'
362
+ await icon.updateComplete
363
+
364
+ expect(icon.name).toBe('menu')
365
+ expect(icon.getAttribute('name')).toBe('menu')
366
+ })
367
+ })
368
+ })