Browser Testing With Playwright, Selenium, Dusk, And Codeception
Browser tests drive a real browser against a running application. They can prove that routing, HTML, JavaScript, cookies, sessions, and browser-visible behavior work together, but they are slower and more operationally demanding than service or HTTP tests.
Use browser automation for journeys whose risk genuinely crosses the browser boundary. Keep calculations, validation matrices, and most error cases in faster tests.
Understand The Tool Boundaries
The tools in this lesson overlap, but they enter the problem from different ecosystems.
Playwright
Playwright is a browser-automation framework with first-class support for Chromium, Firefox, and WebKit. Its locator API, automatic waiting, isolated browser contexts, traces, screenshots, and videos make it a strong choice for end-to-end web testing.
Playwright tests are commonly written in TypeScript or JavaScript. A PHP application can still be the system under test: CI starts the PHP application and its dependencies, then Playwright drives the public URL.
Selenium
Selenium WebDriver is a mature browser-automation standard with language bindings and a broad infrastructure ecosystem. It is common in established cross-language test platforms, Selenium Grid installations, and repositories that already have WebDriver expertise.
Selenium is not automatically obsolete because a newer tool exists. Replacing a stable suite requires evidence that reliability, execution time, browser coverage, or maintenance will improve enough to repay migration cost.
Laravel Dusk
Laravel Dusk provides browser testing integrated with Laravel and ChromeDriver. It offers Laravel-aware helpers for application URLs, authentication, database setup, and browser assertions.
Dusk is a practical default when a Laravel project already uses it successfully. It is less attractive when the organization needs one browser stack shared across applications written in several languages or requires browser engines outside its supported workflow.
Codeception
Codeception is a PHP testing framework that can organize unit, functional, API, and acceptance tests. Browser acceptance tests commonly use its WebDriver module and can connect to Selenium-compatible infrastructure.
Its scenario-oriented PHP API may fit a team that wants browser tests in the same language and runner as other acceptance tests. Check the installed modules and configuration rather than assuming browser support is present by default.
Choose From Project Constraints
Start with the repository, not a popularity list. Record:
- which browser journeys already exist and whether they are reliable
- required browser engines and operating systems
- whether tests must be written in PHP or can use TypeScript
- framework integration needs
- CI images, browser installation, and display requirements
- parallel execution and test-data isolation needs
- debugging artifacts available after a failure
- team ownership and migration cost
A maintained Dusk suite should normally remain Dusk. A new framework-neutral application may reasonably choose Playwright. A company-wide Selenium Grid can make WebDriver the lowest-cost option. A PHP acceptance suite already using Codeception may extend that established stack.
Test A Narrow User Journey
A browser test should express a user-visible outcome:
import { test, expect } from '@playwright/test';
test('customer sees an order confirmation', async ({ page }) => {
await page.goto('/basket');
await page.getByRole('button', { name: 'Checkout' }).click();
await expect(
page.getByRole('heading', { name: 'Order confirmed' }),
).toBeVisible();
});
This test deliberately uses accessible role and name information. It does not reach into a controller, assert a database row directly, or depend on a generated CSS class.
The same journey still needs lower-level tests for pricing, validation, payment-state transitions, and failure branches. One browser test proves the critical wiring; it should not duplicate the entire business-rule matrix.
Prefer Stable Selectors
Use selectors based on user-visible semantics where possible:
- roles and accessible names
- labels associated with form controls
- visible text when wording is a stable product contract
- explicit test identifiers when no semantic selector is reliable
Avoid selectors coupled to presentation, such as .grid > div:nth-child(3) button, utility-class chains, or framework-generated IDs. A CSS redesign should not break a checkout test when user behavior is unchanged.
Test identifiers are acceptable when they are treated as a maintained testing interface. Name them by purpose, such as checkout-submit, rather than by color or position.
Wait For Conditions, Not Time
Modern interfaces update asynchronously. Fixed sleeps make suites both slow and unreliable:
wait 2 seconds -> maybe enough locally -> sometimes too short in CI
Wait for the condition that proves progress: a response completed, a button became enabled, a heading appeared, or navigation reached the expected URL. Playwright locators and assertions automatically wait for many actionable conditions. WebDriver-based suites need equivalent explicit waits where the framework does not provide them.
A timeout increase can hide a missing event or incorrect selector. Diagnose the application state before making every test wait longer.
Control Data And Isolation
Each test needs deterministic starting state. Suitable approaches include:
- create records through an approved test API
- load small fixtures or factories
- reset a dedicated test database
- allocate a unique tenant, user, or order identifier
- clean up external side effects
Do not point destructive browser tests at production. Do not share one mutable account across parallel tests. Time, randomness, mail, queues, payment gateways, and third-party APIs need controlled test behavior or isolated test doubles.
A test that passes only after another test created its data is not independent and will fail under filtering or parallel execution.
Make Failures Diagnosable
A useful CI failure preserves evidence:
- screenshot at failure
- browser trace or video when supported
- console errors
- failed network requests
- application and web-server logs
- the exact browser and viewport
- the seed or identifier for created test data
Upload artifacts only on failure or with a short retention period when storage is significant. Scrub secrets, personal data, tokens, and payment information from screenshots and traces.
Run Browser Tests Deliberately In CI
A browser job should:
- install the locked application and browser dependencies
- start isolated database and supporting services
- migrate and seed controlled test data
- start the application and wait for a readiness endpoint
- run the narrow browser suite
- preserve failure artifacts
- stop services and clean up data
Run fast unit and integration checks first. Browser tests may run on every pull request for a small stable suite, or in a separate required job when setup is heavier. Critical release journeys should not be silently optional.
Common Failure Patterns
- Rewriting an established suite without a measurable benefit.
- Testing every validation branch through the browser.
- Sharing mutable data between parallel workers.
- Using fixed sleeps instead of observable conditions.
- Selecting elements by fragile layout details.
- Treating a screenshot as the assertion instead of checking behavior.
- Keeping traces that contain credentials or customer data.
- Rerunning flaky tests until they pass without finding the cause.
What To Remember
Browser tests prove a narrow set of browser-visible integrations. Choose Playwright, Selenium, Dusk, or Codeception from repository and operational constraints, use stable semantic selectors, wait for real conditions, isolate data, and retain useful failure evidence. Keep most business logic in faster tests so the browser suite remains focused and trustworthy.
Before moving on, make sure you can explain why an existing Dusk suite may be preferable to a Playwright rewrite, why fixed sleeps create flakes, and which artifacts would make a CI-only browser failure diagnosable.
Practice
Task: Design A Reliable Checkout Browser Test
- a signed-in customer opens a basket containing one known product
- the customer submits checkout
- the interface waits for an asynchronous order response
- an
Order confirmedheading and order number appear
Design the test and tool decision. Include:
- whether to keep Dusk, migrate immediately, or run a bounded Playwright trial
- deterministic setup for the customer and basket
- stable selectors for checkout and confirmation
- the condition used instead of a fixed sleep
- lower-level cases that should not be duplicated in the browser
- CI startup/readiness requirements
- failure artifacts to preserve
- safeguards against leaking credentials or customer data
State one measurable result that would justify a wider Playwright migration.
Show solution
Create a unique test customer and basket through Laravel factories or a test-only setup endpoint. Use a generated order reference so parallel workers do not share state. Sign in through the suite's supported authentication helper rather than storing a real password in the test.
Select the submit control by accessible text or a maintained identifier such as checkout-submit. Assert the confirmation through the heading Order confirmed and a purpose-named order-number element. Wait for the heading or completed checkout response instead of sleeping for an arbitrary number of seconds.
Keep price calculations, validation boundaries, payment failures, and inventory rules in service or HTTP tests. The browser journey should prove that one successful checkout is wired through the rendered interface.
CI must start an isolated database and application, run migrations, seed the product, and wait for a readiness endpoint before launching the browser. On failure, retain a screenshot, browser console output, network failure details, application logs, browser version, and test order reference. Redact session tokens, payment details, email addresses, and other personal data from artifacts.
Expand the Playwright trial only if repeated runs show a measurable improvement, such as materially fewer flaky failures, required cross-browser defects being detected, or significantly better diagnosis time without losing existing journey coverage.