Buy Access to Course
15.

Practice: Find Elements, Login with 1 Step and Debug

Share this awesome video!

|

Keep on Learning!

Look at the Product Admin Feature. When we built this earlier, we were planning the feature and learning how to write really nice scenarios. Now we know that most of the language we used matches the built-in definitions that Mink gives us for free.

Time to make these pass! Run just the "List available products" scenario on line 6. To do that type, ./vendor/bin/behat point to the file and then add :6:

./vendor/bin/behat features/product_admin.feature:6

The number 6 is the line where the scenario starts. This prints out some missing step definitions, so go head and copy and paste them into the FeatureContext class:

129 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 76
/**
* @Given there are :count products
*/
public function thereAreProducts($count)
{
// ... lines 82 - 91
}
// ... line 93
/**
* @When I click :linkName
*/
public function iClick($linkName)
{
// ... line 99
}
/**
* @Then I should see :count products
*/
public function iShouldSeeProducts($count)
{
// ... lines 107 - 110
}
// ... lines 112 - 129

Say what you need, but not more

For the thereAreProducts() function, change the variable to count and create a for loop:

129 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 76
/**
* @Given there are :count products
*/
public function thereAreProducts($count)
{
for ($i = 0; $i < $count; $i++) {
// ... lines 83 - 88
}
// ... lines 90 - 91
}
// ... lines 93 - 129

Inside, create some products and put some dummy data on each one:

129 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 81
for ($i = 0; $i < $count; $i++) {
$product = new Product();
$product->setName('Product '.$i);
$product->setPrice(rand(10, 1000));
$product->setDescription('lorem');
// ... lines 87 - 88
}
// ... lines 90 - 129

Why dummy data? The definition says that we need 5 products: but it doesn't say what those products are called or how much they cost, because we don't care about that for this scenario. The point is: only include details in your scenario that you actually care about.

We'll need the entity manager in a lot of places, so create a private function getEntityManager() and return $this->getContainer()->get() and pass it the service name that points directly to the entity manager:

129 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 120
/**
* @return \Doctrine\ORM\EntityManager
*/
private function getEntityManager()
{
return $this->getContainer()->get('doctrine.orm.entity_manager');
}
// ... lines 128 - 129

Perfect!

Back up in thereAreProducts(), add $em = $this->getEntityManager(); and the usual $em->persist($product); and an $em->flush(); at the bottom. This is easy stuff now that we've got Symfony booted:

129 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 81
for ($i = 0; $i < $count; $i++) {
// ... lines 83 - 87
$this->getEntityManager()->persist($product);
}
$this->getEntityManager()->flush();
// ... lines 92 - 129

Using "I Click" to be more Natural

Go to the next method - iClick() - and update the argument to $linkText:

129 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 93
/**
* @When I click :linkName
*/
public function iClick($linkName)
// ... lines 98 - 129

We want this to work just like the built-in "I follow" function. In fact, the only reason we're not just re-using that language is that nobody talks like that: we click things.

Anyways, the built-in functionality finds the link by its text, not a CSS selector. To use the named selector, add $this->getPage()->findLink(), pass it $linkText and then call click(); on that. Oh heck, let's be even lazier: just say, ->clickLink(); and be done with it:

129 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 98
$this->getPage()->clickLink($linkName);
// ... lines 100 - 129

This looks for a link inside of page and then clicks it.

Finally, in iShouldSeeProducts(), we're asserting that a certain number of products are shown on the page:

129 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 101
/**
* @Then I should see :count products
*/
public function iShouldSeeProducts($count)
// ... lines 106 - 129

In other words, once we get into the Admin section, we're looking for the number of rows in the product table.

There aren't any special classes to help find this table, but there's only one on the page, so find it via the table class:

129 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 106
$table = $this->getPage()->find('css', 'table.table');
// ... lines 108 - 129

Next, use assertNotNull() in case it doesn't exist:

129 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 107
assertNotNull($table, 'Cannot find a table!');
// ... lines 109 - 129

