Skip to content

Repository files navigation

English | 繁體中文

Web E2E Automation Testing Framework

A general-purpose web end-to-end (E2E) testing framework built with Playwright, Pytest, and Allure, following the Page Object Model (POM). The framework core — BasePage, browser / context fixtures, layered configuration, and YAML-driven selectors / test data — is site-agnostic and reusable across projects; YouTube (www.youtube.com) serves as the demo target.

Demo

Live Allure report: https://dopiz.github.io/web-e2e-automation-testing-playwright/

Demo

The demo scenarios drive YouTube and verify that a video actually starts playing — the video's readyState must reach HAVE_CURRENT_DATA.

Test Case Expected
test_search_channel_and_play_video Search mrbeast, open the channel, play a video ✅ pass (smoke)
test_search_channel_and_play_video Search aespa, open the channel, play a video ⏭️ skipped — is_skip declared in test data
test_search_shows_video_results Search term playwright tutorial ✅ pass (regression)
test_search_shows_video_results Search term pytest fixtures ✅ pass (regression)

Tech Stack

Tool Version Purpose
uv latest Python package / venv manager
Pytest 9.0.2 Test framework
Playwright 1.58.0 Browser automation (chromium / firefox / webkit)
pytest-rerunfailures 16.1 Auto-retry flaky tests
allure-pytest 2.15.3 Emits Allure results, @allure.step, attachments
Allure latest Report generator & viewer (allure serve)
PyYAML 6.0.3 Config, selector & test-data loading
Ruff 0.15.7 Linter & formatter

Project Structure

The tree below shows the site-agnostic core (top) and the demo target (youtube/ subfolders). To automate a new site, add sibling folders under elements/, pages/, testdata/, and tests/ — the core is untouched.

.
├── .github/workflows/              # CI: run tests + publish Allure report to Pages, Claude PR review
├── pages/
│   ├── base_page.py                # BasePage: locator resolution, @step decorator, click_if_visible   ← CORE
│   └── youtube/                    # Demo POM (one Page Object per screen state)
│       ├── components/             # Reusable cross-page components (search_bar)
│       ├── base.py                 # YouTubeBasePage: config setup / component composition
│       ├── home.py  search.py  channel.py  video.py
├── elements/                       # YAML selectors (XPath / CSS), mirrors pages/ layout
│   └── youtube/                    # channel / home / search / search_bar / video .yaml
├── testdata/                       # Data-driven cases — one yaml per test class, one key per test
│   └── youtube/                    # search_and_play.yaml / search_results.yaml
├── tests/
│   ├── conftest.py                 # Fixture wiring: browser / context / page / data / auth_session   ← CORE
│   └── youtube/                    # test_search_and_play.py (smoke) / test_search_results.py
├── configuration/                  # Layered config (deep-merged)                                     ← CORE
│   ├── default.yaml                # default_timeout, viewport, responsive profile, URLs
│   └── staging.yaml                # Environment overrides
├── common/                         # Business-semantic constants (e.g. VideoReadyState)
├── utils/                          # Site-agnostic utilities                                           ← CORE
│   ├── helper.py                   # Cached YAML loaders (Config / Element / Data), singleton
│   ├── auth.py                     # AuthSession: login-once storage_state reuse across fresh contexts
│   ├── allure_report.py            # Allure integration (per-case parameter splitting)
│   ├── decorator.py                # `parametrize` shorthand for data-driven tests
│   └── singleton.py                # Singleton metaclass
├── conftest.py                     # Root pytest hooks: logging, Allure screenshots / HTML snapshots   ← CORE
├── pytest.ini                      # Default pytest options & markers
├── ruff.toml                       # Ruff lint / format config
├── .pre-commit-config.yaml         # Ruff pre-commit hooks
└── pyproject.toml                  # Dependencies (managed by uv, locked by uv.lock)

Design Highlights

Site-agnostic framework core

Everything under pages/base_page.py, configuration/, and utils/ (YAML loaders, AuthSession, Allure integration) carries no business semantics — it knows how to resolve selectors, build contexts, and collect artifacts, but nothing about YouTube. A new site plugs in by adding a folder per layer:

  • BasePage resolves selectors from YAML into Playwright Locators (which auto-wait on every action) and provides shared utilities (open, locator, click_if_visible, scroll).
  • Page Objects subclass a per-site base (e.g. YouTubeBasePage) that composes shared components (search bar) and exposes open() from config. One class per screen state; actions return the next Page Object for fluent chaining; assertions stay in tests.
  • Browser fixtures (tests/conftest.py) map --browser onto Playwright's engines — chromium / firefox / webkit — and an unknown name fails loudly with a usage error instead of silently falling back.

YAML-based element management

Selectors live in YAML under elements/, decoupled from page logic. When the UI changes, update the YAML — no Python touched. Selectors support {placeholder} templating, filled at call time via kwargs.

Because YouTube is an SPA, previous pages stay in the DOM as hidden nodes — so selectors are scoped to the active page container and filtered by :visible, and unions cover A/B-served layout variants:

