← Back

Playwright Page Navigation

How Playwright handles page navigation, why navigation becomes easier with web-first synchronization, and how it compares with Selenium.

1. What is Page Navigation?

Page navigation means moving the browser from one URL or application state to another.

Direct URL

Open a page directly using page.goto().

Link Navigation

Click a link or button and allow the application to navigate.

Application Redirect

Login, checkout, payment, and other workflows may redirect automatically.

Test starts
→
Navigate
→
Wait for application state
→
Validate destination
2. How Playwright Handles Navigation

Playwright provides navigation APIs and automatic synchronization around browser actions. The test can perform an action and then validate the resulting application state.

Click / Goto
→
Browser Navigation
→
Application Ready
→
Assertion
Important The goal is not to manually sleep for a fixed number of seconds. The goal is to synchronize with the application's actual state.
3. Basic Navigation with page.goto()

The simplest way to navigate to a URL is page.goto().

Python Playwright
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()

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

    print(page.url)

    browser.close()
Real-world usage This is commonly used when starting a test from a known application entry point such as login, dashboard, product listing, or checkout.
4. Navigation Through Link Click

Most real applications navigate because the user clicks a link, button, menu item, or other interactive element.

Python Playwright
page.get_by_role("link", name="Products").click()

print(page.url)

Prefer semantic locators such as:

5. Browser Navigation Controls

Playwright also supports browser-style navigation operations.

Back, Forward, Reload
page.goto("https://example.com/products")

page.go_back()

page.go_forward()

page.reload()

print(page.url)

go_back()

Moves the page to the previous history entry.

go_forward()

Moves to the next browser history entry.

reload()

Reloads the current page.

6. Validate Navigation with Web-First Assertions

After navigation, the test should validate the resulting state. Playwright's web-first assertions automatically wait and retry until the expected condition is satisfied or the assertion times out.

Example: Validate URL After Login

Python Playwright
from playwright.sync_api import expect

page.get_by_label("Email").fill("user@example.com")
page.get_by_label("Password").fill("secret")
page.get_by_role("button", name="Sign in").click()

expect(page).to_have_url(r".*/dashboard")

❌ Avoid

# Read the URL immediately assert "/dashboard" in page.url

This can make synchronization the test's responsibility. If navigation is still in progress, the check may happen before the expected URL is available.

✅ Preferred

from playwright.sync_api import expect expect(page).to_have_url(r".*/dashboard")

Playwright waits and retries the assertion until the URL matches or the assertion timeout is reached.

Validate Page Content After Navigation

❌ Avoid

assert page.locator("h1").is_visible()

✅ Preferred

expect( page.get_by_role("heading", name="Dashboard") ).to_be_visible()
Key idea Navigation and validation should be connected to observable application state rather than fixed timing assumptions.
7. Real-Time Example — Login Redirect

Consider an enterprise application where a successful login redirects the user from:

/login
→
Authentication
→
/dashboard
Complete Test
from playwright.sync_api import expect

def test_successful_login(page):
    page.goto("https://example.test/login")

    page.get_by_label("Email").fill("test@example.com")
    page.get_by_label("Password").fill("secret")

    page.get_by_role("button", name="Sign in").click()

    expect(page).to_have_url(r".*/dashboard")
    expect(
        page.get_by_role("heading", name="Dashboard")
    ).to_be_visible()

Notice that there is no fixed sleep() between clicking Sign in and validating the dashboard.

8. Multiple Tabs and Pages

Modern applications often open reports, payment pages, documentation, or external systems in a new tab.

New Tab / Popup
with page.expect_popup() as popup_info:
    page.get_by_role("link", name="Open Report").click()

report_page = popup_info.value

expect(report_page).to_have_url(r".*/report")
Professional pattern Capture the new page while performing the action that opens it. This makes the relationship between the action and new tab explicit.
9. Real-Time Example — Business Report

Imagine an admin application where clicking Open Customer Report opens a report in another tab.

Playwright
from playwright.sync_api import expect

with page.expect_popup() as popup_info:
    page.get_by_role(
        "link",
        name="Open Customer Report"
    ).click()

report_page = popup_info.value

