From b5f6eb59f1021379598d4f0876b402e4f3f199b0 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Sun, 11 Oct 2026 22:08:56 +0200 Subject: [PATCH] fix: event details dialog crashed on a string timestamp Since #335 the generated SDK turns date-time fields into Dates, but hey-api's transformer skips a field whose schema is a union, and the event detail's `event` is the DomainEvent union. Its timestamp stayed a string, the formatter called getTime on it, and the dialog never opened. The events store now converts that one field when it loads the detail. A unit test and a Playwright test that opens a real event cover it; the Playwright one fails without the fix. --- frontend/e2e/admin-events.spec.ts | 8 ++++++++ .../src/lib/admin/stores/__tests__/eventsStore.test.ts | 10 ++++++++++ frontend/src/lib/admin/stores/eventsStore.svelte.ts | 9 ++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/frontend/e2e/admin-events.spec.ts b/frontend/e2e/admin-events.spec.ts index 3a408a873..b823722ef 100644 --- a/frontend/e2e/admin-events.spec.ts +++ b/frontend/e2e/admin-events.spec.ts @@ -40,6 +40,14 @@ test.describe('Admin Events', () => { await adminPage.goto(PATH); await expectTableColumn(adminPage, 'Time', EMPTY_PATTERN); }); + + test('opens the details of an event with its timestamp', async ({ adminPage }) => { + await adminPage.goto(PATH); + await adminPage.getByRole('button', { name: /View details for event/ }).first().click(); + const dialog = adminPage.getByRole('dialog', { name: 'Event Details' }); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole('row', { name: /Timestamp/ })).toContainText(/\d{4}/); + }); }); test.describe('Refresh', () => { diff --git a/frontend/src/lib/admin/stores/__tests__/eventsStore.test.ts b/frontend/src/lib/admin/stores/__tests__/eventsStore.test.ts index e7587787f..b393fa35a 100644 --- a/frontend/src/lib/admin/stores/__tests__/eventsStore.test.ts +++ b/frontend/src/lib/admin/stores/__tests__/eventsStore.test.ts @@ -156,6 +156,16 @@ describe('EventsStore', () => { ); }); + it('turns the event timestamp the generated transformer skips into a Date', async () => { + const event = { ...createMockEvent(), timestamp: '2026-10-11T08:00:00Z' }; + mockApi(getEventDetailApiV1AdminEventsEventIdGet).ok({ event, related_events: [], timeline: [] }); + + createStore(); + await store.openEventDetail('evt-1'); + + expect(store.selectedEvent?.event.timestamp).toEqual(new Date('2026-10-11T08:00:00Z')); + }); + it('leaves selectedEvent empty when the load fails', async () => { mockApi(getEventDetailApiV1AdminEventsEventIdGet).err({ detail: 'Event not found' }); diff --git a/frontend/src/lib/admin/stores/eventsStore.svelte.ts b/frontend/src/lib/admin/stores/eventsStore.svelte.ts index d3750dcb8..72d2653d1 100644 --- a/frontend/src/lib/admin/stores/eventsStore.svelte.ts +++ b/frontend/src/lib/admin/stores/eventsStore.svelte.ts @@ -86,7 +86,14 @@ class EventsStore { async openEventDetail(eventId: string): Promise { // A failed load already showed the interceptor's toast, so the modal just stays closed this.selectedEvent = await this.#queryClient - .query(getEventDetailApiV1AdminEventsEventIdGetOptions({ path: { event_id: eventId } })) + .query({ + ...getEventDetailApiV1AdminEventsEventIdGetOptions({ path: { event_id: eventId } }), + // hey-api's transformer skips a union-typed field, so `event.timestamp` arrives as a string + select: (detail) => ({ + ...detail, + event: { ...detail.event, timestamp: new Date(detail.event.timestamp) }, + }), + }) .catch(() => null); }