132 test executions across Chromium, Firefox, WebKit and mobile Chrome — sharded 4 ways in CI, with the merged HTML report published on every run.
A production-shaped UI automation framework, not a tutorial suite. It covers authentication, catalogue sorting, cart state and end-to-end checkout against a public e-commerce demo application.
📊 Open the latest test report →
| Capability | Where to look |
|---|---|
| Page Object Model with a shared base class | src/pages/ |
| Component objects for cross-page UI | src/pages/components/HeaderComponent.ts |
| Custom fixtures and dependency injection | src/fixtures/test-fixtures.ts |
| Data-driven negative testing | tests/auth/login.spec.ts |
| Test data builders | src/data/checkout.ts |
| Environment configuration | src/config/env.ts |
Tag-based suite selection (@smoke / @regression / @e2e) |
any spec file |
| Cross-browser + mobile projects | playwright.config.ts |
| CI sharding, report merging, Pages publishing | .github/workflows/ci.yml |
| Suite | Tests | Focus |
|---|---|---|
| Authentication | 7 | Valid sign-in, locked-out account, four rejected-credential cases, sign-out, direct-URL access control |
| Product catalogue | 6 | Item count, all four sort orders, set-preservation after re-sorting |
| Shopping cart | 7 | Badge counting, add/remove, state persistence across navigation, price consistency |
| Checkout | 7 | Full end-to-end order, total arithmetic, three required-field validations, cancel behaviour, post-order cart state |
| Configuration | 5 | Environment-variable resolution — regression guard for the CI defect below |
33 tests × 4 browser projects = 132 executions per run.
npm ci
npx playwright install
npm testnpm run test:smoke # tagged @smoke only — fast pipeline gate
npm run test:regression # full regression pass
npm run test:chromium # single browser
npm run test:headed # watch it run
npm run test:ui # Playwright UI mode for debugging
npm run report # open the HTML report
npm run typecheck # strict TypeScript, no test executionNo .env file is required — every setting has a working default. Copy .env.example to .env to point the suite at a different host.
src/
├── config/env.ts # environment resolution (dev / staging / prod)
├── data/ # typed test data and builders
│ ├── users.ts # accounts by intent, plus expected error copy
│ ├── products.ts # catalogue and sort options
│ └── checkout.ts # checkout detail builder
├── fixtures/test-fixtures.ts # page-object injection + authenticated session
└── pages/ # page objects
├── BasePage.ts # navigation, readiness, money parsing
├── LoginPage.ts
├── InventoryPage.ts
├── CartPage.ts
├── CheckoutPage.ts # three checkout steps
└── components/ # header/menu shared across pages
tests/
├── auth/ inventory/ cart/ checkout/
Page objects return locators; specs make the assertions. A page object that asserts internally produces failure messages pointing at a helper three files away. Keeping expect in the spec means a red test names the business rule that broke.
Money is compared in cents. Floating-point currency arithmetic makes 0.1 + 0.2 !== 0.3 surface as a spurious checkout failure. BasePage.parseMoneyToCents parses the rendered string once and every comparison is integer.
Prices are read from the page, never hardcoded. The catalogue data selects which product to use; the expected value comes from the application. A price change is then a content update, not a suite-wide failure.
Data-driven cases generate one test each. The four rejected-login scenarios are separate tests rather than a loop inside one test, so a regression names the exact input that broke instead of collapsing four scenarios into one red line.
Retries are diagnostic, not cosmetic. CI retries twice to absorb public-demo-site flakiness; anything that only passes on retry is reported as flaky rather than silently green.
Artefacts only on failure. Traces, screenshots and video are captured with retain-on-failure. Recording everything inflates CI storage without adding diagnostic value.
Teardown failures are not swallowed. The authenticated fixture resets cart state after each test, checking explicitly for a closed or signed-out page rather than wrapping the reset in a bare catch. A blanket catch hides a broken reset, which then surfaces as an unrelated test failing later — this actually happened while building the suite, and the explicit checks are the fix.
Two real issues surfaced while writing this suite:
-
"Reset App State" leaves stale buttons. The action clears the cart badge but the catalogue continues showing "Remove" until the page is reloaded — the UI and the cart state disagree. The framework reloads after reset (
HeaderComponent.resetAppState) so the defect cannot leak into the next test. -
The menu toggle's test id is unclickable.
data-test="open-menu"is on the icon<img>, which the real<button>overlays, so clicking the documented test id fails with "intercepts pointer events". Those two locators target the button by id, with a comment explaining the exception.
The very first CI run failed on all four shards while the identical suite passed on a developer machine — the useful kind of failure.
Cause. The workflow passes TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}. When that secret is not configured, GitHub interpolates an empty string rather than leaving the variable unset. The framework read it as process.env.TEST_USER_PASSWORD ?? 'secret_sauce', and ?? falls back only on null/undefined — never on ''. Every password went blank, so the app answered "Password is required" where the tests expected a credentials error.
Fix. envValue treats blank and whitespace-only values as unset, and every environment read now goes through it.
Guard. tests/config/env.spec.ts pins all five cases — unset, empty, whitespace, configured, and a value that legitimately contains spaces. Reproduce the original failure condition with:
TEST_USER_PASSWORD="" npm run test:chromiumThis is the argument for running the suite in CI rather than trusting a green local run: the environment difference was the bug.
typecheck ─→ test (4 parallel shards) ─→ merge reports ─→ publish to Pages
- Runs on push, pull request, nightly at 18:00 UTC, and on demand with a suite selector
- Each shard emits a
blobreport; a merge job stitches them into one HTML report - Blob reports upload even when tests fail — a failing run's report is the one worth reading
forbidOnlyfails the build on a straytest.only- In-progress runs are cancelled when a newer commit lands on the same branch
- The application under test is saucedemo.com, a public demo site published for automation practice. Its credentials are printed on its own sign-in page.
- All three environment entries resolve to that single host. The pattern is the point — swapping in real dev/staging hosts is a one-line change per entry, and no test or page object needs to know which environment it runs against.
- This repository contains no code, data, or credentials from any employer. Every line was written for this portfolio.
MIT