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