thelemon2020/pest-plugin-pom
| Install | |
|---|---|
composer require thelemon2020/pest-plugin-pom |
|
| Latest Version: | 0.9 |
| PHP: | ^8.3 |
| License: | MIT |
| Last Updated: | Jun 16, 2026 |
| Links: | GitHub · Packagist |
Pest Plugin POM
A Pest plugin for writing browser tests using the Page Object Model pattern. Integrates with pest-plugin-browser.
Early Release: Pre-v1, bugs are expected. Please open an issue if you find one.
Requirements
- PHP ^8.3
- Pest ^4.0
- pest-plugin-browser ^4.0
Installation
composer require thelemon2020/pest-plugin-pom --dev
Config
php artisan vendor:publish --tag=pest-plugin-pom-config
Creates config/pest-plugin-pom.php:
return [
'path' => 'tests/Browser/Pages',
];
Page Objects
Extend Page, define a url(), and add interaction methods:
namespace Tests\Browser\Pages;
use Thelemon2020\PestPom\Page;
use Thelemon2020\PestPom\Concerns\InteractsWithForms;
class LoginPage extends Page
{
use InteractsWithForms;
public static function url(): string
{
return '/login';
}
public function loginAs(string $email, string $password): static
{
return $this
->fillForm(['Email' => $email, 'Password' => $password])
->submitForm('Log in');
}
}
Navigate to a page with page() or ::open():
$page = page(LoginPage::class);
// or
$page = LoginPage::open();
Parameterized URLs
class ProductPage extends Page
{
public static function url(): string
{
return '/products/{id}';
}
}
ProductPage::open(['id' => 42]);
$page->navigateTo(ProductPage::class, ['id' => 42]);
Navigating Between Pages
navigateTo() sends the browser to a page's URL. nowOn() re-wraps the current session as a different page type without reloading — useful after a server-side redirect. nowOn() also verifies the current URL matches the destination.
// explicit navigation
$page->navigateTo(DashboardPage::class);
// after a redirect
$page->submitForm('Log in')->nowOn(DashboardPage::class);
navigateTo() |
nowOn() |
|
|---|---|---|
| Navigates the browser | Yes | No |
| Verifies current URL | No | Yes |
Accepts {param} values |
Yes | Pattern-matched |
Tests
Tests using Page Objects live in tests/Browser/ — Pest's browser plugin handles the rest.
it('allows a user to log in', function () {
page(LoginPage::class)
->loginAs('jane@example.com', 'password')
->assertSee('Dashboard');
});
Components
Components encapsulate a reusable piece of UI. They work like pages but have no URL and are always obtained through a page instance.
php artisan pest:component SearchBar
namespace Tests\Browser\Components;
use Thelemon2020\PestPom\Component;
class SearchBarComponent extends Component
{
public static function selector(): string
{
return '#search-bar';
}
public function search(string $query): static
{
return $this
->fill('input[name=query]', $query)
->click('button[type=submit]');
}
}
page(DashboardPage::class)
->component(SearchBarComponent::class)
->search('pest php')
->assertSee('pest-plugin-browser');
Selectors passed to interaction methods are scoped to the component's root element automatically.
Selector types
selector() supports all Pest selector forms:
| Form | Example | Matches |
|---|---|---|
| CSS | 'nav', '.card', '#main', 'my-element' |
CSS selector |
@ data-test |
'@search-panel' |
data-test or data-testid attribute |
| Text content | 'Featured Items' |
Element with that visible text |
Tip: If a text content selector isn't behaving as expected, use Playwright's explicit
text=prefix — e.g.'text=submit'.
Typed Accessors
class DashboardPage extends Page
{
public function searchBar(): SearchBarComponent
{
return $this->component(SearchBarComponent::class);
}
}
Multiple Instances
Use components()->item(n) (1-based) to target a specific occurrence:
$page->components(CardComponent::class)->assertTotal(3);
$page->components(CardComponent::class)->item(2)->assertTitle('Advanced Usage');
Sub-Components
Call component() on a component to create a child scoped within the parent:
// resolves to: #nav .user-menu
$page->header()->userMenu()->assertSee('Jane Doe');
Scoped Assertions
| Method | Description |
|---|---|
assertSee(string $text) |
Text appears within the component |
assertDontSee(string $text) |
Text does not appear within the component |
assertVisible() |
Root element is visible |
assertPresent() |
Root element is in the DOM |
assertMissing() |
Root element is absent from the DOM |
assertCount(string $selector, int $expected) |
Count of child elements matching selector |
assertSeeIn(string $selector, string $text) |
Text appears within a child element |
assertDontSeeIn(string $selector, string $text) |
Text does not appear within a child element |
assertTotal(int $expected) |
Number of elements matching this component's selector |
Scoped Interactions
| Method | Description |
|---|---|
click(string $selector) |
Click an element |
rightClick(string $selector) |
Right-click an element |
type(string $field, string $value) |
Type into a field |
typeSlowly(string $field, string $value, int $delay = 100) |
Type slowly into a field |
fill(string $field, string $value) |
Fill a field |
append(string $field, string $value) |
Append text to a field |
clear(string $field) |
Clear a field |
hover(string $selector) |
Hover over an element |
select(string $field, array|string|int $option) |
Select a dropdown option |
radio(string $field, string $value) |
Select a radio button |
check(string $field, ?string $value = null) |
Check a checkbox |
uncheck(string $field, ?string $value = null) |
Uncheck a checkbox |
attach(string $field, string $path) |
Attach a file |
keys(string $selector, array|string $keys) |
Send keyboard input |
drag(string $from, string $to) |
Drag one element to another |
text(string $selector): ?string |
Return element text content |
attribute(string $selector, string $attribute): ?string |
Return element attribute value |
press(), pressAndWaitFor(), and withKeyDown() are not scoped — they pass through via __call.
Generators
php artisan pest:page Login
php artisan pest:page Register --concerns=forms,alerts
php artisan pest:component SearchBar
php artisan pest:component DataTable --concerns=navigation,modals
Available concerns: forms, alerts, modals, navigation.
The Page/Component suffix is optional and won't be doubled.
Concerns
InteractsWithForms
| Method | Description |
|---|---|
fillForm(array $fields) |
Fill multiple fields, keyed by label |
submitForm(string $button = 'Submit') |
Click a submit button by label |
checkBox(string $label) |
Check a checkbox by label |
choose(string $field, array|string|int $option) |
Select a dropdown option by label |
InteractsWithAlerts
| Method | Description |
|---|---|
assertSuccessMessage(string $message) |
Assert a success alert is visible |
assertErrorMessage(string $message) |
Assert an error alert is visible |
assertFieldError(string $field, string $message) |
Assert a validation error for a field |
InteractsWithModals
| Method | Description |
|---|---|
openModal(string $trigger) |
Click the element that opens the modal |
confirmModal(string $button = 'Confirm') |
Click the confirm button |
dismissModal(string $button = 'Cancel') |
Click the cancel button |
closeModal(string $button = 'Close') |
Close without confirming |
InteractsWithNavigation
| Method | Description |
|---|---|
clickLink(string $label) |
Click a link by visible text |
goBack() |
Navigate back |
goForward() |
Navigate forward |
refresh() |
Reload the page |
Expectations
expect($page)->toBeOnPage(DashboardPage::class);
expect($page)->toSee('Welcome back');
License
MIT