@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,279 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { useConfig } from '@/composables/useConfig'
|
|
3
|
+
|
|
4
|
+
describe('useConfig composable', () => {
|
|
5
|
+
const config = useConfig()
|
|
6
|
+
|
|
7
|
+
describe('Configuration constants', () => {
|
|
8
|
+
it('should define timezone', () => {
|
|
9
|
+
expect(config.timeZone).toBe('Europe/Helsinki')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('should define date format locale', () => {
|
|
13
|
+
expect(config.dateTimeFormatLocale).toBe('fi-FI')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should define pan limits', () => {
|
|
17
|
+
expect(config.panLimits).toEqual({ x: 175, y: 275 })
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should define coverage criterion', () => {
|
|
21
|
+
expect(config.coverageCriterion).toBe(0.2)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should define max merged weight', () => {
|
|
25
|
+
expect(config.maxMergedWeight).toBe(7)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should define max update delays', () => {
|
|
29
|
+
expect(config.maxUpdateDelay).toHaveProperty('weather_update_time')
|
|
30
|
+
expect(config.maxUpdateDelay).toHaveProperty('flood_update_time')
|
|
31
|
+
expect(config.maxUpdateDelay.weather_update_time).toBe(
|
|
32
|
+
12 * 60 * 60 * 1000
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('Warning types map', () => {
|
|
38
|
+
it('should have warningTypes map', () => {
|
|
39
|
+
expect(config.warningTypes).toBeInstanceOf(Map)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should map land warning types', () => {
|
|
43
|
+
const landTypes = [
|
|
44
|
+
'thunderStorm',
|
|
45
|
+
'wind',
|
|
46
|
+
'rain',
|
|
47
|
+
'trafficWeather',
|
|
48
|
+
'pedestrianSafety',
|
|
49
|
+
'forestFireWeather',
|
|
50
|
+
'grassFireWeather',
|
|
51
|
+
'hotWeather',
|
|
52
|
+
'coldWeather',
|
|
53
|
+
'uvNote',
|
|
54
|
+
'floodLevel',
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
landTypes.forEach((type) => {
|
|
58
|
+
expect(config.warningTypes.get(type)).toBe('land')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should map sea warning types', () => {
|
|
63
|
+
const seaTypes = [
|
|
64
|
+
'seaWind',
|
|
65
|
+
'seaThunderStorm',
|
|
66
|
+
'seaWaterHeightHighWater',
|
|
67
|
+
'seaWaterHeightShallowWater',
|
|
68
|
+
'seaWaveHeight',
|
|
69
|
+
'seaIcing',
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
seaTypes.forEach((type) => {
|
|
73
|
+
expect(config.warningTypes.get(type)).toBe('sea')
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('Region IDs', () => {
|
|
79
|
+
it('should have regionIds array', () => {
|
|
80
|
+
expect(Array.isArray(config.regionIds)).toBe(true)
|
|
81
|
+
expect(config.regionIds.length).toBeGreaterThan(0)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should include county regions', () => {
|
|
85
|
+
expect(config.regionIds).toContain('county.1')
|
|
86
|
+
expect(config.regionIds).toContain('county.2')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should include sea regions', () => {
|
|
90
|
+
expect(config.regionIds).toContain('sea_region.B1N')
|
|
91
|
+
expect(config.regionIds).toContain('sea_region.B2')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should include municipality regions', () => {
|
|
95
|
+
expect(config.regionIds).toContain('municipality.615')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('Geometries', () => {
|
|
100
|
+
it('should have geometries object', () => {
|
|
101
|
+
expect(config.geometries).toBeDefined()
|
|
102
|
+
expect(config.geometries).toHaveProperty('2021')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should have geometry data for counties', () => {
|
|
106
|
+
const geometryData = config.geometries[2021]
|
|
107
|
+
expect(geometryData).toBeDefined()
|
|
108
|
+
|
|
109
|
+
const county1 = geometryData?.['county.1']
|
|
110
|
+
expect(county1).toBeDefined()
|
|
111
|
+
expect(county1).toHaveProperty('name')
|
|
112
|
+
expect(county1).toHaveProperty('type')
|
|
113
|
+
expect(county1).toHaveProperty('weight')
|
|
114
|
+
expect(county1).toHaveProperty('center')
|
|
115
|
+
expect(county1).toHaveProperty('pathLarge')
|
|
116
|
+
expect(county1).toHaveProperty('pathSmall')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should mark land regions correctly', () => {
|
|
120
|
+
const geometryData = config.geometries[2021]
|
|
121
|
+
const county = geometryData?.['county.1'] as
|
|
122
|
+
| { type: string; name: string }
|
|
123
|
+
| undefined
|
|
124
|
+
expect(county?.type).toBe('land')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should mark sea regions correctly', () => {
|
|
128
|
+
const geometryData = config.geometries[2021]
|
|
129
|
+
const seaRegion = geometryData?.['sea_region.B1N'] as
|
|
130
|
+
| { type: string; name: string }
|
|
131
|
+
| undefined
|
|
132
|
+
expect(seaRegion?.type).toBe('sea')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should have borders data', () => {
|
|
136
|
+
const geometryData = config.geometries[2021]
|
|
137
|
+
expect(geometryData).toBeDefined()
|
|
138
|
+
|
|
139
|
+
const borders = geometryData?.borders
|
|
140
|
+
expect(borders).toBeDefined()
|
|
141
|
+
expect(borders).toHaveProperty('land')
|
|
142
|
+
expect(borders).toHaveProperty('sea')
|
|
143
|
+
expect(borders?.land).toHaveProperty('pathLarge')
|
|
144
|
+
expect(borders?.sea).toHaveProperty('pathLarge')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should include parent-child relationships', () => {
|
|
148
|
+
const geometryData = config.geometries[2021]
|
|
149
|
+
const municipality = geometryData?.['municipality.615'] as
|
|
150
|
+
| { parent: string; name: string }
|
|
151
|
+
| undefined
|
|
152
|
+
|
|
153
|
+
expect(municipality).toBeDefined()
|
|
154
|
+
expect(municipality).toHaveProperty('parent')
|
|
155
|
+
expect(municipality?.parent).toBe('county.17')
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('Colors configuration', () => {
|
|
160
|
+
it('should have colors for all themes', () => {
|
|
161
|
+
expect(config.colors).toHaveProperty('light-theme')
|
|
162
|
+
expect(config.colors).toHaveProperty('dark-theme')
|
|
163
|
+
expect(config.colors).toHaveProperty('light-gray-theme')
|
|
164
|
+
expect(config.colors).toHaveProperty('dark-gray-theme')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should define severity levels colors', () => {
|
|
168
|
+
const lightTheme = config.colors['light-theme']
|
|
169
|
+
|
|
170
|
+
expect(lightTheme.levels).toHaveLength(5)
|
|
171
|
+
expect(lightTheme).toHaveProperty('sea')
|
|
172
|
+
expect(lightTheme).toHaveProperty('missing')
|
|
173
|
+
expect(lightTheme).toHaveProperty('stroke')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should have different colors for gray themes', () => {
|
|
177
|
+
const lightTheme = config.colors['light-theme']
|
|
178
|
+
const grayTheme = config.colors['light-gray-theme']
|
|
179
|
+
|
|
180
|
+
expect(lightTheme.levels).not.toEqual(grayTheme.levels)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('Warning icon method', () => {
|
|
185
|
+
it('should generate wind icon with direction', () => {
|
|
186
|
+
const warning = {
|
|
187
|
+
type: 'wind',
|
|
188
|
+
direction: 90,
|
|
189
|
+
severity: 3 as const,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const icon = config.warningIcon(warning)
|
|
193
|
+
|
|
194
|
+
expect(icon).toHaveProperty('aspectRatio')
|
|
195
|
+
expect(icon).toHaveProperty('geom')
|
|
196
|
+
expect(icon.geom).toContain('wind-symbol')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should generate thunder storm icon', () => {
|
|
200
|
+
const warning = {
|
|
201
|
+
type: 'thunderStorm',
|
|
202
|
+
severity: 3 as const,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const icon = config.warningIcon(warning)
|
|
206
|
+
|
|
207
|
+
expect(icon.geom).toContain('thunder-symbol')
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('should generate rain icon', () => {
|
|
211
|
+
const warning = {
|
|
212
|
+
type: 'rain',
|
|
213
|
+
severity: 3 as const,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const icon = config.warningIcon(warning)
|
|
217
|
+
|
|
218
|
+
expect(icon.geom).toContain('rain-symbol')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should generate traffic weather icon', () => {
|
|
222
|
+
const warning = {
|
|
223
|
+
type: 'trafficWeather',
|
|
224
|
+
severity: 3 as const,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const icon = config.warningIcon(warning)
|
|
228
|
+
|
|
229
|
+
expect(icon.geom).toContain('traffic-symbol')
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should generate sea wind icon with text', () => {
|
|
233
|
+
const warning = {
|
|
234
|
+
type: 'seaWind',
|
|
235
|
+
direction: 180,
|
|
236
|
+
text: '15',
|
|
237
|
+
severity: 3 as const,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const icon = config.warningIcon(warning)
|
|
241
|
+
|
|
242
|
+
expect(icon.geom).toContain('seawind-symbol')
|
|
243
|
+
expect(icon.geom).toContain('15')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should generate flood icon based on severity', () => {
|
|
247
|
+
const warning = {
|
|
248
|
+
type: 'floodLevel',
|
|
249
|
+
severity: 3 as const,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const icon = config.warningIcon(warning)
|
|
253
|
+
|
|
254
|
+
expect(icon.geom).toContain('flood-level-3')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('should generate multiple warnings icon', () => {
|
|
258
|
+
const warning = {
|
|
259
|
+
type: 'multiple',
|
|
260
|
+
severity: 3 as const,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const icon = config.warningIcon(warning)
|
|
264
|
+
|
|
265
|
+
expect(icon.geom).toContain('multiple-symbol')
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it('should set scale for specific warning types', () => {
|
|
269
|
+
const warning = {
|
|
270
|
+
type: 'hotWeather',
|
|
271
|
+
severity: 3 as const,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const icon = config.warningIcon(warning)
|
|
275
|
+
|
|
276
|
+
expect(icon.scale).toBe(1.2)
|
|
277
|
+
})
|
|
278
|
+
})
|
|
279
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import { useI18n } from '@/composables/useI18n'
|
|
4
|
+
|
|
5
|
+
describe('useI18n composable', () => {
|
|
6
|
+
describe('t() function with plain language string', () => {
|
|
7
|
+
it('should return Finnish translation', () => {
|
|
8
|
+
const { t } = useI18n('fi')
|
|
9
|
+
|
|
10
|
+
const result = t('noWarnings')
|
|
11
|
+
expect(result).toBeTruthy()
|
|
12
|
+
expect(typeof result).toBe('string')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should return English translation', () => {
|
|
16
|
+
const { t } = useI18n('en')
|
|
17
|
+
|
|
18
|
+
const result = t('noWarnings')
|
|
19
|
+
expect(result).toBeTruthy()
|
|
20
|
+
expect(typeof result).toBe('string')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should return Swedish translation', () => {
|
|
24
|
+
const { t } = useI18n('sv')
|
|
25
|
+
|
|
26
|
+
const result = t('noWarnings')
|
|
27
|
+
expect(result).toBeTruthy()
|
|
28
|
+
expect(typeof result).toBe('string')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should return empty string for unknown language', () => {
|
|
32
|
+
const { t } = useI18n('de')
|
|
33
|
+
|
|
34
|
+
const result = t('noWarnings')
|
|
35
|
+
expect(result).toBe('')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should return empty string for null key', () => {
|
|
39
|
+
const { t } = useI18n('fi')
|
|
40
|
+
|
|
41
|
+
const result = t(null)
|
|
42
|
+
expect(result).toBe('')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should return empty string for undefined key', () => {
|
|
46
|
+
const { t } = useI18n('fi')
|
|
47
|
+
|
|
48
|
+
const result = t(undefined)
|
|
49
|
+
expect(result).toBe('')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should return empty string for non-existent key', () => {
|
|
53
|
+
const { t } = useI18n('fi')
|
|
54
|
+
|
|
55
|
+
const result = t('nonExistentKey12345')
|
|
56
|
+
expect(result).toBe('')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should sanitize HTML in translations', () => {
|
|
60
|
+
const { t } = useI18n('fi')
|
|
61
|
+
|
|
62
|
+
// Assuming translations don't contain scripts
|
|
63
|
+
const result = t('noWarnings')
|
|
64
|
+
expect(result).not.toContain('<script>')
|
|
65
|
+
expect(result).not.toContain('javascript:')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should handle all common translation keys', () => {
|
|
69
|
+
const { t } = useI18n('fi')
|
|
70
|
+
|
|
71
|
+
const commonKeys = [
|
|
72
|
+
'noWarnings',
|
|
73
|
+
'validWarnings',
|
|
74
|
+
'toContent',
|
|
75
|
+
'supportedBrowsers',
|
|
76
|
+
'floodLink',
|
|
77
|
+
'floodLinkText',
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
commonKeys.forEach((key) => {
|
|
81
|
+
const result = t(key)
|
|
82
|
+
expect(typeof result).toBe('string')
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('t() function with reactive language ref', () => {
|
|
88
|
+
it('should return translation based on current ref value', () => {
|
|
89
|
+
const language = ref('fi')
|
|
90
|
+
const { t } = useI18n(language)
|
|
91
|
+
|
|
92
|
+
const result = t('noWarnings')
|
|
93
|
+
expect(result).toBeTruthy()
|
|
94
|
+
expect(typeof result).toBe('string')
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should return empty string when ref value is null', () => {
|
|
98
|
+
const language = ref<string | null>(null)
|
|
99
|
+
const { t } = useI18n(language)
|
|
100
|
+
|
|
101
|
+
const result = t('noWarnings')
|
|
102
|
+
expect(result).toBe('')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('should return translation for different languages', () => {
|
|
106
|
+
const language = ref('en')
|
|
107
|
+
const { t } = useI18n(language)
|
|
108
|
+
|
|
109
|
+
const resultEn = t('noWarnings')
|
|
110
|
+
expect(resultEn).toBeTruthy()
|
|
111
|
+
|
|
112
|
+
// Note: In a real scenario, changing the ref would require re-calling t()
|
|
113
|
+
// The composable evaluates the language at call time
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { useKeyCodes } from '@/composables/useKeyCodes'
|
|
3
|
+
|
|
4
|
+
describe('useKeyCodes composable', () => {
|
|
5
|
+
const keyCodes = useKeyCodes()
|
|
6
|
+
|
|
7
|
+
it('should define KEY_CODE_END', () => {
|
|
8
|
+
expect(keyCodes.KEY_CODE_END).toBe(35)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should define KEY_CODE_HOME', () => {
|
|
12
|
+
expect(keyCodes.KEY_CODE_HOME).toBe(36)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should define KEY_CODE_LEFT', () => {
|
|
16
|
+
expect(keyCodes.KEY_CODE_LEFT).toBe(37)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should define KEY_CODE_RIGHT', () => {
|
|
20
|
+
expect(keyCodes.KEY_CODE_RIGHT).toBe(39)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should have all arrow key codes', () => {
|
|
24
|
+
expect(keyCodes.KEY_CODE_LEFT).toBeLessThan(keyCodes.KEY_CODE_RIGHT)
|
|
25
|
+
expect(keyCodes.KEY_CODE_HOME).toBeLessThan(keyCodes.KEY_CODE_RIGHT)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
uncapitalize,
|
|
4
|
+
twoDigits,
|
|
5
|
+
warningType,
|
|
6
|
+
regionFromReference,
|
|
7
|
+
relativeCoverageFromReference,
|
|
8
|
+
toTimeZone,
|
|
9
|
+
msSinceStartOfDay,
|
|
10
|
+
validInterval,
|
|
11
|
+
} from '@/composables/useUtils'
|
|
12
|
+
|
|
13
|
+
describe('useUtils composable - pure functions', () => {
|
|
14
|
+
describe('uncapitalize', () => {
|
|
15
|
+
it('should uncapitalize first letter', () => {
|
|
16
|
+
expect(uncapitalize('HelloWorld')).toBe('helloWorld')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should handle empty string', () => {
|
|
20
|
+
expect(uncapitalize('')).toBe('')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should handle null', () => {
|
|
24
|
+
expect(uncapitalize(null)).toBe('')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should handle single character', () => {
|
|
28
|
+
expect(uncapitalize('A')).toBe('a')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('warningType', () => {
|
|
33
|
+
it('should parse thunderStorm type correctly', () => {
|
|
34
|
+
const result = warningType({
|
|
35
|
+
warning_context: 'thunder-storm',
|
|
36
|
+
})
|
|
37
|
+
expect(result).toBe('thunderStorm')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should parse wind type correctly', () => {
|
|
41
|
+
const result = warningType({
|
|
42
|
+
warning_context: 'wind',
|
|
43
|
+
})
|
|
44
|
+
expect(result).toBe('wind')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should handle sea-wind with extension', () => {
|
|
48
|
+
const result = warningType({
|
|
49
|
+
warning_context: 'sea-wind',
|
|
50
|
+
})
|
|
51
|
+
expect(result).toBe('seaWind')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should handle context with extension', () => {
|
|
55
|
+
const result = warningType({
|
|
56
|
+
warning_context: 'sea-water-height',
|
|
57
|
+
context_extension: 'high-water',
|
|
58
|
+
})
|
|
59
|
+
expect(result).toBe('seaWaterHeightHighWater')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should handle multi-word contexts', () => {
|
|
63
|
+
const result = warningType({
|
|
64
|
+
warning_context: 'forest-fire-weather',
|
|
65
|
+
})
|
|
66
|
+
expect(result).toBe('forestFireWeather')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('regionFromReference', () => {
|
|
71
|
+
it('should parse single region reference', () => {
|
|
72
|
+
const result = regionFromReference('fi-warning#county.1')
|
|
73
|
+
expect(result).toBe('county.1')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should parse merged region reference', () => {
|
|
77
|
+
const result = regionFromReference(
|
|
78
|
+
'fi-warning#county.1,fi-warning#county.2'
|
|
79
|
+
)
|
|
80
|
+
expect(result).toBe('county_1.2')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should parse multiple merged regions', () => {
|
|
84
|
+
const result = regionFromReference(
|
|
85
|
+
'fi-warning#county.1,fi-warning#county.2,fi-warning#county.3'
|
|
86
|
+
)
|
|
87
|
+
expect(result).toBe('county_1_2.3')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should handle Saimaa special case (lake region)', () => {
|
|
91
|
+
const result = regionFromReference(
|
|
92
|
+
'fi-warning#sea_region_south.FI-115978'
|
|
93
|
+
)
|
|
94
|
+
expect(result).toBe('sea_region_south.FI-115978')
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe('relativeCoverageFromReference', () => {
|
|
99
|
+
it('should extract coverage from reference URL', () => {
|
|
100
|
+
const result = relativeCoverageFromReference('fi-warning#county.1?c=75')
|
|
101
|
+
expect(result).toBe(75)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should return 0 when no coverage parameter', () => {
|
|
105
|
+
const result = relativeCoverageFromReference('fi-warning#county.1')
|
|
106
|
+
expect(result).toBe(0)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should return 0 when no query string', () => {
|
|
110
|
+
const result = relativeCoverageFromReference('county.1')
|
|
111
|
+
expect(result).toBe(0)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should return 0 for null reference', () => {
|
|
115
|
+
const result = relativeCoverageFromReference(null)
|
|
116
|
+
expect(result).toBe(0)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should handle URL with hash fragment', () => {
|
|
120
|
+
const result = relativeCoverageFromReference(
|
|
121
|
+
'fi-warning#county.1?c=50#fragment'
|
|
122
|
+
)
|
|
123
|
+
expect(result).toBe(50)
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('twoDigits', () => {
|
|
128
|
+
it('should pad single digit with zero', () => {
|
|
129
|
+
expect(twoDigits(5)).toBe('05')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should not pad double digit', () => {
|
|
133
|
+
expect(twoDigits(15)).toBe('15')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should handle zero', () => {
|
|
137
|
+
expect(twoDigits(0)).toBe('00')
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('toTimeZone', () => {
|
|
142
|
+
const timeZone = 'Europe/Helsinki'
|
|
143
|
+
const locale = 'fi-FI'
|
|
144
|
+
|
|
145
|
+
it('should convert UTC to Helsinki timezone', () => {
|
|
146
|
+
const date = new Date('2025-10-31T12:00:00Z')
|
|
147
|
+
const result = toTimeZone(date, timeZone, locale)
|
|
148
|
+
|
|
149
|
+
expect(result.timeZone).toBe('Europe/Helsinki')
|
|
150
|
+
expect(result.year).toBe(2025)
|
|
151
|
+
expect(result.month).toBe(10)
|
|
152
|
+
expect(result.day).toBe(31)
|
|
153
|
+
// Helsinki is UTC+2 during DST (summer) or UTC+2 after DST ends
|
|
154
|
+
expect(result.hour).toBe(14)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should handle different date formats', () => {
|
|
158
|
+
const date = '2025-10-31T12:00:00Z'
|
|
159
|
+
const result = toTimeZone(date, timeZone, locale)
|
|
160
|
+
|
|
161
|
+
expect(result.year).toBe(2025)
|
|
162
|
+
expect(result.month).toBe(10)
|
|
163
|
+
expect(result.day).toBe(31)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('msSinceStartOfDay', () => {
|
|
168
|
+
const timeZone = 'Europe/Helsinki'
|
|
169
|
+
const locale = 'fi-FI'
|
|
170
|
+
|
|
171
|
+
it('should calculate milliseconds since start of day', () => {
|
|
172
|
+
// 12:00:00 UTC = 14:00 in Helsinki (UTC+2)
|
|
173
|
+
const timestamp = new Date('2025-10-31T12:00:00Z').getTime()
|
|
174
|
+
const result = msSinceStartOfDay(timestamp, timeZone, locale)
|
|
175
|
+
|
|
176
|
+
// 14 * 60 * 60 * 1000 = 50400000 ms
|
|
177
|
+
expect(result).toBeGreaterThan(40000000) // At least 11+ hours
|
|
178
|
+
expect(result).toBeLessThan(60000000) // Less than 17 hours
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('should handle midnight', () => {
|
|
182
|
+
const timestamp = new Date('2025-10-31T00:00:00Z').getTime()
|
|
183
|
+
const result = msSinceStartOfDay(timestamp, timeZone, locale)
|
|
184
|
+
|
|
185
|
+
expect(result).toBeGreaterThanOrEqual(0)
|
|
186
|
+
expect(result).toBeLessThan(24 * 60 * 60 * 1000)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('validInterval', () => {
|
|
191
|
+
const timeZone = 'Europe/Helsinki'
|
|
192
|
+
const locale = 'fi-FI'
|
|
193
|
+
|
|
194
|
+
it('should format time interval correctly', () => {
|
|
195
|
+
const start = '2025-10-31T12:00:00Z'
|
|
196
|
+
const end = '2025-11-01T18:00:00Z'
|
|
197
|
+
const result = validInterval(start, end, timeZone, locale)
|
|
198
|
+
|
|
199
|
+
// Should contain both dates and times
|
|
200
|
+
expect(result).toContain('31.10.')
|
|
201
|
+
expect(result).toContain('1.11.')
|
|
202
|
+
expect(result).toContain('–') // en-dash separator
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('should include time in HH:MM format', () => {
|
|
206
|
+
const start = '2025-10-31T12:00:00Z'
|
|
207
|
+
const end = '2025-10-31T18:00:00Z'
|
|
208
|
+
const result = validInterval(start, end, timeZone, locale)
|
|
209
|
+
|
|
210
|
+
expect(result).toMatch(/\d{2}:\d{2}/)
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"jsx": "preserve",
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedIndexedAccess": true,
|
|
23
|
+
|
|
24
|
+
/* Path aliases */
|
|
25
|
+
"baseUrl": ".",
|
|
26
|
+
"paths": {
|
|
27
|
+
"@/*": ["./src/*"]
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
/* Types */
|
|
31
|
+
"types": ["vitest/globals", "node"]
|
|
32
|
+
},
|
|
33
|
+
"include": [
|
|
34
|
+
"src/**/*.ts",
|
|
35
|
+
"src/**/*.tsx",
|
|
36
|
+
"src/**/*.vue",
|
|
37
|
+
"src/**/*.d.ts",
|
|
38
|
+
"tests/**/*.ts",
|
|
39
|
+
"vite.config.ts"
|
|
40
|
+
],
|
|
41
|
+
"exclude": ["node_modules", "dist"],
|
|
42
|
+
"references": [{ "path": "./tsconfig.node.json" }]
|
|
43
|
+
}
|