Skip to content

test(e2e): delivery address #3607

Draft
itsjustriley wants to merge 21 commits intomainfrom
test/e2e-delivery-addresses
Draft

test(e2e): delivery address #3607
itsjustriley wants to merge 21 commits intomainfrom
test/e2e-delivery-addresses

Conversation

@itsjustriley
Copy link
Contributor

@itsjustriley itsjustriley commented Mar 19, 2026

WHY are these changes introduced?

Adds E2E test coverage for delivery address CRUD in the skeleton template. This is a gap in our test suite — the account.addresses.tsx route had zero E2E coverage. Uses MSW to mock the Customer Account API, enabling fast, deterministic tests without a real storefront.

Depends on MSW infrastructure from PR #3537.

Related: https://github.com/Shopify/developer-tools-team/issues/1038

WHAT is this pull request doing?

New files:

  • e2e/fixtures/delivery-address-utils.tsDeliveryAddressUtil fixture following the deep module pattern. Hides form-filling, button mechanics, and completion signals behind a simple action interface.
  • e2e/specs/skeleton/deliveryAddresses.spec.ts — 8 serial tests covering all CRUD flows + default address toggling.

Modified files:

  • e2e/fixtures/msw/handlers.ts — New delivery-addresses MSW scenario with mutable closure state. Mocks CustomerDetailsQuery, CustomerOrdersQuery, and all three address mutations. Seed data exported as DELIVERY_ADDRESS_SEED_COUNT for spec consumption.
  • e2e/fixtures/msw/scenarios.ts — Added deliveryAddresses scenario constant.
  • e2e/fixtures/index.ts — Removed orphaned AccountUtil export.
  • templates/skeleton/app/routes/account.addresses.tsx — Fixed aria-label="territoryCode"aria-label="Country code" (was a raw developer string, meaningless to screen readers).

Test coverage:

Group Tests
Read Existing addresses render, create form visible, default checkbox state
Create Fill and submit new address, verify count and data
Update Modify city field, verify change
Default Address Toggle default to a different address, verify checkbox states swap
Delete Remove one address (count decreases), delete all (empty state)

HOW to test your changes?

npx playwright test --project=skeleton --workers=1 e2e/specs/skeleton/deliveryAddresses.spec.ts

Checklist

  • I've read the Contributing Guidelines
  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've added a changeset if this PR contains user-facing or noteworthy changes
  • I've added tests to cover my changes
  • I've added or updated the documentation

fredericoo and others added 13 commits March 5, 2026 19:08
Why: PR review identified a latent bug (localStorage polyfill skipped in
workerd), type safety gaps, magic numbers, dead code, and structural
inconsistencies with existing fixture patterns.

How: Fix hasWorkingLocalStorage null detection by adding an explicit null
check before the try/catch — optional chaining silently no-ops when
localStorage is undefined, causing the function to return true and skip
the polyfill in exactly the environment (workerd) where it's needed.

Augment the global Env interface with HYDROGEN_E2E_MSW_SCENARIO to remove
the unsafe double cast in getMswScenario. Extract E2E_TUNNEL_HOSTNAME and
SESSION_TTL_IN_MS as named constants with "why" comments. Add JSDoc to
ensureNodeProcessForMsw explaining the NODE_ENV/versions.node strategy,
and a comment on module-level currentMswScenarioMeta explaining the
architectural constraint.

Convert AccountUtil to a Playwright fixture (matching CartUtil, DiscountUtil,
GiftCardUtil patterns), remove the pass-through expectLoggedInState method,
and update account.spec.ts to use the fixture.

Add explicit MswScenarioMeta return type and defensive throw after Map.get()
in getHandlersForScenario. Remove dead code guard in graphql.ts parseOperation
(regex only captures 'query'|'mutation'). Remove unused env option from
DevServer.
…ation

We've agreed to prioritise test utilities that take a `page` arg over
Playwright fixtures that override the test function signature. Revert
the AccountUtil fixture registration while keeping all other review
feedback changes from the prior commit.
@shopify
Copy link
Contributor

shopify bot commented Mar 19, 2026

Oxygen deployed a preview of your test/e2e-delivery-addresses branch. Details:

Storefront Status Preview link Deployment details Last update (UTC)
Skeleton (skeleton.hydrogen.shop) ✅ Successful (Logs) Preview deployment Inspect deployment March 19, 2026 8:10 PM

Learn more about Hydrogen's GitHub integration.

