Skip to content

test(stripe-connect): add Tier 1 e2e suite for the Stripe Connect revamp - #3387

Open
shohan0120 wants to merge 19 commits into
developfrom
tests/stripe-connect-tier1-e2e
Open

test(stripe-connect): add Tier 1 e2e suite for the Stripe Connect revamp#3387
shohan0120 wants to merge 19 commits into
developfrom
tests/stripe-connect-tier1-e2e

Conversation

@shohan0120

@shohan0120 shohan0120 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

All Submissions:

  • My code follow the WordPress' coding standards
  • My code satisfies feature requirements
  • My code is tested
  • My code passes the PHPCS tests
  • My code has proper inline documentation
  • I've included related pull request(s) (optional)
  • I've included developer documentation (optional)
  • I've added proper labels to this pull request

Changes proposed in this Pull Request:

Stripe Connect has had zero automated coverage since getdokan/dokan#3272 was closed, while Stripe Express has a live suite. This adds a Tier 1 suite for the Stripe Connect revamp.

18 Tier 1 cases across 25 tests. Checkout on both surfaces, 3D Secure, saved cards, the multi-vendor split, refunds and reversal idempotency, webhook replay, and payment-integrity attacks.

New files:

Path What
tests/pw/tests/e2e/stripe-connect/ 10 spec files, helpers.ts, stripeConnectPage.ts
tests/pw/mu-plugins/dokan-stripe-connect-test-helpers.php test-only REST routes, namespace dokan-test-connect/v1

Changed files:

  • tests/pw/utils/stripeApi.ts becomes a makeStripeApi(secretFn) factory. Stripe Connect is a different Stripe platform account from Express, so it cannot share the key. stripeApi keeps its existing behaviour and env var; stripeConnectApi is new.
  • .github/workflows/e2e_api_tests.yml gains the five Connect secrets and a STRIPE_CONNECT_REQUIRED gate, matching the existing Express gate so fork PRs stay soft and internal runs fail loud.

Two design points worth knowing before review:

  1. Express and Connect are mutually exclusive. Only one Stripe gateway can own the checkout. Every Connect spec activates Connect in beforeAll and hands the checkout back to Express in afterAll, reusing the Express helper's own configure route, so an Express spec sharing a shard still finds its gateway configured. Both directions of that flip are verified.
  2. The suite requires --workers=1. It mutates the active gateway, the connected-vendor meta and the seller-pays-fee setting. Two workers would fight over all three.

Related Pull Request(s)

  • Depends on dokan-pro PR #5646 (feat/stripe-connect-revemp). These specs exercise the revamp and are intended to merge alongside it.
  • Replaces the closed getdokan/dokan#3272.

Closes

  • Closes #

How to test the changes in this Pull Request:

cd wp-content/plugins/dokan-lite/tests/pw
# .env needs TEST_PUBLISH_KEY_STRIPE_CONNECT, TEST_SECRET_KEY_STRIPE_CONNECT,
# TEST_CLIENT_ID_STRIPE_CONNECT, STRIPE_VENDOR1_ACCT, STRIPE_VENDOR2_ACCT
NO_SETUP=true npx playwright test tests/e2e/stripe-connect \
  --project=e2e_tests --workers=1 --retries=0 --reporter=line

Expect 24 passed, 1 skipped. Four of the passes are test.fail() cases that pass by failing.

The client id is not optional: Helper::is_ready() returns false without one and the gateway never reaches the checkout, so a key pair alone would leave every spec skipping to green.

Without the keys the suite self-skips, and stripeConnectPreflight.spec.ts turns that silent skip into a loud failure whenever STRIPE_CONNECT_REQUIRED is set.

Changelog entry

Add Tier 1 end-to-end test coverage for the Stripe Connect revamp

Stripe Connect previously had no automated end-to-end coverage, so regressions in vendor payouts, refunds and payment integrity could only be caught by hand. This adds a Playwright suite covering card checkout on both the block and classic surfaces, 3D Secure completion and abandonment, saved cards, the multi-vendor split, refund reversal and its idempotency, webhook replay, and two payment-integrity attacks. Money assertions are made against the Stripe API and the Dokan REST API rather than the database, so a test can only pass when the money actually moved.

Verification

Three consecutive full-folder runs are green, two headless and one headed, 24 passed and 1 skipped each, roughly 14 minutes per run at --workers=1.

Every passing case was red-checked: one load-bearing assertion inverted, the run confirmed to fail with the real values in the message, the inversion reverted, and the case re-run green. Nothing is accepted from observation alone.

