@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,503 @@
1
+ import { describe, it, expect, afterEach } from 'vitest'
2
+ import { mount, VueWrapper } from '@vue/test-utils'
3
+ import Regions from '@/components/Regions.vue'
4
+ import type {
5
+ RegionsData,
6
+ WarningsMap,
7
+ Warning,
8
+ Theme,
9
+ Language,
10
+ } from '@/types'
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ type ComponentInstance = any
14
+
15
+ const mockWarning: Warning = {
16
+ type: 'wind',
17
+ id: 'test-warning-1',
18
+ regions: { 'county.1': true },
19
+ covRegions: new Map(),
20
+ coveragesLarge: [],
21
+ coveragesSmall: [],
22
+ effectiveFrom: '2025-10-31T12:00:00Z',
23
+ effectiveUntil: '2025-11-01T12:00:00Z',
24
+ effectiveDays: [true, true, false, false, false],
25
+ validInterval: '31.10.2025 14:00 – 1.11.2025 14:00',
26
+ severity: 3,
27
+ direction: 270,
28
+ value: 25,
29
+ text: '25',
30
+ info: {
31
+ fi: 'Kovaa tuulta',
32
+ sv: 'Hårt blåsväder',
33
+ en: 'Strong wind',
34
+ },
35
+ link: '',
36
+ linkText: '',
37
+ }
38
+
39
+ const mockRegionsData: RegionsData = [
40
+ {
41
+ land: [
42
+ {
43
+ key: 'county.1',
44
+ regionIndex: 1,
45
+ name: 'Uusimaa',
46
+ warnings: [
47
+ {
48
+ type: 'wind',
49
+ identifiers: ['test-warning-1'],
50
+ coverage: 100,
51
+ },
52
+ ],
53
+ },
54
+ ],
55
+ sea: [
56
+ {
57
+ key: 'sea_region.B1N',
58
+ regionIndex: 1,
59
+ name: 'Northern Baltic',
60
+ warnings: [
61
+ {
62
+ type: 'seaWind',
63
+ identifiers: ['test-warning-sea-1'],
64
+ coverage: 100,
65
+ },
66
+ ],
67
+ },
68
+ ],
69
+ },
70
+ { land: [], sea: [] },
71
+ { land: [], sea: [] },
72
+ { land: [], sea: [] },
73
+ { land: [], sea: [] },
74
+ ]
75
+
76
+ const mockWarnings: WarningsMap = {
77
+ 'test-warning-1': mockWarning,
78
+ }
79
+
80
+ describe('Regions.vue', () => {
81
+ let wrapper: VueWrapper | null = null
82
+
83
+ afterEach(() => {
84
+ if (wrapper) {
85
+ wrapper.unmount()
86
+ wrapper = null
87
+ }
88
+ })
89
+
90
+ describe('Component mounting', () => {
91
+ it('should mount with default props', () => {
92
+ wrapper = mount(Regions, {
93
+ props: {},
94
+ })
95
+
96
+ expect(wrapper.exists()).toBe(true)
97
+ })
98
+
99
+ it('should mount with all props', () => {
100
+ wrapper = mount(Regions, {
101
+ props: {
102
+ input: mockRegionsData,
103
+ selectedDay: 0,
104
+ warnings: mockWarnings,
105
+ parents: {},
106
+ geometryId: 2021,
107
+ theme: 'light-theme' as Theme,
108
+ language: 'fi' as Language,
109
+ },
110
+ })
111
+
112
+ expect(wrapper.exists()).toBe(true)
113
+ })
114
+ })
115
+
116
+ describe('Computed properties', () => {
117
+ it('should compute landText from translations', () => {
118
+ wrapper = mount(Regions, {
119
+ props: {
120
+ language: 'fi' as Language,
121
+ },
122
+ })
123
+
124
+ expect(typeof (wrapper.vm as ComponentInstance).landText).toBe('string')
125
+ })
126
+
127
+ it('should compute seaText from translations', () => {
128
+ wrapper = mount(Regions, {
129
+ props: {
130
+ language: 'fi' as Language,
131
+ },
132
+ })
133
+
134
+ expect(typeof (wrapper.vm as ComponentInstance).seaText).toBe('string')
135
+ })
136
+
137
+ it('should compute fromLandToNextContentText from translations', () => {
138
+ wrapper = mount(Regions, {
139
+ props: {
140
+ input: mockRegionsData,
141
+ selectedDay: 0,
142
+ language: 'fi' as Language,
143
+ },
144
+ })
145
+
146
+ expect(
147
+ typeof (wrapper.vm as ComponentInstance).fromLandToNextContentText
148
+ ).toBe('string')
149
+ })
150
+
151
+ it('should compute fromSeaToNextContentText from translations', () => {
152
+ wrapper = mount(Regions, {
153
+ props: {
154
+ input: mockRegionsData,
155
+ selectedDay: 0,
156
+ language: 'fi' as Language,
157
+ },
158
+ })
159
+
160
+ expect(
161
+ typeof (wrapper.vm as ComponentInstance).fromSeaToNextContentText
162
+ ).toBe('string')
163
+ })
164
+
165
+ it('should compute regions for selected day', () => {
166
+ wrapper = mount(Regions, {
167
+ props: {
168
+ input: mockRegionsData,
169
+ selectedDay: 0,
170
+ },
171
+ })
172
+
173
+ const regions = (wrapper.vm as ComponentInstance).regions
174
+ expect(regions).toBeDefined()
175
+ expect(regions.land).toBeDefined()
176
+ expect(regions.sea).toBeDefined()
177
+ })
178
+
179
+ it('should return empty regions for empty input', () => {
180
+ wrapper = mount(Regions, {
181
+ props: {
182
+ input: [],
183
+ selectedDay: 0,
184
+ },
185
+ })
186
+
187
+ const regions = (wrapper.vm as ComponentInstance).regions
188
+ expect(regions.land).toEqual([])
189
+ expect(regions.sea).toEqual([])
190
+ })
191
+
192
+ it('should compute anyLandWarnings', () => {
193
+ wrapper = mount(Regions, {
194
+ props: {
195
+ input: mockRegionsData,
196
+ selectedDay: 0,
197
+ },
198
+ })
199
+
200
+ expect(typeof (wrapper.vm as ComponentInstance).anyLandWarnings).toBe(
201
+ 'boolean'
202
+ )
203
+ })
204
+
205
+ it('should compute anySeaWarnings', () => {
206
+ wrapper = mount(Regions, {
207
+ props: {
208
+ input: mockRegionsData,
209
+ selectedDay: 0,
210
+ },
211
+ })
212
+
213
+ expect(typeof (wrapper.vm as ComponentInstance).anySeaWarnings).toBe(
214
+ 'boolean'
215
+ )
216
+ })
217
+
218
+ it('should compute fromLandToNextContentHref based on sea warnings', () => {
219
+ wrapper = mount(Regions, {
220
+ props: {
221
+ input: mockRegionsData,
222
+ selectedDay: 0,
223
+ },
224
+ })
225
+
226
+ expect(
227
+ typeof (wrapper.vm as ComponentInstance).fromLandToNextContentHref
228
+ ).toBe('string')
229
+ })
230
+
231
+ it('should compute fromSeaToNextContentId based on land warnings', () => {
232
+ wrapper = mount(Regions, {
233
+ props: {
234
+ input: mockRegionsData,
235
+ selectedDay: 0,
236
+ },
237
+ })
238
+
239
+ expect(
240
+ typeof (wrapper.vm as ComponentInstance).fromSeaToNextContentId
241
+ ).toBe('string')
242
+ })
243
+ })
244
+
245
+ describe('Methods', () => {
246
+ it('should have anyRegionWarnings method', () => {
247
+ wrapper = mount(Regions, {
248
+ props: {
249
+ input: mockRegionsData,
250
+ selectedDay: 0,
251
+ },
252
+ })
253
+
254
+ expect(typeof (wrapper.vm as ComponentInstance).anyRegionWarnings).toBe(
255
+ 'function'
256
+ )
257
+ })
258
+
259
+ it('should return true for land with warnings', () => {
260
+ wrapper = mount(Regions, {
261
+ props: {
262
+ input: mockRegionsData,
263
+ selectedDay: 0,
264
+ },
265
+ })
266
+
267
+ expect((wrapper.vm as ComponentInstance).anyRegionWarnings('land')).toBe(
268
+ true
269
+ )
270
+ })
271
+
272
+ it('should return true for sea with warnings', () => {
273
+ wrapper = mount(Regions, {
274
+ props: {
275
+ input: mockRegionsData,
276
+ selectedDay: 0,
277
+ },
278
+ })
279
+
280
+ expect((wrapper.vm as ComponentInstance).anyRegionWarnings('sea')).toBe(
281
+ true
282
+ )
283
+ })
284
+
285
+ it('should return false for day without warnings', () => {
286
+ wrapper = mount(Regions, {
287
+ props: {
288
+ input: mockRegionsData,
289
+ selectedDay: 1,
290
+ },
291
+ })
292
+
293
+ expect((wrapper.vm as ComponentInstance).anyRegionWarnings('land')).toBe(
294
+ false
295
+ )
296
+ })
297
+
298
+ it('should have fromLandToNextContentClicked method', () => {
299
+ wrapper = mount(Regions, {
300
+ props: {},
301
+ })
302
+
303
+ expect(
304
+ typeof (wrapper.vm as ComponentInstance).fromLandToNextContentClicked
305
+ ).toBe('function')
306
+ })
307
+
308
+ it('should have fromSeaToNextContentClicked method', () => {
309
+ wrapper = mount(Regions, {
310
+ props: {},
311
+ })
312
+
313
+ expect(
314
+ typeof (wrapper.vm as ComponentInstance).fromSeaToNextContentClicked
315
+ ).toBe('function')
316
+ })
317
+ })
318
+
319
+ describe('Props handling', () => {
320
+ it('should accept selectedDay prop', () => {
321
+ wrapper = mount(Regions, {
322
+ props: {
323
+ selectedDay: 2,
324
+ },
325
+ })
326
+
327
+ expect((wrapper.vm as ComponentInstance).selectedDay).toBe(2)
328
+ })
329
+
330
+ it('should accept parents prop', () => {
331
+ const parents = { 'county.1': [true, false, false, false, false] }
332
+
333
+ wrapper = mount(Regions, {
334
+ props: {
335
+ parents,
336
+ },
337
+ })
338
+
339
+ expect((wrapper.vm as ComponentInstance).parents).toEqual(parents)
340
+ })
341
+
342
+ it('should accept geometryId prop', () => {
343
+ wrapper = mount(Regions, {
344
+ props: {
345
+ geometryId: 2021,
346
+ },
347
+ })
348
+
349
+ expect((wrapper.vm as ComponentInstance).geometryId).toBe(2021)
350
+ })
351
+
352
+ it('should accept warnings prop', () => {
353
+ wrapper = mount(Regions, {
354
+ props: {
355
+ warnings: mockWarnings,
356
+ },
357
+ })
358
+
359
+ expect(wrapper.exists()).toBe(true)
360
+ })
361
+ })
362
+
363
+ describe('Theme support', () => {
364
+ it('should accept theme prop', () => {
365
+ wrapper = mount(Regions, {
366
+ props: {
367
+ theme: 'dark-theme' as Theme,
368
+ },
369
+ })
370
+
371
+ expect((wrapper.vm as ComponentInstance).theme).toBe('dark-theme')
372
+ })
373
+
374
+ it('should default to light-theme', () => {
375
+ wrapper = mount(Regions, {
376
+ props: {},
377
+ })
378
+
379
+ expect((wrapper.vm as ComponentInstance).theme).toBe('light-theme')
380
+ })
381
+ })
382
+
383
+ describe('Language support', () => {
384
+ it('should support Finnish language', () => {
385
+ wrapper = mount(Regions, {
386
+ props: {
387
+ language: 'fi' as Language,
388
+ },
389
+ })
390
+
391
+ expect(wrapper.exists()).toBe(true)
392
+ })
393
+
394
+ it('should support Swedish language', () => {
395
+ wrapper = mount(Regions, {
396
+ props: {
397
+ language: 'sv' as Language,
398
+ },
399
+ })
400
+
401
+ expect(wrapper.exists()).toBe(true)
402
+ })
403
+
404
+ it('should support English language', () => {
405
+ wrapper = mount(Regions, {
406
+ props: {
407
+ language: 'en' as Language,
408
+ },
409
+ })
410
+
411
+ expect(wrapper.exists()).toBe(true)
412
+ })
413
+ })
414
+
415
+ describe('Structure', () => {
416
+ it('should render region-warnings container', () => {
417
+ wrapper = mount(Regions, {
418
+ props: {},
419
+ })
420
+
421
+ expect(wrapper.find('#region-warnings').exists()).toBe(true)
422
+ })
423
+
424
+ it('should render land header when land warnings exist', () => {
425
+ wrapper = mount(Regions, {
426
+ props: {
427
+ input: mockRegionsData,
428
+ selectedDay: 0,
429
+ },
430
+ })
431
+
432
+ expect(wrapper.find('#header-land').exists()).toBe(true)
433
+ })
434
+
435
+ it('should render sea header when sea warnings exist', () => {
436
+ wrapper = mount(Regions, {
437
+ props: {
438
+ input: mockRegionsData,
439
+ selectedDay: 0,
440
+ },
441
+ })
442
+
443
+ expect(wrapper.find('#header-sea').exists()).toBe(true)
444
+ })
445
+
446
+ it('should not render land section when no land warnings', () => {
447
+ wrapper = mount(Regions, {
448
+ props: {
449
+ input: mockRegionsData,
450
+ selectedDay: 1,
451
+ },
452
+ })
453
+
454
+ expect(wrapper.find('#header-land').exists()).toBe(false)
455
+ })
456
+
457
+ it('should not render sea section when no sea warnings', () => {
458
+ wrapper = mount(Regions, {
459
+ props: {
460
+ input: mockRegionsData,
461
+ selectedDay: 1,
462
+ },
463
+ })
464
+
465
+ expect(wrapper.find('#header-sea').exists()).toBe(false)
466
+ })
467
+
468
+ it('should render accordion groups', () => {
469
+ wrapper = mount(Regions, {
470
+ props: {
471
+ input: mockRegionsData,
472
+ selectedDay: 0,
473
+ },
474
+ })
475
+
476
+ expect(wrapper.find('#accordion-group-land').exists()).toBe(true)
477
+ expect(wrapper.find('#accordion-group-sea').exists()).toBe(true)
478
+ })
479
+ })
480
+
481
+ describe('Accessibility', () => {
482
+ it('should have skip link for land content', () => {
483
+ wrapper = mount(Regions, {
484
+ props: {
485
+ input: mockRegionsData,
486
+ selectedDay: 0,
487
+ },
488
+ })
489
+
490
+ const skipLink = wrapper.find('#fmi-warnings-region-content')
491
+ expect(skipLink.exists()).toBe(true)
492
+ expect(skipLink.classes()).toContain('visually-hidden-focusable')
493
+ })
494
+
495
+ it('should have end of regions marker', () => {
496
+ wrapper = mount(Regions, {
497
+ props: {},
498
+ })
499
+
500
+ expect(wrapper.find('#fmi-warnings-end-of-regions').exists()).toBe(true)
501
+ })
502
+ })
503
+ })