diff --git a/apps/app/src/app/pick/page.test.tsx b/apps/app/src/app/pick/page.test.tsx new file mode 100644 index 0000000..eb21f79 --- /dev/null +++ b/apps/app/src/app/pick/page.test.tsx @@ -0,0 +1,39 @@ +import { afterEach, expect, it, vi } from 'vitest'; +import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import PickPage from './page'; +vi.mock('next/navigation', () => ({ useSearchParams: () => new URLSearchParams({ session: 'test-session' }) })); +afterEach(() => { cleanup(); vi.unstubAllGlobals(); }); + +function pendingSubmission() { + let resolve!: (response: Response) => void; + let reject!: (error: Error) => void; + const submission = new Promise((yes, no) => { resolve = yes; reject = no; }); + vi.stubGlobal('fetch', vi.fn((_url, options) => options.method === 'POST' ? submission : Promise.resolve(new Response('{}')))); + render(); + fireEvent.click(screen.getAllByRole('button')[0]); + return { resolve, reject }; +} + +it('waits for accepted delivery before showing Sent', async () => { + const request = pendingSubmission(); + expect(screen.queryByText('Sent!')).not.toBeInTheDocument(); + expect(screen.getByRole('status')).toHaveTextContent('Sending'); + expect(screen.getAllByRole('button')).toHaveLength(3); + for (const option of screen.getAllByRole('button')) expect(option).toBeDisabled(); + await act(async () => { request.resolve(new Response('{}', { status: 200 })); }); + expect(screen.getByText('Sent!')).toBeInTheDocument(); +}); + +it('restores options after a delivery failure and clears stale errors on retry', async () => { + const request = pendingSubmission(); + await act(async () => { request.reject(new Error('Network unavailable')); }); + expect(screen.queryByText('Sent!')).not.toBeInTheDocument(); + expect(screen.getByText('Something went wrong. Try again.')).toBeInTheDocument(); + for (const option of screen.getAllByRole('button')) expect(option).toBeEnabled(); + const retry = new Promise(() => {}); + vi.stubGlobal('fetch', vi.fn(() => retry)); + fireEvent.click(screen.getAllByRole('button')[0]); + expect(screen.queryByText('Something went wrong. Try again.')).not.toBeInTheDocument(); + expect(screen.queryByText('Sent!')).not.toBeInTheDocument(); +}); diff --git a/apps/app/src/app/pick/page.tsx b/apps/app/src/app/pick/page.tsx index b43973b..d17ce5c 100644 --- a/apps/app/src/app/pick/page.tsx +++ b/apps/app/src/app/pick/page.tsx @@ -87,6 +87,7 @@ function PickPage() { const sessionId = searchParams.get("session"); const [picked, setPicked] = useState(false); + const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(""); const options = useMemo(() => pickRandom(ALL_PROMPTS, 3), []); @@ -103,8 +104,9 @@ function PickPage() { }, [sessionId]); const handlePick = async (prompt: string) => { - if (!sessionId || picked) return; - setPicked(true); + if (!sessionId || picked || submitting) return; + setSubmitting(true); + setError(""); try { const res = await fetch("/api/pick", { method: "POST", @@ -112,9 +114,12 @@ function PickPage() { body: JSON.stringify({ sessionId, prompt }), }); if (!res.ok) throw new Error("Failed to submit"); + setPicked(true); } catch { setError("Something went wrong. Try again."); setPicked(false); + } finally { + setSubmitting(false); } }; @@ -148,6 +153,7 @@ function PickPage() {