Skip to content

Setup E2E testing#158

Merged
damianlegawiec merged 11 commits into
mainfrom
feature/e2e
Jun 12, 2026
Merged

Setup E2E testing#158
damianlegawiec merged 11 commits into
mainfrom
feature/e2e

Conversation

@damianlegawiec

@damianlegawiec damianlegawiec commented May 28, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • End-to-end test suite added (Playwright) with a guest checkout "golden-path" test and commands for interactive/CI runs
    • Local test backend via Docker Compose to run E2E against a real storefront; automated bootstrap and API key provisioning included
  • Documentation

    • New Testing section detailing unit/integration and E2E workflows, env setup, and test payment key
  • Chores

    • CI updated to run E2E, cache browsers, collect artifacts on failures; ignore patterns and pre-commit exclusions adjusted for E2E files

@vercel

vercel Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
storefront Ready Ready Preview, Comment Jun 12, 2026 6:44am

Request Review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@damianlegawiec, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 4 minutes and 56 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 92775d3b-4489-437e-8061-b0efb7fdf14a

📥 Commits

Reviewing files that changed from the base of the PR and between 9ef8d0b and 3eefa40.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • e2e-backend/docker-compose.yml
  • e2e/checkout.spec.ts
  • playwright.config.ts
  • scripts/e2e/bootstrap-spree.sh
  • scripts/e2e/dev-with-env.sh

Walkthrough

Adds Playwright E2E tests and supporting Spree backend bootstrapping. Includes a guest checkout golden-path test, Docker Compose backend, bootstrap scripts that seed data and mint an API key, Playwright config and npm scripts, CI e2e job, .gitignore updates, and README testing docs.

Changes

E2E Testing Infrastructure

Layer / File(s) Summary
E2E Checkout Golden Path Test
e2e/checkout.spec.ts
Playwright test drives guest checkout: home → product detail → add to cart → checkout page → fill email/address → select delivery → pay via Stripe iframe. Includes fillAddress() helper for address form population and safeFill() helper for visibility-gated field interaction.
Spree Backend Docker and Bootstrap Scripts
e2e-backend/docker-compose.yml, e2e-backend/.env, scripts/e2e/bootstrap-spree.sh, scripts/e2e/dev-with-env.sh
Docker Compose spins up postgres, redis, and Spree web service with healthchecks. Bootstrap script validates Stripe keys, polls /up, seeds data via @spree/cli, configures a Stripe gateway inside the container, creates a publishable API key, and writes .env.e2e. Dev script sources .env.e2e and starts next dev on port 3001 with Turbopack.
Playwright Configuration and Test Dependencies
playwright.config.ts, package.json
Playwright config targets http://localhost:3001, defines Desktop Chrome project, configures CI retry/forbid-only behavior, and launches webServer via dev-with-env.sh. package.json adds @playwright/test and @spree/cli devDependencies, plus test:e2e, test:e2e:ui, e2e:up, and e2e:down npm scripts.
CI Workflow Job and Build Artifact Cleanup
.github/workflows/ci.yml, .gitignore, lefthook.yml
GitHub Actions: pinned action SHAs and persist-credentials: false across lint/typecheck/test jobs; new e2e job caches Playwright browsers, starts Docker Compose with --wait, runs bootstrap script, executes npm run test:e2e, uploads Playwright report and backend logs only on failure, and tears down stack with volumes. .gitignore updated to exclude .env.e2e, Playwright reports, and caches. lefthook.yml excludes package-lock.json from Biome.
E2E Testing Documentation
README.md
New Testing section documents Vitest and Playwright workflows, backend startup and seeding with @spree/cli, running E2E against next dev, interactive mode, teardown, and Stripe test key configuration in scripts/e2e/bootstrap-spree.sh.

Sequence Diagram(s)

sequenceDiagram
  participant Playwright as Playwright(Test)
  participant Next as NextDev(Server)
  participant Spree as Spree(Backend)
  participant Stripe as Stripe(Gateway)
  Playwright->>Next: navigate storefront, add to cart, open checkout
  Playwright->>Next: submit contact & address forms
  Next->>Spree: create/confirm order, fetch shipping rates
  Playwright->>Stripe: complete Payment Element (iframe)
  Stripe->>Spree: payment capture/notification
  Spree->>Next: order completed
  Next->>Playwright: render /order-placed/ with order number
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hop through browsers, scripts in paw,
Spree wakes, seeds, and shows the store,
A Stripe test card hums the tune,
The checkout blooms beneath the moon,
Reports and logs tucked safely in a drawer.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Setup E2E testing' directly and accurately summarizes the primary objective of the pull request, which is to establish end-to-end testing infrastructure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
README.md (1)

