Choosing the right locator strategy is crucial for creating maintainable, reliable tests. This guide covers proven strategies used in the project, from basic to advanced techniques.
Based on the project’s notes.txt and actual implementation, follow this priority order:
1
1. Data-test attributes (Highest Priority)
Most reliable and maintainable
# From login.py:7-9self.username_input = page.locator('[data-test="username"]')self.password_input = page.locator('[data-test="password"]')self.login_button = page.locator('[data-test="login-button"]')
Data-test attributes are specifically designed for testing and won’t change with UI updates.
2
2. IDs
Good for unique elements
# From cart_page.py:8self.checkout_button = page.locator("#checkout")# From test_login.py:16login_button = page.locator("input#login-button")
Combine element type with ID for more specific selection.
3
3. Playwright's built-in methods
Semantic and accessible
# From test_login.py:10username_input = page.get_by_placeholder("Username")password_input = page.get_by_placeholder("Password")# From test_login.py:19page.get_by_test_id("title").is_visible# From test_login.py:34error_message = page.get_by_text("Epic sadface: Username and password do not match")
4
4. CSS classes (Use with caution)
May break with CSS changes
# From cart_page.py:7self.cart_items = page.locator(".cart_item")# From cart_page.py:12self.page.locator(".shopping_cart_link").click()
CSS classes often change during UI refactoring. Use only when no better option exists.
Playwright provides semantic locators that align with how users interact with your application:
# Best for accessible elementspage.get_by_role("button", name="Login")page.get_by_role("textbox", name="Username")page.get_by_role("link", name="Checkout")
# From test_functionalities.py:34cart_page.cart_badge.locator(".shopping_cart_badge")# More examplespage.locator(".product-grid").locator("button", has_text="Add to cart")page.get_by_role("list").get_by_role("listitem").first
# From test_login.py:16 - combines element type + IDlogin_button = page.locator("input#login-button")# More examplespage.locator('button[type="submit"]')page.locator('input[name="username"][type="text"]')
# Get specific item from listpage.locator(".cart_item").nth(0) # First itempage.locator(".cart_item").first # Same as nth(0)page.locator(".cart_item").last # Last item
# From test_login.py:19assert page.get_by_test_id("title").is_visible# From test_login.py:35assert error_message.is_visible# From test_login.py:55assert login_button.is_visible()
from playwright.sync_api import expect# From test_functionalities.py:24expect(cart_page.cart_badge).to_contain_text("2")# From test_functionalities.py:34expect(cart_page.cart_badge.locator(".shopping_cart_badge")).to_be_hidden()# From test_assertions.py:14-15expect(page.locator('[data-test="add-to-cart-sauce-labs-backpack"]')).to_have_css("color", "rgb(19, 35, 34)")expect(page.locator('[data-test="add-to-cart-sauce-labs-backpack"]')).to_have_css("background-color", "rgb(255, 255, 255)")
The project consistently uses data-test attributes for critical elements:
# login.py uses data-test for all inputspage.locator('[data-test="username"]')page.locator('[data-test="password"]')page.locator('[data-test="login-button"]')# Also in checkout.py and cart_page.pypage.locator('[data-test="checkout"]')page.locator('[data-test="shopping-cart-link"]')
This makes tests resilient to CSS and HTML changes.
Mix approaches when needed
The project uses different strategies appropriately:
data-test for critical user actions (login, checkout)
IDs for unique elements (#checkout, #login-button)
# Check if locator matches elementsprint(page.locator(".cart_item").count())# Get element textprint(page.locator('[data-test="title"]').text_content())# Verify attributesprint(page.locator("#checkout").get_attribute("id"))