Build Testing Rules for Your Design System

Discover how to build advanced testing rules with ESLint to ensure compliance with Design System best practices

Backlight
Quick Code
10 min readMar 16, 2022

--

Welcome to part 2 of the 3-parts “Design System Best Practices with ESLint”.

This series aims to help you encourage those consuming your design system to follow your best practices.

We covered a lot of theory in part 1 (Use ESLint to Enforce Design System, if you have not read it yet, you should start with it), such as how ESLint works under the hood. We built some first simple rules with ESLint.

In this part 2, we will cover how to build more complex ESLint rules.

A quick reminder of Part 1

Back in the first article, we talked about how you’ll be creating two rules:

  • Avoiding inline styles in your elements
  • Ensuring that tooltips don’t contain interactive content.

Let’s take a moment to talk about why we’ve chosen these rules.

For starters, they’re both great rules to act as introductions to writing rules. While the implementations we cover in these articles may not be production-ready, they’ll help solidify the learning you’ve made so far.

Aside from being a teaching aid, there’s also a practical application for both these rules.

For our first rule, you might want to encourage your end-users to be consistent with their styling implementation, which may help with readability and maintenance. CSS preference varies from person-to-person, and from library-to-library, so you may find that this particular rule doesn’t apply to your library. However, targeting a specific attribute is very useful. For instance, you might want to discourage the use of tab-indexes on non-interactive elements, or you might want to encourage the use of an attribute for a certain element, like the type attribute for a button component.

We’ve chosen the second rule because we’ll be using an existing design system as an example, the Simba design system, which is written using the Lion web components.

In the Simba design system docs for the tooltip element, the opening description states:

“Interactive content should not be placed in a tooltip content slot. Tooltips are just meant for showing additional information.”

This might be enough to prevent some end-users from not using interactive content within the tooltip, but some developers may not read (or remember) all rules from the docs. Having an ESLint rule that enforces the rules stated will make it much easier for people to remember and abide by the rules of your framework.

Setting up our project

Let’s begin by cloning the starter repo. The repo covers a lot of the tedious setup such as:

  • Installing packages
  • ESLint boilerplate
  • Setting up test suites

To get started with the repo, follow the following steps:

The demo repo was set up using the ESLint guide.

Here’s a quick rundown of the starter repo:

lib/rules

lib/rules contain our rules. We've prepared the first one for you by writing the boilerplate. We've added some JSDoc annotations to the file, as per the working with rules' recommendation. This will give you some sweet, sweet intellisense to help when writing our lint logic.

tests/lib/rules

tests/lib/rules contain the tests we'll write for our rules. We've added a handful for the no-inline-styles, but you're welcome to add even more if you like. We're using ESLint's built-in RuleTester to test our rules. We've created an instance of the RuleTester and am calling run with several valid and invalid cases.

valid is an array of stringified JavaScript that shouldn't report linting errors.

invalid is an array of objects that contains two properties: code and errors. code is the stringified JavaScript that will report linting errors. The errors property is an array of objects. These objects can hold different types of assertions, so not only can we check to see if the snippet fails the lint rule, but we can check to see the error message we get back and the location of the error in our IDE, this is how VSCode determines where to place the squiggly lines.

Once you’re familiar with the repo, you can start the test runner by running npm test. The test runner will run every time you make a change to the rules file, which will provide immediate feedback when something goes wrong or, more hopefully, right

Creating a simple ESLint rule

The first rule we’ll be writing will display an error message if a lit element contains inline styles.

Creating our visitor function

Let’s begin by jumping into the lib/rules/no-inline-styles.js file.

If you remember from the first article when our JavaScript gets parsed the html function gets represented as a TaggedTemplateExpression node in the AST. We can ask ESLint to visit this node whenever it reaches it by defining a TaggedTemplateExpression in the rule's create function.

Don’t forget that not all TaggedTemplateExpressions will be the html expression that we want to run our lint rule against. We'll want to check the function's name and to see if it's "html".

Try filling out the create function with the above logic, or you can peek below for our answer.

The next step is to take the node and parse it into a valid HTML AST.

Parsing our HTML

Since the logic to parse our node isn't part of our rule's business logic, we’ve created a class stub in a separate utilities directory. So jump over to the utils/index.js file.

We need to do two things here:

  • Convert our template expression into an HTML string
  • Parse our string into an HTML AST and store it as an instance variable

For the first task, let’s take the easy route and behave as if our Lit expressions will contain no dynamic content.

In other words let’s just worry about:

and not:

We’ll provide the solution below, but try and use the tools we’ve run through in this article to solve this problem yourself.

You can either give it a shot yourself, or view the solution below:

Tip 1: Don’t forget to use the AST explorer to understand how to get the HTML string from the template expression

Tip 2: To parse your string to HTML, use parse5’s parseFragment function.

What’s the deal with templateExpressionToHtml?

The above code is ignoring more complex use cases by only pulling the first item in the quasis array.

This means that for the following lit expressions these tests will run fine:

This is because these lit expressions don't contain any JavaScript expressions inside of the template literals.

This means the node.quasi object will look something like this:

Unfortunately, this means that the tests will fail for the following lit expression:

Because this lit expression contains a JavaScript expression within the template literal, the node.quasi object will look something like this:

The third test case will fail until we fully implement the templateExpressionToHtml function.

If you’ve got the tests running, all the valid cases should still be passing and all the invalid cases should be failing. This is a good indicator to see whether there are any fundamental parsing issues within our TemplateAnalyzer’s constructor.

