← Back
PLAYWRIGHT TEST ARCHITECTURE

Test Structure & Robust Solutions

Learn how to organize Playwright tests from simple beginner scripts into scalable, maintainable and professional automation frameworks.

1. What Is Test Structure?

Test structure means how we organize test cases, test data, locators, reusable functions, setup, teardown and reporting.

A good structure becomes increasingly important when an automation project grows from a few tests to hundreds or thousands of tests.

Test Data
→
Test Setup
→
Test Action
→
Assertion
→
Report
Core idea: A robust test should clearly separate setup, action, validation and cleanup.
2. Beginner Test Structure

A beginner can start with a single Python test file.

test_login.py
from playwright.sync_api import sync_playwright, expect


def test_login():

    with sync_playwright() as p:

        browser = p.chromium.launch()

        page = browser.new_page()

        page.goto("https://example.com/login")

        page.get_by_label("Username").fill("testuser")

        page.get_by_label("Password").fill("Password123")

        page.get_by_role(
            "button",
            name="Login"
        ).click()

        expect(page).to_have_url(
            "https://example.com/dashboard"
        )

        expect(
            page.get_by_role(
                "heading",
                name="Dashboard"
            )
        ).to_be_visible()

        browser.close()
Good for learning, but not ideal for a large project. Browser creation, test actions, locators and assertions are all mixed together.
3. Why Large Test Suites Need Structure
1

Maintainability

A locator or workflow should ideally be updated in one reusable place instead of many test files.

2

Reusability

Login, navigation and common business operations can be reused.

3

Isolation

Tests should not unexpectedly depend on another test's execution order or state.

4

Debugging

When a test fails, the structure should make it easier to identify where and why it failed.

4. Professional Playwright Project Structure

A scalable Playwright Python project can separate tests, page objects, fixtures, configuration and test data.

playwright-project/
│
├── tests/
│   ├── test_login.py
│   ├── test_checkout.py
│   ├── test_customer.py
│   └── test_orders.py
│
├── pages/
│   ├── login_page.py
│   ├── dashboard_page.py
│   └── checkout_page.py
│
├── fixtures/
│   └── test_fixtures.py
│
├── data/
│   ├── users.json
│   └── products.json
│
├── utils/
│   └── helpers.py
│
├── conftest.py
├── pytest.ini
└── requirements.txt
Important: The exact folder structure can vary by project. The goal is separation of responsibility.
5. Separate Test From Page Implementation

Instead of putting every locator directly into every test, a Page Object can contain page-specific interactions.

login_page.py

Page Object
from playwright.sync_api import Page


class LoginPage:

    def __init__(self, page: Page):

        self.page = page

        self.username = page.get_by_label("Username")

        self.password = page.get_by_label("Password")

        self.login_button = page.get_by_role(
            "button",
            name="Login"
        )


    def open(self):

        self.page.goto(
            "https://example.com/login"
        )


    def login(self, username, password):

        self.username.fill(username)

        self.password.fill(password)

        self.login_button.click()

test_login.py

Test Layer
from playwright.sync_api import expect

from pages.login_page import LoginPage


def test_valid_login(page):

    login = LoginPage(page)

    login.open()

    login.login(
        "testuser",
        "Password123"
    )

    expect(page).to_have_url(
        "https://example.com/dashboard"
    )

    expect(
        page.get_by_role(
            "heading",
            name="Dashboard"
        )
    ).to_be_visible()
Separation of responsibility:

Page Object → knows how to interact with the page.
Test → knows what business behavior should be validated.
6. Playwright Fixtures Handle Test Setup

Playwright works well with pytest fixtures. A fixture can provide a browser page to a test while keeping setup outside the test itself.

conftest.py
import pytest
from playwright.sync_api import Page


@pytest.fixture
def logged_in_page(page: Page):

    page.goto(
        "https://example.com/login"
    )

    page.get_by_label(
        "Username"
    ).fill("testuser")

    page.get_by_label(
        "Password"
    ).fill("Password123")

    page.get_by_role(
        "button",
        name="Login"
    ).click()

    return page

Using the Fixture

test_dashboard.py
from playwright.sync_api import expect


def test_dashboard(logged_in_page):

    expect(
        logged_in_page.get_by_role(
            "heading",
            name="Dashboard"
        )
    ).to_be_visible()
