UNPKG

@fmidev/smartmet-alert-client

Version:

Web application for viewing weather and flood alerts

702 lines (578 loc) 19.1 kB
import { describe, it, expect, afterEach, vi } from 'vitest' import { mount, flushPromises, VueWrapper } from '@vue/test-utils' import AlertClient from '@/components/AlertClient.vue' import Days from '@/components/Days.vue' import Regions from '@/components/Regions.vue' import Legend from '@/components/Legend.vue' import { mockWarningsData } from '../../fixtures/mockWarningData' import type { Language, Theme, WarningsData } from '@/types' // eslint-disable-next-line @typescript-eslint/no-explicit-any type ComponentInstance = any describe('AlertClient.vue', () => { let wrapper: VueWrapper | null = null afterEach(() => { if (wrapper) { wrapper.unmount() wrapper = null } vi.restoreAllMocks() }) describe('Component mounting', () => { it('should mount with default props and render child components', () => { wrapper = mount(AlertClient, { props: { language: 'fi' as Language, }, }) // Verify component exists expect(wrapper.exists()).toBe(true) // Verify child components are rendered expect(wrapper.findComponent(Days).exists()).toBe(true) expect(wrapper.findComponent(Regions).exists()).toBe(true) expect(wrapper.findComponent(Legend).exists()).toBe(true) // Verify default state const vm = wrapper.vm as ComponentInstance expect(vm.selectedDay).toBe(0) expect(vm.theme).toBe('light-theme') expect(vm.geometryId).toBe(2021) expect(vm.visibleWarnings).toEqual([]) expect(vm.warnings).toBeNull() }) it('should use provided default day prop', () => { wrapper = mount(AlertClient, { props: { defaultDay: 2, language: 'fi' as Language, }, }) expect((wrapper.vm as ComponentInstance).selectedDay).toBe(2) }) it('should initialize with correct data', () => { wrapper = mount(AlertClient, { props: { language: 'fi' as Language, }, }) const vm = wrapper.vm as ComponentInstance expect(vm.selectedDay).toBeDefined() expect(vm.visibleWarnings).toEqual([]) expect(vm.warnings).toBeNull() expect(vm.days).toEqual([]) }) }) describe('Props validation', () => { it('should accept refresh interval prop', () => { wrapper = mount(AlertClient, { props: { refreshInterval: 60000, language: 'fi' as Language, }, }) expect((wrapper.vm as ComponentInstance).refreshInterval).toBe(60000) }) it('should use default refresh interval', () => { wrapper = mount(AlertClient, { props: { language: 'fi' as Language, }, }) expect((wrapper.vm as ComponentInstance).refreshInterval).toBe(900000) // 15 minutes }) it('should accept warnings data prop', () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) expect((wrapper.vm as ComponentInstance).warningsData).toBeDefined() }) it('should accept theme prop', () => { wrapper = mount(AlertClient, { props: { theme: 'dark-theme' as Theme, language: 'fi' as Language, }, }) expect((wrapper.vm as ComponentInstance).theme).toBe('dark-theme') }) it('should accept geometry ID prop', () => { wrapper = mount(AlertClient, { props: { geometryId: 2021, language: 'fi' as Language, }, }) expect((wrapper.vm as ComponentInstance).geometryId).toBe(2021) }) }) describe('Timer functionality', () => { it('should initialize timer with correct interval', () => { const setIntervalSpy = vi.spyOn(global, 'setInterval') wrapper = mount(AlertClient, { props: { refreshInterval: 60000, language: 'fi' as Language, }, }) expect((wrapper.vm as ComponentInstance).timer).toBeDefined() expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 60000) }) it('should not initialize timer when refresh interval is 0', () => { wrapper = mount(AlertClient, { props: { refreshInterval: 0, language: 'fi' as Language, }, }) // Timer should not be initialized when interval is 0 expect((wrapper.vm as ComponentInstance).timer).toBeNull() }) it('should have cancelTimer method', () => { wrapper = mount(AlertClient, { props: { refreshInterval: 60000, language: 'fi' as Language, }, }) const vm = wrapper.vm as ComponentInstance expect(vm.timer).toBeDefined() expect(typeof vm.cancelTimer).toBe('function') // cancelTimer should be callable without errors expect(() => vm.cancelTimer()).not.toThrow() }) it('should have initTimer method', () => { wrapper = mount(AlertClient, { props: { refreshInterval: 60000, language: 'fi' as Language, }, }) const vm = wrapper.vm as ComponentInstance expect(typeof vm.initTimer).toBe('function') expect(vm.timer).toBeDefined() }) it('should handle multiple timer cancellations safely', () => { wrapper = mount(AlertClient, { props: { refreshInterval: 60000, language: 'fi' as Language, }, }) const vm = wrapper.vm as ComponentInstance // Multiple cancellations should not throw errors expect(() => { vm.cancelTimer() vm.cancelTimer() vm.cancelTimer() }).not.toThrow() }) }) describe('Event emissions', () => { it('should emit loaded event on data loaded', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() ;(wrapper.vm as ComponentInstance).onLoaded(true) expect(wrapper.emitted('loaded')).toBeTruthy() }) it('should emit themeChanged event', () => { wrapper = mount(AlertClient, { props: { language: 'fi' as Language, }, }) ;(wrapper.vm as ComponentInstance).onThemeChanged('dark-theme') expect(wrapper.emitted('themeChanged')).toBeTruthy() expect(wrapper.emitted('themeChanged')![0]).toEqual(['dark-theme']) }) it('should emit update-warnings event on update', () => { wrapper = mount(AlertClient, { props: { refreshInterval: 60000, language: 'fi' as Language, }, }) ;(wrapper.vm as ComponentInstance).update() expect(wrapper.emitted('update-warnings')).toBeTruthy() }) it('should not emit themeChanged if theme is same', () => { wrapper = mount(AlertClient, { props: { theme: 'light-theme' as Theme, language: 'fi' as Language, }, }) ;(wrapper.vm as ComponentInstance).onThemeChanged('light-theme') expect(wrapper.emitted('themeChanged')).toBeFalsy() }) }) describe('Data processing', () => { it('should process warnings data when provided', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() const vm = wrapper.vm as ComponentInstance expect(vm.warnings).toBeDefined() expect(vm.days).toBeDefined() expect(vm.days.length).toBe(5) }) it('should create visible warnings from legend', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() expect( Array.isArray((wrapper.vm as ComponentInstance).visibleWarnings) ).toBe(true) }) it('should update regions data', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() const vm = wrapper.vm as ComponentInstance expect(vm.regions).toBeDefined() expect(Array.isArray(vm.regions)).toBe(true) }) }) describe('Day selection', () => { it('should update selected day on daySelected event', () => { wrapper = mount(AlertClient, { props: { language: 'fi' as Language, }, }) ;(wrapper.vm as ComponentInstance).onDaySelected(2) expect((wrapper.vm as ComponentInstance).selectedDay).toBe(2) }) it('should start with default day', () => { wrapper = mount(AlertClient, { props: { defaultDay: 3, language: 'fi' as Language, }, }) expect((wrapper.vm as ComponentInstance).selectedDay).toBe(3) }) }) describe('Warnings toggle', () => { it('should update visible warnings on toggle', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() const newVisibleWarnings = ['wind', 'rain'] ;(wrapper.vm as ComponentInstance).onWarningsToggled(newVisibleWarnings) expect((wrapper.vm as ComponentInstance).visibleWarnings).toEqual( newVisibleWarnings ) }) it('should update legend visibility', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() const vm = wrapper.vm as ComponentInstance if (vm.legend.length > 0) { const firstWarningType = vm.legend[0].type vm.onWarningsToggled([firstWarningType]) const legendItem = vm.legend.find( (item: { type: string }) => item.type === firstWarningType ) expect(legendItem.visible).toBe(true) } }) }) describe('Error handling', () => { it('should handle errors in error array', () => { wrapper = mount(AlertClient, { props: { language: 'fi' as Language, }, }) const testError = 'test_error' ;(wrapper.vm as ComponentInstance).handleError(testError) expect((wrapper.vm as ComponentInstance).errors).toContain(testError) }) it('should not duplicate errors', () => { wrapper = mount(AlertClient, { props: { language: 'fi' as Language, }, }) const testError = 'test_error' const vm = wrapper.vm as ComponentInstance vm.handleError(testError) vm.handleError(testError) expect(vm.errors.filter((e: string) => e === testError).length).toBe(1) }) it('should handle null warnings data gracefully', async () => { wrapper = mount(AlertClient, { props: { warningsData: null as unknown as WarningsData, language: 'fi' as Language, }, }) await flushPromises() const vm = wrapper.vm as ComponentInstance expect(vm.warnings).toBeNull() expect(vm.days).toEqual([]) expect(wrapper.exists()).toBe(true) }) it('should handle undefined warnings data gracefully', async () => { wrapper = mount(AlertClient, { props: { warningsData: undefined, language: 'fi' as Language, }, }) await flushPromises() expect((wrapper.vm as ComponentInstance).warnings).toBeNull() expect(wrapper.exists()).toBe(true) }) it('should handle malformed warnings data', async () => { const malformedData = { invalid: 'data', structure: 'wrong', } wrapper = mount(AlertClient, { props: { warningsData: malformedData as unknown as WarningsData, language: 'fi' as Language, }, }) await flushPromises() // Component should still exist and not crash expect(wrapper.exists()).toBe(true) }) it('should handle empty warnings data', async () => { const emptyData = { weather_update_time: '2025-10-31T12:00:00Z', flood_update_time: '2025-10-31T12:00:00Z', weather_finland_active_all: { features: [] }, flood_finland_active_all: { features: [] }, } wrapper = mount(AlertClient, { props: { warningsData: emptyData as unknown as WarningsData, language: 'fi' as Language, }, }) await flushPromises() const vm = wrapper.vm as ComponentInstance expect(vm.warnings).toBeDefined() expect(vm.days).toHaveLength(5) expect(wrapper.exists()).toBe(true) }) }) describe('Computed properties', () => { it('should compute toContentText correctly', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, regionListEnabled: true, }, }) await flushPromises() const vm = wrapper.vm as ComponentInstance expect(vm.toContentText).toBeDefined() expect(typeof vm.toContentText).toBe('string') }) it('should compute validData correctly with warnings', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() expect(typeof (wrapper.vm as ComponentInstance).validData).toBe('boolean') }) it('should compute numWarnings correctly', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() const vm = wrapper.vm as ComponentInstance expect(typeof vm.numWarnings).toBe('number') expect(vm.numWarnings).toBeGreaterThanOrEqual(0) }) }) describe('Accessibility', () => { it('should have proper ARIA labels on main container', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() const container = wrapper.find('#fmi-warnings-view') expect(container.exists()).toBe(true) }) it('should provide accessible navigation structure', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() // Check that Days component can be navigated const days = wrapper.findComponent(Days) expect(days.exists()).toBe(true) }) it('should support keyboard navigation', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() // Verify focus-ring class is used for keyboard navigation const focusableElements = wrapper.findAll('.focus-ring') expect(focusableElements.length).toBeGreaterThan(0) }) it('should have proper language attribute', () => { wrapper = mount(AlertClient, { props: { language: 'fi' as Language, }, }) expect((wrapper.vm as ComponentInstance).language).toBe('fi') }) }) describe('Edge cases', () => { it('should handle invalid geometry ID', () => { wrapper = mount(AlertClient, { props: { geometryId: -1, language: 'fi' as Language, }, }) expect(wrapper.exists()).toBe(true) expect((wrapper.vm as ComponentInstance).geometryId).toBe(-1) }) it('should handle very large refresh interval', () => { wrapper = mount(AlertClient, { props: { refreshInterval: 2147483647, // Max 32-bit signed integer language: 'fi' as Language, }, }) expect((wrapper.vm as ComponentInstance).timer).toBeDefined() }) it('should handle invalid theme gracefully', () => { wrapper = mount(AlertClient, { props: { theme: 'invalid-theme' as Theme, language: 'fi' as Language, }, }) expect(wrapper.exists()).toBe(true) expect((wrapper.vm as ComponentInstance).theme).toBe('invalid-theme') }) it('should accept valid default day range', () => { wrapper = mount(AlertClient, { props: { defaultDay: 4, language: 'fi' as Language, }, }) // Component should mount with valid day value expect(wrapper.exists()).toBe(true) expect((wrapper.vm as ComponentInstance).selectedDay).toBe(4) }) it('should handle concurrent day selections', async () => { wrapper = mount(AlertClient, { props: { warningsData: mockWarningsData, language: 'fi' as Language, }, }) await flushPromises() const vm = wrapper.vm as ComponentInstance // Rapidly change days vm.onDaySelected(1) vm.onDaySelected(2) vm.onDaySelected(3) vm.onDaySelected(4) // Should end up with last selected day expect(vm.selectedDay).toBe(4) }) }) describe('Performance', () => { it('should handle multiple mount and unmount cycles', () => { // Create and destroy multiple instances for (let i = 0; i < 3; i++) { const w = mount(AlertClient, { props: { refreshInterval: 60000, language: 'fi' as Language, }, }) expect(w.exists()).toBe(true) w.unmount() } // Test completed without errors expect(true).toBe(true) }) it('should handle large warning datasets efficiently', async () => { const largeDataset = { ...mockWarningsData, weather_finland_active_all: { type: 'FeatureCollection' as const, features: Array(100) .fill(null) .map((_, i) => ({ ...mockWarningsData.weather_finland_active_all!.features[0], properties: { ...mockWarningsData.weather_finland_active_all!.features[0]! .properties, identifier: `warning-${i}`, }, })), }, } const startTime = performance.now() wrapper = mount(AlertClient, { props: { warningsData: largeDataset as unknown as WarningsData, language: 'fi' as Language, }, }) await flushPromises() const endTime = performance.now() const duration = endTime - startTime // Processing should complete in reasonable time (< 5000ms) expect(duration).toBeLessThan(5000) expect(wrapper.exists()).toBe(true) }) }) })