Skip to content
Free shipping over $150

Testing

text
npm test           # once
npm run test:watch # on change
npm run build      # docs → tests → build; a failing test fails the build

81 tests, no test framework. node --test is already in Node, and a theme that ships three runtime dependencies should not quadruple its install to check arithmetic. Node runs each file in its own process, so every file gets a clean database singleton for free — which is what makes the migration and cross-tab tests possible at all.

The browser, in 60 lines

test/helpers/browser.mjs puts localStorage, sessionStorage and a window that dispatches storage events onto globalThis. Two details do the real work:

  • Import it before any app module. ES imports are hoisted, so app modules

must come in through await import() afterwards. loadApp() does that.

  • `localStorage` is shared and `sessionStorage` is not, exactly as in a

browser. That asymmetry is the only reason a single process can play "the other tab" convincingly.

settle() waits 900ms because the mock adapter simulates 220ms of latency on every call, deliberately — without it no loading state is ever exercised. Three cross-tab tests failed first time against a 30ms wait, and the tempting fix was to change the code rather than the wait.

What is covered

FileWhat it protects
moneyMinor units, the two-decimal round trip, the 5% sale threshold
catalogEvery product enriched, regions agreeing, a photograph per colour, the image strip never empty
pricingPer-variant prices, ranges before a size is chosen, overrides surviving a repricing
cacheDe-duplication, stale-while-revalidate, revalidation reaching the caller, namespace purges
cross-tabA write in one tab reaching another: catalogue, settings, bag, hearts, sign-out
migrationA store several versions behind gaining new fields without losing edits
accountRegister → buy → order history → sign out, and orders not leaking between accounts
libraryAttributes learned from products, blocks saved explicitly, kinds validated
adaptersBoth adapters implementing the same surface, read from source
ui-logicThe specification editor, delivery tokens, lightbox zoom and pan
paymentsOverselling, the refund ledger, the write-only credential store
reference-serverSignature verification, webhooks and admin auth, over HTTP
prerenderWhat dist/ actually contains after a build
analyticsConsent, Do Not Track, event shapes, and staying silent by default
imagesEvery srcset candidate existing, and being smaller than its source
error-boundaryThe reset, which is the part that fails silently
order-lookupBoth fields matching, and failing identically when they do not
seorobots.txt and the sitemap, by running the scripts for real
recently-viewedPer-customer scoping, the sign-in merge, corrupt storage
ordersServer-side placement, idempotent replays, admin vs owner scoping
gatesThe contrast formula, and the budget measuring the real initial download

Two rules that make it worth having

Never copy a list the code already owns. adapters.test.mjs parses SURFACE out of src/lib/api/index.js. The first version hand-copied it and was missing fifteen names — a hand-maintained duplicate of a list is a list that is wrong.

Never extract a calculation you are testing. pricing.test.mjs reads the shown price calculation out of ProductView.jsx and evaluates it. A copy would keep passing after the real one broke, which is precisely the bug it exists to catch.

Proving the suite is not decorative

A passing suite proves nothing on its own. Eight known regressions were reintroduced one at a time and all eight failed the suite:

Regression
Variant price stops cascading correctlycaught
The 5% sale threshold is removedcaught
A cross-tab write stops invalidating the cachecaught
Revalidation stops waking listenerscaught
Order history ignores the session againcaught
Backfill goes back to a shallow mergecaught
The library stops learning attributescaught
Variants point at one shared image againcaught

Worth repeating whenever a test is added: break the thing it claims to protect and confirm it goes red.

What is not covered

`prerender` asserts output, not code. It skips when dist/ is missing, so npm run build removes it first — otherwise the pre-build run would assert the *previous* build and pass while the new one was broken. The pass after the build is where those nine run.

No rendering tests. The error boundary's two decisions were pulled into src/lib/errors.js so they could be tested at all — Node cannot import .jsx, and mounting a component properly means a DOM and a testing library. The logic is covered; the markup is not. Nothing here mounts a component, so a broken layout, an unreadable contrast pairing or a button that does not respond to a click will pass. The logic behind the components is tested; the components are not. Adding that means jsdom and a testing library, which is a real cost against three runtime dependencies — worth paying when the first layout regression ships, and not before.

No integration test against a real backend. http.js is checked for shape, never for behaviour. The reference payments server *is* run for real, but against forged signatures rather than Razorpay's — the crypto is verified, the provider round trip is not.