The test no longer needs to know how the login setup happens. It receives a ready-to-use page through the fixture.
7. Test Lifecycle

A robust automation framework normally follows a predictable lifecycle.

Fixture
→
Browser
→
Context
→
Page
→
Test
→
Assertion

Playwright's pytest integration provides browser/page fixtures, while teams can define additional fixtures for application-specific setup.

8. Test Isolation - One of the Most Important Concepts

Test isolation means that one test should not unexpectedly depend on another test's data, execution order or browser state.

Example: If test_edit_customer() only works because test_create_customer() happened before it, the tests are coupled.

❌ Fragile - Tests Depend On Each Other

This approach assumes the previous test has already created the customer.

test_customer.py
from playwright.sync_api import expect


def test_create_customer(page):

    page.goto(
        "https://example.com/customers"
    )

    page.get_by_role(
        "button",
        name="Add Customer"
    ).click()

    page.get_by_label(
        "Name"
    ).fill("John Smith")

    page.get_by_label(
        "Email"
    ).fill("john@example.com")

    page.get_by_role(
        "button",
        name="Save"
    ).click()

    expect(
        page.get_by_text("John Smith")
    ).to_be_visible()


def test_edit_customer(page):

    # Depends on test_create_customer()
    # having already created John Smith.

    page.goto(
        "https://example.com/customers"
    )

    page.get_by_text(
        "John Smith"
    ).click()

    page.get_by_label(
        "Name"
    ).fill("John Updated")

    page.get_by_role(
        "button",
        name="Save"
    ).click()

    expect(
        page.get_by_text("John Updated")
    ).to_be_visible()


def test_delete_customer(page):

    # Depends on the customer still existing.

    page.goto(
        "https://example.com/customers"
    )

    page.get_by_text(
        "John Updated"
    ).click()

    page.get_by_role(
        "button",
        name="Delete"
    ).click()

    expect(
        page.get_by_text(
            "Customer deleted"
        )
    ).to_be_visible()
Problem: If the create test fails, the edit and delete tests may fail even when their own implementation is correct.

✓ Robust - Each Test Controls Its State

Each test creates the data required for its own scenario.

test_customer.py
from playwright.sync_api import expect


def create_customer(page, name, email):

    page.goto(
        "https://example.com/customers"
    )

    page.get_by_role(
        "button",
        name="Add Customer"
    ).click()

    page.get_by_label(
        "Name"
    ).fill(name)

    page.get_by_label(
        "Email"
    ).fill(email)

    page.get_by_role(
        "button",
        name="Save"
    ).click()


def test_create_customer(page):

    create_customer(
        page,
        "Create User",
        "create@example.com"
    )

    expect(
        page.get_by_text("Create User")
    ).to_be_visible()


def test_edit_customer(page):

    create_customer(
        page,
        "Edit User",
        "edit@example.com"
    )

    page.get_by_text(
        "Edit User"
    ).click()

    page.get_by_label(
        "Name"
    ).fill("Updated User")

    page.get_by_role(
        "button",
        name="Save"
    ).click()

    expect(
        page.get_by_text("Updated User")
    ).to_be_visible()


def test_delete_customer(page):

    create_customer(
        page,
        "Delete User",
        "delete@example.com"
    )

    page.get_by_text(
        "Delete User"
    ).click()

    page.get_by_role(
        "button",
        name="Delete"
    ).click()

    expect(
        page.get_by_text(
            "Customer deleted"
        )
    ).to_be_visible()
Benefit: Each test creates the state it needs and validates its own business behavior.

Isolation With Unique Test Data

When tests can execute in parallel, using the same customer name or email can create conflicts. Generate unique test data where the application requires uniqueness.

Unique Test Data
from uuid import uuid4


def generate_customer():

    unique_id = uuid4().hex[:8]

    return {
        "name": f"Test User {unique_id}",
        "email": f"user_{unique_id}@example.com"
    }


def test_create_customer(page):

    customer = generate_customer()

    # Use customer["name"]
    # Use customer["email"]

    page.goto(
        "https://example.com/customers"
    )

    page.get_by_role(
        "button",
        name="Add Customer"
    ).click()

    page.get_by_label(
        "Name"
    ).fill(customer["name"])

    page.get_by_label(
        "Email"
    ).fill(customer["email"])

    page.get_by_role(
        "button",
        name="Save"
    ).click()

    expect(
        page.get_by_text(customer["name"])
    ).to_be_visible()