Add E2E tests for delivery address management in the skeleton template,
using MSW to mock the Customer Account API with mutable closure state.

The MSW scenario seeds 2 addresses and tracks mutations so subsequent
queries reflect CRUD changes. The DeliveryAddressUtil fixture follows
the deep module pattern — entity locators are public, button mechanics
and form-filling are hidden behind action methods.

8 serial tests cover all meaningful user flows:
- Read: existing addresses render, create form visible, default checkbox
- Create: fill and submit new address, verify count and data
- Update: modify city field, verify change
- Default Address: toggle default to a different address
- Delete: remove one address, delete all to empty state

Design decisions:
- Serial mode because MSW mutable state accumulates across tests
- FIELD_LABEL_MAP as single source of truth for field-to-label mapping
- DELIVERY_ADDRESS_SEED_COUNT exported from handlers so the spec derives
  the count from the source of truth
- FORM_SUBMISSION_TIMEOUT_IN_MS constant for completion signal timeouts
- Forms identified by button presence (Create vs Save) not hardcoded IDs
- assertAddressVisible accepts Partial<AddressFormData> for flexibility
- All selectors use getByRole/getByLabel per project conventions
The address form's territory code input had aria-label="territoryCode"
(a raw developer string) instead of a meaningful label for assistive
technology. A screen reader would announce "territoryCode" which is
meaningless to users. Changed to "Country code" to match the visible
label's intent.
@itsjustriley itsjustriley reopened this Mar 19, 2026
@itsjustriley itsjustriley force-pushed the test/e2e-delivery-addresses branch from 4ca6de0 to fa84d86 Compare March 19, 2026 16:45
The update and default-address-toggle tests were asserting against DOM
state that persists regardless of mutation success. The form uses
uncontrolled inputs (defaultValue/defaultChecked), so user-typed values
survive React re-renders even when the MSW mutation fails silently.

Both tests now re-navigate after the mutation, forcing fresh component
mounts that can only render data from MSW closure state. This ensures
the assertions verify server-side persistence, not stale DOM values.

The completion signal (button text check) was also a race condition -
it could pass immediately before React re-rendered to the loading state.
Replaced with waitForLoadState('networkidle') which reliably waits for
the full action + revalidation cycle to complete.

Also uses MSW_SCENARIOS constant in smoke test for consistency.
The delete method's waitForLoadState('networkidle') fires prematurely
in CI because React Router's action-then-revalidation has a brief
network gap between steps. The original not.toBeVisible() signal is
stronger: it waits for the form to disappear from the DOM, which
proves the full delete -> revalidate -> re-render cycle completed.
…erts

The delete completion signal (not.toBeVisible on the delete button)
fails in multi-delete loops because Playwright locators are lazy -
after the first form is deleted, .first() shifts to the next form
whose delete button IS visible. Replaced with a count-based signal:
capture count before, click delete, wait for count to decrease by 1.

Also strengthened assertions to match gift card/discount util patterns:
- deleteAddress: assert button toBeVisible before clicking (before assert)
- empty state test: assert empty state toHaveCount(0) before deleting
  (not.toBeVisible can pass on hidden elements; toHaveCount(0) asserts
  non-existence in the DOM)
networkidle is an antipattern for Playwright tests - it's timing-
dependent and fires prematurely in React Router's multi-step request
chain (action response -> gap -> revalidation request).

Each mutation now uses an appropriate signal:
- create: count-based (new form appears in existing addresses list)
- update: waitForResponse (intercepts the action response, proving
  the server processed the mutation before tests re-navigate)
- delete: count-based (already using assertAddressCount)
The e2e guidelines say to wait for visible effects, not network
requests. But the skeleton's AddressForm has no visible success
feedback and uses uncontrolled inputs (defaultValue), so there is
no user-visible effect to wait for after a successful update.

Added a DEVIATION comment explaining why waitForResponse is used
as a pragmatic compromise, and noting that callers must re-navigate
afterward to verify persistence via fresh mount from MSW state.
Follows the established POM pattern from discounts.spec.ts and
giftCards.spec.ts: register the utility in base.extend, destructure
from test args ({addresses}), and use beforeEach for shared navigation
setup. This replaces the manual `new DeliveryAddressUtil(page)` +
`navigateToAddresses()` repetition in every test.
@itsjustriley itsjustriley changed the title test(e2e): delivery address CRUD tests with MSW test(e2e): delivery address Mar 19, 2026
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.

2 participants