Add Playwright e2e tests for supporter mode and donation upsell#63
Add Playwright e2e tests for supporter mode and donation upsell#63
Conversation
package.json, .wp-env.json, and playwright.config.ts for the new @ck/join-e2e package. wp-env targets WordPress 6.5 with the join-block plugin mounted; Playwright is configured for Chromium against port 8889 (wp-env tests environment).
seed.sh builds join-flow with REACT_APP_USE_TEST_DATA=true (so getTestDataIfEnabled() pre-fills all fields), then calls setup.php via wp-env to create the two test pages and flush rewrite rules. setup.php creates e2e-standard-join (£5/month) and e2e-free-join (£0/month) pages using carbon-fields/ck-join-form block markup with the membership plan data embedded in the "data" block attribute, which Carbon Fields passes directly to the render callback's $fields argument. get-page-url.sh retrieves a stored page URL from the wp_options table.
01-render.spec.ts verifies:
1.1 — .ck-join-form is visible; script#env is attached and parseable JSON
with WP_REST_API and at least one MEMBERSHIP_PLANS entry.
1.2 — input#firstName is visible after React mounts; no console errors.
1.3 — .progress-steps is rendered; .progress-step--current contains
"Your Details" on the initial page load.
02-form-progression.spec.ts covers:
2.1 — pre-filled test data values visible in form inputs.
2.2a-i — required-field validation for firstName, lastName, email (including
format check), phoneNumber, addressLine1, addressCity,
addressPostcode, and addressCountry; each test restores the field
after asserting the error.
2.3 — clicking Continue on the details stage advances to the plan stage;
the breadcrumb updates to "Your Membership".
2.4 — selecting the Standard plan and clicking Continue advances past plan
to the payment-details stage; breadcrumb shows "Payment".
2.5 — the POST to /wp-json/join/v1/step carries stage="enter-details",
email, firstName, lastName, addressPostcode, and addressCity from
the test data.
Both /step and /join endpoints are intercepted and fulfilled with 200 so
that missing server credentials never block navigation.
03-free-membership.spec.ts exercises the shouldSkipPayment branch in app.tsx:
when the selected plan has amount=0 and no donation is requested, the flow
jumps directly from plan to confirm, bypassing all payment stages.
3.1 — navigating through Details and Plan on the free page reaches
'Confirm your details' without any payment step.
3.2 — the confirm stage shows the member's email address in the Summary
and a Confirm/Join button that is enabled.
3.3 — clicking Confirm POSTs to /wp-json/join/v1/join with email,
firstName, lastName, addressPostcode, addressCity, membership="free",
and no non-zero donationAmount.
The /join endpoint is intercepted and fulfilled with a 200 success response
so tests pass without real service credentials.
e2e.yml runs on pushes to master and feature/** branches, and on PRs.
Steps:
1. Install root deps (yarn)
2. Build join-flow bundle with REACT_APP_USE_TEST_DATA=true
3. Install join-e2e deps and Playwright Chromium
4. Start wp-env (Docker-based isolated WordPress on port 8889)
5. Seed the test environment via setup.php + rewrite flush
(seed.sh's build step is skipped since the bundle is already built
in step 2 above)
6. Run Playwright tests
7. Upload playwright-report artifact on failure
Convenience script that sets REACT_APP_USE_TEST_DATA=true so getTestDataIfEnabled() pre-fills all form fields during E2E runs. The join-e2e seed.sh and the CI workflow both call this target.
Instead of asserting .invalid-feedback toBeVisible() (which depends on
Bootstrap's .is-invalid ~ .invalid-feedback CSS display rule and was
consistently 'hidden' in CI), assert that the input element itself has the
is-invalid class added by react-hook-form. The <input>/<select> element is
always visible so this check is CSS-display-independent.
2.2i is relaxed: the country <select> has no empty <option> so a truly blank
value cannot be triggered via selectOption(''); the test now just restores
the GB selection without asserting an error.
The country select has no empty option, so selecting index 0 picks a valid
country and the form advances on Continue, causing the subsequent restore
selectOption('GB') to time out. Replace with a simpler assertion that the
field is present and defaults to GB.
…aptureJoinBodyViaStripeRedirect)
…#59 sections 9, 10, 13)
…e redirect recurDonation: true was applied as an override *after* getSavedState(), which meant a user who explicitly chose the one-off tab (recurDonation: false in session) had their choice reset on the Stripe return-redirect page load. Move recurDonation: true to a default *before* getSavedState() so the session can override it. Keep donationSupporterMode: true after getSavedState() since that flag must always reflect the current block's config, not stale session data.
conatus
left a comment
There was a problem hiding this comment.
This looks good but feel these comments need addressing to tighten things up.
docs/e2e-testing.md
Outdated
|
|
||
| ## Overview | ||
|
|
||
| The CK Join Form plugin ships with a Playwright end-to-end test suite that exercises the React join flow inside a real WordPress environment. Tests run against a wp-env (Docker WordPress) instance seeded with purpose-built test pages. |
There was a problem hiding this comment.
The software is called "Join Flow" or "Join" rather than "CK Join Form".
docs/e2e-testing.md
Outdated
| | Component | Location | Purpose | | ||
| |-----------|----------|---------| | ||
| | Test suite | `packages/join-e2e/tests/` | Playwright spec files | | ||
| | Helpers | `packages/join-e2e/tests/helpers.ts` | Shared utilities for mocking, env injection, and payment simulation | |
There was a problem hiding this comment.
Environment injection please.
|
|
||
| The suite validates the join form's UI behaviour and the data contracts it sends to the backend. It does not exercise live payment provider APIs. Instead, it uses helpers that simulate the outcome of payment flows (redirect-based success) and captures the `/join` request body to assert correctness. | ||
|
|
||
| This is a deliberate trade-off: payment provider SDKs require live credentials, load scripts from CDNs, and introduce external redirect flows that are fragile in CI. By mocking the REST layer and simulating payment completion, the tests stay fast, deterministic, and credential-free while still covering the most important contract: what the frontend sends to the backend. |
There was a problem hiding this comment.
This should also note that the patching the Javascript environment script block means that the WordPress database is not truly in the correct format. To do this, we would need to simulate operation of the backend, or injection of the result of these changes into the database. Which would be good for true end to end tests, but is good enough for now.
There was a problem hiding this comment.
Should these reports be committed?
There was a problem hiding this comment.
Same here, should this report be committed?
There was a problem hiding this comment.
Equally, it feels like this report should not be committed.
| // that were explicitly present in the URL. | ||
| return Object.fromEntries( | ||
| Object.entries(cast).filter(([k]) => k in queryParams) | ||
| ); |
There was a problem hiding this comment.
Could you make extra sure this change is safe and doesn't break anything?
There was a problem hiding this comment.
Safe. The change narrows what getProvidedStateFromQueryParams returns: previously it called FormSchema.cast(queryParams) which fills in Yup schema defaults for every field absent from the URL, including recurDonation: false and membership: "". Those defaults were then spread after the DONATION_SUPPORTER_MODE env override, silently resetting recurDonation to false on every Stripe return redirect regardless of whether the user had chosen monthly or one-off. The fix filters the cast result to only include keys that were actually present in the query string.
For the common case (no query params at all) the function now returns {} instead of an object full of defaults, which has no effect on the state spread. For the case where query params genuinely pre-fill form fields (e.g. ?membership=standard), those specific values still go through FormSchema.cast and are coerced correctly — only the spurious defaults for absent fields are dropped.
Two bugs that this unmasked are covered by passing tests: test 7.4 (monthly recurDonation=true) and test 7.5 (one-off recurDonation=false) together prove both the monthly and one-off paths produce the correct value through the Stripe redirect.
packages/join-flow/package.json
Outdated
| "test": "jest" | ||
|
|
||
| "test": "jest", | ||
| "build:e2e": "REACT_APP_USE_TEST_DATA=true webpack --config './webpack/production.js'" |
There was a problem hiding this comment.
Feels like we want this command in the sort of test namespace. So something like test:e2e. Unless this is doing something different from my expectation here (running the end to end tests).
…rect with expect.poll
The previous 500ms hard wait could allow joinBody to remain null if the
/join POST arrived after the timeout, causing the helper to return {} and
producing silent false positives in downstream assertions.
expect.poll waits up to 10s for joinBody to be non-null, failing with a
clear error rather than silently returning an empty object.
….spec.ts Both were already exported by helpers.ts and imported by every other spec file. The local copies risked drifting out of sync with the shared versions.
…peRedirect callers
Each test that calls captureJoinBodyViaStripeRedirect now asserts the
returned object is non-empty before making field-level assertions, so a
silent capture failure causes an explicit test failure rather than vacuously
passing assertions against {}.
Also removed the ?? joinBody.recurDonation fallback from the
donationSupporterMode assertion in supporter-mode-monthly.spec.ts — the
intent is to verify donationSupporterMode is present, not to allow it to
be absent as long as recurDonation is truthy.
Summary
Adds Playwright e2e tests covering the supporter mode and donation upsell functionality introduced in PR #59, built on top of the existing
feature/join-e2escaffold. The branch is rebased onto master so it includes the merged JOIN-124 code.donation-upsell.spec.ts: donation upsell flow: page appears after plan selection, "Not right now" skips it, one-off and recurring donation amounts recorded in session statesupporter-mode-monthly.spec.ts: supporter mode monthly: donation page is first step, monthly default selected, tier/custom-amount CTA updates,/joinbody assertionssupporter-mode-oneoff.spec.ts: supporter mode one-off: tab enabled under correct env flags, CTA reads "Donate £X now",/joinbody hasrecurDonation=falseanddonationAmount>0supporter-mode-edge-cases.spec.ts: Direct Debit only disables one-off tab with explanatory note; no-plans warning; product naming assertions on/joinbodyhelpers.ts: shared utilities:mockRestEndpoints,injectEnvOverrides(HTML route injection for env overrides),captureJoinBodyViaStripeRedirect(simulates Stripe return to capture/joinbody without live credentials)setup.php: four new seed pages:e2e-donation-upsell,e2e-supporter,e2e-supporter-custom,e2e-supporter-no-plansdocs/e2e-testing.md: testing plan documenting current coverage, known gaps, and future workstreamsAlso fixes two bugs surfaced by the tests:
getProvidedStateFromQueryParamsinapp.tsxwas applying Yup schema defaults for all fields absent from the URL query string, overriding session-restored state and theDONATION_SUPPORTER_MODEenv override on the Stripe return redirect.wp-env.jsonpointed at the GitHub source archive for WordPress core, which times out in CI; changed to the directwordpress.orgzip URLWhat is not covered (and why)
JoinServiceMailchimpTest.phpfree-membership.spec.tsSee
docs/e2e-testing.mdfor the full gap analysis and future workstreams.Key design decision: env overrides via
injectEnvOverridesGlobal settings like
USE_STRIPEandSTRIPE_DIRECT_DEBIT_ONLYcome from WordPress plugin options, not block attributes. Rather than configuring the wp-env instance with real plugin options,injectEnvOverridesintercepts the page HTML response viapage.route()and modifies the<script id="env">JSON before the browser receives it. This keeps tests self-contained and avoids global state bleed between scenarios.Test plan
cd packages/join-e2e && npm run pretest: seed script runs without errors, all 6 pages creatednpx playwright test: all 52 tests pass