In search.feature:
| When I fill in "searchTerm" with "<term>" |
| And I press "search_submit" |
This searchTerm is the name attribute of the search box. And search_submit is the id of its submit button. Well, listen up y'all, I'm about to tell you one of the most important things about working with Behat: Almost every built-in definitions finds elements using the "named" selector, not CSS.
For example, look at the definition for
I fill in "field" with "value"
To use this, you should pass the label text to "field", not the id, name attribute or CSS selector. Clicking a link is the same. That's done with the
I follow "link"
Where the link must be the text of the link. If you pass a CSS selector, it's not going to work. If I changed search_submit to be a CSS selector, it'll fail. Believe me, I've tried it a bunch of times.
Got it? Ok: in reality, the named selector lets you cheat a little bit. In addition to the true "text" of a field, it also searches for the name attribute and the id attribute. That's why our scenario works.
But please please - don't use the name or id. In fact, The cardinal rule in Behat is that you should never use CSS selectors or other technical things in your scenario. Why? Because the person who is benefiting from the feature is a web user, and we're writing this from their point of view. A web user doesn't understand what searchTerm or search_submit means. That makes your scenario less useful: it's technical jargon instead of behavior descriptions.
So why did we cheat? Well, the search field doesn't have a label and the button doesn't have any text. I can't use the named selector to find these, unless I cheat.
XPath for Click()
/**
* @When I click :arg1
*/
public function iClick($arg1)
{
$clickOn = $this->getSession()
->getPage()
->find("xpath",".//a[text()='" . $arg1 . "']")
->click();
}
Traversing Pages
Most usages of Mink will involve working with the page opened in your browser. This is done thanks to the powerful Element API. This API allows to traverse the page (similar to the DOM in Javascript), manipulate page elements and to interact with them, which will be covered in the next chapters.
DocumentElement and NodeElement
The Element API consists of 2 main classes. The DocumentElement instance represents the page being displayed in the browser, while the NodeElement class is used to represent any element inside the page. Both classes share a common set of methods to traverse the page (defined in TraversableElement).
The DocumentElement instance is accessible through the Session::getPage method:
$page = $session->getPage();
// You can now manipulate the page.
Note
The DocumentElement instance represents the <html> node in the DOM. It is equivalent to document.documentElement in the Javascript DOM API.
Traversal Methods
Elements have 2 main traversal methods: ElementInterface::findAll returns an array of NodeElement instances matching the provided selector inside the current element while ElementInterface::find returns the first match or null when there is none.
The TraversableElement class also provides a bunch of shortcut methods on top of find() to make it easier to achieve many common use cases:
ElementInterface::has
- Checks whether a child element matches the given selector but without returning it.
TraversableElement::findById
- Looks for a child element with the given id.
TraversableElement::findLink
- Looks for a link with the given text, title, id or
alt attribute (for images used inside links).
TraversableElement::findButton
- Looks for a button with the given text, title, id,
name attribute or alt attribute (for images used inside links).
TraversableElement::findField
- Looks for a field (
input, textarea or select) with the given label, placeholder, id or name attribute.
Note
These shortcuts return a single element. If you need to find all matches, you will need to use findAll with the named selector.
Nested Traversing
Every find*() method will return a Behat\Mink\Element\NodeElement instance and findAll() will return an array of such instances. The fun part is that you can use the same methods of traversing on such elements as well:
$registerForm = $page->find('css', 'form.register');
if (null === $registerForm) {
throw new \Exception('The element is not found');
}
// find some field INSIDE form with class="register"
$field = $registerForm->findField('Email');
Selectors
The ElementInterface::find and ElementInterface::findAll methods support several kinds of selectors to find elements.
CSS Selector
The css selector type lets you use CSS expressions to search for elements on the page:
$title = $page->find('css', 'h1');
$buttonIcon = $page->find('css', '.btn > .icon');
XPath Selector
The xpath selector type lets you use XPath queries to search for elements on the page:
$anchorsWithoutUrl = $page->findAll('xpath', '//a[not(@href)]');
Caution
This selector searches for an element inside the current node (which is <html> for the page object). This means that trying to pass it the XPath of an element retrieved with ElementInterface::getXpath will not work (this query includes the query for the root node). To check whether an element object still exists on the browser page, use ElementInterface::isValid instead.
Named Selectors
Named selectors provide a set of reusable queries for common needs. For conditions based on the content of elements, the named selector will try to find an exact match first. It will then fallback to partial matching if there is no result for the exact match. The named_exact selector type can be used to force using only exact matching. The named_partial selector type can be used to apply partial matching without preferring exact matches.
For the named selector type, the second argument of the find() method is an array with 2 elements: the name of the query to use and the value to search with this query:
$topLink = $page->find('named', array('link', $escapedValue));
The following queries are supported by the named selector:
id
- Searches for an element by its id.
id_or_name
- Searches for an element by its id or name.
link
- Searches for a link by its id, title, img alt, rel or text.
button
- Searches for a button by its name, id, text, img alt or title.
link_or_button
- Searches for both links and buttons.
content
- Searches for a specific page content (text).
field
- Searches for a form field by its id, name, label or placeholder.
select
- Searches for a select field by its id, name or label.
checkbox
- Searches for a checkbox by its id, name, or label.
radio
- Searches for a radio button by its id, name, or label.
file
- Searches for a file input by its id, name, or label.
optgroup
- Searches for an optgroup by its label.
option
- Searches for an option by its content or value.
fieldset
- Searches for a fieldset by its id or legend.
table
- Searches for a table by its id or caption.
Custom Selector
Mink lets you register your own selector types through implementing the Behat\Mink\Selector\SelectorInterface. It should then be registered in the SelectorsHandler which is the registry of available selectors.
The recommended way to register a custom selector is to do it when building your Session:
$selector = new \App\MySelector();
$handler = new \Behat\Mink\Selector\SelectorsHandler();
$handler->registerSelector('mine', $selector);
$driver = // ...
$session = new \Behat\Mink\Session($driver, $handler);
/**
* Click on the element with the provided xpath query
*
* @When /^I click on the element with xpath "([^"]*)"$/
*/
public function iClickOnTheElementWithXPath($xpath)
{
$session = $this->getSession(); // get the mink session
$element = $session->getPage()->find(
'xpath',
$session->getSelectorsHandler()->selectorToXpath('xpath', $xpath)
); // runs the actual query and returns the element
// errors must not pass silently
if (null === $element) {
throw new \InvalidArgumentException(sprintf('Could not evaluate XPath: "%s"', $xpath));
}
// ok, let's click on it
$element->click();
}
|
And how do we use it inside a “feature” file?
|
|
Given I click on the element with xpath "//a[@id='14']"
|
UPDATE:
Using a jQuery like CSS Selector is pretty easy. Looks like Mink also allows CSS Selectors by default. Here’s the same method with CSS Selector:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/**
* Click on the element with the provided CSS Selector
*
* @When /^I click on the element with css selector "([^"]*)"$/
*/
public function iClickOnTheElementWithCSSSelector($cssSelector)
{
$session = $this->getSession();
$element = $session->getPage()->find(
'xpath',
$session->getSelectorsHandler()->selectorToXpath('css', $cssSelector) // just changed xpath to css
);
if (null === $element) {
throw new \InvalidArgumentException(sprintf('Could not evaluate CSS Selector: "%s"', $cssSelector));
}
$element->click();
}
|
And we use it in the feature file like:
|
|
Given I click on the element with css selector "a#14"
|
The test should fail in case a matching element was not found. Let me know if you run into any issues or find even better or elegant ways to do this.
I was just wondering if you could point out where the problem is here, if it's Behat, the CSS selector, or the Sahi Driver.
We have just upgraded to Behat 3 and are using Sahi Driver (most recent open source version). We have found that any Behat test that uses the pseudo-element first-child now seems to break.
Example:
Step:
And I follow "#the-list tr:first-child .row-title"
(which contains an anchor element with the class row-title on it, see HTML)
Error:
Link with id|title|alt|text "#the-list tr:first-child .row-title" not found. (Behat\Mink\Exception\ElementNotFoundException)
HTML:
<tbody id="the-list">
<tr id="post-162382" class="post-162382 type-post status-publish format-standard hentry category-uncategorized alternate iedit author-other level-0">
<th class="check-column" scope="row"></th>
<td class="post-title page-title column-title">
<strong>
<a class="row-title" title="Edit “Post Title”" href="https://domain.com"></a>
</strong>
<div class="locked-info"></div>
<div class="row-actions"></div>
<div id="inline_162382" class="hidden"></div>
</td>
CSSSelector.php (override we used with our old Behat, we left this file in)
/**
* Makes all of the form field related steps allow CSS selectors.
*/
class CSSSelectorContext extends MinkContext
{
/**
* Finds an element via CSS or fails with a given error
*
* @param $element string A CSS selector string
* @param $error Exception An error to throw in case the element can not be found
* @return object An Element object
*/
protected function findOrFail($element, $error){
$element = $this->getSession()->getPage()->find('css', $element);
if (!isset($element)){
throw $error;
}
return $element;
}
public function clickLink($link) {
try {
parent::clickLink($link);
return;
}catch (Exception $error){
$link = $this->fixStepArgument($link);
$link = $this->findOrFail($link, $error);
$link->press();
}
}
When using the css selector in the Chrome console with jquery it selects the appropriate element. I went through the code and looked at the css -> xpath translations and then validated the xpath against the html that is produced on the site we are testing and it seems to be valid as well. The css selector also works with Goutte driver.
Generated XPath:
find(function(){
var count = 0;
while (_sahi._byXPath("("+"\/\/html\/descendant-or-self::*[@id = 'the-list']\/descendant-or-self::*\/*[name() = 'tr' and (position() = 1)]\/descendant-or-self::*\/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' row-title ')]"+")["+(count+1)+"]")) count++;
return count;
})()
descendant-or-self::*[@id = 'the-list']/descendant-or-self::*/*[name() = 'tr' and (position() = 1)]/descendant-or-self::*/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' row-title ')]
//html/descendant-or-self::*[@id = 'the-list']/descendant-or-self::*/*[name() = 'tr' and (position() = 1)]/descendant-or-self::*/*[@class and contains(concat(' ', normalize-space(@class), ' '), ' row-title ')]
When I change the CSS to:
Step:
And I follow "#the-list tr .row-title"
It works because I believe it just picks the first tr from a list of them anyway, but we want to be able to use first-child of course.
Thanks for your help!
asked Nov 26 '14 at 16:16
Sorry for being late to the party
Your problem here is the fact that Minks own step "I follow"/"clickLink" doesn't accept the following:
- "#"s for ID's
- Anything other than an ID, Text, Title or Alternate Value.
I suggest using a "click on the" step instead of a "follow", something like this:
/**
* @Then /^(?:|I )click (?:|on )(?:|the )"([^"]*)"(?:|.*)$/
*/
public
function iClickOn($arg1)
{
$findName = $this->getSession()->getPage()->find("css", $arg1);
if (!$findName) {
throw new Exception($arg1 . " could not be found");
} else {
$findName->click();
}
}
You're using CSS right here, which will allow for this to be written:
Then I click on the "#the-list tr:first-child .row-title" link
It is a mistake I also made, and this is the solution we decided on then, and we haven't had to look back.
Added some protected variables and setup of main constructor (which is empty by default):
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/**
* @var \RemoteWebDriver
*/
protected $webDriver;
protected $session;
/**
* 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()
{
$this->webDriver = new \Behat\Mink\Driver\Selenium2Driver();
$this->session = new \Behat\Mink\Session($this->webDriver);
}
|
NOTE: you can use aliases for shorter typing, but in this example I left all as it is, for simplicity.
I struggled to maximize the window of browser on initial test runs, that caused me some issues in testing, but thanks to James Morgan (https://gist.github.com/jimjam88/6596194) a bypass was made and i added it into a beforeScenario section:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
/** @BeforeScenario */
public function before($event)
{
$wd = $this->getSession()->getDriver()->getWebDriverSession();
$wd->window($wd->window_handle())->maximize();
}
/** @AfterScenario */
public function after($event)
{
$this->getSession()->stop();
}
|
The @BeforeScenario and @AfterScenario tags will give us additional handlers what to happen between scenarios. As mentioned before each scenario the code from James Morgan will maximize the browser so we can have better handling. In the after scenario section i decided to stop the Session in order to force Behat to reopen the browser for each scenario. Remowing that code itself will execute the test faster in a single test, but for some own purposes i did it this way.
Gherkin/Behat mostly can create background code to execute commands, and it is well set, but as mentioned i decided to handle the links on my own, so i created a function:
|
1
2
3
4
5
6
7
8
9
10
|
/**
* @When I click :arg1
*/
public function iClick($arg1)
{
$clickOn = $this->getSession()
->getPage()
->find("xpath",".//a[text()='" . $arg1 . "']")
->click();
}
|
his gives us access to a bunch of functions: the most important being getSession() and another called visitPath() that we'll use later:
| public function getSession($name = null) |
| return $this->getMink()->getSession($name); |
| public function visitPath($path, $sessionName = null) |
| $this->getSession($sessionName)->visit($this->locatePath($path)); |
On the first method, change arg1 to term:
| * @When I fill in the search box with :term |
| public function iFillInTheSearchBoxWith($term) |
Once you're inside of FeatureContext it's totally OK to use CSS selectors to get your work done.
Back in the browser, inspect the search box element. It doesn't have an id but it does have a name attribute - let's find it by that. Start with $searchBox = $this->getSession()->getPage(). Then, to drill down via CSS, add ->find('css', '[name="searchTerm"]');. I'm going to add an assertNotNull() in case the search box isn't found for some reason. Fill that in with $searchBox, 'The search box was not found':
| $searchBox = $this->getSession() |
| ->find('css', 'input[name="searchTerm"]'); |
| assertNotNull($searchBox, 'Could not find the search box!'); |
Now that we have the individual element, we can take action on it with one of the cool functions that come with being an individual element, like attachFile, blur, check, click and doubleClick. One of them is setValue() that works for field. Set the value to $term.
| $searchBox->setValue($term); |
This is a perfect step definition: find an element and do something with it.
To press the search button, we can do the exact same thing. $button = $this->getSession()->getPage()->find('css', '#search_submit');. And assertNotNull($button, 'The search button could not be found'). It's always a good idea to code defensively. This time, use the press() method:
| public function iPressTheSearchButton() |
| $button = $this->getSession() |
| ->find('css', '#search_submit'); |
| assertNotNull($button, 'Could not find the search button!'); |
We're ready to run the scenario again. It passes!
That was more work, but it's a better solution. With no CSS inside of our scenarios, they're less dependent on the markup on our site and this is a heck of a lot easier to understand than before with the cryptic name and ids.
To save time in the future, create a private function getPage() and return $this->getSession()->getPage();:
| * @return \Behat\Mink\Element\DocumentElement |
| private function getPage() |
| return $this->getSession()->getPage(); |
I'll put a little PHPDoc above this so next month we'll remember what this is.