@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,385 @@
1
+ import { describe, it, expect, afterEach } from 'vitest'
2
+ import { mount, VueWrapper } from '@vue/test-utils'
3
+ import DescriptionWarning from '@/components/DescriptionWarning.vue'
4
+ import type { Warning, Theme, Language } from '@/types'
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ type ComponentInstance = any
8
+
9
+ const mockWarning: Warning = {
10
+ type: 'wind',
11
+ id: 'test-warning-1',
12
+ regions: { 'county.1': true },
13
+ covRegions: new Map(),
14
+ coveragesLarge: [],
15
+ coveragesSmall: [],
16
+ effectiveFrom: '2025-10-31T12:00:00Z',
17
+ effectiveUntil: '2025-11-01T12:00:00Z',
18
+ effectiveDays: [true, true, false, false, false],
19
+ validInterval: '31.10.2025 14:00 – 1.11.2025 14:00',
20
+ severity: 3,
21
+ direction: 270,
22
+ value: 25,
23
+ text: '25',
24
+ info: {
25
+ fi: 'Kovaa tuulta',
26
+ sv: 'Hårt blåsväder',
27
+ en: 'Strong wind',
28
+ },
29
+ link: 'https://example.com',
30
+ linkText: 'Lisätietoja',
31
+ }
32
+
33
+ describe('DescriptionWarning.vue', () => {
34
+ let wrapper: VueWrapper | null = null
35
+
36
+ afterEach(() => {
37
+ if (wrapper) {
38
+ wrapper.unmount()
39
+ wrapper = null
40
+ }
41
+ })
42
+
43
+ describe('Component mounting', () => {
44
+ it('should mount with required props', () => {
45
+ wrapper = mount(DescriptionWarning, {
46
+ props: {
47
+ input: mockWarning,
48
+ language: 'fi' as Language,
49
+ theme: 'light-theme' as Theme,
50
+ },
51
+ })
52
+
53
+ expect(wrapper.exists()).toBe(true)
54
+ })
55
+ })
56
+
57
+ describe('Computed properties', () => {
58
+ it('should compute warningTitle from translations', () => {
59
+ wrapper = mount(DescriptionWarning, {
60
+ props: {
61
+ input: mockWarning,
62
+ language: 'fi' as Language,
63
+ },
64
+ })
65
+
66
+ expect(typeof (wrapper.vm as ComponentInstance).warningTitle).toBe(
67
+ 'string'
68
+ )
69
+ })
70
+
71
+ it('should compute warningLevel from translations', () => {
72
+ wrapper = mount(DescriptionWarning, {
73
+ props: {
74
+ input: mockWarning,
75
+ language: 'fi' as Language,
76
+ },
77
+ })
78
+
79
+ expect(typeof (wrapper.vm as ComponentInstance).warningLevel).toBe(
80
+ 'string'
81
+ )
82
+ })
83
+
84
+ it('should compute warningDetails for wind with direction', () => {
85
+ wrapper = mount(DescriptionWarning, {
86
+ props: {
87
+ input: mockWarning,
88
+ language: 'fi' as Language,
89
+ },
90
+ })
91
+
92
+ expect((wrapper.vm as ComponentInstance).warningDetails).toContain('m/s')
93
+ })
94
+
95
+ it('should return empty warningDetails when text is null', () => {
96
+ const warningNoText: Warning = {
97
+ ...mockWarning,
98
+ text: '',
99
+ direction: 0,
100
+ }
101
+
102
+ wrapper = mount(DescriptionWarning, {
103
+ props: {
104
+ input: warningNoText,
105
+ language: 'fi' as Language,
106
+ },
107
+ })
108
+
109
+ expect((wrapper.vm as ComponentInstance).warningDetails).toBe('')
110
+ })
111
+
112
+ it('should compute info based on language', () => {
113
+ wrapper = mount(DescriptionWarning, {
114
+ props: {
115
+ input: mockWarning,
116
+ language: 'fi' as Language,
117
+ },
118
+ })
119
+
120
+ expect((wrapper.vm as ComponentInstance).info).toBe('Kovaa tuulta')
121
+ })
122
+
123
+ it('should compute info for Swedish', () => {
124
+ wrapper = mount(DescriptionWarning, {
125
+ props: {
126
+ input: mockWarning,
127
+ language: 'sv' as Language,
128
+ },
129
+ })
130
+
131
+ expect((wrapper.vm as ComponentInstance).info).toBe('Hårt blåsväder')
132
+ })
133
+
134
+ it('should compute info for English', () => {
135
+ wrapper = mount(DescriptionWarning, {
136
+ props: {
137
+ input: mockWarning,
138
+ language: 'en' as Language,
139
+ },
140
+ })
141
+
142
+ expect((wrapper.vm as ComponentInstance).info).toBe('Strong wind')
143
+ })
144
+
145
+ it('should compute validText from translations', () => {
146
+ wrapper = mount(DescriptionWarning, {
147
+ props: {
148
+ input: mockWarning,
149
+ language: 'fi' as Language,
150
+ },
151
+ })
152
+
153
+ expect(typeof (wrapper.vm as ComponentInstance).validText).toBe('string')
154
+ })
155
+
156
+ it('should compute linkHidden when link is empty', () => {
157
+ const warningNoLink: Warning = {
158
+ ...mockWarning,
159
+ link: '',
160
+ }
161
+
162
+ wrapper = mount(DescriptionWarning, {
163
+ props: {
164
+ input: warningNoLink,
165
+ language: 'fi' as Language,
166
+ },
167
+ })
168
+
169
+ expect((wrapper.vm as ComponentInstance).linkHidden).toBe(true)
170
+ })
171
+
172
+ it('should compute linkHidden as false when link exists', () => {
173
+ wrapper = mount(DescriptionWarning, {
174
+ props: {
175
+ input: mockWarning,
176
+ language: 'fi' as Language,
177
+ },
178
+ })
179
+
180
+ expect((wrapper.vm as ComponentInstance).linkHidden).toBe(false)
181
+ })
182
+
183
+ it('should compute description from translations', () => {
184
+ wrapper = mount(DescriptionWarning, {
185
+ props: {
186
+ input: mockWarning,
187
+ language: 'fi' as Language,
188
+ },
189
+ })
190
+
191
+ expect(typeof (wrapper.vm as ComponentInstance).description).toBe(
192
+ 'string'
193
+ )
194
+ })
195
+ })
196
+
197
+ describe('Fields mixin integration', () => {
198
+ it('should compute severity', () => {
199
+ wrapper = mount(DescriptionWarning, {
200
+ props: {
201
+ input: mockWarning,
202
+ language: 'fi' as Language,
203
+ },
204
+ })
205
+
206
+ expect((wrapper.vm as ComponentInstance).severity).toBe(3)
207
+ })
208
+
209
+ it('should compute typeClass', () => {
210
+ wrapper = mount(DescriptionWarning, {
211
+ props: {
212
+ input: mockWarning,
213
+ language: 'fi' as Language,
214
+ },
215
+ })
216
+
217
+ expect((wrapper.vm as ComponentInstance).typeClass).toBe('wind')
218
+ })
219
+
220
+ it('should compute rotation', () => {
221
+ wrapper = mount(DescriptionWarning, {
222
+ props: {
223
+ input: mockWarning,
224
+ language: 'fi' as Language,
225
+ },
226
+ })
227
+
228
+ expect((wrapper.vm as ComponentInstance).rotation).toBe(270)
229
+ })
230
+
231
+ it('should compute invertedRotation', () => {
232
+ wrapper = mount(DescriptionWarning, {
233
+ props: {
234
+ input: mockWarning,
235
+ language: 'fi' as Language,
236
+ },
237
+ })
238
+
239
+ expect((wrapper.vm as ComponentInstance).invertedRotation).toBe(90)
240
+ })
241
+ })
242
+
243
+ describe('Content rendering', () => {
244
+ it('should render valid interval', () => {
245
+ wrapper = mount(DescriptionWarning, {
246
+ props: {
247
+ input: mockWarning,
248
+ language: 'fi' as Language,
249
+ },
250
+ })
251
+
252
+ expect(wrapper.html()).toContain(mockWarning.validInterval)
253
+ })
254
+
255
+ it('should render warning text in symbol', () => {
256
+ wrapper = mount(DescriptionWarning, {
257
+ props: {
258
+ input: mockWarning,
259
+ language: 'fi' as Language,
260
+ },
261
+ })
262
+
263
+ expect(wrapper.find('.symbol-text').text()).toBe('25')
264
+ })
265
+
266
+ it('should render link when provided', () => {
267
+ wrapper = mount(DescriptionWarning, {
268
+ props: {
269
+ input: mockWarning,
270
+ language: 'fi' as Language,
271
+ },
272
+ })
273
+
274
+ const link = wrapper.find('.ext-link')
275
+ expect(link.exists()).toBe(true)
276
+ expect(link.attributes('href')).toBe('https://example.com')
277
+ })
278
+
279
+ it('should hide link when not provided', () => {
280
+ const warningNoLink: Warning = {
281
+ ...mockWarning,
282
+ link: '',
283
+ }
284
+
285
+ wrapper = mount(DescriptionWarning, {
286
+ props: {
287
+ input: warningNoLink,
288
+ language: 'fi' as Language,
289
+ },
290
+ })
291
+
292
+ const link = wrapper.find('.ext-link')
293
+ expect(link.classes()).toContain('d-none')
294
+ })
295
+ })
296
+
297
+ describe('CSS classes', () => {
298
+ it('should apply severity level class to image', () => {
299
+ wrapper = mount(DescriptionWarning, {
300
+ props: {
301
+ input: mockWarning,
302
+ language: 'fi' as Language,
303
+ },
304
+ })
305
+
306
+ const image = wrapper.find('.current-description-image')
307
+ expect(image.classes()).toContain('level-3')
308
+ })
309
+
310
+ it('should apply type class to image', () => {
311
+ wrapper = mount(DescriptionWarning, {
312
+ props: {
313
+ input: mockWarning,
314
+ language: 'fi' as Language,
315
+ },
316
+ })
317
+
318
+ const image = wrapper.find('.current-description-image')
319
+ expect(image.classes()).toContain('wind')
320
+ })
321
+
322
+ it('should apply rotation class', () => {
323
+ wrapper = mount(DescriptionWarning, {
324
+ props: {
325
+ input: mockWarning,
326
+ language: 'fi' as Language,
327
+ },
328
+ })
329
+
330
+ const image = wrapper.find('.current-description-image')
331
+ expect(image.classes()).toContain('transform-rotate-270')
332
+ })
333
+
334
+ it('should apply severity class to description rectangle', () => {
335
+ wrapper = mount(DescriptionWarning, {
336
+ props: {
337
+ input: mockWarning,
338
+ language: 'fi' as Language,
339
+ },
340
+ })
341
+
342
+ const rectangle = wrapper.find('.description-rectangle')
343
+ expect(rectangle.classes()).toContain('level-3')
344
+ })
345
+ })
346
+
347
+ describe('Theme support', () => {
348
+ it('should apply theme class', () => {
349
+ wrapper = mount(DescriptionWarning, {
350
+ props: {
351
+ input: mockWarning,
352
+ language: 'fi' as Language,
353
+ theme: 'dark-theme' as Theme,
354
+ },
355
+ })
356
+
357
+ expect(wrapper.find('.current-description-row').classes()).toContain(
358
+ 'dark-theme'
359
+ )
360
+ })
361
+
362
+ it('should support all theme variants', () => {
363
+ const themes: Theme[] = [
364
+ 'light-theme',
365
+ 'dark-theme',
366
+ 'light-gray-theme',
367
+ 'dark-gray-theme',
368
+ ]
369
+
370
+ themes.forEach((theme) => {
371
+ wrapper = mount(DescriptionWarning, {
372
+ props: {
373
+ input: mockWarning,
374
+ language: 'fi' as Language,
375
+ theme,
376
+ },
377
+ })
378
+
379
+ expect(wrapper.find('.current-description-row').classes()).toContain(
380
+ theme
381
+ )
382
+ })
383
+ })
384
+ })
385
+ })
@@ -0,0 +1,318 @@
1
+ import { describe, it, expect, afterEach, vi } from 'vitest'
2
+ import { mount, VueWrapper } from '@vue/test-utils'
3
+ import GrayScaleToggle from '@/components/GrayScaleToggle.vue'
4
+ import type { Theme, Language } from '@/types'
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ type ComponentInstance = any
8
+
9
+ describe('GrayScaleToggle.vue', () => {
10
+ let wrapper: VueWrapper | null = null
11
+
12
+ afterEach(() => {
13
+ if (wrapper) {
14
+ wrapper.unmount()
15
+ wrapper = null
16
+ }
17
+ })
18
+
19
+ describe('Component mounting', () => {
20
+ it('should mount with required props', () => {
21
+ wrapper = mount(GrayScaleToggle, {
22
+ props: {
23
+ language: 'fi' as Language,
24
+ grayScaleSelector: true,
25
+ },
26
+ })
27
+
28
+ expect(wrapper.exists()).toBe(true)
29
+ })
30
+
31
+ it('should not render when grayScaleSelector is false', () => {
32
+ wrapper = mount(GrayScaleToggle, {
33
+ props: {
34
+ language: 'fi' as Language,
35
+ grayScaleSelector: false,
36
+ },
37
+ })
38
+
39
+ expect(wrapper.find('#gray-scale-select-row').exists()).toBe(false)
40
+ })
41
+
42
+ it('should render when grayScaleSelector is true', () => {
43
+ wrapper = mount(GrayScaleToggle, {
44
+ props: {
45
+ language: 'fi' as Language,
46
+ grayScaleSelector: true,
47
+ },
48
+ })
49
+
50
+ expect(wrapper.find('#gray-scale-select-row').exists()).toBe(true)
51
+ })
52
+ })
53
+
54
+ describe('Computed properties', () => {
55
+ it('should detect gray scale from theme', () => {
56
+ wrapper = mount(GrayScaleToggle, {
57
+ props: {
58
+ language: 'fi' as Language,
59
+ grayScaleSelector: true,
60
+ theme: 'light-gray-theme' as Theme,
61
+ },
62
+ })
63
+
64
+ expect((wrapper.vm as ComponentInstance).grayScale).toBe(true)
65
+ })
66
+
67
+ it('should detect non-gray theme', () => {
68
+ wrapper = mount(GrayScaleToggle, {
69
+ props: {
70
+ language: 'fi' as Language,
71
+ grayScaleSelector: true,
72
+ theme: 'light-theme' as Theme,
73
+ },
74
+ })
75
+
76
+ expect((wrapper.vm as ComponentInstance).grayScale).toBe(false)
77
+ })
78
+
79
+ it('should handle dark-gray theme', () => {
80
+ wrapper = mount(GrayScaleToggle, {
81
+ props: {
82
+ language: 'fi' as Language,
83
+ grayScaleSelector: true,
84
+ theme: 'dark-gray-theme' as Theme,
85
+ },
86
+ })
87
+
88
+ expect((wrapper.vm as ComponentInstance).grayScale).toBe(true)
89
+ })
90
+
91
+ it('should return false for empty theme', () => {
92
+ wrapper = mount(GrayScaleToggle, {
93
+ props: {
94
+ language: 'fi' as Language,
95
+ grayScaleSelector: true,
96
+ theme: '' as Theme,
97
+ },
98
+ })
99
+
100
+ expect((wrapper.vm as ComponentInstance).grayScale).toBe(false)
101
+ })
102
+
103
+ it('should return false for null theme', () => {
104
+ wrapper = mount(GrayScaleToggle, {
105
+ props: {
106
+ language: 'fi' as Language,
107
+ grayScaleSelector: true,
108
+ theme: null as unknown as Theme,
109
+ },
110
+ })
111
+
112
+ expect((wrapper.vm as ComponentInstance).grayScale).toBe(false)
113
+ })
114
+
115
+ it('should compute grayScaleText', () => {
116
+ wrapper = mount(GrayScaleToggle, {
117
+ props: {
118
+ language: 'fi' as Language,
119
+ grayScaleSelector: true,
120
+ },
121
+ })
122
+
123
+ expect(typeof (wrapper.vm as ComponentInstance).grayScaleText).toBe(
124
+ 'string'
125
+ )
126
+ })
127
+
128
+ it('should show toggleOn text when gray scale is active', () => {
129
+ wrapper = mount(GrayScaleToggle, {
130
+ props: {
131
+ language: 'fi' as Language,
132
+ grayScaleSelector: true,
133
+ theme: 'light-gray-theme' as Theme,
134
+ },
135
+ })
136
+
137
+ expect(typeof (wrapper.vm as ComponentInstance).toggleText).toBe('string')
138
+ })
139
+
140
+ it('should show toggleOff text when gray scale is inactive', () => {
141
+ wrapper = mount(GrayScaleToggle, {
142
+ props: {
143
+ language: 'fi' as Language,
144
+ grayScaleSelector: true,
145
+ theme: 'light-theme' as Theme,
146
+ },
147
+ })
148
+
149
+ expect(typeof (wrapper.vm as ComponentInstance).toggleText).toBe('string')
150
+ })
151
+ })
152
+
153
+ describe('Toggle functionality', () => {
154
+ it('should emit themeChanged when toggling from normal to gray', () => {
155
+ wrapper = mount(GrayScaleToggle, {
156
+ props: {
157
+ language: 'fi' as Language,
158
+ grayScaleSelector: true,
159
+ theme: 'light-theme' as Theme,
160
+ },
161
+ })
162
+
163
+ const event = { preventDefault: vi.fn() }
164
+ ;(wrapper.vm as ComponentInstance).toggleGrayScale(event)
165
+
166
+ expect(wrapper.emitted('themeChanged')).toBeTruthy()
167
+ expect(wrapper.emitted('themeChanged')![0]).toEqual(['light-gray'])
168
+ expect(event.preventDefault).toHaveBeenCalled()
169
+ })
170
+
171
+ it('should emit themeChanged when toggling from gray to normal', () => {
172
+ wrapper = mount(GrayScaleToggle, {
173
+ props: {
174
+ language: 'fi' as Language,
175
+ grayScaleSelector: true,
176
+ theme: 'light-gray-theme' as Theme,
177
+ },
178
+ })
179
+
180
+ const event = { preventDefault: vi.fn() }
181
+ ;(wrapper.vm as ComponentInstance).toggleGrayScale(event)
182
+
183
+ expect(wrapper.emitted('themeChanged')).toBeTruthy()
184
+ expect(wrapper.emitted('themeChanged')![0]).toEqual(['light'])
185
+ })
186
+
187
+ it('should handle dark theme toggle', () => {
188
+ wrapper = mount(GrayScaleToggle, {
189
+ props: {
190
+ language: 'fi' as Language,
191
+ grayScaleSelector: true,
192
+ theme: 'dark-theme' as Theme,
193
+ },
194
+ })
195
+
196
+ const event = { preventDefault: vi.fn() }
197
+ ;(wrapper.vm as ComponentInstance).toggleGrayScale(event)
198
+
199
+ expect(wrapper.emitted('themeChanged')![0]).toEqual(['dark-gray'])
200
+ })
201
+
202
+ it('should not emit when theme is empty', () => {
203
+ wrapper = mount(GrayScaleToggle, {
204
+ props: {
205
+ language: 'fi' as Language,
206
+ grayScaleSelector: true,
207
+ theme: '' as Theme,
208
+ },
209
+ })
210
+
211
+ const event = { preventDefault: vi.fn() }
212
+ ;(wrapper.vm as ComponentInstance).toggleGrayScale(event)
213
+
214
+ expect(wrapper.emitted('themeChanged')).toBeFalsy()
215
+ })
216
+
217
+ it('should prevent default event', () => {
218
+ wrapper = mount(GrayScaleToggle, {
219
+ props: {
220
+ language: 'fi' as Language,
221
+ grayScaleSelector: true,
222
+ theme: 'light-theme' as Theme,
223
+ },
224
+ })
225
+
226
+ const event = { preventDefault: vi.fn() }
227
+ ;(wrapper.vm as ComponentInstance).preventEvents(event)
228
+
229
+ expect(event.preventDefault).toHaveBeenCalled()
230
+ })
231
+ })
232
+
233
+ describe('CSS classes', () => {
234
+ it('should apply gray-scale-selected class when active', () => {
235
+ wrapper = mount(GrayScaleToggle, {
236
+ props: {
237
+ language: 'fi' as Language,
238
+ grayScaleSelector: true,
239
+ theme: 'light-gray-theme' as Theme,
240
+ },
241
+ })
242
+
243
+ expect(wrapper.find('#gray-scale-select').classes()).toContain(
244
+ 'gray-scale-selected'
245
+ )
246
+ })
247
+
248
+ it('should apply gray-scale-unselected class when inactive', () => {
249
+ wrapper = mount(GrayScaleToggle, {
250
+ props: {
251
+ language: 'fi' as Language,
252
+ grayScaleSelector: true,
253
+ theme: 'light-theme' as Theme,
254
+ },
255
+ })
256
+
257
+ expect(wrapper.find('#gray-scale-select').classes()).toContain(
258
+ 'gray-scale-unselected'
259
+ )
260
+ })
261
+
262
+ it('should apply theme class to container', () => {
263
+ wrapper = mount(GrayScaleToggle, {
264
+ props: {
265
+ language: 'fi' as Language,
266
+ grayScaleSelector: true,
267
+ theme: 'dark-theme' as Theme,
268
+ },
269
+ })
270
+
271
+ expect(wrapper.find('#gray-scale-select-row').classes()).toContain(
272
+ 'dark-theme'
273
+ )
274
+ })
275
+ })
276
+
277
+ describe('Accessibility', () => {
278
+ it('should have correct ARIA attributes when inactive', () => {
279
+ wrapper = mount(GrayScaleToggle, {
280
+ props: {
281
+ language: 'fi' as Language,
282
+ grayScaleSelector: true,
283
+ theme: 'light-theme' as Theme,
284
+ },
285
+ })
286
+
287
+ const toggle = wrapper.find('#gray-scale-select')
288
+ expect(toggle.attributes('role')).toBe('button')
289
+ expect(toggle.attributes('tabindex')).toBe('0')
290
+ expect(toggle.attributes('aria-pressed')).toBe('false')
291
+ })
292
+
293
+ it('should have correct ARIA attributes when active', () => {
294
+ wrapper = mount(GrayScaleToggle, {
295
+ props: {
296
+ language: 'fi' as Language,
297
+ grayScaleSelector: true,
298
+ theme: 'light-gray-theme' as Theme,
299
+ },
300
+ })
301
+
302
+ const toggle = wrapper.find('#gray-scale-select')
303
+ expect(toggle.attributes('aria-pressed')).toBe('true')
304
+ })
305
+
306
+ it('should have aria-label', () => {
307
+ wrapper = mount(GrayScaleToggle, {
308
+ props: {
309
+ language: 'fi' as Language,
310
+ grayScaleSelector: true,
311
+ },
312
+ })
313
+
314
+ const toggle = wrapper.find('#gray-scale-select')
315
+ expect(toggle.attributes('aria-label')).toBeDefined()
316
+ })
317
+ })
318
+ })