expect(report_page).to_have_title(
    "Customer Report"
)

expect(
    report_page.get_by_role(
        "heading",
        name="Customer Report"
    )
).to_be_visible()
10. Playwright vs Selenium — Navigation

Selenium and Playwright both automate browser navigation. The main difference is how much synchronization the test author typically has to manage.

Area Playwright Selenium
Direct navigation page.goto() driver.get()
Element interaction Locators include automatic waiting behavior Explicit synchronization is often used depending on the scenario
Assertions Web-first assertions such as expect() Assertions depend on the test framework and synchronization approach
New tabs expect_popup() Window/tab handles are commonly managed explicitly
Browser isolation BrowserContext provides isolated sessions Isolation is generally managed through driver/session setup
Synchronization style Strong web-first synchronization model Flexible model with explicit waits available
Important Selenium is still a widely used browser automation framework. The practical choice depends on the application's browser needs, existing automation stack, team expertise, CI environment, and required Playwright/Selenium capabilities.
11. Selenium Navigation Example
Selenium Python
from selenium import webdriver

driver = webdriver.Chrome()

driver.get("https://example.test/login")

driver.find_element(
    "id",
    "email"
).send_keys("test@example.com")

driver.find_element(
    "id",
    "password"
).send_keys("secret")

driver.find_element(
    "id",
    "login"
).click()

print(driver.current_url)

driver.quit()
12. Same Navigation Flow in Playwright
Playwright Python
from playwright.sync_api import expect

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

page.get_by_label("Email").fill("test@example.com")
page.get_by_label("Password").fill("secret")

page.get_by_role("button", name="Sign in").click()

expect(page).to_have_url(r".*/dashboard")

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

The key benefit is not simply fewer lines of code. The important difference is the synchronization model used around browser actions and assertions.

13. Why Playwright Navigation Can Be Simpler

Automatic Waiting

Playwright's locator actions wait for the required conditions before interacting with elements.

Web-First Assertions

Assertions such as to_have_url() and to_be_visible() retry until the condition is met or the timeout is reached.

Modern Page Handling

Multiple pages, tabs, contexts, and popup events are modeled directly by the Playwright API.

14. Navigation — Avoid Hard Waits

One of the most important navigation practices is avoiding fixed delays such as time.sleep() or page.wait_for_timeout() for normal synchronization.

❌ Avoid Hard Waits

import time page.get_by_role("button", name="Sign in").click() time.sleep(5) assert "/dashboard" in page.url

This waits five seconds regardless of whether the application is ready after one second or still needs more than five seconds.

page.wait_for_timeout(5000)

wait_for_timeout() can be useful while debugging, but fixed delays should not be the normal synchronization strategy for production tests.

✅ Preferred — Wait for Real State

from playwright.sync_api import expect page.get_by_role("button", name="Sign in").click() expect(page).to_have_url(r".*/dashboard")

The assertion waits for the expected URL instead of waiting for an arbitrary number of seconds.

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

This validates an observable application state.

Another Example — Wait for a Specific Response

When a particular network response is the meaningful synchronization point, wait for that response rather than using a fixed delay.

❌ Avoid

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

✅ Preferred

with page.expect_response( lambda response: "/api/customer" in response.url and response.request.method == "POST" ) as response_info: page.get_by_role( "button", name="Save" ).click() response = response_info.value assert response.ok
Professional rule Wait for the condition that actually proves the application is ready: URL, element state, page state, response, or another observable event. Do not guess how many seconds the application needs.
15. SPA Navigation

Modern applications such as React, Angular, and Vue applications may change the URL or screen without performing a traditional full page reload.

SPA Navigation Example
page.get_by_role("link", name="Orders").click()

expect(page).to_have_url(r".*/orders")

expect(
    page.get_by_role("heading", name="Orders")
).to_be_visible()
Key point Test the application state you care about rather than assuming that every navigation requires a full browser page reload.
16. Professional E-Commerce Navigation Example

Consider an e-commerce flow:

Home
→
Products
→
Product Details
→
Cart
→
Checkout
Professional Test
from playwright.sync_api import expect

