A PHP Page Object Example

You can file this one under the ‘practice what you preach’ category. While getting PHP-WebDriver ready for the OnDemand stuff that was announced earlier in the week I was scripting up the Sauce Labs login page I realized that I was writing things in the poor, hard-to-maintain, non-page-object manner.

Ugh-oh.

So I started to use a Page Object for it. Here is the link directly to it. Although I’m annotating it here.

<?php
namespace WebDriver;
 
// this page object only deals with one other page object
require_once('dashboard.php');
 
// other necessary requires
require_once(dirname(__FILE__) . '/../../../PHPWebDriver/WebDriverWait.php');
require_once(dirname(__FILE__) . '/../../../PHPWebDriver/WebDriverBy.php');
 
class SauceLoginPage {
  // the locators that this page object will use. these won't appear anywhere
  // else in the entire set of page object
  private $locators = array(
      "username" => array(\PHPWebDriver_WebDriverBy::ID, 'username'),
      "password" => array(\PHPWebDriver_WebDriverBy::ID, 'password'),
      "submit button" => array(\PHPWebDriver_WebDriverBy::ID, 'submit'),
      "errors" => array(\PHPWebDriver_WebDriverBy::CSS_SELECTOR, '.error')
  );
 
  // dependency injection
  function __construct($session) {
    $this->session = $session;
  }
 
  // interactions where you pull something out of the browser get handled
  // by manipulating the __get's
  function __get($property) {
    switch($property) {
      case "errors":
        list($type, $string) = $this->locators[$property];
        $e = $this->session->element($type, $string);
        return $e->text();
      case "title":
        return $this->session->title();
      default:
        return $this->$property;
    }
  }
 
  // surprising, when you want to put something into the browser, you
  // manipulate __set
  function __set($property, $value) {
    switch($property) {
      case "username":
      case "password":
        list($type, $string) = $this->locators[$property];
        $e = $this->session->element($type, $string);
        $e->sendKeys($value);
        break;
      default:
        $this->$property = $value;
    }
  }
 
  // if you can get to a page directly, then open does something
  // if not, then it can either traverse through your app to get to it
  // or just 'return $this;'
  function open() {
    $this->session->open("https://saucelabs.com/login");
    return $this;
  }
 
  // synchronization for what 'done' means on this page. 'done' rarely
  // means 'page content loaded' anymore
  function wait_until_loaded() {
    $w = new \PHPWebDriver_WebDriverWait($this->session, 30, 0.5, array("locator" => $this->locators['submit button']));
    $w->until(
      function($session, $extra_arguments) {
        list($type, $string) = $extra_arguments['locator'];
        return $session->element($type, $string);
      }
    );
    return $this;
  }
 
  // still not sure how i feel about this...
  // not asserts to be run every time, maybe once per suite
  // certainly no functionality checks though
  function validate() {
    assert('$this->title == "Login - Sauce Labs" /* title should be "Login - Sauce Labs" */');
    return $this;
  }
 
  // here is an 'action', including a default route (success)
  // notice how there is a page object instance returned in both routes
  function login_as($username, $password, $success=true) {
    $this->username = $username;
    $this->password = $password;
 
    list($type, $string) = $this->locators['submit button'];
    $e = $this->session->element($type, $string);
    $e->click();
 
    if ($success) {
      $p = new \DashboardPage($this->session);
      $p->wait_until_loaded();
      return $p;
    } else {
      $w = new \PHPWebDriver_WebDriverWait($this->session, 30, 0.5, array("locator" => $this->locators['errors']));
        $w->until(
          function($session, $extra_arguments) {
            list($type, $string) = $extra_arguments['locator'];
            $e = $session->element($type, $string);
            return $e->displayed();
          }
        );
      return $this;
    }
  }
}

Of course, when you are using page objects, you scripts look different. As in there is no WebDriver-isms.

public function testFirefox36() {
    $caps = array();
    $caps["platform"] = 'LINUX';
    $caps["version"] = '3.6';
    $this->session = self::$driver->session("firefox", $caps);
 
    $p = new SauceLoginPage($this->session);
    $p->open();
    $p->wait_until_loaded();
    $p->validate();
    $p = $p->login_as("gobblygook", "nonsense", false);
    $this->assertEquals($p->errors, "Incorrect username or password.");
}

One thing I need to really get into the habit of doing is including more Page Objects in my code examples. I’m looking forward to the next version of Selenium 2 Testing Tools: Beginner’s Guide as he mentioned he’ll be using more Page Objects in his examples.

Comments 1

  1. Ben Harmond wrote:

    I’m curious about why some tests are part of the PageObject and some aren’t. For instance, the validate() method asserts that the title is correct, but then you make an assert for incorrect login details in the main test. Shouldn’t this be part of the login_as() method?

    Posted 10 Sep 2012 at 6:25 am

Trackbacks & Pingbacks 1

  1. From A Smattering of Selenium #116 « Official Selenium Blog on 04 Sep 2012 at 11:03 am

    […] I finally annotated A PHP Page Object Example […]

Post a Comment

Your email is never published nor shared. Required fields are marked *