Another Isolation Problem - Shared Browser State

Do Not Share Mutable State Accidentally
# Avoid designing tests around a global
# page object shared by unrelated tests.

current_page = None


def test_admin(page):

    global current_page

    current_page = page

    page.goto(
        "https://example.com/admin"
    )


def test_customer(page):

    # Fragile:
    # assumes state from another test.

    page.goto(
        "https://example.com/customer"
    )
Robust principle: Prefer Playwright's isolated test/page fixtures and application-specific fixtures rather than global mutable browser state.
9. Test Data Should Be Separated

Hard-coding large amounts of test data directly inside tests can make maintenance difficult.

users.json

Test Data
{
    "valid_user": {
        "username": "testuser",
        "password": "Password123"
    },
    "admin_user": {
        "username": "adminuser",
        "password": "AdminPassword123"
    }
}

Reading the Data

Python
import json


with open("data/users.json") as file:

    users = json.load(file)


user = users["valid_user"]

username = user["username"]

password = user["password"]
Never commit real production passwords or secrets into source control. Use environment variables or suitable secret-management mechanisms for sensitive credentials.
10. Test Naming Should Describe Business Behavior

Test names should explain what behavior is being validated.

Weak Name Better Name Why
test_login test_valid_user_can_login Describes the expected behavior.
test_checkout test_customer_can_complete_checkout Describes the business scenario.
test_error test_invalid_password_shows_error Identifies the specific condition.
test_customer test_admin_can_approve_customer Defines actor and action.
11. Arrange → Act → Assert

A clean test can often be organized into three logical stages.

ARRANGE
Prepare
→
ACT
Perform
→
ASSERT
Validate
Arrange → Act → Assert
def test_customer_can_place_order(page):

    # ARRANGE
    page.goto(
        "https://example.com/products"
    )

    product = page.get_by_text("Laptop")

    # ACT
    product.click()

    page.get_by_role(
        "button",
        name="Add to Cart"
    ).click()

    page.get_by_role(
        "button",
        name="Checkout"
    ).click()

    # ASSERT
    expect(
        page.get_by_role(
            "heading",
            name="Order Confirmation"
        )
    ).to_be_visible()
12. Avoid Overloading Tests

A common automation mistake is creating one very large test that validates many unrelated business workflows.

❌ Avoid - One Huge Test

This test has too many responsibilities. If step 7 fails, it can become difficult to determine the actual failure boundary.

test_everything.py
def test_everything(page):

    # Login
    page.goto(
        "https://example.com/login"
    )

    page.get_by_label(
        "Username"
    ).fill("testuser")

    page.get_by_label(
        "Password"
    ).fill("Password123")

    page.get_by_role(
        "button",
        name="Login"
    ).click()


    # Create customer
    page.get_by_role(
        "link",
        name="Customers"
    ).click()

    page.get_by_role(
        "button",
        name="Add Customer"
    ).click()

    page.get_by_label(
        "Name"
    ).fill("John Smith")

    page.get_by_label(
        "Email"
    ).fill("john@example.com")

    page.get_by_role(
        "button",
        name="Save"
    ).click()


    # Create product
    page.get_by_role(
        "link",
        name="Products"
    ).click()

    page.get_by_role(
        "button",
        name="Add Product"
    ).click()

    page.get_by_label(
        "Product Name"
    ).fill("Laptop")

    page.get_by_role(
        "button",
        name="Save"
    ).click()


    # Checkout
    page.get_by_role(
        "link",
        name="Shop"
    ).click()

    page.get_by_text(
        "Laptop"
    ).click()

    page.get_by_role(
        "button",
        name="Add to Cart"
    ).click()

    page.get_by_role(
        "button",
        name="Checkout"
    ).click()


    # Cancel order
    page.get_by_role(
        "button",
        name="Cancel Order"
    ).click()


    # Logout
    page.get_by_role(
        "button",
        name="Logout"
    ).click()
Problems:

• Too many responsibilities
• Harder failure diagnosis
• One failure can prevent later scenarios from running
• Difficult to reuse individual workflows
• More difficult to maintain

✓ Prefer - Focused Tests

Split unrelated business behaviors into separate tests.

test_customer.py
def test_admin_can_create_customer(page):

    login_as_admin(page)

    create_customer(
        page,
        "John Smith",
        "john@example.com"
    )

    expect(
        page.get_by_text("John Smith")
    ).to_be_visible()


