Skip to content

Commit a5c1051

Browse files
Refactor draft order sync flows (#1384)
* resync order shipping of shipping lines and lineItems fulfillment NONE * fix feedback from PR and add tests * show error if sync fails * add confirmCheckout blocker * throw actual error * reuse sync key logic * refactor all update draft order syncs to avoid extra order mutations * add recovery paths and errors for failed syncs * address feedback * add catch and drain noops * minor fixes for payment method selection and address field name * sync and type fixes * add refetchOnWindowFocus for draft order * fix shipping method tax recalc and resync order on order refresh * test(react): cover free payment transitions Task: task-4 * test(react): cover checkout pure utilities Task: task-1 * test(react): cover address and pickup helpers Task: task-2 * test(react): cover totals line-items and tips edges Task: task-10 * test(react): cover draft order sync hydration Task: task-3 * test(react): cover session discount redirect polish Task: task-12 * test(react): add checkout tracking contract coverage Task: task-8 * test(react): finalize checkout tracking contract coverage Task: task-8 * add checkout test suite * update test readme * remove coverage plan * block mutations while confirming checkout and do not override non-order backed rhf fields on sync * adjust changeset * adjust pickup selection test * migrate express checkouts to own confirmation hook * add guarding for duplicate confirm checkout sessions or false positive success redirects * update checkout-api types * only update names if names updated to prevent extra tax calc * pickup slots should check against available pickup window
1 parent 5a15dc3 commit a5c1051

116 files changed

Lines changed: 14508 additions & 1129 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/quiet-checkouts-sync.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@godaddy/react": patch
3+
---
4+
5+
Refactor checkout draft order synchronization to flush pending order updates before confirmation, avoid duplicate mutations, and improve sync failure handling.

packages/react/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"preview": "vite preview",
4141
"typecheck": "tsc --noEmit",
4242
"test": "vitest run",
43+
"test:coverage": "vitest run --coverage",
4344
"lint": "biome check ./src",
4445
"lint:fix": "biome check --write --unsafe ./src",
4546
"prepublishOnly": "pnpm run build"
@@ -102,6 +103,9 @@
102103
},
103104
"devDependencies": {
104105
"@biomejs/biome": "^2.3.2",
106+
"@testing-library/jest-dom": "^6.9.1",
107+
"@testing-library/react": "^16.3.2",
108+
"@testing-library/user-event": "^14.6.1",
105109
"@types/node": "^22.13.1",
106110
"@types/react": "^19.0.8",
107111
"@types/react-dom": "^19.0.3",
@@ -115,6 +119,7 @@
115119
"tsdown": "^0.15.6",
116120
"typescript": "~5.7.3",
117121
"vite": "^6.4.1",
122+
"@vitest/coverage-v8": "4.1.2",
118123
"vitest": "^4.1.2"
119124
},
120125
"publishConfig": {
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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

Comments
 (0)