@fmidev/smartmet-alert-client 4.4.19 → 4.7.0-alpha.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.
Files changed (105) hide show
  1. package/.eslintignore +2 -14
  2. package/.github/workflows/test.yaml +26 -0
  3. package/.nvmrc +1 -0
  4. package/dist/index.html +5 -0
  5. package/dist/index.js +105 -135
  6. package/dist/index.mjs +112 -135
  7. package/dist/locale-en-DCEKDw5G.js +8 -0
  8. package/dist/locale-fi-DPiOM1rB.js +8 -0
  9. package/dist/locale-sv-B0FlbgEF.js +8 -0
  10. package/dist/vendor-Cfkkvdz7.js +21 -0
  11. package/dist/vue/index.mjs +15245 -0
  12. package/dist/vue/style.css +1 -0
  13. package/dist/xml-parser-BiNO9kc-.js +13 -0
  14. package/package.json +60 -24
  15. package/src/AlertClientVue.vue +170 -0
  16. package/src/App.vue +55 -205
  17. package/src/assets/img/ui/arrow-down.svg +4 -11
  18. package/src/assets/img/ui/arrow-up.svg +4 -11
  19. package/src/assets/img/ui/clear.svg +7 -21
  20. package/src/assets/img/ui/close.svg +4 -15
  21. package/src/assets/img/ui/toggle-selected.svg +5 -6
  22. package/src/assets/img/ui/toggle-unselected.svg +5 -6
  23. package/src/assets/img/warning/cold-weather.svg +3 -6
  24. package/src/assets/img/warning/flood-level-3.svg +4 -7
  25. package/src/assets/img/warning/forest-fire-weather.svg +2 -6
  26. package/src/assets/img/warning/grass-fire-weather.svg +2 -6
  27. package/src/assets/img/warning/hot-weather.svg +3 -6
  28. package/src/assets/img/warning/pedestrian-safety.svg +3 -7
  29. package/src/assets/img/warning/rain.svg +2 -7
  30. package/src/assets/img/warning/sea-icing.svg +2 -6
  31. package/src/assets/img/warning/sea-thunder-storm.svg +2 -5
  32. package/src/assets/img/warning/sea-water-height-high-water.svg +3 -8
  33. package/src/assets/img/warning/sea-water-height-shallow-water.svg +3 -7
  34. package/src/assets/img/warning/sea-wave-height.svg +4 -7
  35. package/src/assets/img/warning/sea-wind-legend.svg +2 -5
  36. package/src/assets/img/warning/sea-wind.svg +2 -5
  37. package/src/assets/img/warning/several.svg +2 -5
  38. package/src/assets/img/warning/thunder-storm.svg +2 -5
  39. package/src/assets/img/warning/traffic-weather.svg +2 -6
  40. package/src/assets/img/warning/uv-note.svg +2 -6
  41. package/src/assets/img/warning/wind.svg +2 -5
  42. package/src/components/AlertClient.vue +41 -19
  43. package/src/components/CollapsiblePanel.vue +284 -0
  44. package/src/components/DayLarge.vue +12 -7
  45. package/src/components/DaySmall.vue +16 -6
  46. package/src/components/Days.vue +76 -51
  47. package/src/components/DescriptionWarning.vue +15 -8
  48. package/src/components/GrayScaleToggle.vue +11 -6
  49. package/src/components/Legend.vue +36 -248
  50. package/src/components/MapLarge.vue +41 -42
  51. package/src/components/MapSmall.vue +44 -28
  52. package/src/components/PopupRow.vue +6 -3
  53. package/src/components/Region.vue +30 -15
  54. package/src/components/RegionWarning.vue +6 -5
  55. package/src/components/Regions.vue +50 -19
  56. package/src/components/Warning.vue +18 -10
  57. package/src/components/Warnings.vue +36 -21
  58. package/src/main.js +1 -0
  59. package/src/mixins/alertClientCore.js +210 -0
  60. package/src/mixins/config.js +262 -256
  61. package/src/mixins/utils.js +40 -26
  62. package/src/plugins/index.js +1 -1
  63. package/src/scss/_utilities.scss +193 -0
  64. package/src/scss/constants.scss +2 -1
  65. package/src/scss/warningImages.scss +8 -3
  66. package/src/vue.js +41 -0
  67. package/svgo.config.js +45 -0
  68. package/tests/README.md +430 -0
  69. package/tests/fixtures/mockWarningData.js +135 -0
  70. package/tests/integration/warning-flow.spec.js +452 -0
  71. package/tests/setup.js +41 -0
  72. package/tests/unit/components/AlertClient.spec.js +734 -0
  73. package/tests/unit/components/DayLarge.spec.js +281 -0
  74. package/tests/unit/components/DaySmall.spec.js +278 -0
  75. package/tests/unit/components/Days.spec.js +565 -0
  76. package/tests/unit/components/DescriptionWarning.spec.js +432 -0
  77. package/tests/unit/components/GrayScaleToggle.spec.js +311 -0
  78. package/tests/unit/components/Legend.spec.js +223 -0
  79. package/tests/unit/components/MapLarge.spec.js +276 -0
  80. package/tests/unit/components/MapSmall.spec.js +226 -0
  81. package/tests/unit/components/PopupRow.spec.js +261 -0
  82. package/tests/unit/components/Region.spec.js +430 -0
  83. package/tests/unit/components/RegionWarning.snapshot.spec.js +73 -0
  84. package/tests/unit/components/RegionWarning.spec.js +408 -0
  85. package/tests/unit/components/Regions.spec.js +335 -0
  86. package/tests/unit/components/Warning.snapshot.spec.js +107 -0
  87. package/tests/unit/components/Warning.spec.js +472 -0
  88. package/tests/unit/components/Warnings.spec.js +329 -0
  89. package/tests/unit/components/__snapshots__/RegionWarning.snapshot.spec.js.snap +21 -0
  90. package/tests/unit/components/__snapshots__/Warning.snapshot.spec.js.snap +199 -0
  91. package/tests/unit/mixins/config.spec.js +269 -0
  92. package/tests/unit/mixins/i18n.spec.js +115 -0
  93. package/tests/unit/mixins/keycodes.spec.js +37 -0
  94. package/tests/unit/mixins/utils.spec.js +624 -0
  95. package/vite.config.js +96 -26
  96. package/vitest.config.js +40 -0
  97. package/dist/index.mjs.map +0 -1
  98. package/dist/index.relative.html +0 -19
  99. package/dist/index.start.html +0 -20
  100. package/playwright.config.ts +0 -18
  101. package/public/index.relative.html +0 -19
  102. package/public/index.start.html +0 -20
  103. package/src/mixins/panzoom.js +0 -900
  104. package/test/snapshot.test.ts +0 -126
  105. package/vitest.config.ts +0 -6
