diff --git a/frontend/src/cart/Cart.tsx b/frontend/src/cart/Cart.tsx
index 829eabc..ae85d12 100644
--- a/frontend/src/cart/Cart.tsx
+++ b/frontend/src/cart/Cart.tsx
@@ -5,6 +5,7 @@ import List from 'antd/es/list';
import Button from 'antd/es/button';
import Empty from 'antd/es/empty';
import Card from 'antd/es/card';
+import Alert from 'antd/es/alert';
import Radio from 'antd/es/radio';
import Form from 'antd/es/form';
import Input from 'antd/es/input';
@@ -128,7 +129,10 @@ export default function Cart() {
setCheckingOut(true);
try {
await demoCartPurchase(selectedAddressId);
- message.success('Order complete!');
+ // The item really is marked sold and everyone who favorited it really is
+ // emailed, so the one moment the customer is told what happened is the one
+ // moment a demo order has to stop looking like a real one. #195.
+ message.success('Demo order complete — nothing was charged and nothing will be shipped.');
refreshCartContext();
loadAll();
} catch (err) {
@@ -235,17 +239,38 @@ export default function Cart() {
+ {/*
+ Shown for the whole of demo mode: before an address is picked,
+ and whether or not PayPal is configured. The word on the button
+ is the smaller half of this — it asks the customer to notice a
+ parenthesis on the control they have already decided to press.
+ */}
+ {config?.demoMode && (
+
+ )}
{!selectedAddressId && Select a shipping address to check out.}
{config?.paypalClientId && selectedAddressId && }
{config?.demoMode && selectedAddressId && (
)}
diff --git a/frontend/tests/e2e/demo-checkout.spec.ts b/frontend/tests/e2e/demo-checkout.spec.ts
new file mode 100644
index 0000000..333d398
--- /dev/null
+++ b/frontend/tests/e2e/demo-checkout.spec.ts
@@ -0,0 +1,80 @@
+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);
+ expect((await page.request.post('/api/customers/me/addresses', { data: ADDRESS })).ok()).toBe(true);
+
+ 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.
+ await expect(cart.demoNotice).toBeVisible({ timeout: 20000 });
+ });
+
+ 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 });
+ });
+});
diff --git a/frontend/tests/e2e/pages/CartPage.ts b/frontend/tests/e2e/pages/CartPage.ts
index 2b1979b..d4ceaa2 100644
--- a/frontend/tests/e2e/pages/CartPage.ts
+++ b/frontend/tests/e2e/pages/CartPage.ts
@@ -10,10 +10,18 @@ import { Locator, Page } from '@playwright/test';
export class CartPage {
readonly continueShoppingButton: Locator;
readonly emptyNotice: Locator;
+ readonly checkoutButton: Locator;
+ readonly demoNotice: Locator;
constructor(private readonly page: Page) {
this.continueShoppingButton = page.getByRole('button', { name: 'Continue Shopping' });
this.emptyNotice = page.getByText('Your cart is empty');
+ // Matched on a prefix rather than the whole label, so a test can assert what
+ // the label says instead of having to know it in order to find the button.
+ // That is the point of #195: the label was wrong, and a locator naming it in
+ // full would have gone looking for the right button and found nothing.
+ this.checkoutButton = page.getByRole('button', { name: /^Checkout/ });
+ this.demoNotice = page.getByText('Demonstration only');
}
async goto(): Promise {