def test_admin_can_create_product(page):

    login_as_admin(page)

    create_product(
        page,
        "Laptop"
    )

    expect(
        page.get_by_text("Laptop")
    ).to_be_visible()


def test_customer_can_checkout(page):

    login_as_customer(page)

    add_product_to_cart(
        page,
        "Laptop"
    )

    complete_checkout(page)

    expect(
        page.get_by_text(
            "Order Confirmation"
        )
    ).to_be_visible()


def test_customer_can_cancel_order(page):

    login_as_customer(page)

    create_test_order(page)

    cancel_order(page)

    expect(
        page.get_by_text(
            "Order Cancelled"
        )
    ).to_be_visible()
Benefit: Each test has a clear purpose and can be diagnosed, maintained and executed independently.

Reusable Workflow Functions

Focused tests do not mean duplicating every line of code. Common workflows can be extracted into reusable functions, fixtures or Page Objects.

Reusable Workflow
def login_as_admin(page):

    page.goto(
        "https://example.com/login"
    )

    page.get_by_label(
        "Username"
    ).fill("adminuser")

    page.get_by_label(
        "Password"
    ).fill("AdminPassword123")

    page.get_by_role(
        "button",
        name="Login"
    ).click()


def create_test_order(page):

    page.goto(
        "https://example.com/products"
    )

    page.get_by_text(
        "Laptop"
    ).click()

    page.get_by_role(
        "button",
        name="Add to Cart"
    ).click()

    page.get_by_role(
        "button",
        name="Checkout"
    ).click()

    page.get_by_role(
        "button",
        name="Place Order"
    ).click()
Key principle: Keep the test scenario focused, but move reusable implementation details into appropriate helpers, fixtures or Page Objects.
13. Use Playwright's Stable Locators

A good project structure cannot compensate for fragile locators.

❌ Fragile Locator

page.locator(
    "div.container:nth-child(2) button"
).click()

✓ Semantic Locator

page.get_by_role(
    "button",
    name="Submit Order"
).click()

Prefer user-facing roles and labels, and use dedicated test IDs when the application provides a stable test contract.

14. Assertions Belong to the Validation Layer

Page Objects can perform interactions, while tests should generally express the business-level validation.

checkout_page.py
class CheckoutPage:

    def __init__(self, page):

        self.page = page

        self.place_order_button = page.get_by_role(
            "button",
            name="Place Order"
        )


    def place_order(self):

        self.place_order_button.click()
test_checkout.py
def test_customer_can_place_order(page):

    checkout = CheckoutPage(page)

    checkout.place_order()

    expect(
        page.get_by_role(
            "heading",
            name="Order Confirmation"
        )
    ).to_be_visible()
The test reads almost like a business requirement: customer places order → order confirmation appears
15. Use Playwright's Web-First Assertions

Modern web applications often update asynchronously. Playwright's web-first assertions are designed to wait for the expected condition instead of checking the page only once.

❌ Avoid - Immediate Property Check

This checks the current value immediately. If the UI has not updated yet, the assertion can fail even though the application is still processing the action.

Fragile Assertion
def test_save_customer(page):

    page.get_by_role(
        "button",
        name="Save"
    ).click()

    message = page.locator(
        "#message"
    ).text_content()

    assert message == "Customer saved"
Problem: The application may need time to update #message. The direct property check does not provide the same web-first retry behavior as a Playwright locator assertion.

✓ Prefer - Web-First Assertion

Use Playwright's assertion API for dynamic UI conditions.

Robust Assertion
from playwright.sync_api import expect


def test_save_customer(page):

    page.get_by_role(
        "button",
        name="Save"
    ).click()

    expect(
        page.locator("#message")
    ).to_have_text(
        "Customer saved"
    )
Why? Playwright can wait and retry the locator assertion until the expected condition is satisfied or the assertion timeout is reached.

Visibility Assertion

to_be_visible()
expect(
    page.get_by_role(
        "heading",
        name="Dashboard"
    )
).to_be_visible()

Text Assertion

to_have_text()
expect(
    page.get_by_role(
        "heading",
        name="Order Confirmation"
    )
).to_have_text(
    "Order Confirmation"
)

URL Assertion

Page URL Assertion
expect(page).to_have_url(
    "https://example.com/dashboard"
)

Dynamic Text With Regular Expression

