playwright-php/playwright
Playwright PHP brings browser automation to PHP with a Playwright-powered API for Chromium, Firefox, and WebKit. Automate pages, scrape content, take screenshots, generate PDFs, and write end-to-end tests using familiar PHP code.
Treat this as a PHP client for the Playwright Node driver, not a pure-PHP browser library. You need PHP 8.1+, Composer, and a Node.js runtime. Install the package and then install the browser binaries through the package’s documented install command (often a vendor/bin/playwright wrapper around playwright install):
composer require --dev playwright-php/playwright
vendor/bin/playwright install chromium
A minimal script establishes the core lifecycle: launch a browser, create a page, navigate, assert, capture, close.
use Playwright\Playwright;
$playwright = Playwright::create();
$browser = $playwright->chromium()->launch(['headless' => true]);
try {
$page = $browser->newPage();
$page->goto('https://example.com');
$page->getByRole('heading', ['name' => 'Example Domain'])->waitFor();
$page->screenshot()->save(__DIR__ . '/example.png');
} finally {
$browser->close();
}
Look first at:
Playwright factory and Browser, BrowserContext, Page, Locator, and assertion classes. The API intentionally mirrors upstream Playwright, so the official Playwright docs are your best reference for selector and waiting semantics.tests/ directory for exact constructor options, assertion names, and process-management conventions.The best first use case is a smoke test against a staging URL: navigate, assert a heading or login button, take a screenshot, and fail the command on a missing element. Once that works, expand into authenticated flows and CI integration.
Share the browser, isolate every test. Launch one browser per test suite, but create a fresh BrowserContext and Page per test. This keeps cookies, storage, and service workers isolated while avoiding repeated browser startup costs.
protected static Browser $browser;
protected BrowserContext $context;
protected Page $page;
public static function setUpBeforeClass(): void
{
self::$browser = Playwright::create()->chromium()->launch();
}
protected function setUp(): void
{
$this->context = self::$browser->newContext([
'baseURL' => 'http://localhost:8000',
'viewport' => ['width' => 1440, 'height' => 900],
]);
$this->page = $this->context->newPage();
}
protected function tearDown(): void
{
$this->context->close();
}
public static function tearDownAfterClass(): void
{
self::$browser->close();
}
Drive everything through locators. Prefer role, label, text, and data-testid selectors over brittle CSS paths. Playwright locators auto-wait and retry, which is the main defense against flaky tests.
$this->page->getByLabel('Email')->fill('[email protected]');
$this->page->getByLabel('Password')->fill('secret');
$this->page->getByRole('button', ['name' => 'Sign in'])->click();
$this->expect($this->page->getByRole('heading', ['name' => 'Dashboard']))->toBeVisible();
Use web-first assertions instead of manual sleeps. Assertions should poll the DOM. Do not write sleep(2) or manual retry loops around selectors; adjust timeouts or fix the underlying race instead.
Control the network for deterministic tests. Route interception is the cleanest way to remove third-party dependencies, simulate error states, or stub slow APIs.
$this->page->route('**/api/recommendations', fn ($route) => $route->fulfill([
'status' => 200,
'contentType' => 'application/json',
'body' => json_encode(['items' => []]),
]));
Reuse authentication state. Log in once in a setup step, save storageState to a file, and create authenticated contexts from it. This cuts minutes from suites with many logged-in tests. Keep one state file per role and regenerate it when auth changes.
Capture artifacts on failure. In tearDown(), check the test outcome and save a screenshot, trace, or video with a sanitized test name. In CI, upload these as build artifacts. This is often the difference between a five-minute diagnosis and a two-hour investigation.
if ($this->hasFailed()) {
$this->page->screenshot()->save(
__DIR__ . "/artifacts/{$this->name()}.png",
['fullPage' => true]
);
}
Create page objects for repeated flows. Keep selectors and multi-step journeys in tests/Browser/Pages or equivalent classes: LoginPage, CheckoutPage. Tests should read as business intent, not selector soup. For Laravel applications, pair this with RefreshDatabase, a seeded test database, and either a real php artisan serve process or your existing test HTTP server. Playwright should hit the running application over HTTP, not bootstrap it in-process.
Parallelize with separate contexts, not shared pages. Run tests in parallel via PHPUnit/Pest workers, each with its own browser context. Do not share a Page across processes, and avoid parallel tests that mutate the same user or storage-state file.
finally or teardown. A failed assertion or PHP fatal error can leave a Node process and headless browser running. Long-lived CI runners will accumulate zombies. Register a shutdown cleanup if your test bootstrap allows it.--no-sandbox launch args if running as a non-root user, and increase /dev/shm (--shm-size=1g) for Chromium stability.waitForLoadState assumptions. Single-page apps often render after load. Wait for a visible element or URL change; use waitForURL after navigation-triggering clicks.evaluate must be serializable. You cannot pass PHP closures into browser-side JavaScript. Keep browser logic as string expressions or small serializable payloads, and return plain arrays/scalars.var_dump. Run locally with headless => false and a small slowMo delay. Record traces for CI failures and inspect them in the Playwright trace viewer. Attach console and pageerror listeners early so browser errors appear in PHP output.data-testid attributes in the application where role/text selectors are ambiguous. This is cheaper than repeatedly patching tests.expect($locator)->toBeVisible() polls. A raw count() or isVisible() call is a one-shot check and will be flaky against dynamic pages.How can I help you explore Laravel packages today?