Skip to content

test(e2e): delivery address #3607

Merged
itsjustriley merged 8 commits intomainfrom
test/e2e-delivery-addresses
Mar 20, 2026
Merged

test(e2e): delivery address #3607
itsjustriley merged 8 commits intomainfrom
test/e2e-delivery-addresses

Conversation

@itsjustriley
Copy link
Contributor

@itsjustriley itsjustriley commented Mar 19, 2026

WHY are these changes introduced?

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

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

@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 20, 2026 5:59 PM

Learn more about Hydrogen's GitHub integration.

@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
@itsjustriley itsjustriley changed the title test(e2e): delivery address CRUD tests with MSW test(e2e): delivery address Mar 19, 2026
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.
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 force-pushed the test/e2e-delivery-addresses branch from e61cd22 to 849a7d3 Compare March 20, 2026 17:57
@itsjustriley itsjustriley marked this pull request as ready for review March 20, 2026 18:10
@itsjustriley itsjustriley requested a review from a team as a code owner March 20, 2026 18:10
Comment on lines +105 to +109
const actionResponse = this.page.waitForResponse((res) =>
res.url().includes('/account/addresses'),
);
await saveButton.click();
await actionResponse;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can check if the saveButton text became "Saving" and then if it became "Save" again, better than checking for response

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tried this - the "Saving" text (and the disabled state) is too ephemeral with instant MSW responses for Playwright to observe. the entire fetcher cycle (idle → submitting → loading → idle) completes within a single microtask batch, so Playwright's polling never catches the intermediate state. tested both toBeVisible() on the "Saving" text and toBeDisabled() on the button - both time out.

kept waitForResponse with an updated deviation comment explaining the microtask timing. IIRC this is the only reliable signal when the round-trip has no durable user-visible effect (uncontrolled inputs + instant mock responses).

@itsjustriley itsjustriley merged commit 774a842 into main Mar 20, 2026
20 of 21 checks passed
@itsjustriley itsjustriley deleted the test/e2e-delivery-addresses branch March 20, 2026 19:16
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