Flexible Assertion
import re
from playwright.sync_api import expect


expect(
    page.get_by_role(
        "status"
    )
).to_have_text(
    re.compile(
        r"Order \d+ created"
    )
)
Important: Web-first assertions do not mean every assertion should wait indefinitely. They still use an assertion timeout, and the expected condition must eventually become true.
16. Configuration Belongs Outside Individual Tests

Common settings should be centralized instead of repeated throughout every test.

pytest.ini
[pytest]

addopts =
    -v

testpaths =
    tests

Browser and environment-specific configuration can also be centralized through fixtures, command-line options and environment variables.

Avoid scattering values such as base URLs, credentials, timeouts and environment switches throughout individual test files.
17. Real-Time Example - Customer Approval Workflow

Consider an application where an admin approves a newly registered customer.

Create Customer
→
Login as Admin
→
Find Customer
→
Approve
→
Validate Status

customer_page.py

Customer Page Object
from playwright.sync_api import Page


class CustomerPage:

    def __init__(self, page: Page):

        self.page = page

        self.customer_search = page.get_by_placeholder(
            "Search customers"
        )

        self.approve_button = page.get_by_role(
            "button",
            name="Approve"
        )


    def open(self):

        self.page.goto(
            "https://example.com/admin/customers"
        )


    def search_customer(self, email):

        self.customer_search.fill(email)

        self.page.get_by_role(
            "button",
            name="Search"
        ).click()


    def approve_customer(self):

        self.approve_button.click()

test_customer_approval.py

Business Test
from playwright.sync_api import expect

from pages.customer_page import CustomerPage


def test_admin_can_approve_customer(page):

    customer = CustomerPage(page)

    # ARRANGE
    customer.open()

    # ACT
    customer.search_customer(
        "customer@example.com"
    )

    customer.approve_customer()

    # ASSERT
    expect(
        page.get_by_text("Approved")
    ).to_be_visible()
Notice how the test focuses on the business behavior, while the Page Object handles the UI implementation.
18. Tags and Test Categorization

Large projects often need to execute only a subset of tests: smoke tests, regression tests, checkout tests, API tests, and so on.

Test Markers
import pytest


@pytest.mark.smoke
def test_user_can_login(page):

    ...


@pytest.mark.regression
def test_customer_can_checkout(page):

    ...

Register the markers in pytest.ini.

pytest.ini
[pytest]

markers =
    smoke: critical smoke tests
    regression: regression tests
This lets a CI pipeline select appropriate test groups without modifying the test code.
19. Parallel Execution and Test Independence

A large test suite may benefit from parallel execution. Parallel execution works best when tests do not unexpectedly share mutable state.

Test A
Test B
Test C
Test D
→
Faster Feedback

With pytest, teams commonly use tools such as pytest-xdist when they need distributed or parallel test execution.

Parallel execution does not automatically make a poorly isolated suite reliable. Shared databases, shared users, fixed filenames and global state can create race conditions.
20. Robust Failure Handling

A robust test framework should provide enough information to diagnose failures quickly.

Screenshot

Capture visual state when a failure occurs.

Trace

Use Playwright tracing when detailed execution diagnostics are needed.

Logs

Record useful business and technical information.

Reports

Produce readable results for developers and CI.

The objective is not merely: "test failed."

The objective is: "which test failed, at what step, with what UI state, and why?"
21. Avoid Hard Waits

Hard-coded sleeps pause the test for a fixed amount of time. They do not express the actual condition the test is waiting for.

❌ Avoid - Hard Wait

A fixed sleep can be too short when the application is slow and unnecessarily long when the application is fast.

Fragile Synchronization
import time


def test_customer_save(page):

    page.goto(
        "https://example.com/customers"
    )

    page.get_by_role(
        "button",
        name="Save"
    ).click()

    # ❌ Fixed wait
    time.sleep(5)

    message = page.locator(
        "#message"
    ).text_content()

    assert message == "Customer saved"
Why avoid it?

• Adds unnecessary delay
• May still be insufficient on a slow environment
• Does not describe what the test is waiting for
• Makes failures harder to diagnose

✓ Prefer - Condition-Based Waiting

Express the actual condition that must become true.

Web-First Synchronization
from playwright.sync_api import expect


