From 39c82ff3a4a44fab9f82994b833b621b739e7075 Mon Sep 17 00:00:00 2001 From: Thom Lamb Date: Fri, 28 Aug 2026 12:22:39 -0500 Subject: [PATCH] fix(orders): mark a demo order in the history rather than leaving it to read as real (#205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #195 and #203 made the cart say a demo order is a demo order. That message is an antd toast lasting about three seconds, after which the cart empties and the card unmounts. Order history is what the customer comes back to when they wonder where their item is, and it said nothing. A demo row was a real row: item name, `$80.00`, status `completed` rendered as a neutral tag because `STATUS_COLORS` has no `completed` key, and `demo` printed raw under a heading reading "Processor". That is not an explanation — a customer has no reason to read `demo` as "this did not happen", and "processor" is not a word they have any reason to know. Two things now say it, for the same reason the cart needed two. The `demo` cell renders as a tag reading "Demo (not charged)", which marks *which* order. A notice above the table, shown only when there is one, says what that means — a tag reading "Demo" still assumes the reader knows what a demo order is, and what they actually want to know is whether to expect a parcel. Nothing changes on the backend: `orders.processor = 'demo'` was already written at checkout and already selected for this page. The row is a real row in a real table and stays visible, because hiding it would be its own kind of lie — the customer did do something, and it did have an effect on the catalogue. Written test-first against a browser: the new case drives a real demo purchase through the cart, opens `/orders`, and failed on both assertions before the change. Verified: 4 end-to-end tests in this spec pass, the 5 existing order-history tests still pass, frontend build clean, lint 0 errors (2 pre-existing warnings in `src/filters.ts`). Closes #205 Co-Authored-By: Claude Opus 5 (1M context) --- frontend/src/customer/Orders.tsx | 46 ++++++++++++++++++------ frontend/tests/e2e/demo-checkout.spec.ts | 26 ++++++++++++++ frontend/tests/e2e/pages/OrdersPage.ts | 4 +++ 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/frontend/src/customer/Orders.tsx b/frontend/src/customer/Orders.tsx index 1bd22ea..b70774c 100644 --- a/frontend/src/customer/Orders.tsx +++ b/frontend/src/customer/Orders.tsx @@ -38,7 +38,16 @@ const COLUMNS = [ dataIndex: 'status', render: (v: string) => {v} }, - { title: 'Processor', dataIndex: 'processor' }, + { + title: 'Processor', + dataIndex: 'processor', + // `demo` used to render as a raw column value, which is not an explanation: + // a customer has no reason to read it as "this did not happen", and the row + // was otherwise identical to a real one — real price, `completed` status, + // same neutral tag. The cart says it is a demo (#195, #203) for about three + // seconds; this is the record they come back to. See #205. + render: (v: string) => (v === 'demo' ? Demo (not charged) : v) + }, { title: 'Date', dataIndex: 'created_at', render: (v: string) => new Date(v).toLocaleDateString() } ]; @@ -69,6 +78,12 @@ function OrdersBody({ loading, error, orders, onRetry }: BodyProps) { ); } + // Shown only when there is one to explain. The per-row tag says which order, + // this says what it means — a tag reading "Demo" still assumes the reader + // knows what a demo order is, and the thing they actually want to know is + // whether to expect a parcel. + const hasDemoOrder = orders.some(o => o.processor === 'demo'); + if (orders.length === 0) { return ( @@ -79,15 +94,26 @@ function OrdersBody({ loading, error, orders, onRetry }: BodyProps) { } return ( - + <> + {hasDemoOrder && ( + + )} +
+ ); } diff --git a/frontend/tests/e2e/demo-checkout.spec.ts b/frontend/tests/e2e/demo-checkout.spec.ts index 333d398..70ab312 100644 --- a/frontend/tests/e2e/demo-checkout.spec.ts +++ b/frontend/tests/e2e/demo-checkout.spec.ts @@ -61,6 +61,32 @@ test.describe('Demo mode says so to the customer', () => { await expect(cart.demoNotice).toBeVisible({ timeout: 20000 }); }); + // The toast is three seconds; this is the record the customer comes back to + // when they wonder where their item is. It showed `demo` as a raw value under + // a "Processor" heading, beside a real price and a neutral `completed` tag — + // nothing a customer would read as "this did not happen". See #205. + test('order history marks the demo order rather than showing it as a real one', async ({ + page, + customer, + cart, + orders + }) => { + const name = `Demo h${uniqueSuffix()}`; + const item = await createItem(page.request, { name, price: '80' }); + expect((await page.request.post(`/api/cart/items/${item.id}`)).status()).toBe(201); + expect((await page.request.post('/api/customers/me/addresses', { data: ADDRESS })).ok()).toBe(true); + + await cart.goto(); + await expect(cart.checkoutButton).toBeVisible({ timeout: 20000 }); + await cart.checkoutButton.click(); + await expect(page.getByText(/nothing was charged/i)).toBeVisible({ timeout: 20000 }); + + await orders.goto(); + + await expect(orders.demoOrderMarker).toBeVisible({ timeout: 20000 }); + await expect(orders.demoNotice).toBeVisible(); + }); + test('the confirmation says nothing was charged', async ({ page, customer, cart }) => { const name = `Demo p${uniqueSuffix()}`; const item = await createItem(page.request, { name, price: '80' }); diff --git a/frontend/tests/e2e/pages/OrdersPage.ts b/frontend/tests/e2e/pages/OrdersPage.ts index b13357e..cc49341 100644 --- a/frontend/tests/e2e/pages/OrdersPage.ts +++ b/frontend/tests/e2e/pages/OrdersPage.ts @@ -14,6 +14,8 @@ export class OrdersPage { readonly continueShoppingButton: Locator; readonly backToShopButton: Locator; readonly anyDialog: Locator; + readonly demoNotice: Locator; + readonly demoOrderMarker: Locator; constructor(private readonly page: Page) { this.heading = page.getByRole('heading', { name: 'Order History' }); @@ -21,6 +23,8 @@ export class OrdersPage { this.continueShoppingButton = page.getByRole('button', { name: 'Continue Shopping' }); this.backToShopButton = page.getByRole('button', { name: 'Back to Shop' }); this.anyDialog = page.getByRole('dialog'); + this.demoNotice = page.getByText('Some of these are demo orders'); + this.demoOrderMarker = page.getByText('Demo (not charged)'); } async goto(): Promise { -- 2.54.0