@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,489 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest'
2
+ import { mount, VueWrapper } from '@vue/test-utils'
3
+ import Warning from '@/components/Warning.vue'
4
+ import type { LegendItem, Theme, Language } from '@/types'
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ type ComponentInstance = any
8
+
9
+ const mockWarning: LegendItem = {
10
+ type: 'wind',
11
+ severity: 3,
12
+ visible: true,
13
+ }
14
+
15
+ describe('Warning.vue', () => {
16
+ let wrapper: VueWrapper | null = null
17
+
18
+ afterEach(() => {
19
+ if (wrapper) {
20
+ wrapper.unmount()
21
+ wrapper = null
22
+ }
23
+ })
24
+
25
+ describe('Component mounting', () => {
26
+ it('should mount with required props', () => {
27
+ wrapper = mount(Warning, {
28
+ props: {
29
+ input: mockWarning,
30
+ hideable: true,
31
+ language: 'fi' as Language,
32
+ theme: 'light-theme' as Theme,
33
+ },
34
+ })
35
+
36
+ expect(wrapper.exists()).toBe(true)
37
+ })
38
+ })
39
+
40
+ describe('Computed properties', () => {
41
+ it('should compute id from warning type', () => {
42
+ wrapper = mount(Warning, {
43
+ props: {
44
+ input: mockWarning,
45
+ hideable: true,
46
+ language: 'fi' as Language,
47
+ theme: 'light-theme' as Theme,
48
+ },
49
+ })
50
+
51
+ expect((wrapper.vm as ComponentInstance).id).toBe(
52
+ 'fmi-warnings-flag-wind'
53
+ )
54
+ })
55
+
56
+ it('should compute title from translation', () => {
57
+ wrapper = mount(Warning, {
58
+ props: {
59
+ input: mockWarning,
60
+ hideable: true,
61
+ language: 'fi' as Language,
62
+ theme: 'light-theme' as Theme,
63
+ },
64
+ })
65
+
66
+ expect(typeof (wrapper.vm as ComponentInstance).title).toBe('string')
67
+ })
68
+
69
+ it('should compute warningLevelText', () => {
70
+ wrapper = mount(Warning, {
71
+ props: {
72
+ input: mockWarning,
73
+ hideable: true,
74
+ language: 'fi' as Language,
75
+ theme: 'light-theme' as Theme,
76
+ },
77
+ })
78
+
79
+ expect(typeof (wrapper.vm as ComponentInstance).warningLevelText).toBe(
80
+ 'string'
81
+ )
82
+ })
83
+
84
+ it('should compute toggleText based on visibility', () => {
85
+ wrapper = mount(Warning, {
86
+ props: {
87
+ input: { ...mockWarning, visible: true },
88
+ hideable: true,
89
+ language: 'fi' as Language,
90
+ theme: 'light-theme' as Theme,
91
+ },
92
+ })
93
+
94
+ const visibleText = (wrapper.vm as ComponentInstance).toggleText
95
+
96
+ wrapper = mount(Warning, {
97
+ props: {
98
+ input: { ...mockWarning, visible: false },
99
+ hideable: true,
100
+ language: 'fi' as Language,
101
+ theme: 'light-theme' as Theme,
102
+ },
103
+ })
104
+
105
+ const hiddenText = (wrapper.vm as ComponentInstance).toggleText
106
+
107
+ expect(typeof visibleText).toBe('string')
108
+ expect(typeof hiddenText).toBe('string')
109
+ })
110
+ })
111
+
112
+ describe('Fields mixin computed properties', () => {
113
+ it('should compute typeClass from warning type', () => {
114
+ wrapper = mount(Warning, {
115
+ props: {
116
+ input: {
117
+ type: 'thunderStorm',
118
+ severity: 4,
119
+ visible: true,
120
+ } as LegendItem,
121
+ hideable: true,
122
+ language: 'fi' as Language,
123
+ theme: 'light-theme' as Theme,
124
+ },
125
+ })
126
+
127
+ expect((wrapper.vm as ComponentInstance).typeClass).toBe('thunder-storm')
128
+ })
129
+
130
+ it('should compute typeClass for sea wind', () => {
131
+ wrapper = mount(Warning, {
132
+ props: {
133
+ input: { type: 'seaWind', severity: 3, visible: true } as LegendItem,
134
+ hideable: true,
135
+ language: 'fi' as Language,
136
+ theme: 'light-theme' as Theme,
137
+ },
138
+ })
139
+
140
+ expect((wrapper.vm as ComponentInstance).typeClass).toBe('sea-wind')
141
+ })
142
+
143
+ it('should compute rotation from direction', () => {
144
+ wrapper = mount(Warning, {
145
+ props: {
146
+ input: {
147
+ type: 'wind',
148
+ severity: 3,
149
+ visible: true,
150
+ direction: 90,
151
+ } as LegendItem & { direction: number },
152
+ hideable: true,
153
+ language: 'fi' as Language,
154
+ theme: 'light-theme' as Theme,
155
+ },
156
+ })
157
+
158
+ expect((wrapper.vm as ComponentInstance).rotation).toBe(90)
159
+ })
160
+
161
+ it('should round rotation to nearest 5 degrees', () => {
162
+ wrapper = mount(Warning, {
163
+ props: {
164
+ input: {
165
+ type: 'wind',
166
+ severity: 3,
167
+ visible: true,
168
+ direction: 93,
169
+ } as LegendItem & { direction: number },
170
+ hideable: true,
171
+ language: 'fi' as Language,
172
+ theme: 'light-theme' as Theme,
173
+ },
174
+ })
175
+
176
+ expect((wrapper.vm as ComponentInstance).rotation).toBe(95)
177
+ })
178
+
179
+ it('should handle negative direction', () => {
180
+ wrapper = mount(Warning, {
181
+ props: {
182
+ input: {
183
+ type: 'wind',
184
+ severity: 3,
185
+ visible: true,
186
+ direction: -45,
187
+ } as LegendItem & { direction: number },
188
+ hideable: true,
189
+ language: 'fi' as Language,
190
+ theme: 'light-theme' as Theme,
191
+ },
192
+ })
193
+
194
+ expect((wrapper.vm as ComponentInstance).rotation).toBeGreaterThanOrEqual(
195
+ 0
196
+ )
197
+ expect((wrapper.vm as ComponentInstance).rotation).toBeLessThan(360)
198
+ })
199
+
200
+ it('should compute invertedRotation', () => {
201
+ wrapper = mount(Warning, {
202
+ props: {
203
+ input: {
204
+ type: 'wind',
205
+ severity: 3,
206
+ visible: true,
207
+ direction: 90,
208
+ } as LegendItem & { direction: number },
209
+ hideable: true,
210
+ language: 'fi' as Language,
211
+ theme: 'light-theme' as Theme,
212
+ },
213
+ })
214
+
215
+ expect((wrapper.vm as ComponentInstance).invertedRotation).toBe(270)
216
+ })
217
+
218
+ it('should compute severity from input', () => {
219
+ wrapper = mount(Warning, {
220
+ props: {
221
+ input: mockWarning,
222
+ hideable: true,
223
+ language: 'fi' as Language,
224
+ theme: 'light-theme' as Theme,
225
+ },
226
+ })
227
+
228
+ expect((wrapper.vm as ComponentInstance).severity).toBe(3)
229
+ })
230
+
231
+ it('should return 0 for invalid severity levels', () => {
232
+ const invalidSeverities = [0, 1, 5, 6]
233
+
234
+ invalidSeverities.forEach((severity) => {
235
+ wrapper = mount(Warning, {
236
+ props: {
237
+ input: { ...mockWarning, severity } as LegendItem,
238
+ hideable: true,
239
+ language: 'fi' as Language,
240
+ theme: 'light-theme' as Theme,
241
+ },
242
+ })
243
+
244
+ expect((wrapper.vm as ComponentInstance).severity).toBe(0)
245
+ })
246
+ })
247
+
248
+ it('should accept valid severity levels 2-4', () => {
249
+ const validSeverities = [2, 3, 4]
250
+
251
+ validSeverities.forEach((severity) => {
252
+ wrapper = mount(Warning, {
253
+ props: {
254
+ input: { ...mockWarning, severity } as LegendItem,
255
+ hideable: true,
256
+ language: 'fi' as Language,
257
+ theme: 'light-theme' as Theme,
258
+ },
259
+ })
260
+
261
+ expect((wrapper.vm as ComponentInstance).severity).toBe(severity)
262
+ })
263
+ })
264
+ })
265
+
266
+ describe('Toggle functionality', () => {
267
+ it('should emit warningToggled when toggled on', () => {
268
+ wrapper = mount(Warning, {
269
+ props: {
270
+ input: { ...mockWarning, visible: false },
271
+ hideable: true,
272
+ language: 'fi' as Language,
273
+ theme: 'light-theme' as Theme,
274
+ },
275
+ })
276
+
277
+ const event = { preventDefault: vi.fn() }
278
+ ;(wrapper.vm as ComponentInstance).toggle(event)
279
+
280
+ expect(wrapper.emitted('warningToggled')).toBeTruthy()
281
+ expect(wrapper.emitted('warningToggled')![0]![0]).toEqual({
282
+ warning: 'wind',
283
+ visible: true,
284
+ })
285
+ expect(event.preventDefault).toHaveBeenCalled()
286
+ })
287
+
288
+ it('should emit warningToggled when toggled off', () => {
289
+ wrapper = mount(Warning, {
290
+ props: {
291
+ input: { ...mockWarning, visible: true },
292
+ hideable: true,
293
+ language: 'fi' as Language,
294
+ theme: 'light-theme' as Theme,
295
+ },
296
+ })
297
+
298
+ const event = { preventDefault: vi.fn() }
299
+ ;(wrapper.vm as ComponentInstance).toggle(event)
300
+
301
+ expect(wrapper.emitted('warningToggled')![0]![0]).toEqual({
302
+ warning: 'wind',
303
+ visible: false,
304
+ })
305
+ })
306
+
307
+ it('should call setWarningVisibility with correct value', () => {
308
+ wrapper = mount(Warning, {
309
+ props: {
310
+ input: mockWarning,
311
+ hideable: true,
312
+ language: 'fi' as Language,
313
+ theme: 'light-theme' as Theme,
314
+ },
315
+ })
316
+ ;(wrapper.vm as ComponentInstance).setWarningVisibility(false)
317
+
318
+ expect(wrapper.emitted('warningToggled')).toBeTruthy()
319
+ expect(
320
+ (wrapper.emitted('warningToggled')![0]![0] as { visible: boolean })
321
+ .visible
322
+ ).toBe(false)
323
+ })
324
+
325
+ it('should prevent default event', () => {
326
+ wrapper = mount(Warning, {
327
+ props: {
328
+ input: mockWarning,
329
+ hideable: true,
330
+ language: 'fi' as Language,
331
+ theme: 'light-theme' as Theme,
332
+ },
333
+ })
334
+
335
+ const event = { preventDefault: vi.fn() }
336
+ ;(wrapper.vm as ComponentInstance).preventEvents(event)
337
+
338
+ expect(event.preventDefault).toHaveBeenCalled()
339
+ })
340
+ })
341
+
342
+ describe('CSS classes', () => {
343
+ it('should apply severity level class', () => {
344
+ wrapper = mount(Warning, {
345
+ props: {
346
+ input: mockWarning,
347
+ hideable: true,
348
+ language: 'fi' as Language,
349
+ theme: 'light-theme' as Theme,
350
+ },
351
+ })
352
+
353
+ const image = wrapper.find('.warning-image')
354
+ expect(image.classes()).toContain('level-3')
355
+ })
356
+
357
+ it('should apply type class', () => {
358
+ wrapper = mount(Warning, {
359
+ props: {
360
+ input: mockWarning,
361
+ hideable: true,
362
+ language: 'fi' as Language,
363
+ theme: 'light-theme' as Theme,
364
+ },
365
+ })
366
+
367
+ const image = wrapper.find('.warning-image')
368
+ expect(image.classes()).toContain('wind')
369
+ })
370
+
371
+ it('should apply flag-selected class when visible', () => {
372
+ wrapper = mount(Warning, {
373
+ props: {
374
+ input: { ...mockWarning, visible: true },
375
+ hideable: true,
376
+ language: 'fi' as Language,
377
+ theme: 'light-theme' as Theme,
378
+ },
379
+ })
380
+
381
+ const toggle = wrapper.find('.symbol-list-select')
382
+ expect(toggle.classes()).toContain('flag-selected')
383
+ })
384
+
385
+ it('should apply flag-unselected class when not visible', () => {
386
+ wrapper = mount(Warning, {
387
+ props: {
388
+ input: { ...mockWarning, visible: false },
389
+ hideable: true,
390
+ language: 'fi' as Language,
391
+ theme: 'light-theme' as Theme,
392
+ },
393
+ })
394
+
395
+ const toggle = wrapper.find('.symbol-list-select')
396
+ expect(toggle.classes()).toContain('flag-unselected')
397
+ })
398
+
399
+ it('should apply theme class to container', () => {
400
+ wrapper = mount(Warning, {
401
+ props: {
402
+ input: mockWarning,
403
+ hideable: true,
404
+ language: 'fi' as Language,
405
+ theme: 'dark-theme' as Theme,
406
+ },
407
+ })
408
+
409
+ expect(wrapper.find('.symbol-list-table').classes()).toContain(
410
+ 'dark-theme'
411
+ )
412
+ })
413
+ })
414
+
415
+ describe('Accessibility', () => {
416
+ it('should have correct ARIA attributes on toggle', () => {
417
+ wrapper = mount(Warning, {
418
+ props: {
419
+ input: { ...mockWarning, visible: true },
420
+ hideable: true,
421
+ language: 'fi' as Language,
422
+ theme: 'light-theme' as Theme,
423
+ },
424
+ })
425
+
426
+ const toggle = wrapper.find('.symbol-list-select')
427
+ expect(toggle.attributes('role')).toBe('button')
428
+ expect(toggle.attributes('tabindex')).toBe('0')
429
+ expect(toggle.attributes('aria-pressed')).toBe('true')
430
+ })
431
+
432
+ it('should update aria-pressed when visibility changes', () => {
433
+ wrapper = mount(Warning, {
434
+ props: {
435
+ input: { ...mockWarning, visible: false },
436
+ hideable: true,
437
+ language: 'fi' as Language,
438
+ theme: 'light-theme' as Theme,
439
+ },
440
+ })
441
+
442
+ const toggle = wrapper.find('.symbol-list-select')
443
+ expect(toggle.attributes('aria-pressed')).toBe('false')
444
+ })
445
+
446
+ it('should have aria-label on warning image', () => {
447
+ wrapper = mount(Warning, {
448
+ props: {
449
+ input: mockWarning,
450
+ hideable: true,
451
+ language: 'fi' as Language,
452
+ theme: 'light-theme' as Theme,
453
+ },
454
+ })
455
+
456
+ const image = wrapper.find('.warning-image')
457
+ expect(image.attributes('aria-label')).toBeDefined()
458
+ })
459
+ })
460
+
461
+ describe('Hideable prop', () => {
462
+ it('should show toggle when hideable is true', () => {
463
+ wrapper = mount(Warning, {
464
+ props: {
465
+ input: mockWarning,
466
+ hideable: true,
467
+ language: 'fi' as Language,
468
+ theme: 'light-theme' as Theme,
469
+ },
470
+ })
471
+
472
+ const toggle = wrapper.find('.symbol-list-select')
473
+ expect(toggle.classes()).toContain('d-md-block')
474
+ })
475
+
476
+ it('should respect hideable prop', () => {
477
+ wrapper = mount(Warning, {
478
+ props: {
479
+ input: mockWarning,
480
+ hideable: false,
481
+ language: 'fi' as Language,
482
+ theme: 'light-theme' as Theme,
483
+ },
484
+ })
485
+
486
+ expect((wrapper.vm as ComponentInstance).hideable).toBe(false)
487
+ })
488
+ })
489
+ })