@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,367 @@
1
+ import { describe, it, expect, afterEach } from 'vitest'
2
+ import { mount, VueWrapper } from '@vue/test-utils'
3
+ import MapSmall from '@/components/MapSmall.vue'
4
+ import type { DayRegions, WarningsMap, Theme } from '@/types'
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ type ComponentInstance = any
8
+
9
+ const mockRegions: DayRegions = {
10
+ land: [],
11
+ sea: [],
12
+ }
13
+
14
+ describe('MapSmall.vue', () => {
15
+ let wrapper: VueWrapper | null = null
16
+
17
+ afterEach(() => {
18
+ if (wrapper) {
19
+ wrapper.unmount()
20
+ wrapper = null
21
+ }
22
+ })
23
+
24
+ describe('Component mounting', () => {
25
+ it('should mount with default props', () => {
26
+ wrapper = mount(MapSmall, {
27
+ props: {},
28
+ })
29
+
30
+ expect(wrapper.exists()).toBe(true)
31
+ })
32
+
33
+ it('should mount with all props', () => {
34
+ wrapper = mount(MapSmall, {
35
+ props: {
36
+ index: 0,
37
+ input: mockRegions,
38
+ visibleWarnings: [],
39
+ warnings: null,
40
+ geometryId: 2021,
41
+ loading: true,
42
+ theme: 'light-theme' as Theme,
43
+ },
44
+ })
45
+
46
+ expect(wrapper.exists()).toBe(true)
47
+ })
48
+ })
49
+
50
+ describe('Computed properties', () => {
51
+ it('should compute size as Small', () => {
52
+ wrapper = mount(MapSmall, {
53
+ props: {},
54
+ })
55
+
56
+ expect((wrapper.vm as ComponentInstance).size).toBe('Small')
57
+ })
58
+
59
+ it('should have strokeWidth property', () => {
60
+ wrapper = mount(MapSmall, {
61
+ props: {},
62
+ })
63
+
64
+ expect((wrapper.vm as ComponentInstance).strokeWidth).toBe(0.6)
65
+ })
66
+
67
+ it('should have pathsNeeded property', () => {
68
+ wrapper = mount(MapSmall, {
69
+ props: {},
70
+ })
71
+
72
+ expect(typeof (wrapper.vm as ComponentInstance).pathsNeeded).toBe(
73
+ 'boolean'
74
+ )
75
+ })
76
+ })
77
+
78
+ describe('Props handling', () => {
79
+ it('should accept index prop', () => {
80
+ wrapper = mount(MapSmall, {
81
+ props: {
82
+ index: 2,
83
+ },
84
+ })
85
+
86
+ expect((wrapper.vm as ComponentInstance).index).toBe(2)
87
+ })
88
+
89
+ it('should accept loading prop', () => {
90
+ wrapper = mount(MapSmall, {
91
+ props: {
92
+ loading: false,
93
+ },
94
+ })
95
+
96
+ expect((wrapper.vm as ComponentInstance).loading).toBe(false)
97
+ })
98
+
99
+ it('should accept warnings prop', () => {
100
+ const mockWarnings: WarningsMap = {}
101
+
102
+ wrapper = mount(MapSmall, {
103
+ props: {
104
+ warnings: mockWarnings,
105
+ },
106
+ })
107
+
108
+ expect(wrapper.exists()).toBe(true)
109
+ })
110
+
111
+ it('should accept visibleWarnings prop', () => {
112
+ wrapper = mount(MapSmall, {
113
+ props: {
114
+ visibleWarnings: ['wind', 'rain'],
115
+ },
116
+ })
117
+
118
+ expect(wrapper.exists()).toBe(true)
119
+ })
120
+
121
+ it('should accept geometryId prop', () => {
122
+ wrapper = mount(MapSmall, {
123
+ props: {
124
+ geometryId: 2021,
125
+ },
126
+ })
127
+
128
+ expect((wrapper.vm as ComponentInstance).geometryId).toBe(2021)
129
+ })
130
+
131
+ it('should use default index of 0', () => {
132
+ wrapper = mount(MapSmall, {
133
+ props: {},
134
+ })
135
+
136
+ expect((wrapper.vm as ComponentInstance).index).toBe(0)
137
+ })
138
+
139
+ it('should use default geometryId of 2021', () => {
140
+ wrapper = mount(MapSmall, {
141
+ props: {},
142
+ })
143
+
144
+ expect((wrapper.vm as ComponentInstance).geometryId).toBe(2021)
145
+ })
146
+ })
147
+
148
+ describe('Theme support', () => {
149
+ it('should accept theme prop', () => {
150
+ wrapper = mount(MapSmall, {
151
+ props: {
152
+ theme: 'dark-theme' as Theme,
153
+ },
154
+ })
155
+
156
+ expect((wrapper.vm as ComponentInstance).theme).toBe('dark-theme')
157
+ })
158
+
159
+ it('should default to light-theme', () => {
160
+ wrapper = mount(MapSmall, {
161
+ props: {},
162
+ })
163
+
164
+ expect((wrapper.vm as ComponentInstance).theme).toBe('light-theme')
165
+ })
166
+
167
+ it('should support all theme variants', () => {
168
+ const themes: Theme[] = [
169
+ 'light-theme',
170
+ 'dark-theme',
171
+ 'light-gray-theme',
172
+ 'dark-gray-theme',
173
+ ]
174
+
175
+ themes.forEach((theme) => {
176
+ wrapper = mount(MapSmall, {
177
+ props: {
178
+ theme,
179
+ },
180
+ })
181
+
182
+ expect((wrapper.vm as ComponentInstance).theme).toBe(theme)
183
+ })
184
+ })
185
+ })
186
+
187
+ describe('SVG structure', () => {
188
+ it('should render map-small container', () => {
189
+ wrapper = mount(MapSmall, {
190
+ props: {},
191
+ })
192
+
193
+ expect(wrapper.find('.map-small').exists()).toBe(true)
194
+ })
195
+
196
+ it('should render SVG element', () => {
197
+ wrapper = mount(MapSmall, {
198
+ props: {},
199
+ })
200
+
201
+ expect(wrapper.find('svg.finland-small').exists()).toBe(true)
202
+ })
203
+
204
+ it('should have correct SVG dimensions', () => {
205
+ wrapper = mount(MapSmall, {
206
+ props: {},
207
+ })
208
+
209
+ const svg = wrapper.find('svg.finland-small')
210
+ expect(svg.attributes('width')).toBe('75')
211
+ expect(svg.attributes('height')).toBe('120')
212
+ })
213
+
214
+ it('should have correct viewBox', () => {
215
+ wrapper = mount(MapSmall, {
216
+ props: {},
217
+ })
218
+
219
+ expect(wrapper.find('svg.finland-small').attributes('viewBox')).toBe(
220
+ '0 0 75 120'
221
+ )
222
+ })
223
+
224
+ it('should render SVG group with correct id', () => {
225
+ wrapper = mount(MapSmall, {
226
+ props: {
227
+ index: 2,
228
+ },
229
+ })
230
+
231
+ // Group is conditionally rendered based on pathsNeeded
232
+ expect(wrapper.exists()).toBe(true)
233
+ })
234
+ })
235
+
236
+ describe('Map paths', () => {
237
+ it('should have access to strokeColor', () => {
238
+ wrapper = mount(MapSmall, {
239
+ props: {},
240
+ })
241
+
242
+ expect((wrapper.vm as ComponentInstance).strokeColor).toBeDefined()
243
+ })
244
+
245
+ it('should have access to bluePaths', () => {
246
+ wrapper = mount(MapSmall, {
247
+ props: {},
248
+ })
249
+
250
+ expect(Array.isArray((wrapper.vm as ComponentInstance).bluePaths)).toBe(
251
+ true
252
+ )
253
+ })
254
+
255
+ it('should have access to greenPaths', () => {
256
+ wrapper = mount(MapSmall, {
257
+ props: {},
258
+ })
259
+
260
+ expect(Array.isArray((wrapper.vm as ComponentInstance).greenPaths)).toBe(
261
+ true
262
+ )
263
+ })
264
+
265
+ it('should have access to yellowPaths', () => {
266
+ wrapper = mount(MapSmall, {
267
+ props: {},
268
+ })
269
+
270
+ expect(Array.isArray((wrapper.vm as ComponentInstance).yellowPaths)).toBe(
271
+ true
272
+ )
273
+ })
274
+
275
+ it('should have access to orangePaths', () => {
276
+ wrapper = mount(MapSmall, {
277
+ props: {},
278
+ })
279
+
280
+ expect(Array.isArray((wrapper.vm as ComponentInstance).orangePaths)).toBe(
281
+ true
282
+ )
283
+ })
284
+
285
+ it('should have access to redPaths', () => {
286
+ wrapper = mount(MapSmall, {
287
+ props: {},
288
+ })
289
+
290
+ expect(Array.isArray((wrapper.vm as ComponentInstance).redPaths)).toBe(
291
+ true
292
+ )
293
+ })
294
+
295
+ it('should have access to overlayPaths', () => {
296
+ wrapper = mount(MapSmall, {
297
+ props: {},
298
+ })
299
+
300
+ expect(
301
+ Array.isArray((wrapper.vm as ComponentInstance).overlayPaths)
302
+ ).toBe(true)
303
+ })
304
+
305
+ it('should have access to landBorders', () => {
306
+ wrapper = mount(MapSmall, {
307
+ props: {},
308
+ })
309
+
310
+ expect(Array.isArray((wrapper.vm as ComponentInstance).landBorders)).toBe(
311
+ true
312
+ )
313
+ })
314
+
315
+ it('should have access to seaBorders', () => {
316
+ wrapper = mount(MapSmall, {
317
+ props: {},
318
+ })
319
+
320
+ expect(Array.isArray((wrapper.vm as ComponentInstance).seaBorders)).toBe(
321
+ true
322
+ )
323
+ })
324
+ })
325
+
326
+ describe('Coverage data', () => {
327
+ it('should have access to yellowCoverages', () => {
328
+ wrapper = mount(MapSmall, {
329
+ props: {},
330
+ })
331
+
332
+ expect(
333
+ Array.isArray((wrapper.vm as ComponentInstance).yellowCoverages)
334
+ ).toBe(true)
335
+ })
336
+
337
+ it('should have access to orangeCoverages', () => {
338
+ wrapper = mount(MapSmall, {
339
+ props: {},
340
+ })
341
+
342
+ expect(
343
+ Array.isArray((wrapper.vm as ComponentInstance).orangeCoverages)
344
+ ).toBe(true)
345
+ })
346
+
347
+ it('should have access to redCoverages', () => {
348
+ wrapper = mount(MapSmall, {
349
+ props: {},
350
+ })
351
+
352
+ expect(
353
+ Array.isArray((wrapper.vm as ComponentInstance).redCoverages)
354
+ ).toBe(true)
355
+ })
356
+
357
+ it('should have access to overlayCoverages', () => {
358
+ wrapper = mount(MapSmall, {
359
+ props: {},
360
+ })
361
+
362
+ expect(
363
+ Array.isArray((wrapper.vm as ComponentInstance).overlayCoverages)
364
+ ).toBe(true)
365
+ })
366
+ })
367
+ })
@@ -0,0 +1,270 @@
1
+ import { describe, it, expect, afterEach } from 'vitest'
2
+ import { mount, VueWrapper } from '@vue/test-utils'
3
+ import PopupRow from '@/components/PopupRow.vue'
4
+ import type { PopupRowInput } from '@/types'
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ type ComponentInstance = any
8
+
9
+ const mockWarning: PopupRowInput = {
10
+ type: 'wind',
11
+ severity: 3,
12
+ direction: 270,
13
+ text: '15',
14
+ interval: '14:00 – 18:00',
15
+ }
16
+
17
+ describe('PopupRow.vue', () => {
18
+ let wrapper: VueWrapper | null = null
19
+
20
+ afterEach(() => {
21
+ if (wrapper) {
22
+ wrapper.unmount()
23
+ wrapper = null
24
+ }
25
+ })
26
+
27
+ describe('Component mounting', () => {
28
+ it('should mount with required props', () => {
29
+ wrapper = mount(PopupRow, {
30
+ props: {
31
+ input: mockWarning,
32
+ },
33
+ })
34
+
35
+ expect(wrapper.exists()).toBe(true)
36
+ })
37
+
38
+ it('should use default empty object for input prop', () => {
39
+ wrapper = mount(PopupRow, {
40
+ props: {
41
+ input: {
42
+ type: 'rain',
43
+ severity: 2,
44
+ interval: '',
45
+ },
46
+ },
47
+ })
48
+
49
+ expect(wrapper.exists()).toBe(true)
50
+ expect((wrapper.vm as ComponentInstance).input).toBeDefined()
51
+ })
52
+ })
53
+
54
+ describe('Fields mixin integration', () => {
55
+ it('should compute typeClass from warning type', () => {
56
+ wrapper = mount(PopupRow, {
57
+ props: {
58
+ input: mockWarning,
59
+ },
60
+ })
61
+
62
+ expect((wrapper.vm as ComponentInstance).typeClass).toBe('wind')
63
+ })
64
+
65
+ it('should compute typeClass for thunderStorm', () => {
66
+ wrapper = mount(PopupRow, {
67
+ props: {
68
+ input: {
69
+ type: 'thunderStorm',
70
+ severity: 4,
71
+ interval: '',
72
+ },
73
+ },
74
+ })
75
+
76
+ expect((wrapper.vm as ComponentInstance).typeClass).toBe('thunder-storm')
77
+ })
78
+
79
+ it('should compute rotation from direction', () => {
80
+ wrapper = mount(PopupRow, {
81
+ props: {
82
+ input: mockWarning,
83
+ },
84
+ })
85
+
86
+ expect((wrapper.vm as ComponentInstance).rotation).toBe(270)
87
+ })
88
+
89
+ it('should compute invertedRotation', () => {
90
+ wrapper = mount(PopupRow, {
91
+ props: {
92
+ input: mockWarning,
93
+ },
94
+ })
95
+
96
+ expect((wrapper.vm as ComponentInstance).invertedRotation).toBe(90)
97
+ })
98
+
99
+ it('should compute severity', () => {
100
+ wrapper = mount(PopupRow, {
101
+ props: {
102
+ input: mockWarning,
103
+ },
104
+ })
105
+
106
+ expect((wrapper.vm as ComponentInstance).severity).toBe(3)
107
+ })
108
+
109
+ it('should return 0 for invalid severity', () => {
110
+ wrapper = mount(PopupRow, {
111
+ props: {
112
+ input: {
113
+ type: 'wind',
114
+ severity: 1,
115
+ interval: '',
116
+ },
117
+ },
118
+ })
119
+
120
+ expect((wrapper.vm as ComponentInstance).severity).toBe(0)
121
+ })
122
+ })
123
+
124
+ describe('Content rendering', () => {
125
+ it('should render interval text', () => {
126
+ wrapper = mount(PopupRow, {
127
+ props: {
128
+ input: mockWarning,
129
+ },
130
+ })
131
+
132
+ expect(wrapper.text()).toContain('14:00 – 18:00')
133
+ })
134
+
135
+ it('should render warning text in symbol', () => {
136
+ wrapper = mount(PopupRow, {
137
+ props: {
138
+ input: mockWarning,
139
+ },
140
+ })
141
+
142
+ expect(wrapper.find('.symbol-text').text()).toBe('15')
143
+ })
144
+
145
+ it('should render empty text when not provided', () => {
146
+ const warningNoText: PopupRowInput = {
147
+ type: 'rain',
148
+ severity: 2,
149
+ interval: '10:00 – 12:00',
150
+ }
151
+
152
+ wrapper = mount(PopupRow, {
153
+ props: {
154
+ input: warningNoText,
155
+ },
156
+ })
157
+
158
+ expect(wrapper.find('.symbol-text').text()).toBe('')
159
+ })
160
+ })
161
+
162
+ describe('CSS classes', () => {
163
+ it('should apply severity level class to symbol', () => {
164
+ wrapper = mount(PopupRow, {
165
+ props: {
166
+ input: mockWarning,
167
+ },
168
+ })
169
+
170
+ const symbol = wrapper.find('.popup-table-symbol-cell')
171
+ expect(symbol.classes()).toContain('level-3')
172
+ })
173
+
174
+ it('should apply type class to symbol', () => {
175
+ wrapper = mount(PopupRow, {
176
+ props: {
177
+ input: mockWarning,
178
+ },
179
+ })
180
+
181
+ const symbol = wrapper.find('.popup-table-symbol-cell')
182
+ expect(symbol.classes()).toContain('wind')
183
+ })
184
+
185
+ it('should apply rotation class', () => {
186
+ wrapper = mount(PopupRow, {
187
+ props: {
188
+ input: mockWarning,
189
+ },
190
+ })
191
+
192
+ const symbol = wrapper.find('.popup-table-symbol-cell')
193
+ expect(symbol.classes()).toContain('transform-rotate-270')
194
+ })
195
+
196
+ it('should apply inverted rotation to text', () => {
197
+ wrapper = mount(PopupRow, {
198
+ props: {
199
+ input: mockWarning,
200
+ },
201
+ })
202
+
203
+ const text = wrapper.find('.symbol-text')
204
+ expect(text.classes()).toContain('transform-rotate-90')
205
+ })
206
+
207
+ it('should apply text level class to interval cell', () => {
208
+ wrapper = mount(PopupRow, {
209
+ props: {
210
+ input: mockWarning,
211
+ },
212
+ })
213
+
214
+ const textCell = wrapper.find('.popup-table-text-cell')
215
+ expect(textCell.classes()).toContain('text-level-3')
216
+ })
217
+
218
+ it('should hide level-0 warnings', () => {
219
+ const warningLevel0: PopupRowInput = {
220
+ type: 'wind',
221
+ severity: 0,
222
+ interval: '',
223
+ }
224
+
225
+ wrapper = mount(PopupRow, {
226
+ props: {
227
+ input: warningLevel0,
228
+ },
229
+ })
230
+
231
+ // Level-0 warning images should have display: none via CSS
232
+ const symbol = wrapper.find('.popup-table-symbol-cell')
233
+ expect(symbol.classes()).toContain('level-0')
234
+ })
235
+ })
236
+
237
+ describe('Table structure', () => {
238
+ it('should render as table row', () => {
239
+ wrapper = mount(PopupRow, {
240
+ props: {
241
+ input: mockWarning,
242
+ },
243
+ })
244
+
245
+ expect(wrapper.find('.popup-table-row').exists()).toBe(true)
246
+ })
247
+
248
+ it('should have two table cells', () => {
249
+ wrapper = mount(PopupRow, {
250
+ props: {
251
+ input: mockWarning,
252
+ },
253
+ })
254
+
255
+ const cells = wrapper.findAll('.popup-table-cell')
256
+ expect(cells).toHaveLength(2)
257
+ })
258
+
259
+ it('should have symbol cell and text cell', () => {
260
+ wrapper = mount(PopupRow, {
261
+ props: {
262
+ input: mockWarning,
263
+ },
264
+ })
265
+
266
+ expect(wrapper.find('.popup-table-symbol-cell').exists()).toBe(true)
267
+ expect(wrapper.find('.popup-table-text-cell').exists()).toBe(true)
268
+ })
269
+ })
270
+ })