End-to-end (E2E) tests¶
This module ships a Playwright E2E pipeline that drives a real browser against a real, served Magento storefront. It reuses the same Docker stack, install script, and programmatic seed as the integration tests — it just adds the two things a browser needs that PHPUnit doesn't: an HTTP web server (nginx) in front of php-fpm, and Playwright + Chromium to drive it.
Like the integration suite, E2E is expensive, slow, and manual. It does not run on push or pull requests — only on
workflow_dispatchand release tags (v*). The unit suite remains the fast per-PR feedback loop.
The E2E suite and the integration suite are siblings on a shared foundation,
not parent/child: both are provisioned by scripts/install-magento.sh + the
seed in scripts/seed-test-data.php. E2E only layers HTTP serving and a browser
on top.
Getting started locally (10 minutes)¶
Prerequisites¶
Everything the integration tests need, plus:
- Node.js >= 20 and npm on the host (Playwright runs on your machine and drives a browser that talks to the Dockerized storefront).
- A free TCP port for the storefront (default 8080).
The credentials are the same one-time .env edit as integration — there are
no new secrets:
# from the module root (app/code/Taxcloud/Magento2)
cp .env.example .env
$EDITOR .env # TaxCloud sandbox + Magento Marketplace keys (same as integration)
Run it¶
make e2e-setup # install Magento + nginx and serve it over HTTP (~10-15 min first time)
make e2e-test # run the suite headless (auto-installs Node deps + Chromium on first run)
make e2e-test refuses to run with a clear message if the storefront isn't up,
so the two-step flow is hard to get wrong. That's it — make e2e-test works off
the same .env edit integration uses.
To reuse a Magento version you already installed for integration (skips the Composer download), pass it through:
make e2e-setup MAGENTO_VERSION=2.4.7-p10
Watch the browser¶
make e2e-test-ui # Playwright's interactive UI runner — ▶ a test and watch it live
make e2e-test-headed # run headed (visible window), straight through
make e2e-install # (re)install Node deps + Chromium explicitly
make e2e-trace # open the trace viewer for the last FAILED run
make e2e-clean # remove test-results/ and playwright-report/
The browser runs on your machine, not in Docker, and hits the storefront at
MAGENTO_BASE_URL (default http://localhost:8080). If you change the port,
keep MAGENTO_HTTP_PORT (install) and MAGENTO_BASE_URL (browser) in sync — and
pass MAGENTO_BASE_URL=... to the make e2e-test* targets too.
How it serves Magento (the architecture)¶
The integration stack runs the app (php-fpm) container as sleep infinity:
PHPUnit execs into it and talks to Magento PHP objects directly, so it never
needs HTTP. A browser does. docker-compose.e2e.yml overlays the base stack and:
- runs php-fpm in
app(so it creates the unix socket/sock/phpfpm.sock), - adds an nginx container (
markoshust/magento-nginx) that fastcgi_passes to that socket and publishes the storefront on the host (MAGENTO_HTTP_PORT).
nginx serves plain HTTP on 8080 (the image's default vhost is HTTPS-only with
a self-signed cert; we mount our own HTTP vhost — Test/E2E/docker/default.conf —
to avoid cert-trust flakiness). The actual routing is Magento's own
nginx.conf (copied from nginx.conf.sample by the install script), so we
reuse Magento's official static/media/security handling rather than hand-rolling it.
Integration runs are completely unaffected: they use docker-compose.yml
alone (no nginx, app still sleeps). The overlay is only applied when
scripts/install-magento.sh is run with E2E=1 (which make e2e-setup does).
SOAP mocking (server-side) — current status: DEFERRED¶
This is the one piece that is designed and documented here but not yet implemented in code (by decision — the pipeline this ticket delivers doesn't exercise it).
Why it can't be browser-side. Unlike a typical front-end test, the browser
never makes the TaxCloud SOAP call. Magento's PHP does, server-side, during
cart/checkout. So Playwright's page.route() cannot intercept it — the mock has
to live inside Magento, across the HTTP boundary the request crosses.
What already exists. The integration suite mocks SOAP in-process by swapping
the \Magento\Framework\Webapi\Soap\ClientFactory DI binding for a
RecordingSoapClient that returns canned responses from
Test/Integration/_files/soap_responses/*.php
(see Test/Integration/IntegrationTestCase.php::installSoapMock()). That swap
works only because PHPUnit shares the process with Magento; it's unavailable
to a separate php-fpm request.
Why nothing is needed yet. The only current E2E test loads the home page, which makes zero SOAP calls (tax lookup happens at cart/checkout). So the pipeline is proven end to end without any server-side mock.
The intended approach (lands with the first checkout E2E test, in the "E2E
test coverage" ticket): a small dev-only Magento module, enabled only in the
E2E install, that — when a test-mode config flag is set — rebinds ClientFactory
to a file-backed mock reusing the existing
Test/Integration/_files/soap_responses/ fixtures. Tests assert on UI
outcomes (e.g. the tax shown at checkout), so the first pass needs neither
per-test response injection nor a call-recording endpoint — keeping it to a
tiny di.xml preference rather than a REST-driven support module. The placeholder
fixture Test/E2E/fixtures/soap-mock.ts records this and will host the helper
surface when it lands.
Feature passes (setup/teardown projects)¶
Several features are off in the seeded store, because that is how a real installation ships and what the default-state specs assert. Each one gets a Playwright project that switches it on, a project holding its specs, and a teardown project that switches it back:
| Pass | Switches on | Specs |
|---|---|---|
rest-setup → checkout-rest → rest-teardown |
V3 REST | the US checkout journeys, re-run over v3 |
exemptions-on-* |
Exemption certificates | specs/exemptions-on/ |
self-service-on-* |
Exemption certificates, and customer self-service for the Wholesale group | specs/self-service-on/ |
colorado-on-* |
Colorado Retail Delivery Fee | specs/colorado-on/ |
canada-on-* |
Canadian tax (and V3 REST, which it requires) | specs/canada-on/ |
The self-service pass signs in as trusted-customer@example.com, a seeded
Wholesale-group customer with no certificate of its own, so it can add, switch
and remove certificates without disturbing the exempt customer the other
exemption specs rely on. Its certificates are filed under the run's identity
with a -trusted suffix, and make e2e-cleanup-certificates removes them with
the rest.
A teardown project runs even when the specs it guards fail, which an
afterEach does not when a run is killed — and a feature left switched on
fails a neighbouring spec for reasons invisible in its own code. The passes are
chained (dependencies) so they never interleave, and each directory is listed
in the chromium project's testIgnore so its specs do not also run against
the seeded default.
Canadian tax needs the account, not just the setting. Canada is an add-on TaxCloud enables per account.
canada-on.setup.tsasserts it with the Check Canada Access button before any journey runs, so an account without Canada fails once, clearly, instead of as a golden-value mismatch at checkout. If that is the failure you are looking at, ask TaxCloud support to enable Canadian tax for the account inTAXCLOUD_API_ID/TAXCLOUD_API_KEY.
Writing specs that do not flake¶
Most "flaky" failures here have had a specific, findable cause. Three patterns account for nearly all of them, and each has a rule:
Duplicate ids behind Knockout templates. Luma renders an authentication
popup carrying a second #customer-email, #pass and #send2, and its form
also has id="login-form". The server sends one of each; the duplicate appears
only once KO hydrates the popup. A page-wide id therefore resolves one element
or two depending on timing, and Playwright refuses to guess — "strict mode
violation", on the slower runner, in a spec that changed months ago. Scope
every storefront locator to a container the popup is not in:
.login-container form#login-form, #customer-email-fieldset,
#co-shipping-form.
Overlays that swallow clicks. The admin puts masks and modal backdrops
(.loading-mask, .admin__data-grid-loading-mask, .modals-overlay,
.vex-overlay) in front of the page while it works. A click scheduled under one
is intercepted and retried until the action times out, reported as a button that
would not respond. Call waitForOverlaysToClear(page) from
pages/admin/overlays.ts before clicking in the admin.
Waiting on a clock instead of on the page. waitForTimeout is either
wasted time or, on a loaded runner, too short. Wait for the thing itself — the
searched grid row, the success message, the totals block. The exceptions are
waits on TaxCloud's own asynchronous processing, which no page state reflects;
those are commented where they appear.
Also: if a spec needs a store setting, set it. Do not rely on what another spec left behind — a feature pass switches things on and its teardown switches them back, so anything inherited is a coincidence of ordering.
Test data¶
E2E reuses the same programmatic seed as integration
(scripts/seed-test-data.php) — there are no SQL-dump fixtures in this repo. The
seed already provides everything a browser checkout needs:
- an enabled, in-stock, catalog-visible Test Product ($10) in a Test Category,
- a configurable product with two TIC-distinct variants,
- an active payment method (Check / Money Order) and shipping method (Flat Rate),
- the admin user (
admin/1234567a), - a restricted admin user (
tax-no-diagnostics/1234567a) whose role can edit tax settings and view orders but not export diagnostics, - TaxCloud config + ship-from origin,
- a second website/group/store view (code
second) with the same catalog and TaxCloud disabled at store scope; store codes are in URLs (web/url/use_store= 1), so the stores are browsable side by side at/default/...and/second/...on the same base URL. Themultistore-second-store-no-taxspec checks out on/second/and asserts the customer sees no TaxCloud tax there, while the A.1 checkout spec pins the taxed default-store totals — together they prove per-store scoping through real URL-based store resolution.
E2E-specific data that the smoke test doesn't need (e.g. a registered customer
account for login flows) is deferred: it'll be added as an optional
scripts/seed-e2e-data.php + install flag when the first test that needs it
lands. Do not fork a parallel seed — extend the shared one.
CI¶
E2E runs as the e2e job in the unified pipeline
.github/workflows/test.yml, alongside
unit-tests, integration, lint-code, and security-scan — the same single
workflow that gates a release.
Triggers: like the integration job, the e2e job runs only on
workflow_dispatch (with an optional magento_versions input to subset the
matrix) and push of v* tags. No pull_request, no branch pushes
(only unit-tests + lint + security run on those).
Matrix: mirrors the integration matrix exactly — community + enterprise
across 2.4.7-p10 / 2.4.8-p5 / 2.4.9 (PHP 8.2 / 8.3 / 8.5). Enterprise rows
are auto-skipped (not failed) when Marketplace keys are absent, same as
integration.
Secrets: the same five as integration (TAXCLOUD_API_ID, TAXCLOUD_API_KEY,
TAXCLOUD_API_V3_KEY, MAGENTO_PUBLIC_KEY, MAGENTO_PRIVATE_KEY). No new
secrets beyond those.
Adding a Magento version to the matrix¶
Edit the matrix.include list under the e2e job in test.yml (keep it in step
with the integration job's matrix):
include:
- magento-edition: community
magento-version: '2.4.9'
php-version: '8.5'
# ... add a row here (and the matching enterprise row)
The DB/search-engine versions are derived from the Magento version automatically
by scripts/install-magento.sh. To run a subset on a manual dispatch, use the
Run workflow form's magento_versions field (e.g. 2.4.9).
Debugging a failed CI run¶
Artifacts are uploaded on failure (traces, video, screenshots, the HTML report, and Docker logs):
- Open the failed run in the Actions tab → Artifacts → download
playwright-report-<edition>-<version>-php<x.y>. - Unzip it. To open the interactive trace viewer locally:
cd Test/E2E
npx playwright show-trace /path/to/unzipped/test-results/<test>/trace.zip
# or open the HTML report (which links every trace/video/screenshot):
npx playwright show-report /path/to/unzipped/playwright-report
- The
debug-e2e-...artifact hasdocker compose logs(nginx + php-fpm + DB) for serving/install failures.
Traces/videos/screenshots are retain-on-failure / only-on-failure, so
green runs produce no artifacts and stay fast. make e2e-trace opens the trace
for your most recent local failure.
Layout¶
Test/E2E/
package.json # @playwright/test pinned; engines.node >= 20
package-lock.json
playwright.config.ts # baseURL from MAGENTO_BASE_URL; trace/video/screenshot
# on failure; 1 worker (shared DB); chromium only
docker/
default.conf # HTTP nginx vhost (includes Magento's nginx.conf)
fixtures/
auth.ts # scaffold — logged-in customer/admin helpers (deferred)
soap-mock.ts # documents the deferred server-side SOAP strategy
pages/
admin/ # AdminLoginPage, TaxConfigPage, AdminOrderPage, …
storefront/ # HomePage, ProductPage, CheckoutPage
specs/
smoke/ # the pipeline smoke test
checkout/ # guest and signed-in journeys
admin/ # admin-side journeys
<feature>-on.setup.ts # feature passes, with their .teardown.ts
<feature>-on/ # the specs that pass guards
docs/ # screenshot generators (make docs-screenshots)
Page objects keep selectors out of specs: locators in the constructor,
navigation + assertions as methods. Follow HomePage when adding new ones.