|
| 1 | +# Checkout test infrastructure |
| 2 | + |
| 3 | +Integration tests for `<Checkout />` live here. They mount the real component |
| 4 | +through `renderCheckout()` and assert behavior against an in-memory mock of |
| 5 | +`@/lib/godaddy/godaddy`. Pure-unit tests for transformers / hooks live next |
| 6 | +to their source as `*.test.ts(x)` and don't need any of the helpers below. |
| 7 | + |
| 8 | +`checkout-test-env.tsx` is the file you import from. It re-exports everything |
| 9 | +in `checkout-test-utils.tsx` and additionally installs `vi.mock(...)` calls |
| 10 | +for the wallet / express / Stripe payment buttons so jsdom doesn't try to |
| 11 | +boot real SDKs. Importing **anything** from `./checkout-test-env` in a test |
| 12 | +file is what activates those mocks — keep that import even if you don't |
| 13 | +reference the named export it provides. |
| 14 | + |
| 15 | +## Helpers |
| 16 | + |
| 17 | +### Setup |
| 18 | +- `mockGodaddyApi(options)` — install the in-memory API mock. `renderCheckout` |
| 19 | + calls this for you with sensible defaults; call directly when you need a |
| 20 | + bare-mocked API without rendering. |
| 21 | +- `renderCheckout(options?)` — mount `<Checkout />` inside `<GoDaddyProvider>` |
| 22 | + with a fresh QueryClient. Returns the standard RTL result plus |
| 23 | + `{ user, queryClient, session, draftOrder }`. |
| 24 | +- `renderCheckoutWithProps(checkoutProps, options?)` — same, but spread |
| 25 | + `checkoutProps` onto the rendered `<Checkout />`. |
| 26 | +- `createTestQueryClient()` — a QueryClient with retries off and infinite |
| 27 | + staleTime, suitable for predictable integration tests. |
| 28 | + |
| 29 | +### Builders / fixtures |
| 30 | +- `buildCheckoutSession`, `buildDraftOrder`, `buildLineItem`, |
| 31 | + `buildShippingAddress`, `buildBillingAddress`, `buildPickupLocation`, |
| 32 | + `buildShippingRates` — deep-mergeable factory functions. |
| 33 | +- `buildDraftOrderUpdate(input, session?)` — wrap an `UpdateDraftOrderInput` |
| 34 | + payload with the `context` block the API expects. |
| 35 | +- `noBillingAddress`, `getLastUpdateInput`, `getLastConfirmInput` (in |
| 36 | + `checkout-test-fixtures.ts`). |
| 37 | + |
| 38 | +### Operation log |
| 39 | +- `getOperations(op?)` — recorded API calls, optionally filtered by name. |
| 40 | +- `getOperationNames()` — names only. |
| 41 | +- `getOperationOrder(names)` — array of indices for the first occurrence of |
| 42 | + each named op in the log. Use this to assert relative ordering of recorded |
| 43 | + operations without `.indexOf` chains. |
| 44 | +- `clearOperations()` — reset the log (does not reset draft-order state). |
| 45 | + |
| 46 | +### Error injection |
| 47 | +- `setApiError(key, error)` — make every subsequent call to the matching API |
| 48 | + reject. `key` is the API key form, e.g. `'updateDraftOrder'`, |
| 49 | + `'applyShippingMethod'`, `'applyDiscount'`. |
| 50 | +- `clearApiError(key)` — reset. |
| 51 | +- `setApiErrorOnce(key, error?)` — fire `error` for the **next** matching |
| 52 | + call only, then auto-clear. Useful for "fail then recover" scenarios |
| 53 | + (auto-apply rollback, draft-order sync retry, free → paid coupon round-trip). |
| 54 | + |
| 55 | +### Mutating the in-memory draft order |
| 56 | +- `getCurrentDraftOrder()` / `setCurrentDraftOrder(draftOrder)`. |
| 57 | +- `setFeeTotal(value, currencyCode?)` — adjust `draftOrder.feeTotal` (and the |
| 58 | + recomputed total) for the next refetch. Used to exercise the fees row in |
| 59 | + the totals summary. |
| 60 | +- `setPriceAdjustments(adjustments)` — set the response returned by |
| 61 | + `getDraftOrderPriceAdjustments`. Defaults to `[]`. Only consumed by the |
| 62 | + express-checkout buttons; standard checkout flows ignore it. |
| 63 | + |
| 64 | +### Timing helpers |
| 65 | +- `advanceCheckoutDebounce(ms = 1200)` — advance fake timers and flush |
| 66 | + promises in one shot to drive the draft-order sync debounce. |
| 67 | +- `flushPromises()` — `act(async () => Promise.resolve())`. |
| 68 | +- `waitForCheckoutReady()` — wait until the form has rendered Contact + |
| 69 | + Payment. |
| 70 | +- `waitForCheckoutIdle()` — wait until pending mutations have settled. |
| 71 | +- `waitForOperation(op, count?, timeout?)` — wait until at least `count` |
| 72 | + occurrences of `op` are recorded. |
| 73 | +- `refetchDraftOrder(queryClient, sessionId)` — invalidate the draft-order |
| 74 | + query. |
| 75 | + |
| 76 | +### Tokenize / wallet |
| 77 | +- `MockTokenizeJs` — replaces `window.TokenizeJs`. `setupCheckoutTestGlobals` |
| 78 | + wires it up automatically. |
| 79 | +- `getLastTokenizeInstance` / `getTokenizeInstances`. |
| 80 | +- `WalletSupport` (option on `mockGodaddyApi`) controls what |
| 81 | + `MockTokenizeJs.supportWalletPayments()` resolves with. |
| 82 | + |
| 83 | +### URL / storage / JWT |
| 84 | +- `mockWindowLocation`, `setCheckoutUrl`, `seedCheckoutSessionStorage`, |
| 85 | + `restoreWindowLocation`, `getMockedLocation`. |
| 86 | +- `createMockJwt(payload?)` — produces a JWT with a far-future `exp` by |
| 87 | + default. |
| 88 | + |
| 89 | +### Form interaction |
| 90 | +- `typeIntoNamedField(user, name, value)` / `typeIntoPlaceholder` / |
| 91 | + `getTextbox(name)` / `getNamedInput(name)`. |
| 92 | +- `fillShippingAddress(user, overrides?)` — fill all required shipping |
| 93 | + fields with sensible defaults. |
| 94 | + |
| 95 | +### Tracking (T-005) |
| 96 | +- `mockTrack()` — returns `{ getTrackedEvents, clearTrackedEvents, |
| 97 | + expectTracked }`. Requires the test file to install |
| 98 | + `vi.mock('@/tracking/track', ...)` at the top: |
| 99 | + ```ts |
| 100 | + vi.mock('@/tracking/track', async (importOriginal) => { |
| 101 | + const actual = await importOriginal<typeof import('@/tracking/track')>(); |
| 102 | + return { ...actual, track: vi.fn() }; |
| 103 | + }); |
| 104 | + ``` |
| 105 | + `expectTracked(eventId, propsMatcher)` accepts either an `expect.objectContaining`-style |
| 106 | + partial object or a `(props) => boolean` predicate. `eventId` matches |
| 107 | + either an exact id or a suffix (the production `track` fn prepends |
| 108 | + `godaddy.checkout.` — both forms work). |
| 109 | + |
| 110 | +## Test design guidelines |
| 111 | + |
| 112 | +- Keep integration tests focused on the smallest behavior needed for the |
| 113 | + feature under test. Prefer separate tests for rendering state, validation, |
| 114 | + transitions, confirmation payloads, and tracking instead of one broad test |
| 115 | + that clicks through the entire checkout flow. |
| 116 | +- Avoid extra submit/confirm clicks when the assertion only needs to verify |
| 117 | + that UI rendered or switched state. Payment button clicks trigger React Hook |
| 118 | + Form validation, draft-order sync, query updates, and checkout confirming |
| 119 | + state; exercising those side effects unnecessarily can produce unrelated |
| 120 | + `act(...)` warnings and make tests harder to reason about. |
| 121 | +- Seed fixtures into the intended initial state when possible. For example, if |
| 122 | + a test only needs a valid names-only billing form, put those names in the |
| 123 | + draft-order fixture instead of typing them and triggering debounced sync. |
| 124 | +- Assert the tracking source that actually owns the behavior. Payment lifecycle |
| 125 | + events come from the confirm-checkout path; parent form submit handling is |
| 126 | + intentionally minimal and should not be used as a proxy for payment button |
| 127 | + behavior. |
| 128 | + |
| 129 | +## Gotchas |
| 130 | + |
| 131 | +- **Fake timers are on by default** (`vi.useFakeTimers({ shouldAdvanceTime: true })`). |
| 132 | + When you need to drive the debounce manually, prefer `advanceCheckoutDebounce`. |
| 133 | +- The wallet / express / Stripe button mocks are inert — they render |
| 134 | + recognizable test ids but never tokenize. Don't try to assert on |
| 135 | + authorize/confirm flows that go through them. End-to-end coverage for those |
| 136 | + buttons belongs in real-browser tests. |
| 137 | +- `clearOperations()` only clears the log; it does not reset the draft order. |
| 138 | + Use `setCurrentDraftOrder` or re-call `mockGodaddyApi` for a clean slate. |
0 commit comments