188-188: 💤 Low value

Consider splitting this sentence for improved readability.

This 100+ word sentence packs the entire E2E infrastructure description—Playwright, Docker Compose, services, @spree/cli, and dev dependencies—into a single clause with nested parentheticals. Breaking it into 2-3 shorter sentences would improve scannability.

♻️ Suggested refactor
-End-to-end tests run through Playwright against a real Spree backend booted in Docker. The compose file at `e2e-backend/docker-compose.yml` ships Postgres + Redis + the official `ghcr.io/spree/spree:5.4.3.1` image — no `create-spree-app` setup required. Seeding and API-key creation go through the official [`@spree/cli`](https://spreecommerce.org/docs/developer/cli/quickstart) (`spree seed`, `spree sample-data`, `spree api-key create`), installed as a dev dependency.
+End-to-end tests run through Playwright against a real Spree backend booted in Docker. The compose file at `e2e-backend/docker-compose.yml` ships Postgres, Redis, and the official `ghcr.io/spree/spree:5.4.3.1` image—no `create-spree-app` setup required.
+
+Seeding and API-key creation use the official [`@spree/cli`](https://spreecommerce.org/docs/developer/cli/quickstart) (`spree seed`, `spree sample-data`, `spree api-key create`), installed as a dev dependency.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 188, Split the long single sentence describing E2E tests
into 2–3 shorter sentences: one stating that end-to-end tests run with
Playwright against a real Spree backend booted via Docker, one describing the
compose file (e2e-backend/docker-compose.yml) and included services (Postgres,
Redis, ghcr.io/spree/spree:5.4.3.1), and one explaining seeding and API-key
creation via the `@spree/cli` commands (spree seed, spree sample-data, spree
api-key create) and that `@spree/cli` is a dev dependency; keep the same facts and
references but break at natural clause boundaries for readability.
e2e/checkout.spec.ts (1)

77-102: ⚡ Quick win

Add explicit return types to helper functions.

fillAddress and safeFill should declare return types (Promise<void>) to match strict TS guidance.

Suggested fix
-async function fillAddress(page: Page) {
+async function fillAddress(page: Page): Promise<void> {
   ...
 }

-async function safeFill(locator: Locator, value: string) {
+async function safeFill(locator: Locator, value: string): Promise<void> {
   ...
 }

As per coding guidelines, "Use strict TypeScript type checking. Always define explicit return types for functions, use 'satisfies' for type checking object literals, and avoid 'any' (use 'unknown' instead)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/checkout.spec.ts` around lines 77 - 102, Update the function signatures
to include explicit return types: change async function fillAddress(page: Page)
to async function fillAddress(page: Page): Promise<void> and async function
safeFill(locator: Locator, value: string) to async function safeFill(locator:
Locator, value: string): Promise<void>; this ensures both helper functions
(fillAddress and safeFill) explicitly declare Promise<void> as their return type
to satisfy strict TypeScript checking.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 55-56: The workflow uses mutable action tags for actions/checkout
and actions/setup-node and omits checkout credential hardening; update each
actions/checkout step to include persist-credentials: false and replace both
actions/checkout@v4 and actions/setup-node@v4 with their corresponding full
commit SHAs (pin to exact commit IDs) so the workflow is immutably pinned and
checkout does not persist GITHUB_TOKEN credentials to subsequent steps.

In `@e2e/checkout.spec.ts`:
- Around line 87-95: The current check only tests visibility and then blindly
calls selectOption on stateSelect, which fails if the field renders as a
disabled <select> (loading) or as a text <input>; fix by inspecting the first
locator's actual element type and disabled state before acting: call
stateSelect.first() to get the locator/element handle, verify isVisible(), then
use elementHandle.evaluate(...) or locator.evaluate to read tagName and disabled
attribute; if tagName === 'SELECT' and not disabled call selectOption({ index: 1
}) on that locator, otherwise if tagName === 'INPUT' call fill(...) (or
type(...)) with a valid value for the test; ensure you still catch errors from
isVisible() as before.

---

Nitpick comments:
In `@e2e/checkout.spec.ts`:
- Around line 77-102: Update the function signatures to include explicit return
types: change async function fillAddress(page: Page) to async function
fillAddress(page: Page): Promise<void> and async function safeFill(locator:
Locator, value: string) to async function safeFill(locator: Locator, value:
string): Promise<void>; this ensures both helper functions (fillAddress and
safeFill) explicitly declare Promise<void> as their return type to satisfy
strict TypeScript checking.

In `@README.md`:
- Line 188: Split the long single sentence describing E2E tests into 2–3 shorter
sentences: one stating that end-to-end tests run with Playwright against a real
Spree backend booted via Docker, one describing the compose file
(e2e-backend/docker-compose.yml) and included services (Postgres, Redis,
ghcr.io/spree/spree:5.4.3.1), and one explaining seeding and API-key creation
via the `@spree/cli` commands (spree seed, spree sample-data, spree api-key
create) and that `@spree/cli` is a dev dependency; keep the same facts and
references but break at natural clause boundaries for readability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 226f72fe-d4dd-4b0d-8fcd-057787b995fe

📥 Commits

Reviewing files that changed from the base of the PR and between 36ce7f2 and c0767b6.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • .github/workflows/ci.yml
  • .gitignore
  • README.md
  • e2e-backend/.env
  • e2e-backend/docker-compose.yml
  • e2e/checkout.spec.ts
  • package.json
  • playwright.config.ts
  • scripts/e2e/bootstrap-spree.sh
  • scripts/e2e/dev-with-env.sh

Comment thread .github/workflows/ci.yml Outdated
Comment thread e2e/checkout.spec.ts Outdated
- package-lock.json: regenerated from scratch — previous version had a
  legacy/v3 format mix that npm ci rejected with EUSAGE, breaking all
  four CI jobs on PR #158.
- .github/workflows/ci.yml: set persist-credentials: false on every
  actions/checkout step (CodeRabbit security comment).
- e2e/checkout.spec.ts: state/province field renders as one of enabled
  <select>, disabled <select> (loading), or text <input> in
  AddressFormFields. Wait out the disabled state, then dispatch on
  tagName (CodeRabbit flakiness comment).
CI on Linux failed loading platform-specific optional dependencies
(@biomejs/cli-linux-x64, @rolldown/binding-linux-x64-gnu,
@parcel/watcher-linux-x64-glibc) because the previous lockfile was
regenerated on macOS-arm64 and omitted the Linux entries.

Regenerate with `npm install --package-lock-only --force --os=linux
--cpu=x64 --libc=glibc` so all platform variants are recorded. Local
node_modules still installs only the host-relevant bindings, but
`npm ci` on any platform now resolves correctly.

Also exclude package-lock.json from the biome pre-commit hook so future
lockfile-only commits don't fail on biome's "No files were processed"
error (biome ignores lockfiles by default).
E2E test was clicking Add-to-Cart then immediately navigating to /cart,
racing the cart-cookie write. Spree logs from the failed CI run confirm
the line item was created server-side, but the /cart page rendered
before the cookie was visible, so the Checkout link never appeared.

Switch to using the cart drawer's own "Checkout" link (the drawer
auto-opens after addItem succeeds, so its presence is the natural
synchronization point). Scope the locator to the drawer role=dialog
to avoid matching the header's "Open cart" affordance.

Also pin every actions/* reference to its full commit SHA with the
v-tag in a trailing comment, addressing the second half of CodeRabbit's
recurring security comment on this workflow.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

Two issues surfaced from the previous CI run's Playwright trace:

1. Email field has no <label>/aria-label — only `placeholder`. Switch
   from getByLabel(/email address/i) to getByPlaceholder. Other address
   fields use aria-label so they continue to work with getByLabel.

2. Country dropdown defaults to Canada (alphabetically before US), but
   our test data (NY, ZIP 10001, US phone) is US-specific. Select
   "United States" explicitly before filling the rest.
The Spree storefront checkout is a single page with auto-save, not a
stepped flow. Previous trace confirmed:
- No "Continue to Delivery" button exists (only "Place Order" at the bottom)
- Address persists on container blur (handleContainerBlur), not on a click
- Shipping methods + payment methods only appear once address is saved
- State dropdown had Alabama selected (index:1) instead of New York, which
  may not match the cart's ZIP/city heuristics

Changes:
- Drop the imaginary "Continue to Delivery" click.
- Click the "Shipping Method" heading after filling the form to take focus
  out of the address container and trigger auto-save.
- Pick "New York" state by label (matches city + ZIP), fall back to index:1.
- Check the policy-agreement checkbox before clicking Place Order.
- Bump shipping-rate wait to 30s since the address roundtrip + rate
  recalculation can be slow on first load.
The vanilla Spree 5.4.3.1 image ships with no payment gateway, so the
storefront's checkout shows "No payment methods available" and the test
never reaches the Stripe iframe. Spree's Admin API for creating payment
methods only landed in 5.5 (unreleased), so we fall back to a bin/rails
runner snippet that creates a SpreeStripe::Gateway directly.

- Publishable key (pk_test_TYooMQ…) stays hardcoded; it's the public
  documented sample.
- Secret key is read from the STRIPE_SECRET_KEY env var. GitHub push
  protection flags any test-secret literal regardless of provenance,
  so the script fails fast if the env var is unset. CI consumes it as
  a repository secret; locally export the matching sample from
  https://docs.stripe.com/keys or your own test pair.
- save!(validate: false) skips the Stripe API roundtrip during create
  (the validate_secret_key hook); the actual PaymentIntent creation
  during checkout still hits Stripe for real, exercising the path.
- Idempotent: re-running matches on the unique "E2E Stripe" name.

Once Spree 5.5 ships with the Admin API endpoint, this can be replaced
with a curl POST to /admin/payment_methods.
Stripe no longer publishes a working sample secret key (the old docs
sk_test_… returns 401 api_key_expired), and the docs key that does still
work belongs to a different account than the previously hardcoded
pk_test_TYooMQ… publishable key — a mismatched pair fails PaymentIntent
confirmation. Both keys now come from the environment: in CI,
STRIPE_SECRET_KEY as a repository secret and STRIPE_PUBLISHABLE_KEY as a
repository variable, scoped to the bootstrap step only so npm scripts and
third-party actions never see the secret.

The E2E job skips itself (true "skipped" status) on fork and dependabot
PRs, where repository secrets are never exposed; same-repo runs fail
loudly up front when the keys are missing instead of dying mid-bootstrap.

The bootstrap script now validates both keys' full shape and passes them
to Rails through the container environment instead of interpolating them
into the Ruby heredoc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The previous run surfaced two compounding issues:

1. frameLocator('iframe[name^="__privateStripeFrame"]').first() matched
   the Express Checkout ("Pay with Link") widget, which renders above the
   card form and shares the name prefix — the card-number fill then waited
   on an iframe that never contains it. Target the Payment Element by its
   stable accessible title ("Secure payment input frame") and the fields
   by role/name from the accessibility snapshot instead of placeholders.

2. Playwright's default 30s per-test timeout covers the entire checkout
   walk against dev-mode Next.js + dockerized Spree on a CI runner, so
   each retry died mid-flow at a different step (drawer click, product
   navigation) — the three attempts failing in three places was budget
   exhaustion, not three bugs. Raise the test timeout to 120s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three failure modes from the last run's traces:

1. Multiple Stripe iframes carry title="Secure payment input frame" — an
   accessory frame mounts lazily before or after the real card form, so a
   title-based frameLocator either hits a strict-mode violation (attempt
   1) or silently fills the wrong frame, leaving the real card field
   empty ("Your card number is incomplete", retry 1). Resolve the frame
   that actually contains the Card number field and assert the digits
   landed before paying.

2. The US card form has its own required ZIP field the test never filled
   ("Your ZIP is invalid") — fill it when present.

3. The cart drawer re-renders as the cart revalidates, detaching the
   Checkout link mid-click indefinitely (retry 2 burned its full 120s on
   one click). Navigate to the link's href instead of clicking it.

Also: the expiry field's accessible name varies across Payment Element
mounts, so use its stable MM / YY placeholder.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Quality pass over the branch (no behavior changes intended):

- checkout.spec.ts: replace the MutationObserver/tag-dispatch state-field
  machinery with selectOption's built-in auto-wait (country is pinned to
  US, so the field is always a <select> with New York present); drop the
  safeFill helper (fill() already auto-waits); drop the home-page detour
  and its vacuous title assertion; drop the waitForURL that goto() already
  guarantees; hoist the frame count out of the scan loop.
- bootstrap-spree.sh: replace the hand-rolled curl polling loop with
  curl --retry; fix the stale idempotency comment; document why the
  gateway saves with validate: false.
- docker-compose.yml: inline the single-consumer YAML anchors; stop
  publishing Postgres/Redis host ports nothing uses (also removes the
  LAN-exposed trust-auth surface); fix the stale usage comment.
- ci.yml: key the Playwright browser cache on the Playwright version
  instead of the whole lockfile so unrelated dependency bumps don't evict
  it; merge the two failure-artifact uploads into one.
- dev-with-env.sh: exec npm run dev instead of re-spelling the dev
  command, so the port/flags live only in package.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@damianlegawiec damianlegawiec merged commit c990038 into main Jun 12, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant