Tech Poppy: Challenges with Snapshot Testing in Styled-Components

June 15, 2020
Brooke Noonan

Like many web app frontends these days, Tall Poppy’s is built with React and the CSS-in-JS library styled-components. This pairing comes with a lot of perks: a canonical theme, dynamic styling, and better code readability with no crufty class names.

But it is not without its challenges. In February we upgraded to styled-components 5.0 and were hit with a wave of test failures - specifically, Jest snapshot test failures. After some investigation, we were able to fix the issue and peek under the hood in the process.

If you’re already familiar with the inner workings of styled-components and the utility jest-styled-components, scroll ahead to our fix at the bottom!

CLASS NAMES IN STYLED-COMPONENTS

Styled-components generates unique class names for components in the DOM. We can use one of our styled components, LogoLink, as an example.

As JSX:

 LogoLink href="/dashboard"/

After being rendered in the DOM:

href="/dashboard" class="sc-kv4mmm-3 cfgKHk"

Styled-components has attached two class names to the node.


The first, sc-kv4mmm-3, prefixed by “sc” for “styled component”, is a static class name unique to all LogoLink components. This class name can be used as a component selector, which allows styled components to inherit and override the styling of other components (more on that later).

The second, cfgKHk, is a dynamic class name that changes depending on how LogoLink’s props and styling are interpolated.

DO YOU JEST?

So, how do these class names interact with Jest snapshot testing?

The intention of a snapshot test is to see if styling has changed, intentionally or unintentionally. This is helpful when working with large component libraries where a handful of components may be used all over.

Say we change LogoLink’s color. We want our snapshot test to tell us that its color has changed without any extraneous information that might make our diff confusing.

Unfortunately, we get this:

- Snapshot
+ Received 
href="/dashboard"
-  className="sc-kv4mmm-3 cfgKHk"
+  className="sc-kv4mmm-3 bTgRqt"


We can see that the dynamic class name has changed, but what about the component’s styling? There is no information about the color change. Not exactly helpful for me or whoever’s reviewing my pull request (Hi Erica!).

JEST-STYLED-COMPONENTS

Enter jest-styled-components. This utility helpfully serializes the class names that styled-components generates for cleaner, and more informative, snapshot diffs.

With jest-styled-components installed, our LogoLink snapshot becomes:


- Snapshot
+ Received
+.c0 {
+  font-size: 16px;
+  font-weight: bold;
+}
+ 
href="/dashboard"
-  className="sc-kv4mmm-3 cfgKHk"
+  className="c0" 

Much better! Our component is serialized as c0 and we can see exactly how its styling has changed. Problem solved! End of blog!

🙁

Back to our tidal wave of test failures.

Remember our LogoLink DOM node?

a href="/dashboard" class="sc-kv4mmm-3 cfgKHk"/

Well, we also have a component called LightLogoLink. This is (as you may have guessed) another styled component that overrides the color attribute of LogoLink:

export const LightLogoLink = styled(LogoLink)`
color: white;
`

The styled(LogoLink) syntax allows us to override LogoLink’s color attribute while inheriting the rest of its styling. In the DOM, LightLogoLink is rendered as:

a href="https://tallpoppy.com/" class="sc-kv4mmm-3 sc-kv4mmm-9 cDApPj"/

As we can see, LightLogoLink references LogoLink’s static class name sc-kv4mmm-3, followed by its own static and dynamic class names.

Before upgrading to styled-components 5.0, jest-styled-components handled this situation elegantly: folding a component’s new styles into its inherited styles and serializing both the parent and child class names.

- Snapshot
+ Received
+.c0 {
+  font-size: 16px;
+  font-weight: bold;
+  color: white;
}
+ 
a   href="/dashboard"
-  className="sc-kv4mmm-3 sc-kv4mmm-9 cDApP"
+  className="c1 c0" /

After upgrading to styled-components 5.0, we got snapshots like this:

- Snapshot
+ Received
+.c0 {
+  font-size: 16px;
+  font-weight: bold;
+  color: white; 
}
+ 
a   href="/dashboard"
-  className="sc-kv4mmm-3 sc-kv4mmm-9 cDApP"
+  className="sc-kv4mmm-3 c0" /

There’s that pesky, unserialized, static class name - the one that references LogoLink.

Ok, not pretty, but is it really a problem? Like we said, the static class names generated by styled-components are unique, so it shouldn’t change from snapshot to snapshot, right?

UNIQUE != DETERMINISTIC

As it turns out, the unique, static class name assigned to a styled component can change, even when none of the component’s styling has changed (which is ultimately all we care about when it comes to snapshot testing).

Simply adding a new styled component is all it takes for the static class names to change for every styled component that follows it in the DOM. Which means a lot of broken snapshots.

Jest-styled-components was handling this for us before, so what changed?

Jest-styled-components works by importing masterSheet from the styled-components library - a stylesheet that includes every styled component’s hashed class names and styling.

In styled-components 5.0, certain hashed class names are no longer included in masterSheet. Specifically, the class names of parent components (i.e. LogoLink) aren’t included if the parent component isn’t actually rendered in the DOM.

So now, when jest-styled-components iterates over masterSheet and serializes the hashed class names, these parent components are left out. Their unserialized class names appear in our snapshots, and the tests break.

OUR FIX

One option would be to get styled-components to continue including these class names in their stylesheets, but a library shouldn’t have to change its behavior to support a utility.

So, why not simply strip these static class names from our snapshots? They don’t offer us any new information about styling changes, and jest-styled-components continues to elegantly fold in styling for the child - which means we’ll know if the parent’s styling has changed either way.

First, we add a simple regex to filter out any class names that are prefixed by “sc-” and aren’t included in the masterSheet.

const filterUnreferencedClassNames = (classNames, hashes) =>  
classNames.filter(  
className => className.startsWith('sc-') && !hashes.includes(className)
);

Then, we simply strip those class names from our DOM nodes.

  const stripUnreferencedClassNames = (result, classNames) =>  
classNames.reduce(    
(acc, className) => acc.replace(new RegExp(`${className}\\s?`,'g'), ''), result
);

The resulting snapshot?

- Snapshot
+ Received
+.c0 {
+  font-size: 16px;
+  font-weight: bold;
+  color: white; 
}
+ 
a   href="/dashboard"
-  className="sc-kv4mmm-3 sc-kv4mmm-9 cDApP"
+  className="c0" /

Clean, helpful, and most importantly, deterministic!

View my pull request here.

Brooke is an engineer and client manager. She has been at Tall Poppy since early 2019. You can read more about her on our about page.

gently falling poppy flowers

Protect your team from online harassment, fraud and social engineering.

Get in touch today
gently falling poppy flowers
Find out how