"Good automation doesn't break. It uses stable selectors and validates real bugs."
This project demostrates practical UI automation for the [SAUCEDEMO] (https://www.saucedemo.com) e-commerce site.
This is NOT about:
- 50+ test cases
- Showing off unnecessary framework knowledge
- Over-engineering with complex custom wrappers
This IS about:
- Stable selectors that don't break (data-test > id class)
- Independet tests that can run in any order
- Smart waits (no time.sleep()
, only expect()) - Bug-focused assertions (each assert detects a specific bug)
ui-testing-playwright/
βββ pages/
β βββ cart_page.py # Cart locators & actions
β βββ inventory_page.py # Inventory locators & actions
β βββ login_page.py # Login locators & actions
βββ tests/
β βββ conftest.py # Shared fixtures (POM initialization)
β βββ test_login.py # Authentication tests (3 tests)
β βββ test_products.py # Product catalog tests (3 tests)
β βββ test_cart.py # Shopping cart tests (4 tests)
βββ utils/
β βββ test_data.py # Test credentials (users)
βββ .gitignore
βββ pytest.ini
βββ requirements.txt
βββ README.md
Total: 10 focused tests covering critical user flows
Priority order:
# β
BEST: data-test attributes (semantic, stable)
page.locator('[data-test="login-button"]').click()
# β
GOOD: ID (unique, stable)
page.locator('#react-burger-menu-btn').click()
# β οΈ OK: Short CSS class (acceptable if stable)
page.locator('.shopping_cart_badge').text()
# β BAD: Long CSS chains (fragile)
page.locator('div.login-container > form > button').click()
# β WORSE: Text selectors (changes with i18n)
page.locator('text=Login').click()Real example from this project:
# β Fragile selector (breaks if CSS changes)
page.locator('.inventory_item .btn_inventory').click()
# β
Stable selector (semantic, won't break)
page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click()Never do this:
# β BAD: Arbitrary waits (slow, unreliable)
page.click('#login-button')
time.sleep(3) # Hope page loads in 3 seconds?
assert page.locator('.title').is_visible()Always do this:
# β
GOOD: Smart waits (fast, reliable)
page.click('[data-test="login-button"]')
expect(page.locator('[data-test="title"]')).to_be_visible()Playwright automatically waits for elements. Trust it.
Every test can run alone:
# β
GOOD: Uses fixture, gets fresh login state
def test_add_product_to_cart(logged_in_page: Page):
page = logged_in_page
# Test starts already logged in
page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click()
expect(page.locator('[data-test="shopping-cart-badge"]')).to_have_text('1')Why this matters:
- Tests can run in any order
- Can run single test for debugging
- Parallel execution works reliably
Each assertion validates a specific bug:
def test_login_with_valid_credentials(page: Page):
page.goto("https://www.saucedemo.com")
page.locator('[data-test="username"]').fill('standard_user')
page.locator('[data-test="password"]').fill('secret_sauce')
page.locator('[data-test="login-button"]').click()
# Assert 1: Redirected to correct page
# Bug caught: Wrong redirect (e.g., stays on login page)
expect(page).to_have_url(re.compile(r".*/inventory.html"))
# Assert 2: Products page title visible
# Bug caught: Page loads but content doesn't render
expect(page.locator('[data-test="title"]')).to_have_text('Products')Generic assertions don't help:
# β BAD: What bug does this catch?
assert True
# β BAD: Too vague
assert page.locator('.title').is_visible()
# β
GOOD: Specific, explains what breaks
expect(page.locator('[data-test="title"]')).to_have_text('Products')| Test | Purpose | Bug Detected |
|---|---|---|
| Valid credentials | Successful login redirects to inventory | Login doesn't redirect |
| Invalid credentials | Error message appears | No error feedback |
| Locked user | Locked user cannot login | Security bypass |
Key validations:
- β URL redirects correctly
- β Error messages display
- β User stays on login page when auth fails
| Test | Purpose | Bug Detected |
|---|---|---|
| Products display | Inventory page shows 6 products | Empty catalog |
| Add single product | Cart badge shows "1" | Badge doesn't update |
| Add multiple products | Cart badge shows "3" | Counter broken |
Key validations:
- β Product count is correct (detects missing items)
- β Cart badge appears and increments
- β Page title confirms correct page
| Test | Purpose | Bug Detected |
|---|---|---|
| Product appears in cart | Added item shows in cart | Data loss |
| Remove product | Item disappears from cart | Remove button broken |
| Empty cart | Removing all items clears badge | Badge persists |
| Cart persistence (advanced) | Cart survives logout/login | Session bug |
Key validations:
- β Items added to cart are visible
- β Badge disappears when cart empty
- β Cart state persists across sessions
- Python 3.8 or higher
- pip
- Playwright
- Clone repository:
git clone https://github.com/your-username/ui-testing-playwright.git
cd ui-testing-playwright- Create virtual environment:
python -m venv venv
# Windows
venv\Scripts\activate
# Linux/Mac
source venv/bin/activate- Install dependencies:
pip install -r requirements.txt
playwright install chromiumpytest tests/ -vpytest tests/test_login.py -vpytest tests/test_cart.py::test_product_appears_in_cart -vpytest tests/ -v --headedpytest tests/ -v --headed --slowmo=1000tests/test_login.py::test_login_with_valid_credentials PASSED
tests/test_login.py::test_login_with_invalid_credentials PASSED
tests/test_login.py::test_login_with_locked_user PASSED
tests/test_products.py::test_products_page_displays_correctly PASSED
tests/test_products.py::test_add_single_product_to_cart PASSED
tests/test_products.py::test_add_multiple_products_to_cart PASSED
tests/test_cart.py::test_product_appears_in_cart PASSED
tests/test_cart.py::test_remove_product_from_cart PASSED
tests/test_cart.py::test_cart_is_empty_after_removing_all_items PASSED
tests/test_cart.py::test_cart_persists_after_logout_login PASSED
======================== 10 passed in 15.23s ========================
- β Choosing stable selectors (prevents flaky tests)
- β Writing independent tests (parallel execution)
- β Bug-focused testing (each assert has purpose)
- β Avoiding common pitfalls (no sleep, no text selectors)
- β Playwright with Python
- β Pytest framework and fixtures
- β Test organization and structure
- β Async/await patterns (handled by Playwright)
- β Custom framework abstractions (over-engineering)
- β 50+ test cases (quality > quantity)
- β Advanced Playwright configurations (keep it simple)
This project has been refactored to implement a clean Page Object Model layer under the pages/ directory.
Why? Separating locators and action logic from test code prevents test fragility. If a locator changes on the site, we only fix it in the Page Class without modifying test assertions.
# β
Clean test interacting with Page Objects
def test_login_with_valid_credentials(page: Page, login_page: LoginPage, inventory_page: InventoryPage):
login_page.navigate()
login_page.login(USERS['standard']['username'], USERS['standard']['password'])
# Assert 1: Redirected to correct page
expect(page).to_have_url(re.compile(r".*/inventory.html"))
# Assert 2: Products page title visible
expect(inventory_page.title).to_have_text('Products')Why? This project focuses on selector quality and test design, not DevOps.
If you need CI/CD: See my other projects (TODO API, Postman Newman).
Why? Visual testing isn't the focus here.
If screenshot fails: Test fails. That's enough.
# β BAD: Breaks when CSS changes
page.locator('div.inventory > div:nth-child(1) > button').click()
# β
GOOD: Semantic, won't break
page.locator('[data-test="add-to-cart-sauce-labs-backpack"]').click()# β BAD: test_add_to_cart depends on test_login running first
def test_login():
# Login here
def test_add_to_cart():
# Assumes already logged in from test_login
# β
GOOD: Each test is independent
def test_add_to_cart(logged_in_page: Page):
# Gets fresh login state via fixture# β BAD: Slow and unreliable
page.click('#submit')
time.sleep(5)
assert page.locator('.success').is_visible()
# β
GOOD: Fast and reliable
page.click('#submit')
expect(page.locator('.success')).to_be_visible()π€ Chande De Vargas GitHub: https://github.com/ChandeDeVargas LinkedIn: https://www.linkedin.com/in/chande-de-vargas-b8a51838a/
This project is open source and available under the MIT License.
β If this project helps you write better UI tests, give it a star!