# elements/youtube/channel.yaml
VIDEOS: 'ytd-browse:not([hidden]) a.ytLockupMetadataViewModelTitle[href^="/watch"]:visible, ytd-browse:not([hidden]) ytd-grid-video-renderer a#video-title:visible'

Environment-based configuration

Configuration lives in configuration/ and resolves through three layers (low to high):

  • default.yaml — the shared base. Parameters common to every environment (default locator timeout, viewport, responsive profile, URLs) are declared once and reused everywhere.
  • Per-environment overrides. --env=staging deep-merges staging.yaml onto the base, so each environment inherits the defaults and only re-declares what it changes.
  • TEST_* environment variables. A runtime channel overriding any value — letting CI inject secrets (e.g. account tokens) without committing them. Double underscore maps to nesting, e.g. TEST_YOUTUBE__ENTRY_URLyoutube.entry_url.

Mobile emulation is driven separately by --device (a Playwright device preset name); responsive or any unknown name falls back to the configured browser.responsive viewport (width / height / pixel ratio / user agent).

Page Object Model with auto-waiting locators

Every interaction goes through Playwright Locators, which auto-wait for the element to be actionable (visible, stable, enabled) before acting — no hardcoded sleeps, no wait-condition boilerplate:

  • locator(element_key, **kwargs) resolves a YAML selector into a Locator; clicks / fills auto-wait for actionability
  • expect(...) assertions in tests auto-retry until the condition holds or times out
  • click_if_visible handles genuinely optional elements (ad skip button, collapsed search bar) without failing when they never appear
  • page.wait_for_function asserts the true end state in the browser (video readyState)
youtube_home.open()
search_page = youtube_home.search_bar.search(keyword="mrbeast")
channel_page = search_page.go_to_channel()
video_page = channel_page.go_to_video(index=0)

Data-driven tests

