fix(weather): day-local named anchor, cache ttl and past-days forecast window (#2167)

This commit is contained in:
Maurice
2026-09-01 15:48:37 +02:00
committed by Maurice
parent c21e86e3bd
commit 03bd5c77af
41 changed files with 568 additions and 105 deletions
@@ -163,6 +163,8 @@ describe('MDaySheet', () => {
expect(screen.getByText('20°')).toBeInTheDocument()
expect(screen.getByRole('dialog').textContent).toContain('24° / 12° ·')
expect(screen.getByText('light rain')).toBeInTheDocument()
// #2167 (mrwulf) — the block names the place the forecast is anchored to.
expect(screen.getByText('Forecast for Museum')).toBeInTheDocument()
})
it('FE-MOB-DAYSH-005: converts to Fahrenheit when the user asked for it', async () => {
@@ -193,7 +195,8 @@ describe('MDaySheet', () => {
})
it('FE-MOB-DAYSH-009: skips the weather block entirely without coordinates', async () => {
await renderSheet(makePlanner({ assignments: {}, places: [] }))
// No located stop AND no stay — with a stay the hotel would anchor the weather (#2167).
await renderSheet(makePlanner({ assignments: {}, places: [], tripAccommodations: [] }))
expect(weatherApi.getDetailed).not.toHaveBeenCalled()
expect(screen.queryByText('No weather data available. Add a place with coordinates.')).not.toBeInTheDocument()
})
@@ -374,13 +377,26 @@ describe('MDaySheet', () => {
expect(screen.getByText('Stay')).toBeInTheDocument()
})
it('FE-MOB-DAYSH-026: falls back to any trip place with coordinates as the weather anchor', async () => {
// #2167 — the anchor is day-local: never a place from another day, even when
// the trip pool has located places. The day's stay is the fallback instead.
it('FE-MOB-DAYSH-026: anchors the weather to the day bookend hotel, not some trip place', async () => {
await renderSheet(makePlanner({
assignments: {},
places: [{ id: 200, name: 'Prater', lat: 48.31, lng: 16.41 }],
}))
expect(weatherApi.getDetailed).toHaveBeenCalledWith(48.31, 16.41, '2026-05-02', 'en')
expect(weatherApi.getDetailed).toHaveBeenCalledWith(48.2, 16.35, '2026-05-02', 'en')
expect(screen.getByText('20°')).toBeInTheDocument()
expect(screen.getByText('Forecast for Hotel Sacher')).toBeInTheDocument()
})
it('FE-MOB-DAYSH-026b: shows no weather at all without a day-local anchor (#2167)', async () => {
await renderSheet(makePlanner({
assignments: {},
places: [{ id: 200, name: 'Prater', lat: 48.31, lng: 16.41 }],
tripAccommodations: [],
}))
expect(weatherApi.getDetailed).not.toHaveBeenCalled()
expect(screen.queryByText('20°')).not.toBeInTheDocument()
})
it('FE-MOB-DAYSH-027: hides every editing affordance for a read-only member', async () => {
@@ -95,6 +95,7 @@ function buildTl(over: Record<string, unknown> = {}): MPlanTimelineController {
hotelChips: [],
weather: null,
weatherTemp: null,
weatherPlaceName: null,
upNext: { assignment: MUSEUM, minutesUntil: 45 },
language: 'en',
timeFormat: '24h',
@@ -198,6 +199,18 @@ describe('MPlanTimeline', () => {
expect(screen.getByText('17°')).toBeInTheDocument()
})
it('FE-MOB-PLTL-005b: the weather chip names its anchor place in the accessible label (#2167)', () => {
renderTimeline({
weather: { main: 'Rain', temp: 17 },
weatherTemp: 17,
weatherPlaceName: 'Shibuya',
})
// echoT renders t('day.weatherFor', { name }) as 'day.weatherFor:Shibuya'.
expect(screen.getByRole('button', { name: 'day.overview · day.weatherFor:Shibuya' })).toBeInTheDocument()
expect(screen.getByTitle('Shibuya')).toBeInTheDocument()
})
it('FE-MOB-PLTL-006: a chip opens the day sheet', () => {
const { shell } = renderTimeline({
hotelChips: [{ key: 'stay-3', variant: 'stay', name: 'Capsule Tokyo', time: null }],
@@ -172,8 +172,10 @@ describe('useMPlanTimeline', () => {
it('FE-MOB-PLTL-007: requests the forecast for the first located stop', async () => {
const { result } = await renderTimeline(makePlanner())
await waitFor(() => expect(result.current.weather).not.toBeNull())
expect(weatherApi.get).toHaveBeenCalledWith(48, 16.1, '2026-05-02')
expect(weatherApi.get).toHaveBeenCalledWith(48, 16.1, '2026-05-02', 'en')
expect(result.current.weatherTemp).toBe(20)
// #2167 — the anchor's name rides along so the chip can say where this is for.
expect(result.current.weatherPlaceName).toBe('Museum')
})
it('FE-MOB-PLTL-008: converts the forecast to Fahrenheit on request', async () => {
@@ -200,8 +202,9 @@ describe('useMPlanTimeline', () => {
id: 14, day_id: 2, order_index: 0, place_id: 104,
place: buildPlace({ id: 104, name: 'Idea', lat: null, lng: null }),
})
await renderTimeline(makePlanner({ assignments: { '2': [vague] }, tripAccommodations: [HOTEL] }))
expect(weatherApi.get).toHaveBeenCalledWith(48, 16.05, '2026-05-02')
const { result } = await renderTimeline(makePlanner({ assignments: { '2': [vague] }, tripAccommodations: [HOTEL] }))
expect(weatherApi.get).toHaveBeenCalledWith(48, 16.05, '2026-05-02', 'en')
expect(result.current.weatherPlaceName).toBe('Hotel Sacher')
})
it('FE-MOB-PLTL-012: skips the forecast without any coordinates at all', async () => {
@@ -209,8 +212,9 @@ describe('useMPlanTimeline', () => {
id: 14, day_id: 2, order_index: 0, place_id: 104,
place: buildPlace({ id: 104, name: 'Idea', lat: null, lng: null }),
})
await renderTimeline(makePlanner({ assignments: { '2': [vague] } }))
const { result } = await renderTimeline(makePlanner({ assignments: { '2': [vague] } }))
expect(weatherApi.get).not.toHaveBeenCalled()
expect(result.current.weatherPlaceName).toBeNull()
})
it('FE-MOB-PLTL-045: drops a forecast that settles after the timeline is gone', async () => {