From 9aad003f3640fe19780a3ddafa96bb23cc41c45a Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Thu, 16 Jul 2026 17:18:46 +0100 Subject: [PATCH 1/2] fix(card): don't render '#null' for a waitlist entry without a position getPhysicalWaitlist types position as nullable and the joined branch is gated on joinedAt, so a user on the list with no queue position was told 'You are #null on the list.' Drop the number for that case and format real positions for readability. --- src/components/Card/PhysicalCardScreen.tsx | 4 +- .../__tests__/PhysicalCardScreen.test.tsx | 60 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/components/Card/__tests__/PhysicalCardScreen.test.tsx diff --git a/src/components/Card/PhysicalCardScreen.tsx b/src/components/Card/PhysicalCardScreen.tsx index 8154976a6b..72a033da0c 100644 --- a/src/components/Card/PhysicalCardScreen.tsx +++ b/src/components/Card/PhysicalCardScreen.tsx @@ -76,7 +76,9 @@ const PhysicalCardScreen: FC = ({ cardId, last4, onPrev }) => {

You are on the list!

- {`You are #${data.position} on the list. We'll let you know when cards are ready to be shipped.`} + {data.position === null + ? `You are on the list. We'll let you know when cards are ready to be shipped.` + : `You are #${data.position.toLocaleString()} on the list. We'll let you know when cards are ready to be shipped.`}

) : ( diff --git a/src/components/Card/__tests__/PhysicalCardScreen.test.tsx b/src/components/Card/__tests__/PhysicalCardScreen.test.tsx new file mode 100644 index 0000000000..1d29b79772 --- /dev/null +++ b/src/components/Card/__tests__/PhysicalCardScreen.test.tsx @@ -0,0 +1,60 @@ +/** + * PhysicalCardScreen — waitlist "on the list" copy contract. + * + * The joined branch is gated on joinedAt, but the API types position as + * nullable, so a joined user can have no queue position. Interpolating it + * blindly renders "You are #null on the list." + */ +import React from 'react' +import { render, screen } from '@testing-library/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import PhysicalCardScreen from '@/components/Card/PhysicalCardScreen' +import { rainApi } from '@/services/rain' + +jest.mock('@/context/authContext', () => ({ + useAuth: () => ({ user: { accounts: [] }, fetchUser: jest.fn() }), +})) +jest.mock('next/image', () => ({ + __esModule: true, + // eslint-disable-next-line @next/next/no-img-element -- test stub, not real markup + default: (props: Record) => {String(props.alt, +})) +jest.mock('posthog-js', () => ({ __esModule: true, default: { capture: jest.fn() } })) +jest.mock('@/services/rain', () => ({ + rainApi: { getPhysicalWaitlist: jest.fn(), joinPhysicalWaitlist: jest.fn() }, +})) + +const mockGet = rainApi.getPhysicalWaitlist as jest.Mock + +const renderScreen = () => { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + return render( + + + + ) +} + +describe('PhysicalCardScreen — joined waitlist copy', () => { + beforeEach(() => jest.clearAllMocks()) + + it('shows the queue position when the API returns one', async () => { + mockGet.mockResolvedValue({ joinedAt: '2026-01-01T00:00:00Z', position: 42 }) + renderScreen() + expect(await screen.findByText(/You are #42 on the list\./)).toBeInTheDocument() + }) + + it('omits the position rather than rendering "#null" when there is none', async () => { + mockGet.mockResolvedValue({ joinedAt: '2026-01-01T00:00:00Z', position: null }) + renderScreen() + const body = await screen.findByText(/You are on the list\./) + expect(body).toBeInTheDocument() + expect(body.textContent).not.toMatch(/#/) + }) + + it('formats large positions with thousands separators', async () => { + mockGet.mockResolvedValue({ joinedAt: '2026-01-01T00:00:00Z', position: 1234 }) + renderScreen() + expect(await screen.findByText(/You are #1,234 on the list\./)).toBeInTheDocument() + }) +}) From 3b8466e3e9d7104f0aac1fba2527e1dc29b9a119 Mon Sep 17 00:00:00 2001 From: innolove-dev Date: Fri, 17 Jul 2026 13:54:29 +0100 Subject: [PATCH 2/2] test(card): derive the expected waitlist position from the active locale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hardcoded "1,234" assumed an en-US default locale, so the test failed under any locale with a different thousands separator (de_DE renders 1.234). Assert against toLocaleString output instead — that is the actual contract. --- src/components/Card/__tests__/PhysicalCardScreen.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Card/__tests__/PhysicalCardScreen.test.tsx b/src/components/Card/__tests__/PhysicalCardScreen.test.tsx index 1d29b79772..c787501ce0 100644 --- a/src/components/Card/__tests__/PhysicalCardScreen.test.tsx +++ b/src/components/Card/__tests__/PhysicalCardScreen.test.tsx @@ -52,9 +52,12 @@ describe('PhysicalCardScreen — joined waitlist copy', () => { expect(body.textContent).not.toMatch(/#/) }) + // Derived rather than hardcoded: the separator is locale-dependent, and the + // contract is that the copy delegates to toLocaleString, not that it says "1,234". it('formats large positions with thousands separators', async () => { mockGet.mockResolvedValue({ joinedAt: '2026-01-01T00:00:00Z', position: 1234 }) renderScreen() - expect(await screen.findByText(/You are #1,234 on the list\./)).toBeInTheDocument() + const body = await screen.findByText(/on the list\./) + expect(body.textContent).toContain(`You are #${(1234).toLocaleString()} on the list.`) }) })