def test_customer_save(page):

    page.goto(
        "https://example.com/customers"
    )

    page.get_by_role(
        "button",
        name="Save"
    ).click()

    # ✓ Wait for the actual UI condition
    expect(
        page.locator("#message")
    ).to_have_text(
        "Customer saved"
    )
Why? The assertion describes the condition the test actually cares about. Playwright can wait and retry the assertion until it succeeds or the assertion timeout is reached.

Example - Wait for a Button to Become Enabled

Condition-Based Assertion
from playwright.sync_api import expect


def test_submit_button(page):

    page.goto(
        "https://example.com/form"
    )

    page.get_by_label(
        "Name"
    ).fill("John Smith")

    submit_button = page.get_by_role(
        "button",
        name="Submit"
    )

    expect(
        submit_button
    ).to_be_enabled()

    submit_button.click()

Example - Wait for a Loading Indicator to Disappear

Dynamic UI
from playwright.sync_api import expect


def test_customer_list_loads(page):

    page.goto(
        "https://example.com/customers"
    )

    loading = page.get_by_text(
        "Loading..."
    )

    customers = page.get_by_role(
        "row"
    )

    expect(
        loading
    ).to_be_hidden()

    expect(
        customers
    ).not_to_have_count(0)

Example - Wait for Navigation State Through URL Assertion

Navigation
from playwright.sync_api import expect


def test_login_redirect(page):

    page.goto(
        "https://example.com/login"
    )

    page.get_by_label(
        "Username"
    ).fill("testuser")

    page.get_by_label(
        "Password"
    ).fill("Password123")

    page.get_by_role(
        "button",
        name="Login"
    ).click()

    expect(page).to_have_url(
        "https://example.com/dashboard"
    )
Rule of thumb: Do not ask: "How many seconds should I wait?"

Ask: "What condition tells me the application is ready?"
22. Complete Professional Test Structure
Complete Example
from playwright.sync_api import expect

from pages.login_page import LoginPage
from pages.dashboard_page import DashboardPage


def test_valid_user_can_login(page):

    # =========================
    # ARRANGE
    # =========================

    login = LoginPage(page)

    dashboard = DashboardPage(page)

    login.open()


    # =========================
    # ACT
    # =========================

    login.login(
        "testuser",
        "Password123"
    )


    # =========================
    # ASSERT
    # =========================

    expect(
        dashboard.heading
    ).to_be_visible()

    expect(
        dashboard.heading
    ).to_have_text(
        "Dashboard"
    )


    # =========================
    # BUSINESS VALIDATION
    # =========================

    expect(
        dashboard.user_menu
    ).to_contain_text(
        "testuser"
    )

This structure makes the intent of the test immediately visible to another engineer.

23. Bad Structure vs Robust Structure
Problem Fragile Approach Robust Approach
Locators Repeated CSS/XPath everywhere Centralized reusable locators
Setup Repeated browser/login setup Fixtures
Test dependency Test B requires Test A Independent test state
Synchronization Hard sleeps Playwright actions and web-first assertions
Validation One large generic assertion Specific business assertions
Data Large hard-coded datasets Dedicated test data/configuration
Debugging Only console output Reports, screenshots and traces where appropriate
Execution Tests rely on ordering Tests designed for isolation and parallelism
24. Robust Test Design Checklist
Test has one clear business purpose.
Test name describes expected behavior.
Test does not unexpectedly depend on another test.
Setup is reusable through fixtures where appropriate.
Page-specific UI interactions are reusable.
Locators are stable and meaningful.
Playwright web-first assertions are preferred.
Hard-coded sleeps are avoided.
Test data is separated when useful.
Secrets are not committed to source control.
Failure diagnostics are available.
Tests can safely run independently.
25. Professional Automation Architecture
Test Cases
→
Fixtures
→
Page Objects
→
Playwright
→
Browser
Test Data
→
Environment
→
CI/CD
→
Reports

The key principle is separation of responsibility. Each layer should have a clear purpose and a small number of reasons to change.

26. Final Mental Model
Test
→
Fixture
→
Page Object
→
Locator
→
Action
→
Assertion
Remember:

Test = What should be validated?
Fixture = What setup does the test need?
Page Object = How does the UI work?
Locator = Which element should be used?
Action = What should the user do?
Assertion = What result should be true?
Professional mindset: Don't build automation around individual clicks. Build automation around reliable business behaviors, isolated test state, stable locators, reusable setup, meaningful assertions and useful diagnostics.