Browser
Understand browser engines, contexts, pages, frames, state, and devices as one coherent model.
Playwright PHP is easier to use when you stop thinking in terms of "find an element, click it, sleep, check later". The library is built around a small model:
- a
BrowserContextis one isolated browser session; - a
Pageis one tab inside that session; - a
Locatoris a live description of an element, not a captured element; - an assertion is a wait with a clear expected outcome.
Most reliable tests are just that model repeated: create an isolated session, open a page, act through locators, and assert the result a user would see.
The mental model
A browser test has two sides. The browser owns rendering, JavaScript, network, storage, events, popups, frames, and screenshots. Your PHP code owns orchestration: which session to create, which page to open, which action to perform, and which outcome proves the flow worked.
That separation matters. PHP does not "hold" a real DOM element. It sends commands to a browser process through Playwright. When a page changes, a stored DOM handle may become stale, but a locator can resolve again against the current page state.
Use this model as the default shape:
<?php
require __DIR__.'/vendor/autoload.php';
use Playwright\Playwright;
use function Playwright\Testing\expect;
$context = Playwright::chromium();
$page = $context->newPage();
$page->goto('https://example.com');
expect($page)->toHaveURL('https://example.com/');
expect($page->getByRole('heading'))->toHaveText('Example Domain');
$context->close();
Expected result: the browser opens a page, the assertions retry until the title state is true, and the script exits cleanly.
BrowserContext: one isolated user
Playwright::chromium() launches Chromium and returns a BrowserContext ready to use. The same static entry point exists for Firefox and WebKit.
$context = Playwright::chromium([
'headless' => true,
'context' => [
'viewport' => ['width' => 1280, 'height' => 720],
],
]);
Think of a context as a clean browser profile. Cookies, local storage, permissions, viewport, routes, downloads, video, and tracing are scoped to it.
Use a new context when a scenario needs a clean user. Reuse a context only when shared state is the point of the test.
Warning
Shared contexts make tests depend on order. That can hide bugs locally and create failures in CI. Start isolated, then optimize deliberately.
Browser: when you need more than one context
The static Playwright::chromium() shortcut returns a context because most scripts need exactly one isolated session. When you need the browser object itself, use PlaywrightFactory.
<?php
require __DIR__.'/vendor/autoload.php';
use Playwright\PlaywrightFactory;
$playwright = PlaywrightFactory::create();
$browser = $playwright->chromium()->withHeadless(true)->launch();
$buyer = $browser->newContext();
$admin = $browser->newContext();
$buyerPage = $buyer->newPage();
$adminPage = $admin->newPage();
$buyerPage->goto('https://shop.example.com');
$adminPage->goto('https://shop.example.com/admin');
$buyer->close();
$admin->close();
$browser->close();
$playwright->close();
Use this when the product flow involves several independent users: buyer and seller, admin and regular user, invited user and owner, or two accounts checking shared state.
The key rule: use pages for tabs, contexts for users, and browsers for process-level control.
Page: one tab with its own lifecycle
A Page represents one tab. It can navigate, run actions, evaluate JavaScript, listen to events, take screenshots, and create locators.
$page = $context->newPage();
$page->goto('https://example.com');
$page->getByRole('link', ['name' => 'More information'])->click();
In a simple test, one $page is enough. In a multi-page test, make ownership explicit:
$adminPage = $admin->newPage();
$checkoutPage = $buyer->newPage();
Names like $page2 or $newPage make failures harder to read. Browser tests are already asynchronous; do not add naming ambiguity.
Locator: a recipe for finding an element
A locator is lazy. It does not capture one element when you create it. It describes how to find the element when an action or assertion runs.
$submit = $page->getByRole('button', ['name' => 'Sign in']);
$submit->click();
Before the click, Playwright PHP waits for the target to be actionable. That usually means the element is attached, visible, enabled, stable, and able to receive the action. You should not add a sleep before a normal click.
Prefer semantic locators first:
getByRole()for buttons, links, headings, rows, and controls;getByLabel()for form fields;getByPlaceholder()when the placeholder is the best visible contract;getByText()for stable visible copy;getByTestId()when product markup needs an explicit testing contract.
Use CSS locators when the DOM structure is the product contract or when no user-facing locator exists. Do not make CSS the default for interactive UI.
Assertions: waits with intent
Assertions state the browser condition you need and retry until it becomes true or the timeout expires.
expect($page)->toHaveURL('https://app.example.test/dashboard');
expect($page->getByRole('heading', ['name' => 'Dashboard']))->toBeVisible();
This is different from checking once after a fixed delay. A sleep says "wait this many milliseconds". An assertion says "wait until the user-visible result is true".
That distinction is the core of reliable Playwright tests.
Safe default for a browser test
Start every browser test with this shape:
- Create a fresh context.
- Open one page.
- Navigate to the starting screen.
- Act through user-facing locators.
- Assert one visible outcome.
- Close the context.
Only add complexity when the product demands it: another context for another user, another page for a popup, a frame locator for an iframe, or network routing for backend control.
Common mistakes
Calling browser methods on a context. Playwright::chromium() returns a BrowserContext, not a browser builder. Use PlaywrightFactory when you need launch() or several contexts from one browser.
Using CSS for every locator. CSS often follows implementation detail. A role or label follows user intent and usually survives markup refactors better.
Adding sleeps for readiness. If the page needs to be ready, assert the ready state. If a request must finish, wait for that response. If an element must appear, assert it.
Reusing authentication accidentally. Stored state is useful, but it should be explicit. A test that passes only after another test logs in is not isolated.
Checking actions instead of outcomes. A click is not the result. The result is the heading, URL, toast, table row, file, request, or state change that proves the click mattered.
Verification checklist
Use this checklist when a test feels flaky:
- Does each scenario start from a known context state?
- Does each variable name show which user, page, or frame it controls?
- Do locators describe user-facing UI where possible?
- Does each wait express a real condition?
- Does the final assertion prove the user-visible behavior?
- Are debug settings such as headed mode or slow motion removed from the normal path?
Go next
- Next guide: Browsers and contexts
- Pages and frames: Pages and frames
- Choose locators: Locators
- Assert outcomes: Choosing assertions
- Copy a first task: Fill and submit a form