If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
In search.feature
:
... lines 1 - 9 | |
When I fill in "searchTerm" with "<term>" | |
And I press "search_submit" | |
... lines 12 - 18 |
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.
Whenever you want to cheat or can only find something via CSS, there's a simple solution: use new language and create a custom definition. Change the first line to:
... lines 1 - 9 | |
When I fill in the search box with "<term>" | |
... lines 11 - 18 |
If I can't target it with real text, I'll just use some natural language. PhpStorm highlights the line because we don't have a definition function matching this text. For the second problem line, use
... lines 1 - 10 | |
And I press the search button | |
... lines 12 - 18 |
You know the drill: it's time to run the scenario. It prints out the two functions we need to fill in:
... lines 1 - 85 | |
/** | |
* @When I fill in the search box with :arg1 | |
*/ | |
public function iFillInTheSearchBoxWith($arg1) | |
{ | |
throw new PendingException(); | |
} | |
/** | |
* @When I press the search button | |
*/ | |
public function iPressTheSearchButton() | |
{ | |
throw new PendingException(); | |
} | |
... lines 101 - 102 |
Filling these in shouldn't be hard: we're pretty good with Mink. But,
how can we access the Mink Session? There's a couple ways to get it,
but the easiest is to make FeatureContext
extend RawMinkContext
:
... lines 1 - 6 | |
use Behat\MinkExtension\Context\RawMinkContext; | |
... lines 9 - 13 | |
class FeatureContext extends RawMinkContext implements Context, SnippetAcceptingContext | |
... lines 15 - 115 |
This gives us access to a bunch of functions: the most important being
getSession()
and another called visitPath()
that we'll use later:
... lines 1 - 105 | |
public function getSession($name = null) | |
{ | |
return $this->getMink()->getSession($name); | |
} | |
... lines 111 - 128 | |
public function visitPath($path, $sessionName = null) | |
{ | |
$this->getSession($sessionName)->visit($this->locatePath($path)); | |
} | |
... lines 133 - 166 |
On the first method, change arg1
to term
:
... lines 1 - 86 | |
/** | |
* @When I fill in the search box with :term | |
*/ | |
public function iFillInTheSearchBoxWith($term) | |
... lines 91 - 115 |
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'
:
... lines 1 - 91 | |
$searchBox = $this->getSession() | |
->getPage() | |
->find('css', 'input[name="searchTerm"]'); | |
assertNotNull($searchBox, 'Could not find the search box!'); | |
... lines 97 - 115 |
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
.
... lines 1 - 97 | |
$searchBox->setValue($term); | |
... lines 99 - 115 |
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:
... lines 1 - 103 | |
public function iPressTheSearchButton() | |
{ | |
$button = $this->getSession() | |
->getPage() | |
->find('css', '#search_submit'); | |
assertNotNull($button, 'Could not find the search button!'); | |
$button->press(); | |
} | |
... lines 114 - 115 |
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();
:
... lines 1 - 112 | |
/** | |
* @return \Behat\Mink\Element\DocumentElement | |
*/ | |
private function getPage() | |
{ | |
return $this->getSession()->getPage(); | |
} | |
... lines 120 - 121 |
I'll put a little PHPDoc above this so next month we'll remember what this is.
Now we can shorten both definition functions a bit with $this->getPage()
:
... lines 1 - 89 | |
public function iFillInTheSearchBoxWith($term) | |
{ | |
$searchBox = $this->getPage() | |
->find('css', 'input[name="searchTerm"]'); | |
... lines 94 - 97 | |
} | |
... lines 99 - 102 | |
public function iPressTheSearchButton() | |
{ | |
$button = $this->getPage() | |
->find('css', '#search_submit'); | |
... lines 107 - 110 | |
} | |
... lines 112 - 121 |
Test the final scenarios out. Perfect! Now we have access to Mink inside of
FeatureContext
and we know that including CSS inside of scenarios is
not the best way to make friends.
One more quick shortcut. Thanks to the RawMinkContext
base class, we also have
access to a cool object called WebAssert
through the assertSession()
method.
Replace getPage()
with assertSession()
and find()
with elementExists()
. Now,
remove the assertNotNull()
call:
... lines 1 - 13 | |
class FeatureContext extends RawMinkContext implements Context, SnippetAcceptingContext | |
{ | |
... lines 16 - 89 | |
public function iFillInTheSearchBoxWith($term) | |
{ | |
$searchBox = $this->assertSession() | |
->elementExists('css', 'input[name="searchTerm"]'); | |
$searchBox->setValue($term); | |
} | |
... lines 97 - 115 | |
} |
The elementExists
finds the element and asserts that it exists all at once.
Nice! Make the same changes for pressing the button:
... lines 1 - 100 | |
public function iPressTheSearchButton() | |
{ | |
$button = $this->assertSession() | |
->elementExists('css', '#search_submit'); | |
$button->press(); | |
} | |
... lines 108 - 117 |
The WebAssert class has a bunch of other handy methods on it - check them out.
Hey Sumeet!
Actually, this makes *perfect* sense, and it illustrates the (only) difference between MinkContext and RawMinkContext:
A) RawMinkContext gives you access to the Mink session (via $this->getSession())
B) MinkContext also gives you access to the Mink session (via $this->getSession()). AND, it gives you a big list of built-in definitions (like Given I am on *, and Then I should see *).
If you look at MinkContext (https://github.com/Behat/Mi... you'll see that it extends RawMinkContext and then also adds a bunch of these "definitions".
In our tutorial, we use RawMinkContext... but in the next chapter (https://knpuniversity.com/s... we tell Behat to load *our* FeatureContext file and ALSO MinkContext. There's not really any big advantage of doing it this way (versus making your FeatureContext extend MinkContext), it's just 2 different ways of importing the definitions from MinkContext.
I hope that helps!
Cheers!
What do I do if there are 2 duplicate buttons on the page in different areas. How can I choose one over the other.
Hey Lindsay!
GREAT question - it's one of the trickiest things, but ultimately has an easy solution. First, there's basically an example here: https://knpuniversity.com/s.... Basically, the trick is to use CSS. You could use CSS to select the specific button, but I usually just use CSS to select some wrapper element that is unique to only *one* of the buttons. THEN, I call pressButton('Button Name') on that element.
I hope that helps!
// composer.json
{
"require": {
"php": ">=5.4.0, <7.3.0",
"symfony/symfony": "^2.7", // v2.7.4
"twig/twig": "^1.22", // v1.22.1
"sensio/framework-extra-bundle": "^3.0", // v3.0.16
"doctrine/doctrine-bundle": "^1.5", // v1.5.1
"doctrine/orm": "^2.5", // v2.5.1
"doctrine/doctrine-fixtures-bundle": "^2.2", // v2.2.1
"behat/symfony2-extension": "^2.0" // v2.0.0
},
"require-dev": {
"behat/mink-extension": "^2.0", // v2.0.1
"behat/mink-goutte-driver": "^1.1", // v1.1.0
"behat/mink-selenium2-driver": "^1.2", // v1.2.0
"phpunit/phpunit": "^4.8" // 4.8.18
}
}
When i use MinkContext (Behat\MinkExtension\Context\MinkContext;) instead of RawMinkContext it works for me otherwise I get
FeatureContext has missing steps. Define them with these snippets:
/**
* @Given I am on :arg1
*/
public function iAmOn($arg1)
{
throw new PendingException();
}
/**
* @Then I should see :arg1
*/
public function iShouldSee($arg1)
{
throw new PendingException();
}