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.

Post a Comment

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