@@ -0,0 +1,734 @@
1
+ import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest'
2
+ import { mount, flushPromises } from '@vue/test-utils'
3
+ import AlertClient from '@/components/AlertClient.vue'
4
+ import Days from '@/components/Days.vue'
5
+ import Regions from '@/components/Regions.vue'
6
+ import Legend from '@/components/Legend.vue'
7
+ import { mockWarningsData } from '../../fixtures/mockWarningData'
8
+
9
+ describe('AlertClient.vue', () => {
10
+ let wrapper
11
+
12
+ afterEach(() => {
13
+ if (wrapper) {
14
+ wrapper.unmount()
15
+ }
16
+ vi.restoreAllMocks()
17
+ })
18
+
19
+ describe('Component mounting', () => {
20
+ it('should mount with default props and render child components', () => {
21
+ wrapper = mount(AlertClient, {
22
+ props: {
23
+ language: 'fi',
24
+ },
25
+ })
26
+
27
+ // Verify component exists
28
+ expect(wrapper.exists()).toBe(true)
29
+
30
+ // Verify child components are rendered
31
+ expect(wrapper.findComponent(Days).exists()).toBe(true)
32
+ expect(wrapper.findComponent(Regions).exists()).toBe(true)
33
+ expect(wrapper.findComponent(Legend).exists()).toBe(true)
34
+
35
+ // Verify default state
36
+ expect(wrapper.vm.selectedDay).toBe(0)
37
+ expect(wrapper.vm.theme).toBe('light-theme')
38
+ expect(wrapper.vm.geometryId).toBe(2021)
39
+ expect(wrapper.vm.visibleWarnings).toEqual([])
40
+ expect(wrapper.vm.warnings).toBeNull()
41
+ })
42
+
43
+ it('should use provided default day prop', () => {
44
+ wrapper = mount(AlertClient, {
45
+ props: {
46
+ defaultDay: 2,
47
+ language: 'fi',
48
+ },
49
+ })
50
+
51
+ expect(wrapper.vm.selectedDay).toBe(2)
52
+ })
53
+
54
+ it('should initialize with correct data', () => {
55
+ wrapper = mount(AlertClient, {
56
+ props: {
57
+ language: 'fi',
58
+ },
59
+ })
60
+
61
+ expect(wrapper.vm.selectedDay).toBeDefined()
62
+ expect(wrapper.vm.visibleWarnings).toEqual([])
63
+ expect(wrapper.vm.warnings).toBeNull()
64
+ expect(wrapper.vm.days).toEqual([])
65
+ })
66
+ })
67
+
68
+ describe('Props validation', () => {
69
+ it('should accept refresh interval prop', () => {
70
+ wrapper = mount(AlertClient, {
71
+ props: {
72
+ refreshInterval: 60000,
73
+ language: 'fi',
74
+ },
75
+ })
76
+
77
+ expect(wrapper.vm.refreshInterval).toBe(60000)
78
+ })
79
+
80
+ it('should use default refresh interval', () => {
81
+ wrapper = mount(AlertClient, {
82
+ props: {
83
+ language: 'fi',
84
+ },
85
+ })
86
+
87
+ expect(wrapper.vm.refreshInterval).toBe(900000) // 15 minutes
88
+ })
89
+
90
+ it('should accept warnings data prop', () => {
91
+ wrapper = mount(AlertClient, {
92
+ props: {
93
+ warningsData: mockWarningsData,
94
+ language: 'fi',
95
+ },
96
+ })
97
+
98
+ expect(wrapper.vm.warningsData).toBeDefined()
99
+ })
100
+
101
+ it('should accept theme prop', () => {
102
+ wrapper = mount(AlertClient, {
103
+ props: {
104
+ theme: 'dark-theme',
105
+ language: 'fi',
106
+ },
107
+ })
108
+
109
+ expect(wrapper.vm.theme).toBe('dark-theme')
110
+ })
111
+
112
+ it('should accept geometry ID prop', () => {
113
+ wrapper = mount(AlertClient, {
114
+ props: {
115
+ geometryId: 2021,
116
+ language: 'fi',
117
+ },
118
+ })
119
+
120
+ expect(wrapper.vm.geometryId).toBe(2021)
121
+ })
122
+ })
123
+
124
+ describe('Timer functionality', () => {
125
+ it('should initialize timer with correct interval', () => {
126
+ const setIntervalSpy = vi.spyOn(global, 'setInterval')
127
+
128
+ wrapper = mount(AlertClient, {
129
+ props: {
130
+ refreshInterval: 60000,
131
+ language: 'fi',
132
+ },
133
+ })
134
+
135
+ expect(wrapper.vm.timer).toBeDefined()
136
+ expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 60000)
137
+ })
138
+
139
+ it('should not initialize timer when refresh interval is 0', () => {
140
+ wrapper = mount(AlertClient, {
141
+ props: {
142
+ refreshInterval: 0,
143
+ language: 'fi',
144
+ },
145
+ })
146
+
147
+ // Timer should not be initialized when interval is 0
148
+ expect(wrapper.vm.timer).toBeNull()
149
+ })
150
+
151
+ it('should have cancelTimer method', () => {
152
+ wrapper = mount(AlertClient, {
153
+ props: {
154
+ refreshInterval: 60000,
155
+ language: 'fi',
156
+ },
157
+ })
158
+
159
+ expect(wrapper.vm.timer).toBeDefined()
160
+ expect(typeof wrapper.vm.cancelTimer).toBe('function')
161
+
162
+ // cancelTimer should be callable without errors
163
+ expect(() => wrapper.vm.cancelTimer()).not.toThrow()
164
+ })
165
+
166
+ it('should have initTimer method', () => {
167
+ wrapper = mount(AlertClient, {
168
+ props: {
169
+ refreshInterval: 60000,
170
+ language: 'fi',
171
+ },
172
+ })
173
+
174
+ expect(typeof wrapper.vm.initTimer).toBe('function')
175
+ expect(wrapper.vm.timer).toBeDefined()
176
+ })
177
+
178
+ it('should handle multiple timer cancellations safely', () => {
179
+ wrapper = mount(AlertClient, {
180
+ props: {
181
+ refreshInterval: 60000,
182
+ language: 'fi',
183
+ },
184
+ })
185
+
186
+ // Multiple cancellations should not throw errors
187
+ expect(() => {
188
+ wrapper.vm.cancelTimer()
189
+ wrapper.vm.cancelTimer()
190
+ wrapper.vm.cancelTimer()
191
+ }).not.toThrow()
192
+ })
193
+ })
194
+
195
+ describe('Event emissions', () => {
196
+ it('should emit loaded event on data loaded', async () => {
197
+ wrapper = mount(AlertClient, {
198
+ props: {
199
+ warningsData: mockWarningsData,
200
+ language: 'fi',
201
+ },
202
+ })
203
+
204
+ await flushPromises()
205
+
206
+ wrapper.vm.onLoaded(true)
207
+
208
+ expect(wrapper.emitted('loaded')).toBeTruthy()
209
+ })
210
+
211
+ it('should emit themeChanged event', () => {
212
+ wrapper = mount(AlertClient, {
213
+ props: {
214
+ language: 'fi',
215
+ },
216
+ })
217
+
218
+ wrapper.vm.onThemeChanged('dark-theme')
219
+
220
+ expect(wrapper.emitted('themeChanged')).toBeTruthy()
221
+ expect(wrapper.emitted('themeChanged')[0]).toEqual(['dark-theme'])
222
+ })
223
+
224
+ it('should emit update-warnings event on update', () => {
225
+ wrapper = mount(AlertClient, {
226
+ props: {
227
+ refreshInterval: 60000,
228
+ language: 'fi',
229
+ },
230
+ })
231
+
232
+ wrapper.vm.update()
233
+
234
+ expect(wrapper.emitted('update-warnings')).toBeTruthy()
235
+ })
236
+
237
+ it('should not emit themeChanged if theme is same', () => {
238
+ wrapper = mount(AlertClient, {
239
+ props: {
240
+ theme: 'light-theme',
241
+ language: 'fi',
242
+ },
243
+ })
244
+
245
+ wrapper.vm.onThemeChanged('light-theme')
246
+
247
+ expect(wrapper.emitted('themeChanged')).toBeFalsy()
248
+ })
249
+ })
250
+
251
+ describe('Data processing', () => {
252
+ it('should process warnings data when provided', async () => {
253
+ wrapper = mount(AlertClient, {
254
+ props: {
255
+ warningsData: mockWarningsData,
256
+ language: 'fi',
257
+ },
258
+ })
259
+
260
+ await flushPromises()
261
+
262
+ expect(wrapper.vm.warnings).toBeDefined()
263
+ expect(wrapper.vm.days).toBeDefined()
264
+ expect(wrapper.vm.days.length).toBe(5)
265
+ })
266
+
267
+ it('should create visible warnings from legend', async () => {
268
+ wrapper = mount(AlertClient, {
269
+ props: {
270
+ warningsData: mockWarningsData,
271
+ language: 'fi',
272
+ },
273
+ })
274
+
275
+ await flushPromises()
276
+
277
+ expect(Array.isArray(wrapper.vm.visibleWarnings)).toBe(true)
278
+ })
279
+
280
+ it('should update regions data', async () => {
281
+ wrapper = mount(AlertClient, {
282
+ props: {
283
+ warningsData: mockWarningsData,
284
+ language: 'fi',
285
+ },
286
+ })
287
+
288
+ await flushPromises()
289
+
290
+ expect(wrapper.vm.regions).toBeDefined()
291
+ expect(Array.isArray(wrapper.vm.regions)).toBe(true)
292
+ })
293
+ })
294
+
295
+ describe('Day selection', () => {
296
+ it('should update selected day on daySelected event', () => {
297
+ wrapper = mount(AlertClient, {
298
+ props: {
299
+ language: 'fi',
300
+ },
301
+ })
302
+
303
+ wrapper.vm.onDaySelected(2)
304
+
305
+ expect(wrapper.vm.selectedDay).toBe(2)
306
+ })
307
+
308
+ it('should start with default day', () => {
309
+ wrapper = mount(AlertClient, {
310
+ props: {
311
+ defaultDay: 3,
312
+ language: 'fi',
313
+ },
314
+ })
315
+
316
+ expect(wrapper.vm.selectedDay).toBe(3)
317
+ })
318
+ })
319
+
320
+ describe('Warnings toggle', () => {
321
+ it('should update visible warnings on toggle', async () => {
322
+ wrapper = mount(AlertClient, {
323
+ props: {
324
+ warningsData: mockWarningsData,
325
+ language: 'fi',
326
+ },
327
+ })
328
+
329
+ await flushPromises()
330
+
331
+ const newVisibleWarnings = ['wind', 'rain']
332
+ wrapper.vm.onWarningsToggled(newVisibleWarnings)
333
+
334
+ expect(wrapper.vm.visibleWarnings).toEqual(newVisibleWarnings)
335
+ })
336
+
337
+ it('should update legend visibility', async () => {
338
+ wrapper = mount(AlertClient, {
339
+ props: {
340
+ warningsData: mockWarningsData,
341
+ language: 'fi',
342
+ },
343
+ })
344
+
345
+ await flushPromises()
346
+
347
+ if (wrapper.vm.legend.length > 0) {
348
+ const firstWarningType = wrapper.vm.legend[0].type
349
+ wrapper.vm.onWarningsToggled([firstWarningType])
350
+
351
+ const legendItem = wrapper.vm.legend.find(
352
+ (item) => item.type === firstWarningType
353
+ )
354
+ expect(legendItem.visible).toBe(true)
355
+ }
356
+ })
357
+ })
358
+
359
+ describe('Error handling', () => {
360
+ it('should handle errors in error array', () => {
361
+ wrapper = mount(AlertClient, {
362
+ props: {
363
+ language: 'fi',
364
+ },
365
+ })
366
+
367
+ const testError = 'test_error'
368
+ wrapper.vm.handleError(testError)
369
+
370
+ expect(wrapper.vm.errors).toContain(testError)
371
+ })
372
+
373
+ it('should not duplicate errors', () => {
374
+ wrapper = mount(AlertClient, {
375
+ props: {
376
+ language: 'fi',
377
+ },
378
+ })
379
+
380
+ const testError = 'test_error'
381
+ wrapper.vm.handleError(testError)
382
+ wrapper.vm.handleError(testError)
383
+
384
+ expect(wrapper.vm.errors.filter((e) => e === testError).length).toBe(1)
385
+ })
386
+
387
+ it('should handle null warnings data gracefully', async () => {
388
+ wrapper = mount(AlertClient, {
389
+ props: {
390
+ warningsData: null,
391
+ language: 'fi',
392
+ },
393
+ })
394
+
395
+ await flushPromises()
396
+
397
+ expect(wrapper.vm.warnings).toBeNull()
398
+ expect(wrapper.vm.days).toEqual([])
399
+ expect(wrapper.exists()).toBe(true)
400
+ })
401
+
402
+ it('should handle undefined warnings data gracefully', async () => {
403
+ wrapper = mount(AlertClient, {
404
+ props: {
405
+ warningsData: undefined,
406
+ language: 'fi',
407
+ },
408
+ })
409
+
410
+ await flushPromises()
411
+
412
+ expect(wrapper.vm.warnings).toBeNull()
413
+ expect(wrapper.exists()).toBe(true)
414
+ })
415
+
416
+ it('should handle malformed warnings data', async () => {
417
+ const malformedData = {
418
+ invalid: 'data',
419
+ structure: 'wrong',
420
+ }
421
+
422
+ wrapper = mount(AlertClient, {
423
+ props: {
424
+ warningsData: malformedData,
425
+ language: 'fi',
426
+ },
427
+ })
428
+
429
+ await flushPromises()
430
+
431
+ // Component should still exist and not crash
432
+ expect(wrapper.exists()).toBe(true)
433
+ })
434
+
435
+ it('should handle empty warnings data', async () => {
436
+ const emptyData = {
437
+ weather_update_time: '2025-10-31T12:00:00Z',
438
+ flood_update_time: '2025-10-31T12:00:00Z',
439
+ weather_finland_active_all: { features: [] },
440
+ flood_finland_active_all: { features: [] },
441
+ }
442
+
443
+ wrapper = mount(AlertClient, {
444
+ props: {
445
+ warningsData: emptyData,
446
+ language: 'fi',
447
+ },
448
+ })
449
+
450
+ await flushPromises()
451
+
452
+ expect(wrapper.vm.warnings).toBeDefined()
453
+ expect(wrapper.vm.days).toHaveLength(5)
454
+ expect(wrapper.exists()).toBe(true)
455
+ })
456
+ })
457
+
458
+ describe('Computed properties', () => {
459
+ it('should compute toContentText correctly', async () => {
460
+ wrapper = mount(AlertClient, {
461
+ props: {
462
+ warningsData: mockWarningsData,
463
+ language: 'fi',
464
+ regionListEnabled: true,
465
+ },
466
+ })
467
+
468
+ await flushPromises()
469
+
470
+ expect(wrapper.vm.toContentText).toBeDefined()
471
+ expect(typeof wrapper.vm.toContentText).toBe('string')
472
+ })
473
+
474
+ it('should compute validData correctly with warnings', async () => {
475
+ wrapper = mount(AlertClient, {
476
+ props: {
477
+ warningsData: mockWarningsData,
478
+ language: 'fi',
479
+ },
480
+ })
481
+
482
+ await flushPromises()
483
+
484
+ expect(typeof wrapper.vm.validData).toBe('boolean')
485
+ })
486
+
487
+ it('should compute numWarnings correctly', async () => {
488
+ wrapper = mount(AlertClient, {
489
+ props: {
490
+ warningsData: mockWarningsData,
491
+ language: 'fi',
492
+ },
493
+ })
494
+
495
+ await flushPromises()
496
+
497
+ expect(typeof wrapper.vm.numWarnings).toBe('number')
498
+ expect(wrapper.vm.numWarnings).toBeGreaterThanOrEqual(0)
499
+ })
500
+ })
501
+
502
+ describe('Visibility change handling', () => {
503
+ it('should handle visibility change when document becomes hidden', () => {
504
+ const cancelTimerSpy = vi.spyOn(AlertClient.methods, 'cancelTimer')
505
+
506
+ wrapper = mount(AlertClient, {
507
+ props: {
508
+ refreshInterval: 60000,
509
+ sleep: true,
510
+ language: 'fi',
511
+ },
512
+ })
513
+
514
+ // Simulate document becoming hidden
515
+ Object.defineProperty(document, 'hidden', {
516
+ writable: true,
517
+ value: true,
518
+ })
519
+
520
+ wrapper.vm.visibilityChange()
521
+
522
+ expect(cancelTimerSpy).toHaveBeenCalled()
523
+ })
524
+
525
+ it('should restart timer when document becomes visible again', () => {
526
+ const initTimerSpy = vi.spyOn(AlertClient.methods, 'initTimer')
527
+
528
+ wrapper = mount(AlertClient, {
529
+ props: {
530
+ refreshInterval: 60000,
531
+ sleep: true,
532
+ language: 'fi',
533
+ },
534
+ })
535
+
536
+ // Hide document (cancels timer)
537
+ Object.defineProperty(document, 'hidden', {
538
+ writable: true,
539
+ value: true,
540
+ })
541
+ wrapper.vm.visibilityChange()
542
+
543
+ // Show document again (should try to restart timer)
544
+ Object.defineProperty(document, 'hidden', {
545
+ writable: true,
546
+ value: false,
547
+ })
548
+ wrapper.vm.visibilityChange()
549
+
550
+ expect(initTimerSpy).toHaveBeenCalled()
551
+ })
552
+ })
553
+
554
+ describe('Accessibility', () => {
555
+ it('should have proper ARIA labels on main container', async () => {
556
+ wrapper = mount(AlertClient, {
557
+ props: {
558
+ warningsData: mockWarningsData,
559
+ language: 'fi',
560
+ },
561
+ })
562
+
563
+ await flushPromises()
564
+
565
+ const container = wrapper.find('#fmi-warnings-view')
566
+ expect(container.exists()).toBe(true)
567
+ })
568
+
569
+ it('should provide accessible navigation structure', async () => {
570
+ wrapper = mount(AlertClient, {
571
+ props: {
572
+ warningsData: mockWarningsData,
573
+ language: 'fi',
574
+ },
575
+ })
576
+
577
+ await flushPromises()
578
+
579
+ // Check that Days component can be navigated
580
+ const days = wrapper.findComponent(Days)
581
+ expect(days.exists()).toBe(true)
582
+ })
583
+
584
+ it('should support keyboard navigation', async () => {
585
+ wrapper = mount(AlertClient, {
586
+ props: {
587
+ warningsData: mockWarningsData,
588
+ language: 'fi',
589
+ },
590
+ })
591
+
592
+ await flushPromises()
593
+
594
+ // Verify focus-ring class is used for keyboard navigation
595
+ const focusableElements = wrapper.findAll('.focus-ring')
596
+ expect(focusableElements.length).toBeGreaterThan(0)
597
+ })
598
+
599
+ it('should have proper language attribute', () => {
600
+ wrapper = mount(AlertClient, {
601
+ props: {
602
+ language: 'fi',
603
+ },
604
+ })
605
+
606
+ expect(wrapper.vm.language).toBe('fi')
607
+ })
608
+ })
609
+
610
+ describe('Edge cases', () => {
611
+ it('should handle invalid geometry ID', () => {
612
+ wrapper = mount(AlertClient, {
613
+ props: {
614
+ geometryId: -1,
615
+ language: 'fi',
616
+ },
617
+ })
618
+
619
+ expect(wrapper.exists()).toBe(true)
620
+ expect(wrapper.vm.geometryId).toBe(-1)
621
+ })
622
+
623
+ it('should handle very large refresh interval', () => {
624
+ wrapper = mount(AlertClient, {
625
+ props: {
626
+ refreshInterval: 2147483647, // Max 32-bit signed integer
627
+ language: 'fi',
628
+ },
629
+ })
630
+
631
+ expect(wrapper.vm.timer).toBeDefined()
632
+ })
633
+
634
+ it('should handle invalid theme gracefully', () => {
635
+ wrapper = mount(AlertClient, {
636
+ props: {
637
+ theme: 'invalid-theme',
638
+ language: 'fi',
639
+ },
640
+ })
641
+
642
+ expect(wrapper.exists()).toBe(true)
643
+ expect(wrapper.vm.theme).toBe('invalid-theme')
644
+ })
645
+
646
+ it('should accept valid default day range', () => {
647
+ wrapper = mount(AlertClient, {
648
+ props: {
649
+ defaultDay: 4,
650
+ language: 'fi',
651
+ },
652
+ })
653
+
654
+ // Component should mount with valid day value
655
+ expect(wrapper.exists()).toBe(true)
656
+ expect(wrapper.vm.selectedDay).toBe(4)
657
+ })
658
+
659
+ it('should handle concurrent day selections', async () => {
660
+ wrapper = mount(AlertClient, {
661
+ props: {
662
+ warningsData: mockWarningsData,
663
+ language: 'fi',
664
+ },
665
+ })
666
+
667
+ await flushPromises()
668
+
669
+ // Rapidly change days
670
+ wrapper.vm.onDaySelected(1)
671
+ wrapper.vm.onDaySelected(2)
672
+ wrapper.vm.onDaySelected(3)
673
+ wrapper.vm.onDaySelected(4)
674
+
675
+ // Should end up with last selected day
676
+ expect(wrapper.vm.selectedDay).toBe(4)
677
+ })
678
+ })
679
+
680
+ describe('Performance', () => {
681
+ it('should handle multiple mount and unmount cycles', () => {
682
+ // Create and destroy multiple instances
683
+ for (let i = 0; i < 3; i++) {
684
+ const w = mount(AlertClient, {
685
+ props: {
686
+ refreshInterval: 60000,
687
+ language: 'fi',
688
+ },
689
+ })
690
+ expect(w.exists()).toBe(true)
691
+ w.unmount()
692
+ }
693
+
694
+ // Test completed without errors
695
+ expect(true).toBe(true)
696
+ })
697
+
698
+ it('should handle large warning datasets efficiently', async () => {
699
+ const largeDataset = {
700
+ ...mockWarningsData,
701
+ weather_finland_active_all: {
702
+ features: Array(100)
703
+ .fill(null)
704
+ .map((_, i) => ({
705
+ ...mockWarningsData.weather_finland_active_all.features[0],
706
+ properties: {
707
+ ...mockWarningsData.weather_finland_active_all.features[0]
708
+ .properties,
709
+ identifier: `warning-${i}`,
710
+ },
711
+ })),
712
+ },
713
+ }
714
+
715
+ const startTime = performance.now()
716
+
717
+ wrapper = mount(AlertClient, {
718
+ props: {
719
+ warningsData: largeDataset,
720
+ language: 'fi',
721
+ },
722
+ })
723
+
724
+ await flushPromises()
725
+
726
+ const endTime = performance.now()
727
+ const duration = endTime - startTime
728
+
729
+ // Processing should complete in reasonable time (< 1000ms)
730
+ expect(duration).toBeLessThan(1000)
731
+ expect(wrapper.exists()).toBe(true)
732
+ })
733
+ })
734
+ })