def test_customer_can_reach_checkout(page):

    page.goto("https://shop.example.test")

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

    expect(page).to_have_url(r".*/products")

    page.get_by_role(
        "link",
        name="Laptop Pro"
    ).click()

    expect(page).to_have_url(r".*/products/.*")

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

    page.get_by_role(
        "link",
        name="Cart"
    ).click()

    expect(page).to_have_url(r".*/cart")

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

    expect(page).to_have_url(r".*/checkout")

    expect(
        page.get_by_role(
            "heading",
            name="Checkout"
        )
    ).to_be_visible()
17. BrowserContext and Navigation Isolation

Playwright BrowserContexts allow independent browser sessions. This is useful when different users need to navigate through the same application independently.

Two Independent Users
user1_context = browser.new_context()
user2_context = browser.new_context()

user1_page = user1_context.new_page()
user2_page = user2_context.new_page()

user1_page.goto("https://example.test/dashboard")
user2_page.goto("https://example.test/dashboard")

Each context has its own cookies, local storage, session state, and pages.

18. Real-Time Admin → Customer Workflow

A business application may require navigation through different roles or sessions.

Admin Login
→
Approve Customer
→
Customer Login
→
Customer Dashboard
Isolated Contexts
admin_context = browser.new_context()
customer_context = browser.new_context()

admin_page = admin_context.new_page()
customer_page = customer_context.new_page()

admin_page.goto("https://example.test/admin")

# Admin performs approval...

customer_page.goto("https://example.test/login")

# Customer performs independent login...
19. Selenium Is Not Obsolete

Playwright and Selenium solve browser automation problems using different APIs and synchronization approaches.

Consideration Playwright Selenium
Modern web applications Strong support for modern browser automation patterns Also widely used for modern browser automation
Existing Selenium framework Migration requires engineering effort Existing investment can be retained
Synchronization Web-first synchronization is a central design feature Explicit waits provide flexible synchronization
Team experience Requires Playwright knowledge Existing Selenium expertise may reduce adoption effort
Architecture BrowserContext, Page, Locator model WebDriver-based architecture
20. Playwright Navigation Advantages
Capability Practical Benefit
Web-first assertions Tests can wait for expected application state.
Automatic actionability checks Reduces the need for manual synchronization before many interactions.
BrowserContext Supports isolated browser sessions.
Popup handling Provides a direct event-based API for new pages.
Modern locators Encourages semantic and user-facing element selection.
Navigation assertions URL and title changes can be validated with retrying assertions.
21. Professional Navigation Best Practices
22. Complete Professional Example
Login → Products → Cart → Checkout
from playwright.sync_api import Page, expect


def test_customer_checkout_navigation(page: Page):

    # Arrange
    page.goto("https://shop.example.test/login")

    page.get_by_label("Email").fill("customer@example.com")
    page.get_by_label("Password").fill("secret")

    # Act
    page.get_by_role(
        "button",
        name="Sign in"
    ).click()

    # Assert
    expect(page).to_have_url(r".*/dashboard")

    # Navigate to products
    page.get_by_role(
        "link",
        name="Products"
    ).click()

    expect(page).to_have_url(r".*/products")

    # Open product
    page.get_by_role(
        "link",
        name="Laptop Pro"
    ).click()

    expect(page).to_have_url(r".*/products/.*")

    # Add product
    page.get_by_role(
        "button",
        name="Add to cart"
    ).click()

    # Open cart
    page.get_by_role(
        "link",
        name="Cart"
    ).click()

    expect(page).to_have_url(r".*/cart")

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

    expect(page).to_have_url(r".*/checkout")

    expect(
        page.get_by_role(
            "heading",
            name="Checkout"
        )
    ).to_be_visible()
What makes this robust? The test does not guess how long each navigation takes. Each important transition is followed by a meaningful assertion.
23. Navigation Checklist
24. Final Mental Model
Action
→
Navigation
→
Observable State
→
Web-First Assertion
→
Business Validation
Remember The professional Playwright approach is not:

Click → Sleep → Hope → Assert

It is:

Action → Wait for meaningful state → Assert → Continue

This approach makes navigation tests easier to understand and reduces synchronization code that is based only on elapsed time.