Ecommerce Boilerplate
Getting Started

Testing

The three suites, what each one can prove, and which to run for a given change.

Three suites, in ascending order of what they cost and what they prove. All three run in CI on every push and pull request, and none of them needs a credential.

CommandRunsProves
npm testVitest, plain NodePure logic: pricing, masking, cursors, schemas, access rules
npm run test:sqliteVitest against a real SQLite fileQueries, transactions, keyset paging, upserts
npm run test:e2ePlaywright against a built storeRendered components and whole user journeys

npm test has no DOM and no database in it. A green run there says the pure functions agree with themselves — it cannot tell you a query orders rows correctly or that a component mounts. The repository once had 44 test files, all of them this kind, and shipped a guest checkout that crashed the moment it rendered.

npm test — the unit suite

Plain Vitest in Node. No emulator, no network, no generated Prisma client, so it runs on a fresh clone in under a second.

This is where logic with a pure core belongs, and the convention is to give it one: pricing, discount eligibility, phone masking, cursor encoding, order totals, and the i18n dictionaries are all pure modules under src/lib with a matching file in tests/.

Three suites here are worth knowing about because they fail on changes that look unrelated to them:

SuiteFails when
docs-pathsThe documentation names a repository path that no longer exists
docs-snippetsA code block says it comes from a file, and the file no longer contains those lines
docs-versionsThe tech-stack table quotes a version that does not match package.json

Renaming a file or editing a quoted function therefore means editing content/docs in the same commit. That is deliberate: the docs had drifted badly before these existed — a renamed middleware.ts, a rewritten seed script, and a stock-decrement example still showing the loop that was replaced precisely because it oversold.

npm run test:sqlite — the adapter against a real database

Generates the Prisma client for the SQLite schema, creates a database in a temporary directory, and drives prismaCommerce against it — the same adapter a Postgres deployment runs, over a different engine.

It is opt-in through RUN_SQLITE_INTEGRATION=1 (which the script sets), because the default npm test must keep working with no generated client at all.

Use it for anything a fake client cannot honestly answer:

  • Ordering and paging. A keyset cursor is only correct if the engine agrees. The audit trail's test writes five entries fast enough that several share a created value to the millisecond, then pages through them — which is the only way to prove the id tiebreaker stops rows being skipped or repeated at a page boundary.
  • Filtering that must happen in the query. Paging an unfiltered listing and filtering the results loses rows silently. The category test seeds a mix, pages smaller than the match count, and asserts every match appears exactly once.
  • Constraints doing real work. A newsletter signup twice, with different case and spacing, has to be one row with its original date.
  • Transactions. The checkout webhook writes an order and the stock it consumes in one transaction keyed by the Stripe session id.

npm run test:e2e — a real browser against a real store

Terminal
npm run test:e2e     # provision → build → run
npm run e2e:only     # re-run the specs against an already-built store
npm run e2e:report   # open the HTML report

Provisioning resets a throwaway SQLite database, generates the client, pushes the schema, seeds the catalog from the same catalog.json the demo backend uses, and writes the fixtures the specs need. Then it builds and starts the app and drives Chromium against it.

Sign-in is faked at the row level, and nothing downstream is. With AUTH_PROVIDER=authjs the store offers Google OAuth and Resend magic links, neither of which a credential-free run can complete. So provisioning writes the rows a finished sign-in leaves behind — a users row and a sessions row with a fixed token — and Playwright restores that cookie through storageState. Everything after that is the real path: the session lookup, session.user.id as the commerce uid, per-user carts, and the RPC route's ownership checks.

Writing a spec

Select by accessible role and name, not by CSS class:

await page.getByRole("button", { name: "Add to cart" }).click();
await expect(page.getByRole("listitem").filter({ hasText: name })).toHaveCount(1);

Two reasons. Class names churn and roles do not, so the spec pins behaviour rather than markup. And a locator that cannot find an element by its accessible name has found a real accessibility bug — several of the ones fixed in this repo were discovered exactly that way.

Give a spec its own seeded account if it ends a session or writes a cart. Playwright runs specs in parallel, so two tests sharing one account race: whichever signs out first revokes the row the other is still holding, and the loser fails on something unrelated to what it was testing. The accounts live in e2e/env.ts.

Read back through e2e/support/db.ts to assert what actually landed in the database, rather than trusting the UI's own success message. Use it for verification, not for setting up state a test then asserts on — that tests the fixture instead of the app. The one exception is state the browser genuinely cannot create, like a cart written on another device.

Which to run

You changedRun
A pure function, a schema shape, an access rulenpm test
A query, a cursor, a transaction, a database columnnpm test and npm run test:sqlite
Anything that rendersnpm test and npm run test:e2e
An adapter methodall three

On this page