weather_app/tests/lib/stores/app.test.js
hermes-explorigin ae71c36acc
All checks were successful
Test, Build & Deploy / test-and-build (push) Successful in 53s
Test, Build & Deploy / deploy (push) Successful in 24s
feat(weather): 5-min forecast cache + location-change loading indicator
2026-10-17 16:49:51 +00:00

95 lines
3.1 KiB
JavaScript

import { describe, it, expect, beforeEach, vi } from 'vitest'
import { AppStore } from '../../../src/lib/stores/app.svelte.js'
import { fetchForecast } from '../../../src/lib/api/weather.js'
import { clearForecastCache } from '../../../src/lib/stores/forecast-cache.js'
vi.mock('../../../src/lib/api/weather.js', () => ({ fetchForecast: vi.fn(), searchLocations: vi.fn() }))
vi.mock('../../../src/lib/api/nws.js', () => ({ fetchForecastNWS: vi.fn(), mergeFallbackData: vi.fn((a) => a) }))
describe('AppStore', () => {
let app
beforeEach(() => {
app = new AppStore()
})
it('initializes with default state', () => {
expect(app.locations).toEqual([])
expect(app.selectedLocationId).toBeNull()
expect(app.forecastData).toBeNull()
expect(app.loading).toBe(false)
expect(app.error).toBe('')
expect(app.showAddLocation).toBe(false)
expect(app.showSettings).toBe(false)
expect(app.sidebarOpen).toBe(false)
})
it('has default settings', () => {
expect(app.settings.units).toBe('metric')
expect(app.settings.alertsEnabled).toBe(true)
})
it('selectedLocation is derived correctly', () => {
expect(app.selectedLocation).toBeNull()
app.locations = [
{ id: 'loc1', name: 'Berlin', lat: 52.52, lon: 13.41, isCurrent: false, order: 1 },
]
app.selectedLocationId = 'loc1'
expect(app.selectedLocation).toBeTruthy()
expect(app.selectedLocation.name).toBe('Berlin')
})
it('selectedLocation returns null when id not found', () => {
app.locations = [{ id: 'loc1', name: 'Berlin', lat: 52.52, lon: 13.41, isCurrent: false, order: 1 }]
app.selectedLocationId = 'nonexistent'
expect(app.selectedLocation).toBeNull()
})
})
const LOC = { id: 'loc1', name: 'Home', lat: 36.35, lon: -95.81, isCurrent: false }
describe('AppStore forecast caching', () => {
let app
beforeEach(() => {
vi.clearAllMocks()
clearForecastCache()
app = new AppStore()
app.locations = [LOC]
app.selectedLocationId = LOC.id
fetchForecast.mockResolvedValue({ current: { temperature_2m: 20 } })
})
it('fetches once then serves the cache on the next call', async () => {
await app.refreshForecast()
expect(fetchForecast).toHaveBeenCalledTimes(1)
await app.refreshForecast()
expect(fetchForecast).toHaveBeenCalledTimes(1) // cache hit, no second fetch
})
it('sets loading=true while a cache-miss fetch is in flight, then false', async () => {
const p = app.refreshForecast()
expect(app.loading).toBe(true)
await p
expect(app.loading).toBe(false)
expect(app.forecastData).toEqual({ current: { temperature_2m: 20 } })
})
it('a cache hit does not set loading', async () => {
await app.refreshForecast()
const p = app.refreshForecast()
expect(app.loading).toBe(false) // served from cache synchronously
await p
expect(fetchForecast).toHaveBeenCalledTimes(1)
})
it('force=true bypasses a fresh cache entry', async () => {
await app.refreshForecast()
await app.refreshForecast(null, { force: true })
expect(fetchForecast).toHaveBeenCalledTimes(2)
})
})