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.
-
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.
- commands (verbs) include:
-
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 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).
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.
-
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).
-
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 toclickAndWait
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.
-
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
- 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.
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);')
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.
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 namedwebdriverjs
for a while, now itswebdriverio
) - 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.
For end-to-end UI testing we need a testing component and a browser automation component. For example:
- Python: WebDriver bindings and unittest, nose, or behave if you like Gherkin).
- Ruby: WebDriver bindings and rspec or cucumber).
- PHP: (no official client) Facebook's client
or Mink with PHPUnit or
Behat).
- PHPUnit has its own [extension](https://phpunit.de/manual/current/en/selenium.html that allows you to speak to Selenium. However, note that does not yet fully support WebDriver (it fully supports RC which is the legacy API still maintained in Selenium2).
- JavaScript: WebDriver bindings
and Jasmine or
Mocha.
- One nice thing about using JavaScript is that making a web UI is really easy (in which case you must have a Selenium server running remotely).
-
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
-
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)
- 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 (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
-
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 functioniHaveAFileNamed($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 likephpunit.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 yourbehat.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 variableBEHAT_PARAMS
.
- specify behat extensions and their config, e.g.:
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 viabehat -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 inbehat.yml
we have specified whataside
means.
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 adeleteNode
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.
- Drupal API driver provides three additional Behat
hooks, for 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