Files
redefined-designs/frontend/tests/e2e/demo-checkout.spec.ts
T
bermudalambandClaude Opus 5 b897e99363 fix(cart): say what the demo button does rather than what the shop does (#203)
#195 added a notice reading "Demonstration only — This shop is not taking payments at the moment", gated on `demoMode` alone. That claim is false in a configuration the ops runbook actively steers towards.

`demoMode` and `paypalClientId` are independent. `checkDemoMode` and `checkPayPal` only make the PayPal secrets *required* when `DEMO_MODE=false`; nothing forbids them while it is `true`. And `production-stack-cutover.md:65` says flipping to `false` without all three crash-loops the container — so the only safe order is to populate the secrets while demo mode is still on, verify, then flip. In that window `Cart.tsx` renders live PayPal buttons directly beneath a banner telling the customer the shop takes no payments, and it is precisely the window in which someone is clicking around production checking their work.

That is the same failure #195 fixed, pointed the other way: silent where a warning was needed, then confidently wrong where a customer can actually be charged. Telling someone nothing will be shipped above a live PayPal button is worse than saying nothing.

The notice now describes the button instead of the shop, which is true in both configurations and stays visible in the one with two controls that do different things — where a customer most needs to be told they differ. Gating it on `!paypalClientId` would also have removed the false claim, by hiding the notice exactly there, which is the worse trade.

The test for it was also not testing it. "Says so before the customer commits" seeded an address with `isDefault: true`, and the cart auto-selects the default on load — so an address was already selected and the button already rendered when it asserted. It would have passed with the notice moved inside the `selectedAddressId` guard, which is the regression it exists to catch. It now seeds no address and asserts the notice is up while the checkout button is absent, which states the property directly.

Mutation-tested rather than assumed: moving the Alert inside that guard fails the new test, and would not have failed the old one.

Verified: 3 end-to-end tests pass against a browser, frontend build clean, lint 0 errors (2 pre-existing warnings in `src/filters.ts`).

Closes #203
Refs #195

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 14:05:21 -05:00

112 lines
4.7 KiB
TypeScript

import { test, expect, createItem, uniqueSuffix } from './fixtures';
/**
* Demo mode as production actually runs it: `DEMO_MODE=true` with no PayPal
* credentials configured, which is the interim #191 put in place and the shape
* this suite already runs in.
*
* That combination used to render a full-width primary button reading plainly
* "Checkout". The "(Demo)" suffix was gated on a PayPal client id being present,
* so the one configuration that needs the label was the only one that did not
* get it — and the button is not decorative, it marks real inventory sold and
* emails everyone who favorited it. See #195.
*
* These assert what a customer can see, because that is the whole defect. The
* server was always right about what it was doing.
*/
const ADDRESS = {
fullName: 'Dana Holt',
addressLine1: '118 Cedar Street',
city: 'Asheville',
state: 'NC',
postalCode: '28801',
isDefault: true
};
test.describe('Demo mode says so to the customer', () => {
test('labels the checkout button as a demo when no PayPal is configured', async ({
page,
customer,
cart
}) => {
const name = `Demo c${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();
// The cart selects the default address on load, so the checkout controls
// render with nothing clicked.
await expect(cart.checkoutButton).toBeVisible({ timeout: 20000 });
await expect(cart.checkoutButton).toHaveText('Checkout (Demo)');
});
test('says so before the customer commits, not only on the button', async ({
page,
customer,
cart
}) => {
const name = `Demo n${uniqueSuffix()}`;
const item = await createItem(page.request, { name, price: '80' });
expect((await page.request.post(`/api/cart/items/${item.id}`)).status()).toBe(201);
// Deliberately NO address. The cart auto-selects the default one on load,
// so seeding an address here would mean the button is already rendered and
// this would pass even with the notice moved inside the selectedAddressId
// guard — which is the regression it exists to catch. #203.
await cart.goto();
// A label on the button alone asks the customer to notice a parenthesis on
// the thing they are already clicking. The notice is the part that has to
// survive someone not reading carefully, so it has to be up before there is
// anything to press.
await expect(cart.demoNotice).toBeVisible({ timeout: 20000 });
await expect(cart.checkoutButton).toHaveCount(0);
});
// 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' });
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();
// "Order complete!" is true and is exactly what a real order would say. The
// item really is marked sold and favoriters really are emailed, so the one
// moment the customer is told what happened has to distinguish the two.
await expect(page.getByText(/nothing was charged/i)).toBeVisible({ timeout: 20000 });
});
});