Skip to content

Instantly share code, notes, and snippets.

@amirkdv amirkdv/web-ui-testing.md
Last active Jul 11, 2019

Embed
What would you like to do?
Web UI Testing Survey

Web UI Testing Survey

Table of Contents

The following report covers a handpicked variety of testing tools with a focus on end-to-end UI testing and tools that integrate with PHP applications, specifically Drupal. For each tool, code snippets are provided. Tests are written against DrupalSun, a Drupal powered site.

Browser Automation

Selenium

  • written in Java: https://code.google.com/p/selenium/source/browse/

  • abstracts your browser away and provides a basic API for automating user interaction with browser. It supports almost everything: IE, Chrome, Firefox, Safari, Opera. It does not have its own JavaScript Engine (it's the browser specific driver that executes JS) but its API allow you to make DOM queries.

  • Although typical usage is for testing it does not give you a library/framework to devise test suites. It does have subprojects (like Selenium IDE) that help you write tests.

  • has a slightly confusing project history with a variety of people/companies/subprojects/project names.

  • It has a bunch of subprojects. The one we want (for the purpose of automating UI tests is 'Selenium Webdriver' see below on Selenium 1 vs 2).

  • To run the browser headless (to speed things up), you need to install Xvfb and set the environment variable DISPLAY to the display port where Xvfb is running.

  • Selenium is not necessarily headless; the following opens the browser GUI:

      $ ruby -e 'require "selenium-webdriver"; Selenium::WebDriver.for(:firefox);'
    
  • Selenese HTML: a DSL for defining test cases for Selenium. It's merely an HTML table with three columns, the first being verbs (e.g. open, clickAndWait, verifyTextPresent):

    • commands (verbs) include:
      • Actions: click (on object at (x,y) coordinates), fire all sorts of events, type text, wait for events, context menus,
      • Accessing data from DOM: used to store artifacts, e.g. storeAllWindowTitles (returns array of strings), storeElementPresent (returns boolean), etc.
      • Asserting statements
    • locating elements is done using: HTML attribute, id, text, CSS selector, XPath.
  • CircleCI environment is ready for headless Selenium. Docs say:

        Xvfb runs on port 99, and the appropriate DISPLAY environment variable
        has already been set.
    
  • TravisCI seems to be OK with it as well.

Selenium 1 vs 2

Selenium 1 and 2 (RC vs WebDriver) are simply different APIs and different mechanisms:

  • Legacy Selenium (1.0 also called RC: Remote Control) drives the browser by injecting JavaScript after every page load. New Selenium (2.0 also called WebDriver) uses each browser's own automation API (so Firefox is automated in one way and Chrome in another); docs and docs
  • The WebDriver wire protocol seems to be the de facto standard. The idea is to have all browser automation jobs "use a common wire protocol. This wire protocol defines a RESTful web service using JSON over HTTP."
    • Wire protocol draft on google code.
    • W3C WebDriver API spec (draft; as of Feb 2015).
  • However, Selenium2 still supports the legacy RC API as well (in fact the PHPUnit Selenium extension uses the legacy RC API not WebDriver).

Selenium remote vs local

Remote vs local: in either Selenium RC or WebDriver there are two distinct scenarios:

  • automation code directly communicates with the browser; browser code and driver must be locally available (eg for chrome the driver is here; you place the binary executable in PATH)

      $ cat local.rb
      require "selenium-webdriver"
    
      driver = Selenium::WebDriver.for(:chrome)
      driver.navigate.to('http://evolvingweb.ca')
      puts driver.title
    
      driver.quit
    
      $ ruby local.rb
      Evolving Web | We design and develop websites with Drupal, provide public and private training, and write open source code.
    
  • Client/Server: There is a Selenium server (distributed as a JAR file) which has to run on a machine with access to browser code and drivers. Automation code (the client) speaks to this server over HTTP using any of the existing libraries (in Ruby/Python/etc.):

      $ java -jar selenium-server-standalone-2.44.0.jar &> selenium.log & # listening on port 4444
      $ cat client.rb
      require "selenium-webdriver"
    
      driver = Selenium::WebDriver.for(:remote,
                                       :url=> 'http://localhost:4444/wd/hub',
                                       :desired_capabilities => :chrome)
      driver.navigate.to('http://evolvingweb.ca')
      puts driver.title
    
      driver.quit
      $ ruby client.rb
      Evolving Web | We design and develop websites with Drupal, provide public and private training, and write open source code.
    

Selenium IDE

  • is a firefox addon that allows you to "record" what you manually do into HTML "scripts" of this type:

  • allows you to export Selenese HTML tests to Ruby/Python/PHP tests (e.g. Rspec/PHPUnit).

    • See this and this for the PHPUnit plugin features/screenshots:
  • has a JavaScript UI accessible to firefox (after installing the plugin) at:

    chrome://selenium-ide/content/selenium-core/TestRunner.html?baseUrl=http://localhost:8001&test=file:///path/to/suite.html&auto=true
    
  • generated scripts are'nt great:

    • would sometimes use plain text identifiers instead of CSS selectors,
    • docs suggest that when a recorded click action has a latency (e.g. new page being loaded) you change the generated script to clickAndWait instead.
    • I had to manually fix a bunch of things in the generated rspec script having to do with recent versions of rspec and selenium-client.
  • Acquia uses Selenium IDE generated tests and exports them to Java and use JUnit with Selenium Grid. This article complains about it a bit though.

PhantomJS

  • written in C++ https://github.com/ariya/phantomjs/

  • It's a commandline-friendly WebKit. It's always headless (no need for Xvfb in CI).

  • Allows you to capture the page as an image (perfect for test artifacts):

      $ cat render.js
      var page = require('webpage').create();
    
      page.open('http://evolvingweb.ca/', function(){
        page.render('artifact.png');
        phantom.exit();
      });
    
      $ phantomjs render.js # gives you "artifact.png"
    
  • Installation on Linux is easy:

      $ apt-get install -y phantomjs
      $ phantomjs --version
      1.9.0
      $ phantomjs <( echo 'console.log("hi"); phantom.exit(0);' )
      hi
    
  • Allows you to execute arbitrary JavaScript inside a loaded page's DOM. This is done by passing a function that is presumably converted to JS code and stuck inside the DOM. Here a simple test case for DrupalSun frontpage:

    // 'webpage' is provided by PhantomJS
    var page = require('webpage').create(),
        system = require('system'),
        url  = system.args[1];
    
    page.open(url, function() {
      var result = page.evaluate(function () {
        var result = []
        result.push(jQuery('.views-hover').length)
        elem = jQuery(jQuery('.views-row')[0])
    
        elem.mouseenter();
        result.push(jQuery('.views-hover').length)
    
        elem.mouseleave();
        result.push(jQuery('.views-hover').length)
        return result
      });
    
      if (result[0] == 0 && result[1] == 1 && result[2] == 0) {
        phantom.exit(0);
      } else {
        page.render('failure.png');
        phantom.exit(1);
      }
    });
    

    Usage:

    $ phantom.js test.js
    

Mink

  • written in PHP; distributed over Composer.
  • is solely concerned with browser automation (it integrates with testing frameworks, but it's not concerned with tests itself). It allows you to swap backends. The only backend that's not a mere HTML parser is Selenium (supports both 1 and 2).
  • Integrates with Behat through the Behat MinkExtension. In fact, Mink is the main way to do browser automation with Behat. The section below on Behat talks more about Mink.

Headless mode

All of the examples above start the browser in the current display as per the value of $DISPLAY in the environment of the Selenium process (assuming Linux and X). For local dev purposes we'd want all to be headless:

$ Xvfb :19 -screen 0 1024x768x16 &> xvfb.log
$ (export DISPLAY=:19; java -jar selenium-server-standalone-2.44.0.jar &> selenium.log)

$ # now talk to Selenium

Notes:

  • The above only works in an X environment. For OS X you should probably use PhantomJS or similar as a backend to Selenium (unless you want to build/run your browsers for X and run Xvfb). See section below for using PhantomJS as a backend to Selenium.

  • The above assumes you're using Selenium server. Otherwise, for example as in the following, where there is no Selenium server, the trick is the same:

    $ Xvfb :19 -screen 0 1024x768x16 &> xvfb.log
    $ (export DISPLAY=:19; ruby -e 'require "selenium-webdriver"; Selenium::WebDriver.for(:firefox);')
    

PhantomJS as a Selenium backend

Selenium driving Chrome/Firefox/etc can be run in headless mode. Although all testing tools are smart enough to boot the browser only once, we might want to use phantomjs (which is generally lighter than the rest since it supports only headless mode). This can be done now but the workflow is slightly different:

  • There exists a GhostDriver which implements the WebDriver protocol for PhantomJS: https://github.com/detro/ghostdriver.

  • GhostDriver offers a WebDriver host URL that client code (e.g Ruby code using selenium-webdriver gem) contacts to get a PhantomJS instance. This is different from the way you switch from Firefox (Selenium default) to Chrome (where you have to download the Google proprietary driver and put it in the path).

  • GhostDriver was merged into PhantomJS in the 1.8 release (current version is 1.9) and now you can just use it directly from phantomjs's UI.

  • There are two similar ways of using GhostDriver:

    • Without a Selenium server:

        $ phantomjs --webdriver=8123 &> ghostdriver.log &
      
        $ ruby test.rb http://localhost:8123
        Evolving Web | We design and develop websites with Drupal, provide public and private training, and write open source code.
      
    • With a Selenium server acting as a hub (start selenium with -role hub and phantomjs ghostdriver with --webdriver-selenium-grid-hub=http://127.0.0.1:4444):

      $ java -jar selenium-server-standalone-2.44.0.jar -role hub &> selenium.log &
      $ phantomjs --webdriver=8123 --webdriver-selenium-grid-hub=http://127.0.0.1:4444 &> ghostdriver.log &
      
      $ ruby test.rb http://localhost:4444/wd/hub
      Evolving Web | We design and develop websites with Drupal, provide public and private training, and write open source code.
      
  • The client Ruby code used in both cases:

      $ cat test.rb
      require "selenium-webdriver"
    
      webdriver_url = ARGV[0]
    
      driver = Selenium::WebDriver.for(:remote, :url => webdriver_url, :desired_capabilities => :phantomjs)
      driver.navigate.to('http://evolvingweb.ca')
    
      puts driver.title
      driver.quit
    
  • The JavaScript client (WebDriverJS, npm module selenium-webdriver) seems to not need the above dance to pick up phantomjs. It automatically picks it up if the npm module is installed.

WebDriver Language Bindings

Bindings exists for many languages. Ruby, Python, Java, C#, and JavaScript are seemingly maintained by the Selenium project, and the rest are all third party.

  • Ruby: the official bindings for Ruby is the selenium-webdriver gem. There used to be an older gem to speak with the Selenium server which is now merged into one: docs Related tools:
  • PHP: the official bindings seems to be non-existent
    • There are a bunch of 3rd party libraries, including one from Facebook

    • PHPUnit has an extension (distributed over composer as phpunit/phpunit-selenium) that integrates with selenium but not with WebDriver (supports RC and the fact that Selenium2 did not break support for RC).

    • Mink supports Selenium (1 and 2) plus a bunch of other scrapers. Mink's Selenium2 driver seems to only exclusively work with a Selenium server.

      $ cat composer.json
      {
        "require": {
          "behat/mink": "~1.6",
          "behat/mink-selenium2-driver": "*"
        }
      }
      $ composer install
      $ cat test.php
      <?php
      require_once 'vendor/autoload.php';
      
      $driver = new \Behat\Mink\Driver\Selenium2Driver('chrome');
      
      $session = new \Behat\Mink\Session($driver);
      $session->start();
      $session->visit('http://evolvingweb.ca');
      echo $session->getPage()->find('css', '#site-slogan')->getText();
      
      $session->stop();
      $ php test.php
      We design and develop websites with Drupal, provide public and private training, and write open source code.
      
  • JavaScript: https://code.google.com/p/selenium/wiki/WebDriverJs
    • docs: http://selenium.googlecode.com/git/docs/api/javascript/index.html
    • distributed over npm as selenium-webdriver, not to be confused with the node bindings WebDriverIO (npm package was confusingly named webdriverjs for a while, now its webdriverio)
    • The fact that WebDriverJS is asynchronous (and only partly so apparently) can make things confusing (quite a few SO questions, GH issues, blogposts).
    • The usual timeout issue exists (your timeouts may end up being short during peak hours of load on CI).
    • There is the webdriver-sync module which tries to make everything synchronous like other clients and get rid of the complications.

End-to-End UI testing

For end-to-end UI testing we need a testing component and a browser automation component. For example:

Ruby WebDriver + Rspec

  • In this case, you may want to consider Watir.

  • Here is a simple Rspec test case for DrupalSun:

      $ cat spec/drupalsun_spec.rb
      require "rspec"
      require "selenium-webdriver"
    
      ROOT_URL = ENV['URL'] || 'http://drupalsun.com'
    
      describe 'DrupalSun' do
        before do
          @driver = Selenium::WebDriver.for(:chrome)
        end
    
        it "shows teaser upon hover" do
          @driver.navigate.to(ROOT_URL)
          element = @driver.find_element(:class, 'views-row')
          @driver.mouse.move_to(element)
          expect(@driver.find_elements(:class, 'views-hover').length).to eq(1)
        end
    
        after do
          @driver.quit
        end
      end
    

Mink + PHPunit

  • Dependencies:

      $ cat composer.json
      {
        "require": {
          "behat/mink": "~1.6",
          "behat/mink-selenium2-driver": "*",
          "phpunit/phpunit": "4.5.*"
        }
      }
    
  • PHPUnit configuration:

      $ cat phpunit.xml
      <?xml version="1.0" encoding="UTF-8"?>
    
      <phpunit colors="true"
               convertErrorsToExceptions="true"
               convertNoticesToExceptions="true"
               convertWarningsToExceptions="true"
               stopOnFailure="false"
               bootstrap="./vendor/autoload.php"
               verbose="true">
          <testsuites>
              <testsuite name="integration-tests">
                  <directory suffix=".php">tests/</directory>
              </testsuite>
          </testsuites>
      </phpunit>
    
  • Test case:

      $ cat tests/mink_phpunit_drupalsun.php
      <?php
      use \Behat\Mink\Driver\Selenium2Driver;
      use \Behat\Mink\Session;
    
      class DrupalSunMinkTest extends PHPUnit_Framework_TestCase {
        public function setUp() {
          $this->driver = new Selenium2Driver('chrome');
          $this->session = new Session($this->driver);
          $this->session->start();
        }
    
        public function tearDown() {
          $this->session->stop();
        }
    
        public function testHover() {
          $this->session->visit('http://localhost:8080');
          $this->session->getPage()->find('css', '.views-row')->mouseOver();
          $this->assertFalse(null === $this->session->getPage()->find('css', '.views-hover'));
        }
      }
    
  • Usage:

      $ java -jar selenium-server-standalone-2.45.0.jar &> selenium.log &
      $ composer install
      $ vendor/bin/phpunit
      PHPUnit 4.5.0 by Sebastian Bergmann and contributors.
    
      Configuration read from /tmp/mink_phpunit/phpunit.xml
    
      .
    
      Time: 5.33 seconds, Memory: 4.00Mb
    
      OK (1 test, 1 assertion)
    

PhantomJS + CasperJS

  • CasperJS is written in JavaScript and allows you to:
    • access and manipulate DOM and evaluate JS,
    • located elements by: CSS selectors or XPath,
    • trigger browser events and traverse pages,
    • set up and tear down test cases, make assertions: docs
    • organize tests into suites (and properly report over the entire suite)
  • Page traversals are done using a step stack
  • Drupal example.
  • Lullabot seems to be using it: blogpost and drupal/casperjs foundation.
  • Here's an example for DrupalSun:
    • test suite:

          $ cat test.js
          // A simple CasperJS Test Suite for Drupal Sun
          //
          // usage: casperjs test.js --url=http://localhost:8001
      
          var drupalsun_url = casper.cli.get('url');
      
          var suite_config = {
            setUp: function(test){
              // nothing to do
            },
            tearDown: function(test){
              // nothing to do
            },
            test: function(test){
              casper.start(drupalsun_url, function() {
                test.assertHttpStatus(200, 'HTTP response code is 200');
                test.assertTitleMatch(/Drupal Sun/, 'Homepage title says "Drupal Sun".');
                test.assertDoesntExist('.views-hover', 'No hover boxes exists yet.');
      
                test.comment('hovering over a feed item');
                this.mouse.move('.views-row');
                test.assertExists('.views-hover .field-content', 'There exists a hover box now.');
      
                test.comment('hovering away from the feed item');
                this.mouse.move(0, 0);
                test.assertDoesntExist('.views-hover .field-content', 'No hover boxes anymore.');
              });
      
              casper.then(function(){
                var num = function(){
                  return jQuery('.view-content div.views-row').length;
                };
                var n1 = this.evaluate(num);
                test.comment('Scrolling down the page (current number of feeds: ' + n1 + ').');
                this.scrollToBottom();
                this.wait(3000, function(){ // let it load more items
                  var n2 = this.evaluate(num);
                  test.assertTrue(n2 > n1, 'Number of loaded feeds increased when ' +
                                           'scrolled to bottom (' + n1 + ' -> ' + n2 + ')');
                });
              });
      
              casper.then(function(){
                this.click('.views-field-title');
                test.comment('Visiting a feed item');
                var visible_bodies = function(){
                  return jQuery('.views-field-field-feed-item-description').filter(':visible').length;
                }
                var visible_titles = function(){
                  return jQuery('.views-field-title').filter(':visible').length;
                }
                test.assertEquals(this.evaluate(visible_bodies), 1, 'Only one feed body is visible');
                test.assertTrue(this.evaluate(visible_titles) > 3, 'More than 3 feed titles are visible');
              });
      
              casper.run(function () {
                test.done();
              });
            }
          };
      
          casper.test.begin('Drupal Sun', suite_config);
      
    • usage:

        $ npm install phantomjs casperjs
        $ node_modules/.bin/casperjs test.js --url=http://localhost:8001
      

WebDriverJS + Mocha

WebDriverJS (the JavaScript bindings for WebDriver) also provide a testing API using Mocha (alternatively you can use jasmine-node. Here is an example for Drupal Sun:

  • Mocha is a DOM-less JavaScript testing framework (written in JS); gives you typical utilities to write test suites.

  • Here's a simple test suite for DrupalSun that shows how promises are used:

      // usage: URL=http://localhost:8001 mocha -t <TIMEOUT> spec.js
      var webdriver = require('selenium-webdriver'),
          By = webdriver.By,
          until = webdriver.until,
          test = require('selenium-webdriver/testing');
          url = process.env.URL || 'http://drupalsun.com';
    
      test.describe('DrupalSun frontpage', function() {
        var driver;
    
        test.before(function() {
          driver = new webdriver.Builder()
              .forBrowser('firefox')
              //.usingServer('http://localhost:4444/wd/hub')
              .build();
        });
    
        test.it('should load more feeds when I scroll down', function() {
          var sel = '.view-content div.views-row';
          driver.get(url);
          driver.findElements(By.css(sel)).then(function(elems_before) {
            var n1 = elems_before.length;
            driver.findElement(By.tagName('body')).sendKeys(webdriver.Key.END);
            driver.wait(function(){
              // return a "promise"
              return driver.findElements(By.css(sel)).then(function(elems_after) {
                return elems_after.length > n1;
              });
            }, 5000);
          });
        });
    
        test.after(function() {
          driver.quit();
        });
      });
    
  • usage:

      $ npm install selenium-webdriver mocha
      $ URL=http://localhost:8001 node_modules/.bin/mocha -t 5000 spec
    

Mink + Behat

  • written in PHP; distributed over Composer

  • is solely concerned with BDD (you could test a CLI app using it) and is much like Rspec/Cucumber (uses annotations, behavioral lingo e.g "context", gherkin syntax). Here's an example feature from docs:

      Feature: ls
    
        Scenario:
          Given I am in a directory "test"
          And I have a file named "foo"
          And I have a file named "bar"
          When I run "ls"
          Then I should get:
            """
            bar
            foo
            """
    
  • Features: like above in text files,

  • Steps: each of the steps to get to the desired sandbox e.g. I have a file named "foo". This has to be implemented by a properly annotated function iHaveAFileNamed($name).

  • Hooks allow you to do things before/after a single test, an entire suite, after a failed test, etc.

  • there is a behat.yml that is much like phpunit.xml (involved in grouping suites together, defining where helpers -here contexts- should come from, and formatting output) and additionally to configure the extensions (e.g. mink). So if you're testing a Web UI you want Mink and in your behat.yml you specify the Mink config (e.g. backend driver, which browser, base url)

  • Browser automation is achieved via the behat/mink-extension which then can be configured to use, say, Selenium2.

  • It's mainly a Cucumber clone in PHP, most generic parts (non-BDD parts) are composed of Symfony components.

  • Seems to be generally nice to use,

    • behat --init gives you a bare bones directory structure to get started,
    • behat runs your tests as per $PWD/behat.yml, you can also specify a tagged/named subset of the tests to run,
    • behat -di shows you all contexts and their definitions, behat -dl is less verbose.
    • All PHP code is picked up by Behat through proper annotation, see examples below,
    • When you write a scenario that references non-existing contexts (e.g. When I hover over text "RSS TAGS" in region "menu"), it gives you a template of the PHP code you have to write to make it work.
  • Has a complex behat.yml (can get pretty long and deep). Allows you to configure the following (among others):

    • specify behat extensions and their config, e.g.:
      • mink and its driver (say selenium, the server URL, browser instance configuration),
      • drupal extensions (described below)
    • Contents of behat.yml can be appended (not overriddenn!) in JSON (!! and this fact seems to be not documented yet) using the environment variable BEHAT_PARAMS.

Behat Drupal Extension

Behat integrates with Drupal through an extension

  • Drupal interaction can be done in 3 ways:
    • Blackbox: assumes no privileges (cannot create nodes/users etc.), all communication over HTTP. The only benefit of the Drupal extension to Behat when using this driver is the set of predefined context steps),
    • Drush: similar to above except for users can be created. Some communication is over SSH (configuration is done using drush aliases),
    • Drupal API: the most powerful of all drivers, can perform all privileged actions (create nodes/users/vocabulary/taxonomy). Some communication is directly through Drupal source code (therefore this driver requires that Drupal is running on the same machine as the tests; in configuration docroot must be specified), Feature comparison of different drivers:
  • a bunch of (124 as of version 3.0.15) predefined contexts specifically for Drupal (see them via behat -dl), e.g. Given I am logged in as a user with the "administer-content" permission.
  • named regions can be specified via CSS selectors and then used in contexts, e.g. When I press "Contact" in "aside" region works because in behat.yml we have specified what aside means.
Behat Drupal Extension with Drupal API Driver

There are two components involved (separate composer packages): the Drupal Behat extension drupal/drupal-extension and the Drupal driver which the Behat extension uses: drupal/drupal-driver:

  • Drupal API Driver:
    • Regardless of your testing framework (be it Behat or PHPUnit) it allows you to drive Drupal.
    • Does not seem to have proper docs, but code is generally clear and all "modern PHP".
    • Provides an API (wrapping Drupal core's API). I can't find a full list but general cases are intended to be covered: Create/Read/Update/Delete nodes/entities/users/taxonomies/etc, e.g: Drupal\Driver\DrupalDriver::createNode().
    • It bootstraps Drupal and tries to avoid unnecessarily repeating bootstraps,
  • Drupal Behat Extension (w/ Drupal API driver):
    • Drupal API driver provides three additional Behat hooks, for example, @beforeUserCreate
    • has to know where the Drupal docroot is,
    • when privileged steps are defined in a test Scenario, it uses the Drupal Driver API to do things (actually writes to DB),
    • all such changes are recorded, and when tests are done all changes are undone (every createNode throughout the tests causes a deleteNode in the end),
    • Allows you to have breakpoints! You simply stick Then I break in your scenario in the appropriate place.
    • aside (unrelated to Drupal Extension): Behat hooks allow you to do nice things like this: capture a screenshot and HTML source for any test that fails.

Complete Behat example

Here's an example usage of Behat (with Drupal Extension and Drupal API driver) for DrupalSun:

  • a custom region:

      $ cat behat.yml
      default:
        suites:
          default:
            contexts:
              - FeatureContext # features/FeatureContext contains our custom steps
              - Drupal\DrupalExtension\Context\DrupalContext
              - Drupal\DrupalExtension\Context\MessageContext
              - Drupal\DrupalExtension\Context\MinkContext
              - Drupal\DrupalExtension\Context\MarkupContext
        extensions:
          Behat\MinkExtension:
            # base_url: http://localhost:8001 # don't specify since we cannot override!
            selenium2:
              browser: chrome # testing Chrome, requires the chrome driver: http://chromedriver.storage.googleapis.com/index.html
              # wd_host: http://localhost:4444/wd/hub # don't specify since we cannot override!
          Drupal\DrupalExtension:
            blackbox: ~ # use default configuration for blackbox driver
            api_driver: drupal
            drupal:
              drupal_root: /drupal/site
            region_map:
              menu: .region-highlighted # the CSS selector for DrupalSun's top menu
    
  • a custom context:

      $ cat features/bootstrap/FeatureContext.php
      <?php
    
      use Drupal\DrupalExtension\Context\RawDrupalContext;
      use Behat\Behat\Context\SnippetAcceptingContext;
      use Behat\Gherkin\Node\PyStringNode;
      use Behat\Gherkin\Node\TableNode;
    
      /**
       * Defines application features from the specific context.
       */
      class FeatureContext extends RawDrupalContext implements SnippetAcceptingContext {
    
        /**
         * Initializes context.
         *
         * Every scenario gets its own context instance.
         * You can also pass arbitrary arguments to the
         * context constructor through behat.yml.
         */
        public function __construct() {
        }
    
        /**
         * @When I hover over text :text in :region region
         */
        public function iHoverOverTextInRegion($text, $region) {
          $session = $this->getSession();
          $region_obj = $session->getPage()->find('region', $region);
          $items = $region_obj->findAll('css', '.region-highlighted .block-facetapi h2');
          foreach ($items as $item) {
            if (strpos($item->getText(), $text) !== FALSE) {
              $item->mouseOver();
              return;
            }
          }
          throw new \InvalidArgumentException(sprintf('No such thing: "%s"', $text));
        }
    
        /**
         * @Given the search index has been updated
         */
        public function updateSearchIndex() {
          // HACK search_api_index_items() updates the index but somehow does not
          // update the frontpage. When the same thing is called from drush ev it works!
          shell_exec("drush -r /drupal/site/ ev \"search_api_index_items(search_api_index_load('2'), -1)\"");
          // search_api_index_items(search_api_index_load('2'), -1);
        }
    
        /**
         * Hook to put field values into proper nested array format. This works around
         * the behavior of Drupal\Driver\Fields\Drupal7\DefaultHandler::expand():
         * It chokes if the object property (e.g. $node->field_something) is not
         * already an array.
         *
         * Hook provided by Drupal Extension to Behat:
         *
         * @beforeNodeCreate
         */
        public function fixFields($event) {
          $entity = $event->getEntity();
          $fields = array();
          foreach (get_object_vars($entity) as $name => $value){
            if (strpos($name, 'field_') === 0){
              $fields[] = $name;
            }
          }
          foreach ($fields as $field){
            $entity->$field = array($entity->$field);
          }
        }
      }
    
  • a feature with a scenario:

      $ cat features/test.feature
      Feature: Show more links
        Scenario: See option to show more authors
          Given I am an anonymous user
          When I hover over text "RSS TAGS" in "menu" region
          Then I should see the text "Show more"
    
  • extra Behat configuration (note the \\ escaping). These are the parts of the config that we only can specify once we are inside the test/CI environment:

      $ cat behat.json.extra
      {
        "extensions":{
          "Behat\\MinkExtension": {
            "base_url": "http://localhost:8080",
            "selenium2": {
              "wd_host": "http://172.17.42.1/wd/hub"
            }
          }
        }
      }
    
  • usage:

      $ java -jar selenium-server-standalone-2.45.0.jar &> selenium.log &
    
      $ tree features/
      features/
      |-- bootstrap
      |   `-- FeatureContext.php
      `-- test.feature
    
      $ cat composer.json
      {
        "require": {
          "behat/behat": "3.*`stable",
          "behat/mink-extension": "`stable",
          "behat/mink-selenium2-driver": "*",
          "drupal/drupal-extension": "*"
        }
      }
      $ composer install
      $ BEHAT_PARAMS="$(cat behat.json.extra)" vendor/bin/behat
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.