@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,445 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
2
|
+
import { mockWarningsData } from '../fixtures/mockWarningData'
|
|
3
|
+
import {
|
|
4
|
+
processWarnings,
|
|
5
|
+
type WarningsProcessorContext,
|
|
6
|
+
} from '@/composables/useWarningsProcessor'
|
|
7
|
+
import { useConfig } from '@/composables/useConfig'
|
|
8
|
+
import { useI18n } from '@/composables/useI18n'
|
|
9
|
+
import geojsonsvg from '@/mixins/geojsonsvg'
|
|
10
|
+
import type { WarningsData } from '@/types'
|
|
11
|
+
|
|
12
|
+
// Bind all methods so 'this' works correctly when called standalone
|
|
13
|
+
const geoJSONToSVG = geojsonsvg.methods.geoJSONToSVG.bind(geojsonsvg.methods)
|
|
14
|
+
|
|
15
|
+
describe('Warning data flow integration', () => {
|
|
16
|
+
let config: ReturnType<typeof useConfig>
|
|
17
|
+
let ctx: WarningsProcessorContext
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
config = useConfig()
|
|
21
|
+
const { t } = useI18n('fi')
|
|
22
|
+
|
|
23
|
+
// Create a context object that processWarnings needs
|
|
24
|
+
ctx = {
|
|
25
|
+
geometryId: '2021',
|
|
26
|
+
geometries: config.geometries,
|
|
27
|
+
warningTypes: config.warningTypes,
|
|
28
|
+
regionIds: config.regionIds,
|
|
29
|
+
dailyWarningTypes: [],
|
|
30
|
+
currentTime: new Date('2025-10-31T12:00:00Z').getTime(),
|
|
31
|
+
startFrom: '',
|
|
32
|
+
staticDays: true,
|
|
33
|
+
timeZone: config.timeZone,
|
|
34
|
+
locale: config.dateTimeFormatLocale,
|
|
35
|
+
bbox: config.bbox as WarningsProcessorContext['bbox'],
|
|
36
|
+
geoJSONToSVG,
|
|
37
|
+
maxUpdateDelay:
|
|
38
|
+
config.maxUpdateDelay as WarningsProcessorContext['maxUpdateDelay'],
|
|
39
|
+
t,
|
|
40
|
+
handleError: () => {
|
|
41
|
+
// Mock error handler - does nothing in tests
|
|
42
|
+
},
|
|
43
|
+
onDataError: () => {
|
|
44
|
+
// Mock data error handler - does nothing in tests
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('Complete warning processing', () => {
|
|
50
|
+
it('should process full warning dataset', () => {
|
|
51
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
52
|
+
|
|
53
|
+
expect(result).toHaveProperty('warnings')
|
|
54
|
+
expect(result).toHaveProperty('days')
|
|
55
|
+
expect(result).toHaveProperty('regions')
|
|
56
|
+
expect(result).toHaveProperty('parents')
|
|
57
|
+
expect(result).toHaveProperty('legend')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should create 5 days from warnings', () => {
|
|
61
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
62
|
+
|
|
63
|
+
expect(result.days).toHaveLength(5)
|
|
64
|
+
result.days.forEach((day) => {
|
|
65
|
+
expect(day).toHaveProperty('weekdayName')
|
|
66
|
+
expect(day).toHaveProperty('day')
|
|
67
|
+
expect(day).toHaveProperty('month')
|
|
68
|
+
expect(day).toHaveProperty('year')
|
|
69
|
+
expect(day).toHaveProperty('severity')
|
|
70
|
+
expect(day).toHaveProperty('updatedDate')
|
|
71
|
+
expect(day).toHaveProperty('updatedTime')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should parse weather warnings correctly', () => {
|
|
76
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
77
|
+
|
|
78
|
+
const windWarning = result.warnings['test-warning-wind-1']
|
|
79
|
+
expect(windWarning).toBeDefined()
|
|
80
|
+
expect(windWarning!.type).toBe('wind')
|
|
81
|
+
expect(windWarning!.severity).toBe(3)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should parse flood warnings correctly', () => {
|
|
85
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
86
|
+
|
|
87
|
+
const floodWarning = result.warnings['test-warning-flood-1']
|
|
88
|
+
expect(floodWarning).toBeDefined()
|
|
89
|
+
expect(floodWarning!.type).toBe('floodLevel')
|
|
90
|
+
expect(floodWarning!.severity).toBe(3)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should create legend sorted by severity', () => {
|
|
94
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
95
|
+
|
|
96
|
+
expect(result.legend.length).toBeGreaterThan(0)
|
|
97
|
+
|
|
98
|
+
// Check that legend is sorted by severity (descending)
|
|
99
|
+
for (let i = 0; i < result.legend.length - 1; i++) {
|
|
100
|
+
expect(result.legend[i]!.severity).toBeGreaterThanOrEqual(
|
|
101
|
+
result.legend[i + 1]!.severity
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should create regions with warnings', () => {
|
|
107
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
108
|
+
|
|
109
|
+
expect(result.regions).toHaveLength(5)
|
|
110
|
+
result.regions.forEach((day) => {
|
|
111
|
+
expect(day).toHaveProperty('land')
|
|
112
|
+
expect(day).toHaveProperty('sea')
|
|
113
|
+
expect(Array.isArray(day.land)).toBe(true)
|
|
114
|
+
expect(Array.isArray(day.sea)).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('should handle missing weather data gracefully', () => {
|
|
119
|
+
const incompleteData = {
|
|
120
|
+
weather_update_time: mockWarningsData.weather_update_time,
|
|
121
|
+
flood_update_time: mockWarningsData.flood_update_time,
|
|
122
|
+
// Missing weather and flood warnings
|
|
123
|
+
} as WarningsData
|
|
124
|
+
|
|
125
|
+
const result = processWarnings(incompleteData, ctx)
|
|
126
|
+
expect(result).toBeDefined()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should set updatedAt timestamp', () => {
|
|
130
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
131
|
+
|
|
132
|
+
expect(result.updatedAt).toBeDefined()
|
|
133
|
+
expect(typeof result.updatedAt).toBe('number')
|
|
134
|
+
expect(result.updatedAt).toBeGreaterThan(0)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should filter out invalid warnings', () => {
|
|
138
|
+
const dataWithInvalid = {
|
|
139
|
+
...mockWarningsData,
|
|
140
|
+
weather_finland_active_all: {
|
|
141
|
+
type: 'FeatureCollection' as const,
|
|
142
|
+
features: [
|
|
143
|
+
...mockWarningsData.weather_finland_active_all!.features,
|
|
144
|
+
{
|
|
145
|
+
type: 'Feature' as const,
|
|
146
|
+
properties: {
|
|
147
|
+
identifier: 'invalid-warning',
|
|
148
|
+
warning_context: 'wind',
|
|
149
|
+
severity: 'level-1', // Invalid for wind
|
|
150
|
+
effective_from: '2025-10-31T12:00:00Z',
|
|
151
|
+
effective_until: '2025-11-01T12:00:00Z',
|
|
152
|
+
reference: 'fi-warning#county.1',
|
|
153
|
+
},
|
|
154
|
+
geometry: null,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
} as WarningsData
|
|
159
|
+
|
|
160
|
+
const result = processWarnings(dataWithInvalid, ctx)
|
|
161
|
+
|
|
162
|
+
expect(result.warnings).not.toHaveProperty('invalid-warning')
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('Region hierarchy', () => {
|
|
167
|
+
it('should handle parent-child relationships', () => {
|
|
168
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
169
|
+
|
|
170
|
+
// Check that parents object is created
|
|
171
|
+
expect(result.parents).toBeDefined()
|
|
172
|
+
expect(typeof result.parents).toBe('object')
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('Coverage handling', () => {
|
|
177
|
+
it('should process coverage data when present', () => {
|
|
178
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
179
|
+
|
|
180
|
+
const coverageWarning = result.warnings['test-warning-coverage-1']
|
|
181
|
+
if (coverageWarning) {
|
|
182
|
+
expect(coverageWarning.covRegions).toBeInstanceOf(Map)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('Multi-language support', () => {
|
|
188
|
+
it('should include info in all languages for weather warnings', () => {
|
|
189
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
190
|
+
|
|
191
|
+
const windWarning = result.warnings['test-warning-wind-1']
|
|
192
|
+
expect(windWarning!.info).toHaveProperty('fi')
|
|
193
|
+
expect(windWarning!.info).toHaveProperty('sv')
|
|
194
|
+
expect(windWarning!.info).toHaveProperty('en')
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should decode HTML entities in warning info', () => {
|
|
198
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
199
|
+
|
|
200
|
+
const windWarning = result.warnings['test-warning-wind-1']
|
|
201
|
+
// HTML entities should be decoded by he.decode
|
|
202
|
+
expect(windWarning!.info.fi).not.toContain('&')
|
|
203
|
+
expect(windWarning!.info.fi).not.toContain('<')
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('Time calculations', () => {
|
|
208
|
+
it('should calculate effective days correctly', () => {
|
|
209
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
210
|
+
|
|
211
|
+
const windWarning = result.warnings['test-warning-wind-1']
|
|
212
|
+
expect(windWarning!.effectiveDays).toHaveLength(5)
|
|
213
|
+
expect(windWarning!.effectiveDays.some((day) => day === true)).toBe(true)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should format valid intervals correctly', () => {
|
|
217
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
218
|
+
|
|
219
|
+
const windWarning = result.warnings['test-warning-wind-1']
|
|
220
|
+
expect(windWarning!.validInterval).toContain('–')
|
|
221
|
+
expect(windWarning!.validInterval).toMatch(/\d{1,2}\.\d{1,2}\./)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
describe('Severity calculations', () => {
|
|
226
|
+
it('should calculate day severities from warnings', () => {
|
|
227
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
228
|
+
|
|
229
|
+
const firstDay = result.days[0]
|
|
230
|
+
// Should have severity based on active warnings
|
|
231
|
+
expect(typeof firstDay!.severity).toBe('number')
|
|
232
|
+
expect(firstDay!.severity).toBeGreaterThanOrEqual(0)
|
|
233
|
+
expect(firstDay!.severity).toBeLessThanOrEqual(4)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should show highest severity per day', () => {
|
|
237
|
+
const result = processWarnings(mockWarningsData, ctx)
|
|
238
|
+
|
|
239
|
+
// Check that severity is the max of all warnings for that day
|
|
240
|
+
result.days.forEach((day, dayIndex) => {
|
|
241
|
+
const warningsForDay = Object.values(result.warnings).filter(
|
|
242
|
+
(warning) => warning.effectiveDays[dayIndex]
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if (warningsForDay.length > 0) {
|
|
246
|
+
const maxSeverity = Math.max(...warningsForDay.map((w) => w.severity))
|
|
247
|
+
expect(day.severity).toBe(maxSeverity)
|
|
248
|
+
} else {
|
|
249
|
+
expect(day.severity).toBe(0)
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
describe('Edge cases and error handling', () => {
|
|
256
|
+
it('should handle warnings with some missing properties', () => {
|
|
257
|
+
const incompleteData = {
|
|
258
|
+
weather_update_time: '2025-10-31T12:00:00Z',
|
|
259
|
+
weather_finland_active_all: {
|
|
260
|
+
type: 'FeatureCollection' as const,
|
|
261
|
+
features: [
|
|
262
|
+
{
|
|
263
|
+
type: 'Feature' as const,
|
|
264
|
+
properties: {
|
|
265
|
+
identifier: 'incomplete-warning',
|
|
266
|
+
warning_context: 'wind',
|
|
267
|
+
severity: 'level-2',
|
|
268
|
+
effective_from: '2025-10-31T12:00:00Z',
|
|
269
|
+
effective_until: '2025-11-01T12:00:00Z',
|
|
270
|
+
reference: 'fi-warning#county.1',
|
|
271
|
+
// Missing descriptions, but has essential properties
|
|
272
|
+
},
|
|
273
|
+
geometry: {
|
|
274
|
+
type: 'Point' as const,
|
|
275
|
+
coordinates: [25.0, 60.0] as [number, number],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
} as unknown as WarningsData
|
|
281
|
+
|
|
282
|
+
const result = processWarnings(incompleteData, ctx)
|
|
283
|
+
|
|
284
|
+
// Should process the warning even with missing descriptions
|
|
285
|
+
expect(result).toBeDefined()
|
|
286
|
+
expect(result.warnings).toBeDefined()
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('should handle malformed timestamps', () => {
|
|
290
|
+
const badTimestampData = {
|
|
291
|
+
weather_update_time: 'invalid-timestamp',
|
|
292
|
+
flood_update_time: 'also-invalid',
|
|
293
|
+
weather_finland_active_all: {
|
|
294
|
+
type: 'FeatureCollection' as const,
|
|
295
|
+
features: [],
|
|
296
|
+
},
|
|
297
|
+
} as unknown as WarningsData
|
|
298
|
+
|
|
299
|
+
const result = processWarnings(badTimestampData, ctx)
|
|
300
|
+
expect(result).toBeDefined()
|
|
301
|
+
expect(result.days).toHaveLength(5)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('should handle circular references in region hierarchy', () => {
|
|
305
|
+
const dataWithCircular = {
|
|
306
|
+
...mockWarningsData,
|
|
307
|
+
weather_finland_active_all: {
|
|
308
|
+
type: 'FeatureCollection' as const,
|
|
309
|
+
features: mockWarningsData.weather_finland_active_all!.features.map(
|
|
310
|
+
(f) => ({
|
|
311
|
+
...f,
|
|
312
|
+
properties: {
|
|
313
|
+
...f.properties,
|
|
314
|
+
// Create potential circular reference
|
|
315
|
+
reference: f.properties.identifier,
|
|
316
|
+
},
|
|
317
|
+
})
|
|
318
|
+
),
|
|
319
|
+
},
|
|
320
|
+
} as WarningsData
|
|
321
|
+
|
|
322
|
+
expect(() => {
|
|
323
|
+
processWarnings(dataWithCircular, ctx)
|
|
324
|
+
}).not.toThrow()
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('should handle very long warning descriptions', () => {
|
|
328
|
+
const longDescription = 'A'.repeat(10000)
|
|
329
|
+
const dataWithLongText = {
|
|
330
|
+
...mockWarningsData,
|
|
331
|
+
weather_finland_active_all: {
|
|
332
|
+
type: 'FeatureCollection' as const,
|
|
333
|
+
features: [
|
|
334
|
+
{
|
|
335
|
+
...mockWarningsData.weather_finland_active_all!.features[0],
|
|
336
|
+
properties: {
|
|
337
|
+
...mockWarningsData.weather_finland_active_all!.features[0]!
|
|
338
|
+
.properties,
|
|
339
|
+
description_fi: longDescription,
|
|
340
|
+
description_sv: longDescription,
|
|
341
|
+
description_en: longDescription,
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
} as WarningsData
|
|
347
|
+
|
|
348
|
+
const result = processWarnings(dataWithLongText, ctx)
|
|
349
|
+
expect(result).toBeDefined()
|
|
350
|
+
expect(result.warnings).toBeDefined()
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
it('should handle warnings spanning across midnight', () => {
|
|
354
|
+
const midnightData = {
|
|
355
|
+
weather_update_time: '2025-10-31T23:00:00Z',
|
|
356
|
+
weather_finland_active_all: {
|
|
357
|
+
type: 'FeatureCollection' as const,
|
|
358
|
+
features: [
|
|
359
|
+
{
|
|
360
|
+
type: 'Feature' as const,
|
|
361
|
+
properties: {
|
|
362
|
+
identifier: 'midnight-warning',
|
|
363
|
+
warning_context: 'wind',
|
|
364
|
+
severity: 'level-2',
|
|
365
|
+
effective_from: '2025-10-31T23:30:00Z',
|
|
366
|
+
effective_until: '2025-11-01T01:30:00Z',
|
|
367
|
+
reference: 'fi-warning#county.1',
|
|
368
|
+
description_fi: 'Kova tuuli',
|
|
369
|
+
description_sv: 'Hårt väder',
|
|
370
|
+
description_en: 'Strong wind',
|
|
371
|
+
},
|
|
372
|
+
geometry: {
|
|
373
|
+
type: 'Point' as const,
|
|
374
|
+
coordinates: [25.0, 60.0] as [number, number],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
} as unknown as WarningsData
|
|
380
|
+
|
|
381
|
+
const result = processWarnings(midnightData, ctx)
|
|
382
|
+
expect(result.warnings['midnight-warning']).toBeDefined()
|
|
383
|
+
expect(result.warnings['midnight-warning']!.effectiveDays).toBeDefined()
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('should handle duplicate warning identifiers', () => {
|
|
387
|
+
const duplicateData = {
|
|
388
|
+
weather_update_time: '2025-10-31T12:00:00Z',
|
|
389
|
+
weather_finland_active_all: {
|
|
390
|
+
type: 'FeatureCollection' as const,
|
|
391
|
+
features: [
|
|
392
|
+
mockWarningsData.weather_finland_active_all!.features[0],
|
|
393
|
+
mockWarningsData.weather_finland_active_all!.features[0], // Duplicate
|
|
394
|
+
],
|
|
395
|
+
},
|
|
396
|
+
} as unknown as WarningsData
|
|
397
|
+
|
|
398
|
+
const result = processWarnings(duplicateData, ctx)
|
|
399
|
+
|
|
400
|
+
// Should handle duplicates gracefully
|
|
401
|
+
expect(result).toBeDefined()
|
|
402
|
+
expect(Object.keys(result.warnings).length).toBeGreaterThan(0)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('should handle special characters in warning text', () => {
|
|
406
|
+
const specialCharsData = {
|
|
407
|
+
weather_update_time: '2025-10-31T12:00:00Z',
|
|
408
|
+
weather_finland_active_all: {
|
|
409
|
+
type: 'FeatureCollection' as const,
|
|
410
|
+
features: [
|
|
411
|
+
{
|
|
412
|
+
type: 'Feature' as const,
|
|
413
|
+
properties: {
|
|
414
|
+
identifier: 'special-chars-warning',
|
|
415
|
+
warning_context: 'wind',
|
|
416
|
+
severity: 'level-2',
|
|
417
|
+
effective_from: '2025-10-31T12:00:00Z',
|
|
418
|
+
effective_until: '2025-11-01T12:00:00Z',
|
|
419
|
+
reference: 'fi-warning#county.1',
|
|
420
|
+
description_fi:
|
|
421
|
+
'Test <script>alert("xss")</script> & special chars',
|
|
422
|
+
description_sv:
|
|
423
|
+
'Test <script>alert("xss")</script> & special chars',
|
|
424
|
+
description_en:
|
|
425
|
+
'Test <script>alert("xss")</script> & special chars',
|
|
426
|
+
},
|
|
427
|
+
geometry: {
|
|
428
|
+
type: 'Point' as const,
|
|
429
|
+
coordinates: [25.0, 60.0] as [number, number],
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
},
|
|
434
|
+
} as unknown as WarningsData
|
|
435
|
+
|
|
436
|
+
const result = processWarnings(specialCharsData, ctx)
|
|
437
|
+
const warning = result.warnings['special-chars-warning']
|
|
438
|
+
|
|
439
|
+
// HTML entities should be decoded
|
|
440
|
+
expect(warning!.info.fi).not.toContain('<')
|
|
441
|
+
expect(warning!.info.fi).not.toContain('>')
|
|
442
|
+
expect(warning!.info.fi).not.toContain('&')
|
|
443
|
+
})
|
|
444
|
+
})
|
|
445
|
+
})
|
package/tests/setup.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { vi } from 'vitest'
|
|
2
|
+
import '@testing-library/jest-dom'
|
|
3
|
+
import { config } from '@vue/test-utils'
|
|
4
|
+
|
|
5
|
+
// Mock environment variables
|
|
6
|
+
process.env.VITE_LANGUAGE = 'fi'
|
|
7
|
+
|
|
8
|
+
// Configure global stubs for components
|
|
9
|
+
config.global.stubs = {
|
|
10
|
+
GrayScaleToggle: true,
|
|
11
|
+
CollapsiblePanel: true,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Mock window.getComputedStyle
|
|
15
|
+
global.window.getComputedStyle = vi.fn(() => ({
|
|
16
|
+
fontSize: '16px',
|
|
17
|
+
})) as unknown as typeof window.getComputedStyle
|
|
18
|
+
|
|
19
|
+
// Mock IntersectionObserver
|
|
20
|
+
global.IntersectionObserver = class IntersectionObserver {
|
|
21
|
+
constructor() {}
|
|
22
|
+
disconnect() {}
|
|
23
|
+
observe() {}
|
|
24
|
+
takeRecords(): IntersectionObserverEntry[] {
|
|
25
|
+
return []
|
|
26
|
+
}
|
|
27
|
+
unobserve() {}
|
|
28
|
+
} as unknown as typeof IntersectionObserver
|
|
29
|
+
|
|
30
|
+
// Mock ResizeObserver
|
|
31
|
+
global.ResizeObserver = class ResizeObserver {
|
|
32
|
+
constructor() {}
|
|
33
|
+
disconnect() {}
|
|
34
|
+
observe() {}
|
|
35
|
+
unobserve() {}
|
|
36
|
+
} as unknown as typeof ResizeObserver
|
|
37
|
+
|
|
38
|
+
// Mock fetch if not available
|
|
39
|
+
if (!global.fetch) {
|
|
40
|
+
global.fetch = vi.fn() as unknown as typeof fetch
|
|
41
|
+
}
|