The above code snippets are a drastically simplified example and do not match the AST structure that ESLint parses our JavaScript into.

Creating our visitor functions

The next step is to create a traversal function for our analyzer. This will enter each HTML node, as ESLint does for JavaScript, which will call a visitor function that we’ll use to make our assertions.

It will take our visitor functions as an input, and then visit every node and call our visitor.

To keep things simple, we’ll need to check to see if the node is an element, and then call the correct visitor. Don’t worry about other node types, like comment nodes or text nodes.

If you’re unsure about how to implement the visitor. You can go back to the CodeSandbox from the first article.

Our traverse function will:

  • Take an object called visitors which contains our visitor functions
  • Visit each child node of this.ast
  • Call the element visitor function for each element node

Once implemented, the traverse function will look something like this:

This is what the function is doing:

  • A function visit, is declared, that handles the core logic.
  • visit returns early if no node is present
  • visit checks to see if our node is an HTML element
  • if it is, visit calls our visitor function
  • visit is called recursively with the child nodes.

The only other piece of functionality that’s left to implement is the isElement helper. This helper will take a node as an argument and check to see if the node.tagName value is truthy.

Writing our assertions

Let’s revisit our ESLint rule now that we can start writing assertions. Let’s use the new visitor function to visit each element.

This simple helper was inspired by the parse5 repo.

We can now continue in TDD fashion by fixing the failing tests. The first failing test is for the following case:

This case fails because the style attribute is present. If the attribute is present, we should report an ESLint error. Take a few minutes to try and implement the attribute check before continuing. Don't forget to use the AST explorer if you're not sure about the shape of the nodes.

The next step is to report the error and we’ll do this using the context.report function that ESLint provides. The report function takes an object which we'll use to supply to values:

  1. message, which will be no-inline-styles
  2. node, which will be our ESLint node.

Take some time looking over the AST and give it a shot yourself. You’ll know you’ve implemented a passing solution if all, but one of, your tests pass

Here’s the solution:

Back to templateExpressionToHtml

The last thing we need to do is fix the final failing test. This means we need to make changes to our templateExpressionToHtml function.

Since our node’s quasi value looks like this:

All we need to do is reconcile them in the following pattern: quasis[0] → expression[0] → quasi[1] → etc.

This sounds like a job for a simple loop!

Because we’re not concerned with the value of the expression, we can apply a placeholder to help distinguish it from the rest of the HTML string. This is helpful when we run checks later in our HTML traverser.

We’ll copy the placeholder template that eslint-plugin-lit uses for their own placeholders: {{__Q:${i}__}}.

Try writing the loop, you’ll know you’ve nailed it once the final test passes. Here’s howtemplateExpressionToHtml function looks after completing it:

There’s nothing special about this syntax. It’s specific enough so that it shouldn’t clash with any of our JavaScript code. The unique identifier for our expression is just the expression’s position in the array.

Let’s go through this step-by-step:

  • value is initialised with an empty string, which is used to build the HTML
  • We calculate the length of the quasis array, which will be used to inform our loop's iteration count.
  • We kick off our loop and we:
    – Access the quasi by the current index
    – Access the expression by the current index
    – Append the current quasi to our value string
    – And if an expression exists, we also append it to value.
  • We return value which we can now use to parse into an HTML AST.

And to really hit the point home, let’s run through the loop for the following code snippet:

As a reminder, the node we’ll be given will look like this:

  • We’ll use value to build the HTML
  • We’ll look at the length of node.quasi.quasis to determine our loop's iteration count. In this case we have two elements in the array. One for <div style=" and another for "></div>.
  • Loop 1:
    – Grab the first quasi, <div style=" and append it to our value string.
    – Because an element exists in the first index of the expressions array, we'll create a placeholder for val
    – At the end of the first loop iteration, our value looks like <div style="{{__Q:i__}}
  • Loop 2:
    – Grab the second quasi, "></div> and append it to our value string.
    – Because no element exists in the second index of the expressions array, we skip this step
    – at the end of the second loop iteration, our value looks like <div style="{{__Q:i__}}"></div>
  • We return our fully-formed value string, which is ready to be parsed into an HTML AST.

After implementing the templateExpressionToHtml function, the entirety of test suite passes, and hopefully yours does too!

The series of explanations might feel like overkill. The concepts around quasis and expressions can be confusing, so we wanted to spend a little extra time on them.

Limitations of ESLint

By definition, we use static analysis tools like ESLint, to check our code without running it. We can use ESLint to flag code style problems with our code, like we’ve just done in this article.

What ESLint can’t do is run our code, and report errors on the outputs. As a result, ESLint can’t report a problem for the following code snippet:

This is because expressions we pass as values won’t be evaluated by ESLint.

Going further

We’ve covered and learned how to build more advanced ESLINT rules to enforce design system best practices.

In our next and last article, we’ll take things a little further. We’ll write a more complicated rule along with a handful of unit tests. We’ll implement an ESLint rule for an existing design system’s implementation of a Tooltip component. This rule will be particularly useful, as it will ensure that the component is used in its intended way and it will avoid tricky accessibility issues that may exclude whole populations from being able to use the component

Originally published at https://backlight.dev and written by @AndricoKaroulla on January 14, 2022.

--

--

Backlight
Quick Code

Design System, Code side. Backlight is collaborative devtool to build great Design Systems - by @divRIOTS - https://backlight.dev/