From 79b02e5b354b102e94268dd94f0a2478d7d3b294 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Mon, 31 Aug 2026 02:15:34 +0000 Subject: [PATCH] test(books): cover same-day reading completion Protect equal manual start and finish dates through the client and status API. Closes #1097 --- .../components/__tests__/DetailsTab.spec.ts | 48 +++++++++++++++++++ server/test/reading-attempts.e2e-spec.ts | 8 ++++ 2 files changed, 56 insertions(+) diff --git a/client/src/features/book/components/__tests__/DetailsTab.spec.ts b/client/src/features/book/components/__tests__/DetailsTab.spec.ts index 5071f609c..164266518 100644 --- a/client/src/features/book/components/__tests__/DetailsTab.spec.ts +++ b/client/src/features/book/components/__tests__/DetailsTab.spec.ts @@ -358,6 +358,54 @@ describe('DetailsTab - present state', () => { ) }) + it('allows the finished date to match the started date', async () => { + vi.mocked(api).mockImplementation(async (input, init) => { + if (input === '/api/v1/books/1/status' && init?.method === 'PATCH') { + return makeApiResponse({ + status: 'read', + source: 'manual', + startedAt: '2026-04-10', + finishedAt: '2026-04-10', + updatedAt: '2026-04-10T00:00:00.000Z', + }) + } + if (input === '/api/v1/collections/membership') return makeApiResponse([]) + return makeApiResponse({}, false) + }) + + const wrapper = mount(DetailsTab, { + props: { + book: makeBook({ + readStatus: { + status: 'reading', + source: 'manual', + startedAt: '2026-04-10', + finishedAt: null, + updatedAt: '2026-04-10T00:00:00.000Z', + }, + }), + }, + global: globalStubs, + }) + + await wrapper.find('button[title="Edit date finished"]').trigger('click') + await wrapper.find('input[type="date"]').setValue('2026-04-10') + + const saveButton = wrapper.findAll('button').find((button) => button.text() === 'Save') + expect(saveButton?.exists()).toBe(true) + await saveButton!.trigger('click') + await flushPromises() + + expect(wrapper.text()).not.toContain('Date Finished must be on or after Date Started') + expect(vi.mocked(api)).toHaveBeenCalledWith( + '/api/v1/books/1/status', + expect.objectContaining({ + method: 'PATCH', + body: JSON.stringify({ finishedAt: '2026-04-10' }), + }), + ) + }) + it('shows validation for finished date earlier than started date and blocks save', async () => { vi.mocked(api).mockImplementation(async (input) => { if (input === '/api/v1/collections/membership') return makeApiResponse([]) diff --git a/server/test/reading-attempts.e2e-spec.ts b/server/test/reading-attempts.e2e-spec.ts index 33192c62a..6ab907192 100644 --- a/server/test/reading-attempts.e2e-spec.ts +++ b/server/test/reading-attempts.e2e-spec.ts @@ -644,6 +644,14 @@ describe('Reading attempts main-flow simulation (docker e2e)', { timeout: TIMEOU expect((await listAttempts(bothDates.bookId)).items).toEqual([ expect.objectContaining({ startedOn: '2026-02-01', endedOn: '2026-02-10', outcome: 'completed', origin: 'manual' }), ]); + + const sameDay = await createBook(); + const sameDayResponse = await patchStatus(sameDay.bookId, { startedAt: '2026-02-15', finishedAt: '2026-02-15' }); + expect(sameDayResponse.statusCode).toBe(200); + expect(sameDayResponse.json()).toMatchObject({ status: 'read', startedAt: '2026-02-15', finishedAt: '2026-02-15' }); + expect((await listAttempts(sameDay.bookId)).items).toEqual([ + expect.objectContaining({ startedOn: '2026-02-15', endedOn: '2026-02-15', outcome: 'completed', origin: 'manual' }), + ]); }); it('27. closes active lifecycle states with a date-only finish patch', async () => {