sensiolabs/behat-page-object-extension
Behat extension that helps you apply the Page Object pattern in browser acceptance tests. Provides page and element objects, reusable actions and assertions, and integrates with Mink to keep step definitions clean and maintainable.
Installation
Add to composer.json:
"require": {
"sensiolabs/behat-page-object-extension": "^2.0"
}
Run composer update.
Enable Extension
In behat.yml:
extensions:
SensioLabs\Behat\PageObjectExtension: ~
First Page Object
Create a class in features/bootstrap/Page/ (e.g., LoginPage.php):
<?php
namespace Features\Page;
use Behat\MinkExtension\Page\Page;
class LoginPage extends Page
{
public function getUrl() { return '/login'; }
public function submitLogin($username, $password) {
$this->fillField('username', $username);
$this->fillField('password', $password);
$this->pressButton('Login');
}
}
First Feature Reference the page in a feature file:
Given I am on the login page
When I submit login "user" "pass123"
features/bootstrap/.LoginPage (as above).// features/bootstrap/FeatureContext.php
use Features\Page\LoginPage;
$this->getSession()->getPage()->loginPage = function () {
return new LoginPage($this->getSession());
};
Given I am on the login page
When I submit login "user" "pass123"
Then I should see "Welcome, user"
Organize pages by feature/module:
features/
├── bootstrap/
│ ├── Page/
│ │ ├── Auth/
│ │ │ ├── LoginPage.php
│ │ │ └── DashboardPage.php
│ │ └── Admin/
│ │ └── UserManagementPage.php
│ └── FeatureContext.php
Extract shared UI elements into Page Components:
// features/bootstrap/Page/Component/Navbar.php
namespace Features\Page\Component;
use SensioLabs\Behat\PageObjectExtension\Page\Page;
class Navbar extends Page
{
public function clickAdminLink() {
$this->clickLink('Admin');
}
}
Use in a page:
// features/bootstrap/Page/AdminPage.php
class AdminPage extends Page
{
public function getNavbar() {
return $this->getSubPage($this->getSession(), Navbar::class);
}
}
Generate pages dynamically based on URLs or parameters:
// features/bootstrap/Page/UserProfilePage.php
class UserProfilePage extends Page
{
public function __construct($session, $userId) {
$this->userId = $userId;
parent::__construct($session);
}
public function getUrl() {
return "/profile/{$this->userId}";
}
}
Register in FeatureContext:
$this->getSession()->getPage()->userProfilePage = function ($userId) {
return new UserProfilePage($this->getSession(), $userId);
};
Use BeforeScenario to preload pages:
// features/bootstrap/FeatureContext.php
use Behat\Behat\Hook\Scope\ScenarioScope;
beforeScenario(ScenarioScope $scope) {
$page = $scope->getEnvironment()->getContext()->getSession()->getPage();
$page->loginPage = new LoginPage($page->getSession());
}
Leverage Mink’s drivers (e.g., Selenium, Guzzle) for assertions:
// In a page object
public function assertTitleContains($text) {
$this->assertSession()->titleContains($text);
}
Pass data via Given steps:
Given I am on the login page with credentials "user1" "pass1"
// FeatureContext.php
public function iAmOnTheLoginPageWithCredentials($username, $password) {
$page = $this->getSession()->getPage()->loginPage;
$page->open();
$page->submitLogin($username, $password);
}
$session object to page constructors:
new LoginPage($this->getSession());
LoginPage → DashboardPage → LoginPage) cause errors.getSubPage() or lazy-load pages.waitForElement() or waitFor():
$this->waitForElementVisible('#element');
getParameter() in getUrl():
public function getUrl() {
return $this->getParameter('base_url') . '/login';
}
Add to behat.yml:
default:
extensions:
Behat\MinkExtension:
base_url: http://localhost
goutte: ~
selenium2: ~
debug: true # <-- Enable debug
Add debug logs to page methods:
public function submitLogin($username, $password) {
\Behat\Behat\Temporary\Output\ConsoleOutput::getInstance()->writeln(
"Submitting login for: $username"
);
$this->fillField('username', $username);
// ...
}
Check if the session is active:
// In FeatureContext.php
public function assertSessionIsActive() {
$this->getSession()->start();
$this->assertTrue($this->getSession()->isStarted());
}
Override default Page/ directory in behat.yml:
extensions:
SensioLabs\Behat\PageObjectExtension:
paths:
- %paths.base%/custom/page/dir
Ensure autoloading is configured in composer.json:
"autoload": {
"psr-4": {
"Features\\": "features/bootstrap"
}
}
Pass Behat parameters to pages:
// In getUrl()
public function getUrl() {
return $this->getParameter('app.base_url') . '/login';
}
Define parameters in behat.yml:
default:
parameters:
app.base_url: http://example.com
Extend SensioLabs\Behat\PageObjectExtension\Page\Page for shared logic:
// features/bootstrap/BasePage.php
abstract class BasePage extends \SensioLabs\Behat\PageObjectExtension\Page\Page
{
public function assertElementExists($selector) {
$this->assertSession()->elementExists($selector);
}
}
Create reusable steps:
// features/bootstrap/PageSteps.php
use Features\Page\LoginPage;
$loginPage = new LoginPage($this->getSession());
Given(/^I am on the login page$/) {
$loginPage->open();
};
When(/^I login as "([^"]*)" with password "([^"]*)"$/) {
$loginPage->submitLogin($arg1, $arg2);
};
Trigger events when pages load:
// In a page constructor
public function __construct($session) {
parent::__construct($session);
$this->getSession()->getEventDispatcher()->dispatch(
'page.loaded',
new PageEvent($this)
);
}
Share page objects between
How can I help you explore Laravel packages today?