Custom Assertions in Laravel Dusk

How to extend the Laravel Dusk Browser and write your own custom assertions for happier tests

Laravel's Dusk is a great way to write tests that run inside a real browser. It makes doing so very easy, so if you've ever gone through the process of setting up your own test environment with phantomjs or similar, Dusk will seem like a breeze by comparison.

It ships with a good number of default assertions but in this article I'll show you how to extend Laravel\Dusk\Browser so you can add your own.

Specifically, I wanted to add an assertion that checks whether a link is present that has both specific text and a specific URL. There's already an assertSeeLink($linkText) method which verifies that a link with the given text is present, but it does not check the link's href attribute.

Now, we could use the default assertSee($text) method to scan the whole page for the particular element and its href, but this is error-prone since we'd have to know the entire HTML of the anchor tag, including any classes or other attributes which could change.

Luckily, extending the default Browser and writing our own assertions is easy.

In a typical Dusk test, you'll see something like this:


namespace Tests\Browser;

use Tests\DuskTestCase;
use Laravel\Dusk\Browser;

class ExampleTest extends DuskTestCase
{
    public function testExample()
    {
        $this->browse(function(Browser $browser) {
            $browser->visit('/')->assertLink('Foo');
        });
    }
}

Instead of using the normal Laravel\Dusk\Browser, we'll create our own that extends from it, and insert some custom assertions. Let's start by extending the class, but without adding any custom functionality yet. Somewhere in your tests directory, you'll want to create a new file for your custom Dusk Browser. Mine is directly under tests/ in the Tests namespace. It looks like this:


namespace Tests;
use Laravel\Dusk\Browser;

class DuskBrowser extends Browser
{
}

Now, to get your tests to use this new DuskBrowser (or whatever you name it), you'll have to initialize it inside your test case. Your dusk tests probably inherit from the abstract Tests\DuskTestCase class which should have been created when you first set up Dusk. Inside that class, override the newBrowser method and tell it to return an instance of your custom DuskBrowser.


namespace Tests;

use Tests\DuskBrowser;
use Laravel\Dusk\TestCase as BaseTestCase;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;

abstract class DuskTestCase extends BaseTestCase
{
    use CreatesApplication;

    ...

    /**
     * Create the DuskBrowser instance.
     *
     * @param  \Facebook\WebDriver\Remote\RemoteWebDriver  $driver
     * @return \Laravel\Dusk\Browser
     */
    protected function newBrowser($driver)
    {
        return new DuskBrowser($driver);
    }
}

Now in any test that inherits from DuskTestCase, your custom browser will be available. This is a great time to run your Dusk tests with the php artisan dusk command to make sure that everything is working as expected.

Of course, you don't have to override newBrowser in the parent class. You can override it in any TestCase you'd like. You could use the normal browser for certain tests, and custom browsers for others.

Also, you can optionally typehint the new DuskBrowser in your calls to browse during tests if you want. It should still work if the typehint is set to the normal Browser since your class inherits from it, but it's nice to be specific so that you (and all future yous) know exactly what you're working with:


namespace Tests\Browser;

use Tests\DuskTestCase;
use Tests\DuskBrowser;

class ExampleTest extends DuskTestCase
{
    public function testExample()
    {
        $this->browse(function(DuskBrowser $browser) {
            $browser->visit('/')->assertSee('Foo');
        });
    }
}

With that set up, you can now write your own custom assertions in your new DuskBrowser class! Exciting, I know.

Here's the method I implemented for the use case described above, asserting that a link is present with a specific href and text:


namespace Tests;

use Laravel\Dusk\Browser;
use PHPUnit\Framework\Assert as PHPUnit;

class DuskBrowser extends Browser
{
    public function assertSeeLinkWithHref($link, $href)
    {
        // set up the error messages for PHPUnit
        $message = "Did not see expected link [{$link}] with href [{$href}].";
        if ($this->resolver->prefix) {
            $message .= " within [{$this->resolver->prefix}].";
        }

        // find the anchor element by its href attribute
        $anchor = $this->resolver->find("a[href$='{$href}']");

        // check if the anchor's innerText matches what was passed
        $hasText = $anchor && (strpos($anchor->getText(), $link) !== false);

        // make the PHPUnit assertion for both href & text
        PHPUnit::assertTrue($anchor && $hasText, $message);

        return $this;
    }
}

The method above takes the $link and $href strings, searches for the <a> element using a CSS attribute selector executed by the find method of the Laravel\Dusk\ElementResolver, then makes sure the innerText of the link matches what we expect.

I decided to use the suffix form of the attribute selector [attr$=value] since the trailing part of the URL is what I'm most interested in. This is handy since some links may use FQDNs while others may use relative paths. The suffix selector can match both, assuming you test for the path and not the full domain.

One more method was added called assertSeeNav to make it easier to check that the entire nav was present on each page without having to write a lot of boilerplate. You can do something similar with any code that you find yourself repeating in multiple tests, such as checking for the footer, sign-in buttons, logos, or other common page elements.


public function assertSeeNav()
{
    $this->assertSeeLinkWithHref('Articles', '/articles')
         ->assertSeeLinkWithHref('About', '/about')
         ->assertSeeLinkWithHref('Code', '/code')
         ->assertSeeLinkWithHref('Work', '/work')
         ;

    return $this;
}

Both methods return $this at the end so that the method calls can be chained, which is how all the other Dusk tests work. I encourage you to take a look at the source of Laravel\Dusk\Browser and Laravel\Dusk\Concerns\MakesAssertions to get an idea of how the default assertions work, and especially how they interact with page elements using the classes in Facebook\WebDriver.

Anyway, I hope you've found this useful. You can read more about Dusk in the official documentation and if you have any questions, feel free to contact me!