Now, use assertCount() and pass it intval($count) as the first argument:

129 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 109
assertCount(intval($count), $table->findAll('css', 'tbody tr'));
// ... lines 111 - 129

For the second argument, we need to find all of the <tr> elements inside of the table's <tbody>. Remember, once you find an element, you can search inside of it with find() or $table->findAll() to return an array of elements instead of just one. And don't forget that the first argument is still css: PhpStorm is yelling at me because I like to forget this. Ok, let's try that out!

./vendor/bin/behat features/product_admin.feature:6

Debugging Failures!

Ok, it gets further but still fails. It says:

Link "Products" not found

It's trying to find a link with the word "Products" but isn't having much luck. I wonder why? We need to debug! Right before the error, add:

And print last response

Run that one again:

./vendor/bin/behat features/product_admin.feature:6

Scroll up... up... up... all the way up to the top. Ahhh of course! We're on the login page. We forgot to login, so we're getting kicked back here.

Logging in... in one Step!

We already did all that login stuff in authentication.feature, and I'm tempted to copy and paste all of those lines to the top of this scenario:

14 lines | features/web/authentication.feature
// ... lines 1 - 7
And I am on "/"
When I follow "Login"
And I fill in "Username" with "admin"
And I fill in "Password" with "admin"
And I press "Login"
// ... lines 13 - 14

But, it would be pretty lame to need to put all of this at the top of pretty much every scenario. You know what would be cooler? To just say:

21 lines | features/web/product_admin.feature
// ... lines 1 - 6
Given I am logged in as an admin
// ... lines 8 - 21

Ooo another new step definition will be needed! Rerun the test and copy the function that behat so thoughtfully provides for us. As usual, put this in FeatureContext:

142 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 112
/**
* @Given I am logged in as an admin
*/
public function iAmLoggedInAsAnAdmin()
{
// ... lines 118 - 123
}
// ... lines 125 - 142

Using Mink, we'll do all the steps needed to login. First, go to the login page. Normally you'd say $this->getSession()->visit('/login'). But don't! Instead, wrap /login in a call to $this->visitPath():

142 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 115
public function iAmLoggedInAsAnAdmin()
{
// ... lines 118 - 119
$this->visitPath('/login');
// ... lines 121 - 123
}
// ... lines 125 - 142

This prefixes /login - which isn't a full URL - with our base URL: http://localhost:8000.

Once we're on the login page, we need to fill out the username and password fields and press the button. We could find this stuff with CSS, but the named selector is a lot easier. Say $this->getPage()->findField('Username')->setValue(). Ah, let's be lazier and do this all at once with fillField(). Pass this the label for the field - Username - and the value to fill in:

142 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 115
public function iAmLoggedInAsAnAdmin()
{
// ... lines 118 - 119
$this->visitPath('/login');
$this->getPage()->fillField('Username', 'admin');
// ... lines 122 - 123
}
// ... lines 125 - 142

But hold on: before we fill in the rest, don't we need to make sure that this user exists in the database? Absolutely, and fortunately, we already have a function that creates a user: thereIsAnAdminUserWithPassword(). Call that from our function and pass it the usual admin / admin:

142 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 115
public function iAmLoggedInAsAnAdmin()
{
$this->thereIsAUserWithPassword('admin', 'admin');
$this->visitPath('/login');
// ... lines 121 - 123
}
// ... lines 125 - 142

Finish by filling in the password field and pressing the button. For that, there's another shortcut: instead of findButton() then press(), use pressButton('Login'):

142 lines | features/bootstrap/FeatureContext.php
// ... lines 1 - 115
public function iAmLoggedInAsAnAdmin()
{
// ... lines 118 - 120
$this->getPage()->fillField('Username', 'admin');
$this->getPage()->fillField('Password', 'admin');
$this->getPage()->pressButton('Login');
}
// ... lines 125 - 142

This reproduces the steps from the login scenario, so that should be it! Run it!

./vendor/bin/behat features/product_admin.feature:6

We're in great shape.