Blog

Writing Better CSS Selectors in Dusk

One of the hardest parts of writing acceptance tests in a tool like Laravel Dusk is choosing selectors for targeting elements. Since these tools use CSS selectors, you suffer all the same problems you do in writing good CSS: poorly-chosen selectors make your code base brittle, hard to maintain, and frustrating to reason about. Tests that fail every time someone changes any part of the front-end are not good tests.

Fortunately, there are some tools and tricks you can use to make choosing good selectors in your Dusk tests less painful. Let’s explore a few ways we can improve our workflow and choose more resilient selectors in Dusk.

Choosing specific selectors

When writing selectors in Dusk, you are constantly balancing the specificity of your selectors. Selectors like ul li are generic, and therefore easily overwritten if someone were to introduce another ul li somewhere in the codebase. While selectors like div > .home-nav ul.nav-list li are more specific, and often depend on DOM structure outside the scope of the area under test, they can break when even small changes are made to markup anywhere in the DOM tree.

Managing this balance between generality and specificity involves an arduous process of decision-making and often leads to frustratingly brittle test suites.

Personally, I prefer the complications associated with being overly specific. I find the failures easier to identify and fix. In cases where I have no control over the codebase under test, I utilize Dusk’s “page objects” to alias the full CSS selector of an element:

Page objects are a feature in Dusk that contain class aliases and custom methods, and are able to assert Dusk’s browser is actually on the page. They are helpful for managing duplication and verbosity in your tests.

// Page Object

class TwitterTimeline extends Page
{
  public function elements()
    {
        return [
            '@post-tweet-button' => '#timeline > div.timeline-tweet-box > div > form > div.TweetBoxToolbar > div.TweetBoxToolbar-tweetButton.tweet-button > button',
        ];
    }
}
// Dusk Test

$browser
    ->visit(new TwitterTimeline)
    ->click('@post-tweet-button');

Note: to get the full CSS selector of an element, you can use Chrome DevTool’s “Copy selector” feature.

Chrome Copy Selector

Although this is an improvement to our workflow, it’s still prone to breaking. These selectors are one-sided, and the person working on the front-end is completely unaware of this dependency.

Let’s explore a way we can write selectors that are self-evident, meaning they communicate their purpose to the developer.

Separating concerns

Consider the dual purpose of our CSS classes: not only are they being used as identifiers for styling markup, they are also being used as identifiers for external systems like JavaScript and Dusk.

In the jQuery days, we encountered this same problem when writing event listeners or manipulating the DOM. The solution that emerged was adding prefixes like .js, making JavaScript-only classes like .js-some-selector:

$('.js-some-selector').on('click', function (e) {...

Let’s take a cue from the JavaScript world, and do the same for our browser tests:

<button class="dusk-post-tweet-button">
    Post Tweet
<button>
// Page Object

class TwitterTimeline extends Page
{
  public function elements()
    {
        return [
            '@post-tweet-button' => '.dusk-post-tweet-button',
        ];
    }
}
// Dusk Test

$browser
    ->visit(new TwitterTimeline)
    ->click('@post-tweet-button');

Exploring other identifiers

Although CSS selectors like .dusk-create-post-button solve our dependency problem, we can still do a little bit better. Let’s separate these “Dusk hooks” from our other CSS selectors by creating a stand-alone HTML attribute called dusk:

<button class="..." dusk="post-tweet-button">
    Post Tweet
<button>

Note: most modern browsers support custom attributes; otherwise, we would have to use a data attribute like data-dusk. Thanks to @stauffermatt for the suggestion.

Now, Dusk can “hook” into these elements using selectors like the following:

<button class="..." dusk="post-tweet-button">
    Post Tweet
<button>
// Page Object

class TwitterTimeline extends Page
{
  public function elements()
    {
        return [
            '@post-tweet-button' => '[dusk="post-tweet-button"]',
        ];
    }
}
// Dusk Test
$browser
    ->visit(new TwitterTimeline)
    ->click('@post-tweet-button');

Automatic dusk hooks

I recently pull-requested this feature, and now Dusk 2.0 supports automatically-resolved dusk hooks out of the box. No need for registering your hooks in page objects:

<button class="..." dusk="post-tweet-button">
    Post Tweet
<button>
// Page Object

class TwitterTimeline extends Page
{
  public function elements()
  {
    return [
        // No need to register hook here. 
    ];
  }
}
// Dusk Test

$browser
    ->visit(new TwitterTimeline)
    ->click('@post-tweet-button'); // automatically resolves to: '[dusk="post-tweet-button"]'

Wrapping it up

The selectors you write serve as Dusk’s interface with your application; poorly-written selectors can spell trouble for the dependability of your test suite. By writing more specific, self-evident selectors, you can make your Dusk tests more resilient to changes, saving you time, hassle, and trouble.

Your CI pipeline will thank you!

Good luck, and have fun!

×

Got an idea? Let's talk.

Leave us a note here, or give us a call at (312) 448-7405.