Test data lives in testdata/{site}/*.yamlone file per test class, one key per test function. Each key holds a list of cases; every case carries its own description (used as the test id) plus optional is_skip / is_xfail flags (with skip_reason / xfail_reason), handled centrally by the data fixture.

# testdata/youtube/search_and_play.yaml
search_and_play:
  - keyword: "mrbeast"
    video_index: 3
    description: "Search MrBeast channel and play video"

A small parametrize decorator (utils/decorator.py) wraps the repetitive pytest.mark.parametrize boilerplate and feeds each case into the indirect data fixture:

test_data = DataHelper().read("youtube/search_and_play")

class TestYouTubeSearch:
    @parametrize(test_data["search_and_play"])
    def test_search_channel_and_play_video(self, data, youtube_home):
        ...  # `data` is one case dict

Tests that need no data simply skip the decorator and the data parameter. Each case's fields are also flattened into individual Allure parameters (utils/allure_report.py), so the report shows keyword / description instead of one truncated dict blob.

Login state reuse (AuthSession)

Function-scoped contexts give every test a clean browser state, which also wipes login state. Instead of sharing one context across tests (state leakage), utils/auth.py caches the product of login — Playwright's storage_state (cookies + localStorage) — and injects it into each fresh context via the logged_in_page fixture: login runs once per session, every test still gets an isolated context.

Expiry is handled in two layers: cookies nearing their expires timestamp trigger a proactive re-login, and an optional validate callback catches server-side revocation after injection (one re-login retry, then fail loudly). Services opt in by overriding the auth_session fixture with their own login flow — see the docstring in tests/conftest.py.

Automatic artifact collection

The root conftest.py hooks into pytest's reporting to capture screenshots (page.screenshot()) after every test and HTML snapshots (page.content()) on failure, both attached to the Allure report. Each Page Object action is logged as an Allure step via the @step decorator.

Playwright tracing

A trace is recorded for every test (screenshots + DOM snapshots + sources). On failure it is saved to artifacts/{test_name}.zip; on pass it is discarded to keep artifacts lean.

uv run playwright show-trace artifacts/<test_name>.zip

The Trace Viewer replays the whole test: an action timeline with screenshots, DevTools-style DOM snapshots at each step, network requests, console logs, and the source line each action maps back to.

Code quality & formatting

Ruff is the linter / formatter, wired into a pre-commit hook:

uv run pre-commit install    # one-time setup
uv run ruff check .          # lint
uv run ruff format .         # format

Setup

1. Install dependencies

Dependencies are declared in pyproject.toml and locked in uv.lock:

git clone git@github.com:Dopiz/web-e2e-automation-testing-playwright.git
cd web-e2e-automation-testing-playwright
uv sync

2. Install Playwright browsers

Playwright ships its own browser builds — no local browser or driver setup needed:

uv run playwright install

3. Run tests

uv run pytest                 # all tests
uv run pytest -m smoke        # only smoke tests

Test Options

Custom options defined via pytest_addoption:

Option Default Description
--env default Config environment merged from configuration/ (e.g. default, staging)
--browser chromium Browser engine: chromium / firefox / webkit. An unsupported name fails with a usage error listing the supported browsers.
--headless False Run in headless mode
--device None Playwright device preset name for mobile emulation (e.g. iPhone 12 Pro, Pixel 7). responsive or any unknown name falls back to the browser.responsive profile in config. Firefox does not support mobile emulation — the option is ignored there with a warning.
# Firefox in headless mode
uv run pytest --browser=firefox --headless

# Staging environment
uv run pytest --env=staging

# Emulate a specific mobile device
uv run pytest --device="Pixel 7"

# A single test case
uv run pytest tests/youtube/test_search_and_play.py::TestYouTubeSearch::test_search_channel_and_play_video

# By marker (strict-markers is enabled; smoke / regression / slow / flaky)
uv run pytest -m smoke

# Auto-retry flaky tests as an outer safety net
uv run pytest --reruns 2 --reruns-delay 1

Any config value can also be overridden at runtime via TEST_* environment variables; double underscore maps to nesting, e.g. TEST_YOUTUBE__ENTRY_URLyoutube.entry_url.

Allure Report

# allure-pytest writes results to allure-results/ (enabled by default in pytest.ini)
uv run pytest --alluredir=allure-results

# Generate & open the HTML report
allure serve allure-results

The report includes test results, test steps (each Page Object action via @step), per-case parameters (flattened from test data), screenshots (after every test), and HTML snapshots (on failure). Playwright trace zips for failed tests land in artifacts/ for local replay via playwright show-trace.

Testing Strategy

A full E2E strategy spans several layers — the ones shown in this demo are marked; the rest build on the same framework.

1. Critical-path smoke

A fast sanity check of the core user path — search, open a channel, play a video — marked smoke so CI can gate on it before the heavier suite. (example: test_search_channel_and_play_video)

2. End-to-end user journeys

Drive a real user flow across multiple pages and assert the true end state — not just that elements exist. (example: search → open channel → play video → assert the video reaches HAVE_CURRENT_DATA, i.e. it actually plays)

3. Data-driven equivalence partitioning

Design cases by input class (one representative per class) via testdata/*.yaml, cutting run time without losing coverage — e.g. search keywords spanning distinct content classes (mrbeast vs. playwright tutorial vs. pytest fixtures). Expected-failure and skip cases are declared in data, not code. (example: the aespa case marked is_skip with its reason in data)

4. Cross-browser & responsive coverage

The same suite runs on chromium / firefox / webkit and under mobile emulation via --device (Playwright device presets, with a configurable responsive fallback), so layout and behavior are verified across engines and viewports.

5. Flake control

Auto-waiting locators everywhere, SPA-aware selectors scoped to the active page container and filtered by :visible (so hidden leftovers of previous pages never match), click_if_visible for genuinely optional UI (ads), and pytest-rerunfailures as an outer safety net. On failure, screenshots + HTML snapshots + a full Playwright trace are captured automatically for triage.

6. Further directions

The same framework extends beyond the demo:

  • Authenticated flows — reuse login state across tests via AuthSession (utils/auth.py) instead of logging in per test.
  • Visual regression — the screenshot hook already captures per-test PNGs; diff them against baselines to catch visual regressions.
  • Accessibility / performance — inject axe-core or read performance timings inside a Page Object step and assert on the result.
  • Parallel execution — add pytest-xdist once the suite grows enough to warrant it (function-scoped contexts are already isolation-safe).

Example Test Cases

Tests are data-driven: one YAML file per test class, one key per test function, each key holding a list of cases.

Test Case Expected
test_search_channel_and_play_video Search mrbeast, open channel, play a video ✅ video reaches HAVE_CURRENT_DATA (smoke)
test_search_channel_and_play_video Search aespa, open channel, play a video ⏭️ skipped — is_skip declared in test data
test_search_shows_video_results Search term playwright tutorial ✅ URL carries the query, ≥ 5 results rendered
test_search_shows_video_results Search term pytest fixtures ✅ URL carries the query, ≥ 5 results rendered

CI/CD

Test & Allure Report (test-and-report.yml)

Triggered on push to main or manually via workflow_dispatch.

  1. Test — Sets up Python + uv, installs Playwright browsers, runs uv run pytest tests/youtube --headless --env=staging --browser=chromium --alluredir=allure-results, uploads allure-results
  2. Report — Generates the Allure report and deploys it to GitHub Pages

Claude Code Review (claude-code-review.yml)

Triggered on pull request (opened, synchronize, reopened).

  • Uses Claude Code Action to review PR changes
  • Posts inline comments on new issues, resolves fixed threads, minimizes outdated reviews
  • Submits a review (approve / request changes / comment)
  • Checks: Playwright best practices, POM patterns, Ruff code style, YAML config consistency

Required secret: ANTHROPIC_API_KEY (GITHUB_TOKEN is provided automatically by GitHub Actions)

About

A general-purpose web end-to-end (E2E) testing framework built with Playwright, Pytest, and Allure, following the Page Object Model (POM).

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages