Snippets

Steve Adams PHPUnit and Facebook Webdriver (Selenium) foundation

Created by Steve Adams
<?php

namespace Your\Namespace\Tests\Features;

use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\RemoteWebElement;
use Facebook\WebDriver\Exception\StaleElementReferenceException;

/**
 * @coversNothing
 */
abstract class AbstractBaseFeatureTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @var RemoteWebDriver
     */
    protected $webDriver;

    /**
     * Set this in phpunit.xml
     * @var string
     */
    protected $url = WEBDRIVER_BASE_SITE_URL;

    /**
     * @var DesiredCapabilities
     */
    protected $desiredCapabilities;

    /**
     * Sets the desiredCapabilities field based on the WEBDRIVER_BROWSER_NAME constant defined in phpunit.xml. Currently
     * supports chrome and firefox, other values will result in an exception.
     */
    public function __construct()
    {
        parent::__construct();

        switch (WEBDRIVER_BROWSER_NAME) {
            case 'chrome':
                $this->desiredCapabilities = DesiredCapabilities::chrome();
                break;
            case 'firefox':
                $this->desiredCapabilities = DesiredCapabilities::firefox();
                break;
            default:
                throw new \Exception('Running tests on ' . ucfirst(WEBDRIVER_BROWSER_NAME) . ' is not supported.');
        }
    }

    protected function setUp()
    {
        $this->webDriver = RemoteWebDriver::create(
            WEBDRIVER_HOST,
            $this->desiredCapabilities
        );
    }

    protected function tearDown()
    {
        $this->webDriver->quit();
    }

    /**
     * Loads the page at $url. Returns control to the caller after the page has finished loading.
     *
     * @param string $url
     */
    protected function getPageAndWaitForLoad($url)
    {
        $this->webDriver->get($url);

        $this->webDriver->wait(10, 100)->until(function () {
            return $this->webDriver->executeScript('return document.readyState') == 'complete';
        });
    }

    /**
     * Finds the element identified by $condition and passes it to clickThroughToNewPageByElement. Returns control to
     * the caller after the new page finishes loading.
     *
     * @param WebDriverBy $condition
     */
    protected function clickThroughToNewPageBy(WebDriverBy $condition)
    {
        $element = $this->webDriver->findElement($condition);
        $this->clickThroughToNewPageByElement($element);
    }

    /**
     * Syntax sugar for calling clickThroughToNewPageBy with a link's text.
     *
     * @param string $linkText
     */
    protected function clickThroughToNewPageByLinkText($linkText)
    {
        $this->clickThroughToNewPageBy(WebDriverBy::linkText($linkText));
    }

    /**
     * Syntax sugar for calling clickThroughToNewPageBy with a CSS selector.
     *
     * @param string $cssSelector
     */
    protected function clickThroughToNewPageByCssSelector($cssSelector)
    {
        $this->clickThroughToNewPageBy(WebDriverBy::cssSelector($cssSelector));
    }

    /**
     * Syntax sugar for calling clickThroughToNewPageBy with an ID.
     *
     * @param string $id
     */
    protected function clickThroughToNewPageById($id)
    {
        $this->clickThroughToNewPageBy(WebDriverBy::id($id));
    }

    /**
     * Syntax sugar for calling clickThroughToNewPageBy with an XPath selector.
     *
     * @param string $xPath
     */
    protected function clickThroughToNewPageByXPath($xPath)
    {
        $this->clickThroughToNewPageBy(WebDriverBy::xpath($xPath));
    }

    /**
     * Clicks on the given element and waits for a new page to load.
     *
     * @param RemoteWebElement $element
     */
    protected function clickThroughToNewPageByElement(RemoteWebElement $element)
    {
        $element->click();
        $this->webDriver->wait(10, 100)->until(WebDriverExpectedCondition::stalenessOf($element));
    }

    /**
     * Gets an element using xpath but waits until it's present by a given condition.
     *
     * @param string $xpath     The xpath for the element - Defaults to presenceOfElementLocated.
     * @param string $condition A WebDriverExpectedCondition
     *
     * @return WebDriverRemoteElement
     */
    protected function getElementByXpath($xpath, $condition = "presenceOfElementLocated")
    {
        $this->webDriver->wait(10, 100)->until(
            WebDriverExpectedCondition::$condition(
                WebDriverBy::xpath($xpath)
            )
        );

        return $this->webDriver->findElement(WebDriverBy::xpath($xpath));
    }

    /**
     * Gets elements using xpath
     *
     * @param string $xpath     The xpath for the elements.
     *
     * @return array
     */
    protected function getElementsByXpath($xpath)
    {
        return $this->webDriver->findElements(WebDriverBy::xpath($xpath));
    }

    /**
     * Click 'okay' on an alert.
     *
     * @return AbstractBaseFeatureTest
     */
    protected function acceptAlert()
    {
        $this->webDriver->wait()->until(
            WebDriverExpectedCondition::alertIsPresent()
        );

        $this->webDriver->switchTo()->alert()->accept();

        return $this;
    }

    /**
     * Click 'cancel' on an alert.
     *
     * @return AbstractBaseFeatureTest
     */
    protected function dismissAlert()
    {
        $this->webDriver->wait()->until(
            WebDriverExpectedCondition::alertIsPresent()
        );

        $this->webDriver->switchTo()->alert()->dismiss();

        return $this;
    }

    /**
     * Output text to the console during a test.
     *
     * @param string $value  Text to output.
     * @param string $colour Which colour to make the text.
     */
    protected function output($value, $colour = null)
    {
        if ($colour) {
            $colour = $this->getOutputColour($colour);
        } else {
            $colour = "";
        }

        fwrite(STDOUT, $colour . "{$value} \033[0m \n");
    }

    /**
     * Get a colour code for outputting text.
     *
     * @param int $colour
     *
     * @return string The code for the given colour.
     */
    protected function getOutputColour($colour)
    {
        $colours = [
            "white" => 37,
            "error" => 31,
            "success" => 32,
            "warning" => 33,
            "info" => 34
        ];

        return "\033[{$colours[$colour]}m";
    }

    /**
     * Tries clicking an element. This repeated attempting pattern catches stale
     * element exceptions which, until I wrote this method, were the bane of my existence.\
     * This method isn't essential for every click, but it helps a lot with cases
     * where JavaScript manages the DOM after a previous click occurred. For clicks
     * before Javascript needs to run, this is probably overkill.
     *
     * @param RemoteWebElement $element      The element to try clicking.
     * @param string           $elementXpath Path to the element to try re-referencing it.
     * @param int              $numAttempts  How many times to try clicking.
     * @param int              $wait         How long to wait between clicks in milliseconds.
     *
     * @return AbstractBaseFeatureTest
     */
    protected function tryClicking(RemoteWebElement $element, $elementXpath = null, $numAttempts = 5, $wait = 10)
    {
        $attempts = 0;

        while ($attempts < $numAttempts) {
            $attempts ++;

            try {
                $element->click();

                return $this;
            } catch (StaleElementReferenceException $exception) {
                if ($elementXpath) {
                    $this->output(
                        "Re-referencing element due to stale element reference...",
                        "error"
                    );

                    $element = $this->getElementByXpath($elementXpath, "elementToBeClickable");
                }

                $remaining = $numAttempts - $attempts;

                $this->output(
                    "Caught StaleElementReferenceException; waiting {$wait}ms then clicking again ({$remaining} tries remaining)...",
                    "error"
                );

                $this->webDriver->manage()->timeouts()->implicitlyWait($wait);
            }
        }

        // Looks like we made it here, so we should let the exception happen.
        throw $exception;

        return $this;
    }
}

Comments (0)