Stripe Express was re-run to prove it still works: 46 passed, 3 failed, 10 skipped. All three failures were browser and timeout shaped, all three pass when re-run alone, and every money test that exercises the refactored stripeApi.ts passed in the full run.

Not passing, deliberately

Case State Why
SC-42 (classic) test.fail() Confirmed defect, getdokan/plugin-internal-tasks#2294
XREF-01 purchase leg test.fixme PayPal is not configured on the test site and the shared sandbox buyer is locked out

Every marker is called imperatively, one line before the failing assertion, never as a declaration modifier. A declaration-level test.fail marks the whole body as expected-to-fail, so the positive controls above it would report green on the day they silently broke.

Defects this suite reproduces

Filed under getdokan/plugin-internal-tasks#2293. The issues are the single place each bug is described; the specs cite the issue numbers in their comments rather than carrying a second copy in the repo:

Issue Finding
#2294 Vendor transferred the gross earning instead of the net when the seller pays the fee
#2295 Block checkout replaces Stripe's decline reason with "Something went wrong"
#2297 A checkout recalculation destroys the entered card and warns nobody
#2299 Orders with no 3DS challenge are recorded as "3d secure"

Three further reported issues (#2296, #2298, and the transaction-id half of #2299) did not reproduce here. That is recorded rather than treated as closed, since two of them are reported as intermittent.

#2294 is worth a closer look. It is a race, not a constant: over five classic orders the vendor was over-transferred by exactly the gateway fee three times and matched twice. The root cause is deterministic — _dokan_stripe_awaiting_disbursement is set on the block checkout and never on the classic one, so the classic transfer is made before the fee is known. Because the symptom is only about 60% reproducible, SC-42 (classic) places three orders and requires every one to match, which lifts detection to roughly 94%. A marker over a single order was wrong 40% of the time, and two runs proved it.

CI

Wired but not yet exercised. The workflow builds dokan-pro from develop, which does not carry the revamp, so until #5646 lands these specs would meet the legacy Connect module and fail for a reason unrelated to them. The dokan-pro checkout is deliberately not pinned at the revamp branch, so there is no temporary hack to unpick later.

PR Self Review Checklist:

  • Checkout mechanics (Payment Element re-mount handling, the Blocks place-order press, the settle fallback) are re-implemented here rather than shared with the Express page object. Extracting them would mean editing an 1111-line file that a currently passing suite depends on. Worth revisiting if a third Stripe gateway appears.
  • The suite reads dokan_gateway_fee and similar order meta through the WC REST API, not the database, per the project's oracle rule.
  • tests/pw/utils/stripeApi.ts shows a large diff because the body was indented one level into the factory. stripeApi's behaviour is unchanged.

Stripe Connect had zero automated coverage after PR #3272 was closed, while
Stripe Express has a live suite. This adds a Tier 1 suite covering checkout,
3D Secure, saved cards, the multi-vendor split, refunds, webhook replay and
payment-integrity attacks.

18 Tier 1 cases across 25 tests. 16 pass, each proven able to fail before being
accepted. Two are deliberately not passing:

- SC-42 is a confirmed defect and carries test.fail() on the classic surface.
- XREF-01's PayPal purchase leg is test.fixme: PayPal is not configured on the
  test site and the shared sandbox buyer is locked out.

Stripe Express and Stripe Connect are mutually exclusive, so every spec activates
Connect in beforeAll and hands the checkout back to Express in afterAll. That
keeps an Express spec sharing a shard working. The suite needs --workers=1
because it mutates the active gateway, the connected-vendor meta and the
seller-pays-fee setting.

utils/stripeApi.ts becomes a makeStripeApi(secretFn) factory. Connect is a
separate Stripe platform account from Express, so it cannot share the key.
stripeApi keeps its existing behaviour; stripeConnectApi is new.

Four defects reproduced by this suite are filed under
getdokan/plugin-internal-tasks#2293, with local notes in
tests/pw/tests/e2e/stripe-connect/bugs/.

The CI secrets and a STRIPE_CONNECT_REQUIRED gate are wired, but nothing has run
on CI yet: the workflow builds dokan-pro from develop, which does not carry the
revamp. This is intended to merge alongside dokan-pro PR #5646.
The four markdown files under tests/e2e/stripe-connect/bugs/ duplicated bug
reports that already exist as GitHub issues under
getdokan/plugin-internal-tasks#2293. Nothing in the specs referenced them by
path, so they were a second copy with no reader and every chance of drifting out
of step with the issues.

The specs already cite the issue numbers in their comments, which keeps one place
to read a bug. The existing stripe-express bug files stay as they are; those are
referenced from the specs that fail because of them.
… methods

dokan-pro #5861 made the Address verification method undeletable:
VerificationMethodsApi::delete_item() returns dokan_pro_rest_cannot_delete (403)
for kind === 'address', because the seller-address auto-fill flows query by that
kind and install seeding never recreates it.

deleteAllVerificationMethods still deleted every method, and delete() asserts
response.ok(), so the 403 threw inside beforeAll and took the whole
vendorVerifications spec with it. On develop the spec aborts after 5 tests; with
this change 34 of 36 pass.

Filtering on kind here rather than tolerating a 403 inside delete(), so a genuine
delete failure on a custom method is still loud.

The two tests that now fail, "admin can edit" and "admin can delete", were
previously unreachable because the run aborted before them. They are a separate
pre-existing problem and are not addressed here.
…I change

dokan-pro #5861 ("make method actions discoverable") rewrote the verification
method row: the edit and delete icons are no longer hover-revealed, they lost the
`rounded-full bg-violet` / `rounded-full bg-red` classes, and they gained an
aria-label.

Both selectors still matched on those classes, so the locator never resolved and
every attempt died on a 30s dispatchEvent timeout:

    waiting for locator('//p[...]/../../..//button[contains(@Class, 'rounded-full bg-violet')]')

Target the aria-label instead. It is semantic, it is what the component actually
exposes, and it survives restyling. The hover step and the dispatchEvent
workaround both go with it, since the buttons are permanently visible now.

The same two selectors were duplicated in settings/settingsPage.ts; both copies
are updated so they cannot drift apart.

vendorVerifications.spec.ts now passes 36/36, verified on WordPress 7.1 with
WooCommerce 11.0.1 as well as on the pinned 7.0.3 stack.
removeVendorSubscription() deleted the Stripe Express subscription id but never the
Connect one, so `_stripe_subscription_id` survived the reset and leaked from one
describe into the next.

The lifetime-pack case read that stale value and reported a subscription the
non-recurring purchase had not created, which looks like a product defect and is not
one.
Vendor subscriptions and coupons had no automated coverage, so regressions in either
could only be caught by hand. Both are now release-blocking, which takes Tier 1 from
18 cases to 41 and the suite from 25 tests to 49.

Vendor subscription packs (SCSUB-03, SCSUB-07 to SCSUB-12): the Payment Element
mounts on a pack checkout, a recurring pack activates the vendor behind a real Stripe
subscription, the pack fee stays with the platform, a decline creates no paid order,
a lifetime pack mints no subscription, a free trial reaches `trialing`, and a 3D
Secure card settles after the challenge.

Dashboard and guards (SCSUB-13 to SCSUB-22): the active pack renders, cancel schedules
`cancel_at_period_end` in Stripe while the vendor keeps selling, reactivate clears it,
re-opening the checkout reuses the pending subscription rather than creating a second,
one pack per cart, a subscribed vendor cannot add another, and a guest is sent to log
in.

Lifecycle webhooks (SCSUB-16 to SCSUB-18): a renewal-cycle invoice records a renewal
order, a final payment failure terminates the vendor, and a deleted subscription
revokes the pack. These inject into the module's EventFactory, so they do not exercise
the live `?wc-api=dokan_stripe` endpoint.

WooCommerce Subscriptions (SCSUB-01, SCSUB-02, SCSUB-06): a customer buys a vendor's
recurring product, a renewal charges once and pays the vendor once, and the renewal
payout is booked as exactly one approved auto-withdraw so the vendor cannot withdraw
it a second time. SCSUB-06 guards the RenewalProcessor ledger fix. Renewals are driven
through the gateway's own `woocommerce_scheduled_subscription_payment` hook rather
than a synthetic webhook, so the token is really charged and the transfers really made.

Coupons (CPN-01, CPN-02): a coupon reduces the Stripe capture and the order together
to the cent, and a marketplace coupon on a two-vendor cart still pays both vendors
without the transfers exceeding the capture.

SCTOK-02 is marked `test.fail()` against getdokan/plugin-internal-tasks#2321: a
free-trial checkout draws no card field. It ships with a paid-subscription control so
a broken fixture cannot be mistaken for the defect.

Every case was red-checked: one load-bearing assertion inverted, the failure confirmed
to print the real observed value, then reverted.

Notes on state, both learned the hard way:

- `dokan_product_subscription[enable_pricing]` is set once per spec file and cleared in
  a file-level teardown. Flipping it per describe leaves windows where the module
  empties the pack back out of the cart; leaving it on leaks site-wide and empties
  every other spec's cart.
- Saved cards are cleared before each pack purchase. A stale token is pre-selected at
  checkout, and that path confirmed at Stripe without settling the order locally, since
  it returns no redirect and localhost receives no webhook.

The folder teardown leaves the `product_subscription` module deactivated, which only
holds when the whole folder runs.

Requires `--workers=1`, as before.
SC-48 (getdokan/plugin-internal-tasks#2299, an unchallenged payment recorded as "3d
secure") and the #2295 validation (block checkout replacing Stripe's decline reason)
both reported "expected to fail, but passed" on dokan-pro 6b3f2999a. Each was then
re-run on its own and passed 4 attempts out of 4, so the markers are removed and the
assertions now guard the fixed behaviour.

The repeat runs were not a formality. SC-42, marked for the same reason, reported
"expected to fail, but passed" in the same session and then reproduced again on the
next attempt, so a single passing observation settles nothing. SC-42 keeps its marker
and #2294 stays open.

The XREF-01 PayPal purchase leg is deleted rather than left as a `test.fixme`: PayPal
is out of scope for this work. The Connect-side half, that other gateways still
register while Stripe Connect is active, is kept.
…6 is open

The e2e job built dokan-pro from develop, which does not carry the Stripe
Connect gateway revamp. On that build modules/stripe has no src/ and no
Payment Element markup, so the gateway configures and renders normally and
then every Connect checkout case times out waiting for a mount the build
cannot produce. Run 33358188560 was cancelled for this reason.

Pinning build_pro at feat/stripe-connect-revemp lets the suite meet the
module it was written against. The branch is 0 commits behind develop and
changes 5 files outside modules/stripe, so the other suites are unaffected.
merge-reports still checks out develop, since feature-map.yml is identical
on both branches.

Restore the ref to develop once #5646 merges.
SCTOK-01, save-card checkbox. Stripe keeps adding accordion rows to the
Payment Element after the card fields are filled — the CI trace shows ACH
bank search and Cash App arriving, which a US runner IP is offered and this
location never sees. Each row pushes the checkbox down, so a click aimed at
where it just was is delivered without changing anything, and check() reports
"Clicking the checkbox did not change its state" and gives up without
retrying. The tick is now retried against a re-measured position, and says so
when it needed more than one attempt: a miss now and then is the element
settling, a miss every run would be a late-bound handler and a product
question. The closing assertion is unchanged, so a checkbox that never ticks
still fails.

payments.spec.ts, admin can add stripe payment method. The revamped gateway
ships enable_3d_secure disabled, since Payment Elements keep SCA on
permanently and the toggle survives only for legacy orders. Ticking it can
never succeed. checkIfOperable skips a control the gateway itself renders
read-only and logs what it skipped; every other field keeps using check(), so
one going read-only unexpectedly still fails the run.

Verified locally: payments failed with the identical error before the fix and
passes after. SCTOK-01 was already green locally, so it was verified against
the failure mode instead — a swallowed first click reproduces the CI error and
the fix clears it, while swallowing every click still fails.
…ct-tier1-e2e

# Conflicts:
#	tests/pw/tests/e2e/vendor-verifications/vendorVerificationsPage.ts
#	tests/pw/utils/apiUtils.ts
…ng reasons

SCPE-05 was a shard-state leak, not a defect. It pinned every existing
transfer to vendor1 and treated anything new as belonging to this attempt,
but all twelve shards share one Stripe platform and the same
STRIPE_VENDOR1_ACCT, so a sibling shard paying that vendor mid-test read as
"the abandoned challenge paid the vendor". It failed in-shard on runs
33367338869 and 33482179826 and passed alone both times. The assertion now
keys off this attempt's own charge, which no other shard can produce, and
says which proof it used when there is no charge to check.

SC-42 (classic) is skipped rather than marked. #2294 is real and still
reproduces on dokan-pro 61869ae8f: measured today the vendor was short by
exactly the gateway fee on 7 of 27 orders. But detection is irreducibly
statistical, because on a single order a correct implementation and a
bug-present-but-race-won order are identical. One order was wrong ~40% of the
time, three ~1 run in 8, and six still missed the defect entirely in one of
three local runs; a 2% miss rate would need ~13 live orders per attempt.
There is no deterministic signal to assert instead. The body still asserts
the CORRECT behaviour, so deleting the test.fixme line once #2294 is fixed
turns it into the regression lock.

Verified locally against dokan-pro 61869ae8f, rebuilt: SCPE-04, SCPE-05,
SC-41 and SC-42 (block) pass, SC-42 (classic) skips.
…gate (SC-45)

The suite had nothing here at all. Whether a customer may buy from a vendor
who has never connected Stripe is a gating decision about money, and neither
answer was tested: not that checkout is refused when the admin disallows it,
nor that the sale completes when they allow it.

SC-45a asserts the refusal and that it names the reason, and that no order was
paid. The gate is Validation::check_vendor_configure_stripe on
woocommerce_after_checkout_validation, so the notice is a WooCommerce
validation error rather than the Stripe JS error container the declined-card
helper watches, which is why this does not reuse placeClassicOrderExpectError.

SC-45b asserts the sale settles and that no transfer was made against the
charge. The setting's own description is the specification here: "The payment
will send to admin Stripe account." The check is scoped to this order's charge
rather than an account listing, because all twelve CI shards share one Stripe
platform.

The gate keys on an empty _stripe_connect_access_key, not on
dokan_connected_vendor_id, so a vendor seeded with only the account id would
still be refused. Classic checkout only; whether the block surface enforces
the same rule is declared uncovered rather than assumed.

Both cases red-checked. Flipping the setting in SC-45a removes the notice
entirely and the case fails; inverting SC-45b's transfer count fails reporting
the real observed 0. Teardown restores the setting to its default, verified in
the database, since leaving it on would disable the gate suite-wide.
SC-45a failed on CI run 33590556960 and passed locally, which was the tell.
Both cases drive the classic checkout, but the [woocommerce_checkout] shortcode
page does not exist in a fresh environment and every CI shard provisions its
own. The run died on "Oops! That page can't be found" waiting for #place_order,
a 404 with nothing to do with connected sellers. Locally it passed only because
an earlier spec had already created the page on a long-lived site.

Every other classic-checkout spec here already calls ensureClassicCheckoutPage
in beforeAll. This one did not.

Reproduced locally before fixing rather than inferred: with the page deleted the
case failed with the identical error, and passes with the page absent once the
beforeAll recreates it. Confirmed in a real browser that /classic-checkout/
renders with no console errors, and that teardown still leaves
allow_non_connected_sellers at its default.
The RMA suite failed on CI run 33590556960 with a 403 on its seeding call, in
the same shard as this spec, and its isolation re-run failed too. That was our
doing, not theirs.

WooCommerce Subscriptions promotes a buyer to the `subscriber` role when a
subscription is created, and that REPLACES `customer` rather than adding to it.
Both the role and the actor are global, so this spec left customer1 as a
subscriber for everything that ran after it. Dokan's RMA REST create checks
dokan_is_user_customer and refuses with 403 "Only customers can create warranty
requests". The damage is persistent rather than transient, which is why running
the victim alone did not clear it — worth remembering, since a failing
isolation re-run normally argues against pollution.

Found by bisecting the shard locally, running each spec in CI order and reading
customer1's role back after each: sixteen specs clean, then this one flipped it
to subscriber.

Restored at file scope so it runs whichever describe executed.

Verified as a chain rather than asserted: with the role forced to subscriber the
RMA spec fails with the identical CI error (Expected 201, Received 403 at
line 41); this spec then buys a subscription and still hands back `customer`;
the RMA spec then passes 12/12.
SCSUB-06 failed once in a full-folder run and then passed on three consecutive
attempts, so the fault is real but was not reproducible on demand. Rather than
guess at it, the one un-polled read in the case is hardened.

The auto-withdraw is written as a side effect of the renewal settling. The case
polls for the order to reach a paid status, which is not the same instant the
ledger row lands, and then took a single reading of the withdraw list. A read
that beats the insert reports zero and fails on correct behaviour. Every other
asynchronous check in this case already polls; this one did not.

The assertion is unchanged: still exactly one new approved withdraw, still
matched on amount and on a note carrying the renewal sub-order id. Only the
reading waits.

Red-checked by rigging the poll to observe no booking: it still fails after the
full timeout with the intended message, so the poll cannot pass an unbooked
payout. Rig removed, file green 5/5.
… SC-45a

SC-45a failed on CI run 33604421784, three attempts out of three, and passed
locally every time. The page snapshot at failure showed the checkout still on
screen with the gateway selected, the product in the cart, no order placed and
no error anywhere: the form had never been submitted at all.

The case pressed #place_order once. The first press on a cold runner routinely
does not register, which is why every other classic helper here re-presses up to
three times guarded on whether the submit actually fired. Without that the case
reports a missing error message, which reads as "the gate did not fire" and
sends you looking at the gateway settings instead of at the click.

placeClassicOrderExpectError now takes the error selector and message, so this
case can reuse it: the refusal here comes from woocommerce_after_checkout_validation
and renders as a WooCommerce notice, not in the gateway's inline error container
that a declined card uses. Default behaviour is unchanged.

Verified: SC-45a and SC-45b pass, and the default callers SCPE-06 and SCPE-07
still pass on the refactored helper.
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.

1 participant