This recipe demonstrates the no-login checkout flow that QA Shop ships as of #117. A guest visitor browses, adds an item to cart, captures their email at the auth gate, completes a 3-step wizard, and lands on a tokenized order page — all without ever creating an account.
It is a strict superset of the add-to-cart-checkout recipe: that one stops at the auth gate (since checkout used to require login); this one walks past the gate as a guest and through to the confirmation page.
§ 01 · PREREQUISITES
Prerequisites
You need a project with @playwright/test installed and a Playwright config that points at QA Shop. If you have not done either, run npm init playwright@latest in a fresh directory and accept the defaults.
Bootstrap with npm init playwright@latest if you don't already have a project — it scaffolds the runner, the config, and an example spec.
Set to http://localhost:3002 for a local checkout, or https://automationtestingplatform.com for the hosted demo.
The recipe assumes the server is already up. The test does not start it for you.
§ 02 · THE SPEC
The full spec
Save this file as tests/e2e/guest-checkout.spec.ts and run it with npx playwright test --grep guest-checkout.
import { test, expect } from "@playwright/test";
const PRODUCT_SLUG = "wooden-train-set-100-piece";
const TEST_CARD_SUCCESS = "4242 4242 4242 4242";
test("guest can complete a checkout end-to-end without an account", async ({
page,
}) => {
// 1. Land on a product detail page and add to cart.
await page.goto(`/products/${PRODUCT_SLUG}`);
await expect(page.getByTestId("page-pdp")).toBeVisible();
await page.getByTestId("pdp-add-to-cart").click();
await expect(page.getByTestId("cart-count")).toHaveText("1");
// 2. Open the cart and click "Checkout".
await page.goto("/cart");
await expect(page.getByTestId("page-cart")).toBeVisible();
await page.getByTestId("cart-summary-checkout").click();
// 3. Auth gate. A11y selectors only — the email input's name= is
// randomized as part of the honeypot inversion (#103).
await expect(page.getByTestId("page-checkout-auth-gate")).toBeVisible();
await page.getByLabel("Email").fill(`pw-guest+${Date.now()}@example.test`);
await page.getByRole("button", { name: /^continue$/i }).click();
// 4. Wizard step 1 — shipping.
await expect(page.getByTestId("page-checkout")).toBeVisible();
await page.getByTestId("shipping-field-fullName").fill("Pat Tester");
await page.getByTestId("shipping-field-street").fill("123 Test Lane");
await page.getByTestId("shipping-field-city").fill("Springfield");
await page.getByTestId("shipping-field-state").fill("IL");
await page.getByTestId("shipping-field-zip").fill("62701");
await page.getByTestId("shipping-field-phone").fill("5551234567");
await page.getByTestId("shipping-continue-button").click();
// 5. Step 2 — review. Guest email row surfaces above the order review.
await expect(page.getByTestId("checkout-review-guest-email-row")).toBeVisible();
await page.getByTestId("review-continue-button").click();
// 6. Step 3 — payment. Magic card 4242... = success path.
await page.getByTestId("payment-field-cardName").fill("Pat Tester");
await page.getByTestId("payment-field-cardNumber").fill(TEST_CARD_SUCCESS);
await page.getByTestId("payment-field-expiry").fill("12/30");
await page.getByTestId("payment-field-cvv").fill("123");
await page.getByTestId("place-order-button").click();
// 7. Confirmation. Guest path redirects to /orders/<num>?token=<uuid>.
await page.waitForURL(/\/orders\/[A-Z0-9-]+\?token=[a-f0-9-]+/);
await expect(page.getByTestId("page-guest-order-detail")).toBeVisible();
await expect(page.getByTestId("guest-order-claim-banner")).toBeVisible();
await expect(page.getByTestId("order-detail-number")).toBeVisible();
});§ 03 · WALKTHROUGH
Walkthrough
The auth-gate step is the only part that diverges from a normal Playwright spec. Every other interactive element on the page carries a stable data-testid and is targeted that way; the email input is the exception.
The label text is preserved deliberately. <Label htmlFor="guest-email">Email</Label> is rendered exactly as before #103, so page.getByLabel("Email") is a stable selector that survives every randomization rotation. The same is true for the aria-label="Email" and autoComplete="email" attributes — password managers, screen readers, and accessibility test runners keep working.
The wizard step order is shipping → review → payment. This is a deliberate UX choice: customers confirm the cart and shipping address before entering card details, which minimizes last-minute changes mid-payment. The submit testids — shipping-continue-button, review-continue-button, place-order-button — drive the entire flow. The place-order button lives on the payment step (step 3), not the review step.
The redirect after place-order-button is the most distinctive part of the guest flow. Logged-in customers land on /checkout/confirmation/{orderNumber}; guests land on /orders/{orderNumber}?token={uuid}. The token is a UUIDv4 stored in orders.guest_session_token at insert time, and it is the bearer credential — anyone holding the URL can read the order. The route validates the token via the SECURITY DEFINER get_guest_order RPC; without the token, the route falls through to requireSession() and behaves like the authenticated path.
§ 04 · EXTEND IT
Things to extend
Three obvious variants build on this skeleton.
Swap 4242 4242 4242 4242 for 4000 0000 0000 0002. The place-order action surfaces the decline at data-testid="checkout-error" instead of redirecting to the confirmation page.
Visit /orders/ORD-DOESNOTEXIST?token=00000000-0000-0000-0000-000000000000. The page must render the not-found UI rather than reveal that the order exists. Assert page-guest-order-detail does NOT render and the body contains "404".
Click data-testid="guest-order-claim-cta" from the confirmation page. The flow walks the visitor through signup with the order pre-attached to the new account.
Related
For the cart-only first leg of the flow (without the post-auth-gate walkthrough), see add-to-cart-checkout. For the logged-in counterpart that uses an authenticated fixture, see authenticated-checkout. For the broader testing philosophy and the framework-by-framework comparison, see the QA Shop testing guide.
Frequently asked questions
Last verified: