@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.
- package/.eslintignore +2 -14
- package/.github/workflows/test.yaml +26 -0
- package/.nvmrc +1 -0
- package/AGENTS.md +26 -0
- package/index.html +1 -1
- package/package.json +80 -22
- package/src/AlertClientVue.vue +160 -0
- package/src/App.vue +154 -296
- package/src/assets/img/ui/arrow-down.svg +4 -11
- package/src/assets/img/ui/arrow-up.svg +4 -11
- package/src/assets/img/ui/clear.svg +7 -21
- package/src/assets/img/ui/close.svg +4 -15
- package/src/assets/img/ui/toggle-selected.svg +5 -6
- package/src/assets/img/ui/toggle-unselected.svg +5 -6
- package/src/assets/img/warning/cold-weather.svg +3 -6
- package/src/assets/img/warning/flood-level-3.svg +4 -7
- package/src/assets/img/warning/forest-fire-weather.svg +2 -6
- package/src/assets/img/warning/grass-fire-weather.svg +2 -6
- package/src/assets/img/warning/hot-weather.svg +3 -6
- package/src/assets/img/warning/pedestrian-safety.svg +3 -7
- package/src/assets/img/warning/rain.svg +2 -7
- package/src/assets/img/warning/sea-icing.svg +2 -6
- package/src/assets/img/warning/sea-thunder-storm.svg +2 -5
- package/src/assets/img/warning/sea-water-height-high-water.svg +3 -8
- package/src/assets/img/warning/sea-water-height-shallow-water.svg +3 -7
- package/src/assets/img/warning/sea-wave-height.svg +4 -7
- package/src/assets/img/warning/sea-wind-legend.svg +2 -5
- package/src/assets/img/warning/sea-wind.svg +2 -5
- package/src/assets/img/warning/several.svg +2 -5
- package/src/assets/img/warning/thunder-storm.svg +2 -5
- package/src/assets/img/warning/traffic-weather.svg +2 -6
- package/src/assets/img/warning/uv-note.svg +2 -6
- package/src/assets/img/warning/wind.svg +2 -5
- package/src/components/AlertClient.vue +330 -251
- package/src/components/CollapsiblePanel.vue +281 -0
- package/src/components/DayLarge.vue +146 -110
- package/src/components/DaySmall.vue +97 -81
- package/src/components/Days.vue +229 -159
- package/src/components/DescriptionWarning.vue +63 -38
- package/src/components/GrayScaleToggle.vue +58 -54
- package/src/components/Legend.vue +102 -325
- package/src/components/MapLarge.vue +574 -351
- package/src/components/MapSmall.vue +137 -122
- package/src/components/PopupRow.vue +24 -12
- package/src/components/Region.vue +168 -118
- package/src/components/RegionWarning.vue +40 -33
- package/src/components/Regions.vue +189 -105
- package/src/components/Warning.vue +70 -45
- package/src/components/Warnings.vue +136 -72
- package/src/composables/useAlertClient.ts +360 -0
- package/src/composables/useConfig.ts +573 -0
- package/src/composables/useFields.ts +66 -0
- package/src/composables/useI18n.ts +62 -0
- package/src/composables/useKeyCodes.ts +16 -0
- package/src/composables/useMapPaths.ts +477 -0
- package/src/composables/useUtils.ts +683 -0
- package/src/composables/useWarningsProcessor.ts +1007 -0
- package/src/data/geometries.json +993 -0
- package/src/{main.js → main.ts} +1 -0
- package/src/mixins/geojsonsvg.d.ts +57 -0
- package/src/mixins/geojsonsvg.js +5 -3
- package/src/plugins/index.ts +5 -0
- package/src/scss/_utilities.scss +193 -0
- package/src/scss/constants.scss +2 -1
- package/src/scss/warningImages.scss +8 -3
- package/src/types/index.ts +509 -0
- package/src/vite-env.d.ts +23 -0
- package/src/vue.ts +41 -0
- package/svgo.config.js +45 -0
- package/tests/README.md +430 -0
- package/tests/fixtures/mockWarningData.ts +152 -0
- package/tests/integration/warning-flow.spec.ts +445 -0
- package/tests/setup.ts +41 -0
- package/tests/unit/components/AlertClient.spec.ts +701 -0
- package/tests/unit/components/DayLarge.spec.ts +348 -0
- package/tests/unit/components/DaySmall.spec.ts +352 -0
- package/tests/unit/components/Days.spec.ts +548 -0
- package/tests/unit/components/DescriptionWarning.spec.ts +385 -0
- package/tests/unit/components/GrayScaleToggle.spec.ts +318 -0
- package/tests/unit/components/Legend.spec.ts +295 -0
- package/tests/unit/components/MapLarge.spec.ts +448 -0
- package/tests/unit/components/MapSmall.spec.ts +367 -0
- package/tests/unit/components/PopupRow.spec.ts +270 -0
- package/tests/unit/components/Region.spec.ts +373 -0
- package/tests/unit/components/RegionWarning.snapshot.spec.ts +361 -0
- package/tests/unit/components/RegionWarning.spec.ts +381 -0
- package/tests/unit/components/Regions.spec.ts +503 -0
- package/tests/unit/components/Warning.snapshot.spec.ts +483 -0
- package/tests/unit/components/Warning.spec.ts +489 -0
- package/tests/unit/components/Warnings.spec.ts +343 -0
- package/tests/unit/components/__snapshots__/RegionWarning.snapshot.spec.ts.snap +41 -0
- package/tests/unit/components/__snapshots__/Warning.snapshot.spec.ts.snap +433 -0
- package/tests/unit/composables/useConfig.spec.ts +279 -0
- package/tests/unit/composables/useI18n.spec.ts +116 -0
- package/tests/unit/composables/useKeyCodes.spec.ts +27 -0
- package/tests/unit/composables/useUtils.spec.ts +213 -0
- package/tsconfig.json +43 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.js +96 -26
- package/vitest.config.js +40 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.dark.html +0 -20
- package/dist/index.en.html +0 -15
- package/dist/index.fi.html +0 -15
- package/dist/index.html +0 -15
- package/dist/index.js +0 -281
- package/dist/index.mjs +0 -281
- package/dist/index.mjs.map +0 -1
- package/dist/index.relative.html +0 -19
- package/dist/index.start.html +0 -20
- package/dist/index.sv.html +0 -15
- package/playwright.config.ts +0 -18
- package/public/index.relative.html +0 -19
- package/public/index.start.html +0 -20
- package/src/mixins/config.js +0 -1378
- package/src/mixins/fields.js +0 -26
- package/src/mixins/i18n.js +0 -25
- package/src/mixins/keycodes.js +0 -10
- package/src/mixins/panzoom.js +0 -900
- package/src/mixins/utils.js +0 -900
- package/src/plugins/index.js +0 -3
- package/test/snapshot.test.ts +0 -126
- 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
|
+
})
|