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