style(client): prettier on the files this branch touched

This commit is contained in:
Maurice
2026-10-12 20:01:22 +00:00
parent 00a19679f5
commit 4a6ee6819c
8 changed files with 201 additions and 128 deletions
@@ -1,19 +1,39 @@
// FE-PLANNER-BKPARTS-001 to FE-PLANNER-BKPARTS-002
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { WhenFields } from './bookingParts';
import type { BookingFacts } from './bookingFacts';
import { WhenFields } from './bookingParts';
function facts(day: BookingFacts['day'], time: string | null = '15:35 → 09:55'): BookingFacts {
return {
day, time, startTime: null, endTime: null, startDate: null, endDate: null,
cells: [], endpoints: [], legCodes: [], place: null, accommodation: null, linked: null, url: null, isHotel: false,
day,
time,
startTime: null,
endTime: null,
startDate: null,
endDate: null,
cells: [],
endpoints: [],
legCodes: [],
place: null,
accommodation: null,
linked: null,
url: null,
isHotel: false,
};
}
describe('WhenFields', () => {
it('FE-PLANNER-BKPARTS-001: a long day title is cut, the date beside it never shrinks (#2590)', () => {
render(<WhenFields facts={facts({ label: 'Germany Departure Flight with a much longer custom title → Korea Arrival', date: 'Oct 21 → Oct 22', range: true })} />);
render(
<WhenFields
facts={facts({
label: 'Germany Departure Flight with a much longer custom title → Korea Arrival',
date: 'Oct 21 → Oct 22',
range: true,
})}
/>
);
const title = screen.getByText('Germany Departure Flight with a much longer custom title → Korea Arrival');
const date = screen.getByText('Oct 21 → Oct 22');
@@ -181,16 +181,24 @@ describe('reservation dates', () => {
expect(endTimeBeforeStartTime(overnight)).toBe(true);
// Time zones can make a booking end "before" it starts, so the save stays open.
expect(isEndBeforeStart(overnight)).toBe(false);
expect(endTimeBeforeStartTime(form({ reservation_time: '2026-11-24T09:00', reservation_end_time: '18:00' }))).toBe(false);
expect(endTimeBeforeStartTime(form({ reservation_time: '2026-11-24T18:00', reservation_end_time: '18:00' }))).toBe(false);
expect(endTimeBeforeStartTime(form({ reservation_time: '2026-11-24T09:00', reservation_end_time: '18:00' }))).toBe(
false
);
expect(endTimeBeforeStartTime(form({ reservation_time: '2026-11-24T18:00', reservation_end_time: '18:00' }))).toBe(
false
);
// With the next day as end date the times are fine, and an end date of its own is checked by isEndBeforeStart.
expect(
endTimeBeforeStartTime(form({ reservation_time: '2026-11-24T18:00', end_date: '2026-11-25', reservation_end_time: '09:00' }))
endTimeBeforeStartTime(
form({ reservation_time: '2026-11-24T18:00', end_date: '2026-11-25', reservation_end_time: '09:00' })
)
).toBe(false);
expect(endTimeBeforeStartTime(form({ reservation_time: '2026-11-24', reservation_end_time: '09:00' }))).toBe(false);
expect(endTimeBeforeStartTime(form({ reservation_end_time: '09:00' }))).toBe(false);
expect(
endTimeBeforeStartTime(form({ type: 'hotel', reservation_time: '2026-11-24T18:00', reservation_end_time: '09:00' }))
endTimeBeforeStartTime(
form({ type: 'hotel', reservation_time: '2026-11-24T18:00', reservation_end_time: '09:00' })
)
).toBe(false);
});
+3 -1
View File
@@ -22,7 +22,9 @@ describe('usePlanMapViewport', () => {
});
it('raeumt ihn auch weg, sobald die Karte nicht mehr zu sehen ist, und nimmt dann nichts an', () => {
const { result, rerender } = renderHook(({ shown }) => usePlanMapViewport(5, shown), { initialProps: { shown: true } });
const { result, rerender } = renderHook(({ shown }) => usePlanMapViewport(5, shown), {
initialProps: { shown: true },
});
act(() => result.current(KYOTO));
rerender({ shown: false });
expect(useTripStore.getState().planMapViewport).toBeNull();
+20 -12
View File
@@ -1,7 +1,12 @@
import { useCallback, useEffect } from 'react'
import { useTripStore } from '../store/tripStore'
import { useCallback, useEffect } from 'react';
import { useTripStore } from '../store/tripStore';
interface Bounds { south: number; west: number; north: number; east: number }
interface Bounds {
south: number;
west: number;
north: number;
east: number;
}
/**
* Hands the plan map's viewport to the trip store, where the place search reads
@@ -11,16 +16,19 @@ interface Bounds { south: number; west: number; north: number; east: number }
* to lean on, and falls back to the day and the trip.
*/
export function usePlanMapViewport(tripId: number, shown = true): (bounds: Bounds) => void {
const setPlanMapViewport = useTripStore((s) => s.setPlanMapViewport)
const setPlanMapViewport = useTripStore((s) => s.setPlanMapViewport);
useEffect(() => {
if (!shown) setPlanMapViewport(null)
return () => setPlanMapViewport(null)
}, [shown, tripId, setPlanMapViewport])
if (!shown) setPlanMapViewport(null);
return () => setPlanMapViewport(null);
}, [shown, tripId, setPlanMapViewport]);
return useCallback((bounds: Bounds) => {
if (!shown || !Number.isFinite(tripId)) return
const { south, west, north, east } = bounds
setPlanMapViewport({ tripId, south, west, north, east })
}, [shown, tripId, setPlanMapViewport])
return useCallback(
(bounds: Bounds) => {
if (!shown || !Number.isFinite(tripId)) return;
const { south, west, north, east } = bounds;
setPlanMapViewport({ tripId, south, west, north, east });
},
[shown, tripId, setPlanMapViewport]
);
}
@@ -121,7 +121,9 @@ describe('MReservationCardParts', () => {
});
it('FE-COMP-MRESCARD-009: a booking with a linked expense says the expense is deleted with it (#2604)', () => {
render(<CardDeleteSheet title="LH 400" card={cardState({ confirmingDelete: true, deleteTakesExpense: true })} t={t} />);
render(
<CardDeleteSheet title="LH 400" card={cardState({ confirmingDelete: true, deleteTakesExpense: true })} t={t} />
);
expect(screen.getByText('reservations.confirm.deleteBodyWithExpense:LH 400')).toBeInTheDocument();
});
@@ -106,7 +106,9 @@ export function CardWhenAndCode({
<span className="min-w-0 truncate">{dayValue}</span>
<span className="flex-none font-medium text-m-faint">{dayDate}</span>
</span>
) : dayValue}
) : (
dayValue
)}
</Field>
<Field label={t('reservations.time')} className="flex-1" tabular>
{timeValue}
@@ -161,7 +163,10 @@ export function CardDeleteSheet({ title, card, t }: { title: string; card: CardS
open={card.confirmingDelete}
onClose={card.cancelDelete}
title={t('reservations.confirm.deleteTitle')}
message={t(card.deleteTakesExpense ? 'reservations.confirm.deleteBodyWithExpense' : 'reservations.confirm.deleteBody', { name: title })}
message={t(
card.deleteTakesExpense ? 'reservations.confirm.deleteBodyWithExpense' : 'reservations.confirm.deleteBody',
{ name: title }
)}
confirmLabel={t('common.delete')}
cancelLabel={t('common.cancel')}
danger
@@ -3,111 +3,135 @@
// The planner's view switches the account remembers per trip (#2639): road trip
// mode, the Dawarich trail and the route overview. The tab keeps a copy that
// answers before the settings arrive.
import { act, renderHook, waitFor } from '@testing-library/react'
import { http, HttpResponse } from 'msw'
import { server } from '../../../tests/helpers/msw/server'
import { resetAllStores, seedStore } from '../../../tests/helpers/store'
import { buildSettings } from '../../../tests/helpers/factories'
import { useSettingsStore } from '../../store/settingsStore'
import { usePlannerTripView } from './usePlannerTripView'
import { act, renderHook, waitFor } from '@testing-library/react';
import { http, HttpResponse } from 'msw';
import { buildSettings } from '../../../tests/helpers/factories';
import { server } from '../../../tests/helpers/msw/server';
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
import { useSettingsStore } from '../../store/settingsStore';
import { usePlannerTripView } from './usePlannerTripView';
function captureSettingWrites() {
const writes: Array<{ key: string; value: unknown }> = []
server.use(http.put('/api/settings', async ({ request }) => {
const body = await request.json() as { key: string; value: unknown }
writes.push(body)
return HttpResponse.json({ success: true, key: body.key, value: body.value })
}))
return writes
const writes: Array<{ key: string; value: unknown }> = [];
server.use(
http.put('/api/settings', async ({ request }) => {
const body = (await request.json()) as { key: string; value: unknown };
writes.push(body);
return HttpResponse.json({ success: true, key: body.key, value: body.value });
})
);
return writes;
}
beforeEach(() => {
resetAllStores()
})
resetAllStores();
});
describe('usePlannerTripView', () => {
it('FE-TP-TRIPVIEW-001: a fresh browser opens the trip with the switches the account has on', () => {
seedStore(useSettingsStore, {
settings: buildSettings({ planner_trip_views: { '42': { roadtrip: true } } }),
isLoaded: true,
})
expect(renderHook(() => usePlannerTripView(42, 'roadtrip')).result.current[0]).toBe(true)
expect(renderHook(() => usePlannerTripView(42, 'dawarich')).result.current[0]).toBe(false)
expect(renderHook(() => usePlannerTripView(43, 'roadtrip')).result.current[0]).toBe(false)
})
});
expect(renderHook(() => usePlannerTripView(42, 'roadtrip')).result.current[0]).toBe(true);
expect(renderHook(() => usePlannerTripView(42, 'dawarich')).result.current[0]).toBe(false);
expect(renderHook(() => usePlannerTripView(43, 'roadtrip')).result.current[0]).toBe(false);
});
it('FE-TP-TRIPVIEW-002: the tab copy answers until the settings are here, then the account decides', () => {
sessionStorage.setItem('trip-roadtrip-42', '1')
const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'))
expect(result.current[0]).toBe(true)
sessionStorage.setItem('trip-roadtrip-42', '1');
const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'));
expect(result.current[0]).toBe(true);
// Switched off on another device since this tab last looked.
act(() => { seedStore(useSettingsStore, { settings: buildSettings(), isLoaded: true }) })
expect(result.current[0]).toBe(false)
})
act(() => {
seedStore(useSettingsStore, { settings: buildSettings(), isLoaded: true });
});
expect(result.current[0]).toBe(false);
});
it('FE-TP-TRIPVIEW-003: a switch is saved on the account next to the other trips, and only what is on', async () => {
const writes = captureSettingWrites()
const writes = captureSettingWrites();
seedStore(useSettingsStore, {
settings: buildSettings({ planner_trip_views: { '7': { overview: true }, '42': { dawarich: true } } }),
isLoaded: true,
})
const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'))
});
const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'));
act(() => { result.current[1]() })
expect(result.current[0]).toBe(true)
expect(sessionStorage.getItem('trip-roadtrip-42')).toBe('1')
await waitFor(() => expect(writes).toHaveLength(1))
act(() => {
result.current[1]();
});
expect(result.current[0]).toBe(true);
expect(sessionStorage.getItem('trip-roadtrip-42')).toBe('1');
await waitFor(() => expect(writes).toHaveLength(1));
expect(writes[0]).toEqual({
key: 'planner_trip_views',
value: { '7': { overview: true }, '42': { dawarich: true, roadtrip: true } },
})
});
act(() => { result.current[1]() })
expect(result.current[0]).toBe(false)
await waitFor(() => expect(writes).toHaveLength(2))
expect(writes[1].value).toEqual({ '7': { overview: true }, '42': { dawarich: true } })
})
act(() => {
result.current[1]();
});
expect(result.current[0]).toBe(false);
await waitFor(() => expect(writes).toHaveLength(2));
expect(writes[1].value).toEqual({ '7': { overview: true }, '42': { dawarich: true } });
});
it('FE-TP-TRIPVIEW-004: a switch made before the settings arrived waits for them instead of erasing other trips', async () => {
const writes = captureSettingWrites()
server.use(http.get('/api/settings', () => HttpResponse.json({
settings: buildSettings({ planner_trip_views: { '7': { overview: true } } }),
})))
const { result } = renderHook(() => usePlannerTripView(42, 'dawarich'))
const writes = captureSettingWrites();
server.use(
http.get('/api/settings', () =>
HttpResponse.json({
settings: buildSettings({ planner_trip_views: { '7': { overview: true } } }),
})
)
);
const { result } = renderHook(() => usePlannerTripView(42, 'dawarich'));
act(() => { result.current[1]() })
expect(result.current[0]).toBe(true)
await waitFor(() => expect(writes).toHaveLength(1))
expect(writes[0].value).toEqual({ '7': { overview: true }, '42': { dawarich: true } })
expect(result.current[0]).toBe(true)
})
act(() => {
result.current[1]();
});
expect(result.current[0]).toBe(true);
await waitFor(() => expect(writes).toHaveLength(1));
expect(writes[0].value).toEqual({ '7': { overview: true }, '42': { dawarich: true } });
expect(result.current[0]).toBe(true);
});
it('FE-TP-TRIPVIEW-005: a save that fails keeps the switch for the session', async () => {
let tried = false
server.use(http.put('/api/settings', () => {
tried = true
return HttpResponse.json({ error: 'nope' }, { status: 500 })
}))
seedStore(useSettingsStore, { settings: buildSettings(), isLoaded: true })
const { result } = renderHook(() => usePlannerTripView(42, 'overview'))
let tried = false;
server.use(
http.put('/api/settings', () => {
tried = true;
return HttpResponse.json({ error: 'nope' }, { status: 500 });
})
);
seedStore(useSettingsStore, { settings: buildSettings(), isLoaded: true });
const { result } = renderHook(() => usePlannerTripView(42, 'overview'));
act(() => { result.current[1]() })
await waitFor(() => expect(tried).toBe(true))
await act(async () => { await Promise.resolve() })
expect(result.current[0]).toBe(true)
expect(sessionStorage.getItem('trip-overview-42')).toBe('1')
})
act(() => {
result.current[1]();
});
await waitFor(() => expect(tried).toBe(true));
await act(async () => {
await Promise.resolve();
});
expect(result.current[0]).toBe(true);
expect(sessionStorage.getItem('trip-overview-42')).toBe('1');
});
it('FE-TP-TRIPVIEW-006: settings that cannot be loaded leave the tab copy in charge and write nothing', async () => {
const writes = captureSettingWrites()
server.use(http.get('/api/settings', () => HttpResponse.json({ error: 'offline' }, { status: 503 })))
vi.spyOn(console, 'error').mockImplementation(() => {})
const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'))
const writes = captureSettingWrites();
server.use(http.get('/api/settings', () => HttpResponse.json({ error: 'offline' }, { status: 503 })));
vi.spyOn(console, 'error').mockImplementation(() => {});
const { result } = renderHook(() => usePlannerTripView(42, 'roadtrip'));
act(() => { result.current[1]() })
await act(async () => { await new Promise(r => setTimeout(r, 20)) })
expect(result.current[0]).toBe(true)
expect(writes).toHaveLength(0)
})
})
act(() => {
result.current[1]();
});
await act(async () => {
await new Promise((r) => setTimeout(r, 20));
});
expect(result.current[0]).toBe(true);
expect(writes).toHaveLength(0);
});
});
@@ -1,25 +1,29 @@
import { useCallback, useState } from 'react'
import { PLANNER_TRIP_VIEWS_KEY, compactPlannerTripViews, type PlannerTripViewFlag } from '@trek/shared'
import { useSettingsStore } from '../../store/settingsStore'
import { PLANNER_TRIP_VIEWS_KEY, compactPlannerTripViews, type PlannerTripViewFlag } from '@trek/shared';
import { useCallback, useState } from 'react';
import { useSettingsStore } from '../../store/settingsStore';
function readSession(key: string): boolean | null {
try {
const raw = sessionStorage.getItem(key)
return raw === '1' ? true : raw === '0' ? false : null
const raw = sessionStorage.getItem(key);
return raw === '1' ? true : raw === '0' ? false : null;
} catch {
return null
return null;
}
}
function writeSession(key: string, on: boolean): void {
try { sessionStorage.setItem(key, on ? '1' : '0') } catch { /* the account row still has it */ }
try {
sessionStorage.setItem(key, on ? '1' : '0');
} catch {
/* the account row still has it */
}
}
interface TabCopy {
key: string
on: boolean | null
key: string;
on: boolean | null;
/** A switch made here that the account row has not taken yet. */
pending: boolean
pending: boolean;
}
/**
@@ -31,39 +35,39 @@ interface TabCopy {
* decide, because a switch flipped on another device is newer than this tab.
*/
export function usePlannerTripView(tripId: number, flag: PlannerTripViewFlag): [boolean, () => void] {
const sessionKey = `trip-${flag}-${tripId}`
const saved = useSettingsStore(s => s.settings.planner_trip_views?.[String(tripId)]?.[flag] === true)
const loaded = useSettingsStore(s => s.isLoaded)
const [local, setLocal] = useState<TabCopy>(() => ({ key: sessionKey, on: readSession(sessionKey), pending: false }))
const sessionKey = `trip-${flag}-${tripId}`;
const saved = useSettingsStore((s) => s.settings.planner_trip_views?.[String(tripId)]?.[flag] === true);
const loaded = useSettingsStore((s) => s.isLoaded);
const [local, setLocal] = useState<TabCopy>(() => ({ key: sessionKey, on: readSession(sessionKey), pending: false }));
// A different trip in the same planner reads its own copy instead of keeping this one.
const copy = local.key === sessionKey ? local : { key: sessionKey, on: readSession(sessionKey), pending: false }
const shown = loaded && !copy.pending ? saved : (copy.on ?? saved)
const copy = local.key === sessionKey ? local : { key: sessionKey, on: readSession(sessionKey), pending: false };
const shown = loaded && !copy.pending ? saved : (copy.on ?? saved);
const toggle = useCallback(() => {
const next = !shown
setLocal({ key: sessionKey, on: next, pending: true })
writeSession(sessionKey, next)
const next = !shown;
setLocal({ key: sessionKey, on: next, pending: true });
writeSession(sessionKey, next);
void remember(tripId, flag, next).finally(() => {
setLocal(prev => (prev.key === sessionKey && prev.on === next ? { ...prev, pending: false } : prev))
})
}, [shown, sessionKey, tripId, flag])
setLocal((prev) => (prev.key === sessionKey && prev.on === next ? { ...prev, pending: false } : prev));
});
}, [shown, sessionKey, tripId, flag]);
return [shown, toggle]
return [shown, toggle];
}
async function remember(tripId: number, flag: PlannerTripViewFlag, on: boolean): Promise<void> {
const store = useSettingsStore.getState()
const store = useSettingsStore.getState();
// The row holds every trip, so it is only written over once the account's
// own copy is here; otherwise one switch would erase all the others. The
// store takes the new value before the request goes out, so an offline save
// still shows for the rest of the session.
if (!store.isLoaded) await store.loadSettings()
const { settings, isLoaded, updateSetting } = useSettingsStore.getState()
if (!isLoaded) return
const views = { ...settings.planner_trip_views }
views[String(tripId)] = { ...views[String(tripId)], [flag]: on }
if (!store.isLoaded) await store.loadSettings();
const { settings, isLoaded, updateSetting } = useSettingsStore.getState();
if (!isLoaded) return;
const views = { ...settings.planner_trip_views };
views[String(tripId)] = { ...views[String(tripId)], [flag]: on };
try {
await updateSetting(PLANNER_TRIP_VIEWS_KEY, compactPlannerTripViews(views))
await updateSetting(PLANNER_TRIP_VIEWS_KEY, compactPlannerTripViews(views));
} catch {
// Offline or refused: the store and the tab copy keep the switch for this session.
}