<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nico Vogel</title>
    <description>The latest articles on DEV Community by Nico Vogel (@nicovogel).</description>
    <link>https://dev.to/nicovogel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1034608%2Fad9b94e1-ddc2-41ae-afd8-748b76e9ce25.jpeg</url>
      <title>DEV Community: Nico Vogel</title>
      <link>https://dev.to/nicovogel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nicovogel"/>
    <language>en</language>
    <item>
      <title>Building Visual Regression Testing for Components with Storybook and BackstopJS</title>
      <dc:creator>Nico Vogel</dc:creator>
      <pubDate>Thu, 14 Sep 2023 06:31:16 +0000</pubDate>
      <link>https://dev.to/nicovogel/building-visual-regression-testing-for-components-with-storybook-and-backstopjs-f80</link>
      <guid>https://dev.to/nicovogel/building-visual-regression-testing-for-components-with-storybook-and-backstopjs-f80</guid>
      <description>&lt;p&gt;Reviewing changes to UI components can be a time-consuming task, especially if you want to ensure that everything looks just right.&lt;br&gt;
Traditionally, this process involves running the application locally to manually inspect each alteration, a meticulous and sometimes arduous endeavor.&lt;br&gt;
Wouldn't it be great if you could automatically see which components changed in a pull request without having to run the application locally?&lt;br&gt;
Visual regression testing can help with just that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Visual Regression Tests?
&lt;/h2&gt;

&lt;p&gt;Visual regression tests are a way to detect visual changes in your application's user interface.&lt;br&gt;
They allow you to easily identify any visual changes introduced by code changes without running it locally.&lt;/p&gt;

&lt;p&gt;Here's a step-by-step overview of how visual regression tests can work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Screenshot Components from the Main Branch&lt;/strong&gt;: You take screenshots of your application's UI components as they appear in the main branch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Screenshot Components from the Pull Request&lt;/strong&gt;: Next, you capture screenshots of the same UI components from the pull request branch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compare Screenshots&lt;/strong&gt;: A tool compares these screenshots pixel by pixel, highlighting any differences between them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Generate a Report&lt;/strong&gt;: Finally, you receive a report that clearly shows any visual changes, making it easy to review and address them.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, let's explore how you can custom-build your visual regression testing setup using Storybook, Playwright, and BackstopJS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create Your Own Visual Regression Testing Tool
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;This blog assumes that you have Storybook already setup and ready to use and that you also use the &lt;code&gt;addon-docs&lt;/code&gt; plugin.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;GitHub Link&lt;/strong&gt;: If you're interested in the code right away, you can find it at &lt;a href="https://github.com/nicovogel/nx-lit-vite" rel="noopener noreferrer"&gt;NicoVogel/nx-lit-vite&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note that the code in the repository is a bit more oriented towards the &lt;code&gt;lit&lt;/code&gt; framework, while the code presented here is more generalized.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Steps to Create Your Visual Regression Testing Tool
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Export Stories JSON from Storybook
&lt;/h4&gt;

&lt;p&gt;To automate the process, you need to export a &lt;code&gt;stories.json&lt;/code&gt; file from Storybook.&lt;br&gt;
If you are using Storybook version 7 or later, you can enable the &lt;code&gt;buildStoriesJson&lt;/code&gt; feature.&lt;br&gt;
For earlier versions, use the &lt;code&gt;sb extract&lt;/code&gt; command.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// .storybook/main.ts (Storybook v7+)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="na"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;buildStoriesJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You have two options regarding the &lt;code&gt;stories.json&lt;/code&gt; file.&lt;br&gt;
Either you check it into your repository, or you generate it as part of your build process.&lt;/p&gt;

&lt;p&gt;Before Storybook introduced the &lt;code&gt;buildStoriesJson&lt;/code&gt; feature, it was a good idea to check the file in, to safe time.&lt;br&gt;
However, now it just comes down to personal preference.&lt;/p&gt;

&lt;p&gt;Because the &lt;code&gt;sb extract&lt;/code&gt; command takes a while to&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: Make sure to include the &lt;code&gt;stories.json&lt;/code&gt; file in your repository, especially for older Storybook versions.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Write a Selector Extraction Script
&lt;/h4&gt;

&lt;p&gt;BackstopJS uses scenarios to identify what parts of a page should be captured in a screenshot.&lt;br&gt;
Each scenario requires a URL and a selector.&lt;br&gt;
So, we need to extract the selectors from the &lt;code&gt;stories.json&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The URL is simply the address visited to take the screenshot, while the selector is a CSS selector that identifies the element to capture in the screenshot.&lt;/p&gt;

&lt;p&gt;In case of Storybook, all individual component showcases are rendered in an iframe.&lt;br&gt;
This page is hosted at &lt;code&gt;/iframe.html&lt;/code&gt; and which component is shown is determined by the query parameter.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The selector code is for Storybook version 7. For other versions, you need to inspect the Storybook &lt;code&gt;iframe.html&lt;/code&gt; page to find the suitable selector.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// apps/visual-regression/src/main.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;storyJSON&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./assets/stories.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;constructScenarios&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;storyJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;baseUrl should not end with a /&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stories&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;story&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="c1"&gt;// since version 7, the docs are an overview page and as we are only interested in the stories, we filter them out&lt;/span&gt;
        &lt;span class="nx"&gt;story&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;story&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="c1"&gt;// remove blacklisted stories (e.g. loading spinner, what ever is not static)&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blacklistStories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;story&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backstopCapture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scenarioDefault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// backstop waits for this to appear, before taking the screenshot&lt;/span&gt;
        &lt;span class="na"&gt;readySelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#root-inner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// custom url is required to isolate the storybook component in its own frame&lt;/span&gt;
        &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/iframe.html?viewMode=docs&amp;amp;id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;selectors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#root-inner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;misMatchThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;misMatchThresholdInPercentage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;defined&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// apps/visual-regression/src/config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;blacklist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LoadingSpinner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;misMatchThresholdInPercentage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;backstopCapture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;scenarioDefault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;selectorExpansion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;requireSameDimensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
  
  
  3. Configure Backstop to Use Playwright
&lt;/h4&gt;

&lt;p&gt;Configure BackstopJS to use Playwright and pass along the parsed scenarios.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;console.log&lt;/code&gt; statements make it easier to reason about problems in CI.&lt;br&gt;
Even though, I did not experience any problems in CI, I still left them in.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// apps/visual-regression/src/main.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;backstopjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backstopExecutionMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scenarios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;constructScenarios&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storyJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scenarios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;scenarios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectors&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectors&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;constructBackstopConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scenarios&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;backstop config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;constructBackstopConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scenarios&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nx"&gt;Config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// checkout the file `apps/visual-regression/src/config.ts` for the full config&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;engine_scripts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backstopCapture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backstopCapture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;scripts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backstopCapture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;viewports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backstopCapture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewPorts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backstopCapture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;engine_scripts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;scenarios&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
  
  
  4. Run Backstop
&lt;/h4&gt;

&lt;p&gt;Run BackstopJS with &lt;code&gt;reference&lt;/code&gt; in the case of the main branch and with &lt;code&gt;test&lt;/code&gt; in the case of a pull request.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;

&lt;span class="c1"&gt;// apps/visual-regression/src/main.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;backstop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;backstopjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;backstopExecutionMode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backstopExecutionMode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backstopExecutionMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backstopExecutionMode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`task: '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' with location: '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scenarios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;constructScenarios&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;storyJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scenarios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;scenarios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectors&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectors&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;constructBackstopConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scenarios&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;backstop config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;backstop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; completed`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed, because: `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;referencing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buildProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;referenceServePort&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;testing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buildProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;changedServePort&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h4&gt;
  
  
  5. Define Scripts to Run Everything
&lt;/h4&gt;

&lt;p&gt;Now that you have the code to run BackstopJS, define the default scripts to run it.&lt;br&gt;
This example uses Nx, but you can adapt it to your own setup.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// apps/visual-regression/project.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;// nx does not know that we rely on this project, so we need to tell it&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"implicitDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"@nx-lit-vite/source"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"executor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nx:run-commands"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm reference"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apps/visual-regression"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="c1"&gt;// Ensure that the Storybook build is finished before running visual regression tests&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^vr-reference"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"configurations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ci"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm reference:backstop"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"check-changed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"executor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nx:run-commands"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm check-changed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apps/visual-regression"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^vr-changed"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"configurations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ci"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm check-changed:backstop"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"extract-stories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"executor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nx:run-commands"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cp dist/storybook/@nx-lit-vite/source/stories.json apps/visual-regression/src/assets/stories.json"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"^build-storybook"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// project.json (wherever you define your Storybook build)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@nx-lit-vite/source"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"targets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build-storybook"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"executor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@nx/storybook:build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"{options.outputDir}"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"outputDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/storybook/@nx-lit-vite/source"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"configDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./.storybook"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"configurations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ci"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"quiet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vr-reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"executor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nx:run-commands"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"{options.outputDir}"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="c1"&gt;// you can override configurations via flags, if they are named the same&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="c1"&gt;// so here, we override the outputDir wit a custom output&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm exec nx build-storybook @nx-lit-vite/source --configuration=ci --outputDir=/tmp/reference --skip-nx-cache"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"outputDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tmp/reference"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"vr-changed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"executor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nx:run-commands"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"{options.outputDir}"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="c1"&gt;// skipping the nx cache to be really sure that we have the latest changes&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm exec nx build-storybook @nx-lit-vite/source --configuration=ci --outputDir=/tmp/changed --skip-nx-cache"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"outputDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tmp/changed"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// apps/visual-regression/package.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reference"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"concurrently --success first --kill-others npm:reference:backstop npm:reference:serve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;// the timeout is especially important in CI, in case something goes wrong.&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;// otherwise `wait-on` will never fail&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reference:backstop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wait-on http://localhost:8080 --timeout 3000 &amp;amp;&amp;amp; ts-node src/main.ts reference"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reference:serve"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http-server /tmp/reference --port 8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"check-changed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"concurrently --success first --kill-others npm:check-changed:backstop npm:check-changed:serve"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;// using ts-node to skip the need for a build step&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"check-changed:backstop"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wait-on http://localhost:8081 --timeout 3000 &amp;amp;&amp;amp;  ts-node src/main.ts test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"check-changed:serve"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http-server /tmp/changed --port 8081"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;// if you want to see the local output&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"serve-result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http-server /tmp/backstop_data --port 8080"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;


&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The setup allows for two different modes: running in CI (assuming Storybook is already hosted) and running locally.&lt;br&gt;
To set up these scripts, you'll need dependencies like &lt;code&gt;concurrently&lt;/code&gt;, &lt;code&gt;http-server&lt;/code&gt;, &lt;code&gt;wait-on&lt;/code&gt;, &lt;code&gt;ts-node&lt;/code&gt;, &lt;code&gt;backstopjs&lt;/code&gt;, &lt;code&gt;playwright&lt;/code&gt;, &lt;code&gt;@types/backstopjs&lt;/code&gt;, &lt;code&gt;typescript&lt;/code&gt;, and more.&lt;br&gt;
You can install them with the following &lt;code&gt;pnpm&lt;/code&gt; command:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; concurrently http-server wait-on ts-node backstopjs playwright @types/backstopjs typescript


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Make sure backstopjs and playwright versions are compatible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  6. Set Up GitHub Actions
&lt;/h4&gt;

&lt;p&gt;To automate the process, set up GitHub Actions to run the visual regression tests automatically with each pull request.&lt;br&gt;
The following steps are involved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checkout the main branch.&lt;/li&gt;
&lt;li&gt;Install dependencies.&lt;/li&gt;
&lt;li&gt;Build Storybook.&lt;/li&gt;
&lt;li&gt;Host Storybook.&lt;/li&gt;
&lt;li&gt;Run BackstopJS reference.&lt;/li&gt;
&lt;li&gt;Checkout the pull request branch.&lt;/li&gt;
&lt;li&gt;Install dependencies.&lt;/li&gt;
&lt;li&gt;Build Storybook.&lt;/li&gt;
&lt;li&gt;Host Storybook.&lt;/li&gt;
&lt;li&gt;Run BackstopJS test.&lt;/li&gt;
&lt;li&gt;Upload the report to GitHub.&lt;/li&gt;
&lt;/ol&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

&lt;p&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Visual Regression&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;&lt;br&gt;
  &lt;span class="na"&gt;checks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt; &lt;span class="c1"&gt;# junit-report&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
  &lt;span class="na"&gt;backstop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build target and reference and test&lt;/span&gt;&lt;br&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;&lt;br&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
      &lt;span class="c1"&gt;# BackstopJS installs puppeteer and it wants to install chrome.&lt;/span&gt;&lt;br&gt;
      &lt;span class="c1"&gt;# But we already use playwright for that, so we don't need it.&lt;/span&gt;&lt;br&gt;
      &lt;span class="na"&gt;PUPPETEER_SKIP_CHROMIUM_DOWNLOAD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;br&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout Reference 🛎️&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/&lt;a href="mailto:checkout@v3.5.3"&gt;checkout@v3.5.3&lt;/a&gt;&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.base_ref }}&lt;/span&gt;&lt;br&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup pnpm ⚙️&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;&lt;br&gt;
          &lt;span class="s"&gt;npm i -g &lt;a href="mailto:pnpm@8.6.12"&gt;pnpm@8.6.12&lt;/a&gt;&lt;/span&gt;&lt;br&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install for Reference 📦&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;&lt;br&gt;
          &lt;span class="s"&gt;pnpm install&lt;/span&gt;&lt;br&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host Reference&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Eun/&lt;a href="mailto:http-server-action@v1.0.10"&gt;http-server-action@v1.0.10&lt;/a&gt;&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/reference&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;no-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;index-files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;&lt;br&gt;
            &lt;span class="s"&gt;["index.html"]&lt;/span&gt;&lt;br&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Host Changed&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Eun/&lt;a href="mailto:http-server-action@v1.0.10"&gt;http-server-action@v1.0.10&lt;/a&gt;&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/changed&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8081&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;no-cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;index-files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;&lt;br&gt;
            &lt;span class="s"&gt;["index.html"]&lt;/span&gt;&lt;br&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Visual Regression Reference 📖&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;&lt;br&gt;
          &lt;span class="s"&gt;pnpm exec nx run visual-regression:reference --configuration=ci&lt;/span&gt;&lt;br&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout Target 🛎️&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/&lt;a href="mailto:checkout@v3.5.3"&gt;checkout@v3.5.3&lt;/a&gt;&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.head_ref }}&lt;/span&gt;&lt;br&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install for Target📦&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;&lt;br&gt;
          &lt;span class="s"&gt;pnpm install&lt;/span&gt;&lt;br&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Visual Regression Target 📖&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;&lt;br&gt;
          &lt;span class="s"&gt;pnpm exec nx run visual-regression:check-changed --configuration=ci&lt;/span&gt;&lt;br&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post report&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mikepenz/&lt;a href="mailto:action-junit-report@v3.8.0"&gt;action-junit-report@v3.8.0&lt;/a&gt;&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;post-report&lt;/span&gt;&lt;br&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;report_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/backstop_data/ci_report/*.xml&lt;/span&gt;&lt;br&gt;
          &lt;span class="na"&gt;fail_on_failure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;/p&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Visual Regression Output Look and Feel&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;Let's explore the output generated by this setup and understand what it provides.&lt;br&gt;
After executing the &lt;code&gt;reference&lt;/code&gt; and &lt;code&gt;check-changed&lt;/code&gt; scripts, you'll find the report stored in the &lt;code&gt;/tmp/backstop_data/&lt;/code&gt; directory. &lt;br&gt;
To view it locally, use the &lt;code&gt;serve-result&lt;/code&gt; script included in the visual regression project's &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Upon opening the &lt;code&gt;html_report&lt;/code&gt; folder, you'll see an overview of visual regression tests results:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtx7yjalgb86fqk7wtzs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmtx7yjalgb86fqk7wtzs.png" alt="Visual Regression HTML Report Overview"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you click on a specific test, you'll focus on that test and get a convenient scrubber tool that allows you to compare the two screenshots:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2n3nverdv27vuuaq4u5w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2n3nverdv27vuuaq4u5w.png" alt="Visual Regression HTML Report focused view with scrubber tool"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this simple report, you can quickly assess the visual changes introduced by a pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Potential Improvements
&lt;/h2&gt;

&lt;p&gt;As you continue to refine your visual regression testing setup, consider these potential improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cache the Reference&lt;/strong&gt;: Cache the reference screenshots from the main branch to speed up the testing process.&lt;br&gt;
Ensure you have cancellation and restart logic in place to rerun PRs whenever the reference changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Upload Reports to Storage&lt;/strong&gt;: Instead of just uploading junit reports to GitHub, consider using a storage solution to store reports and make them accessible to reviewers.&lt;br&gt;
Don't forget to clean up old reports after a certain period, e.g., 30 days.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Host the Report&lt;/strong&gt;: Host the report online so that reviewers do not need to run them locally to see the changes.&lt;br&gt;
This can be an alternative or complementary approach to uploading reports to storage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Action to Create &lt;code&gt;stories.json&lt;/code&gt; PR&lt;/strong&gt;: If you check-in the &lt;code&gt;stories.json&lt;/code&gt; file, you can create an action that automatically creates a PR whenever the file changes, ensuring that the &lt;code&gt;stories.json&lt;/code&gt; file is always up-to-date.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Consider Theme Variants&lt;/strong&gt;: If your application supports multiple themes, you may need to capture screenshots for each theme.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Learnings
&lt;/h2&gt;

&lt;p&gt;Throughout this process, several key learnings emerged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Selectors&lt;/strong&gt;: Using &lt;code&gt;#root-selector&lt;/code&gt; ensures that everything is captured in the screenshot.&lt;br&gt;
While you can create more specific selectors to capture only the component itself, it adds complexity without providing additional value.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Storybook Hosting&lt;/strong&gt;: Building and hosting Storybook for efficient visual regression testing is crucial.&lt;br&gt;
Using the serve command is considerably slower.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Visual Regression as a Pull Request Gate&lt;/strong&gt;: Visual regression tests are valuable but can't be mandatory gates.&lt;br&gt;
Sometimes, visual changes are intended and expected.&lt;br&gt;
It's just another tool to help you review changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hosting in GitHub Actions&lt;/strong&gt;: Running a simple HTTP server with &lt;code&gt;npx http-server&lt;/code&gt; doesn't work in GitHub Actions, so use predefined actions for hosting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Breaking Changes with Version Bumps&lt;/strong&gt;: Be aware that updating Storybook, BackstopJS, or Playwright may break your visual regression tests.&lt;br&gt;
For example, when updating Storybook from version 6 to 7, the selectors changed, which broke the visual regression tests.&lt;br&gt;
It was an easy fix, but something to keep in mind when building your own tooling.&lt;br&gt;
Another aspect here is that BackstopJS uses playwright, so you need to find a compatible version.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blacklisting&lt;/strong&gt;: In some cases, you may want to blacklist certain components, such as loading spinners or non-static elements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Playwright Version&lt;/strong&gt;: Ensure that the Playwright version is compatible with the BackstopJS version.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;Building your own visual regression testing setup is not as challenging as it may seem.&lt;br&gt;
Once established, you can reuse it across multiple projects with only minimal adjustments.&lt;br&gt;
The result is an easier pull request review process and increased confidence in the visual integrity of your applications.&lt;br&gt;
Additionally, it makes it easy to see visual changes by using the slider feature in the report.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>webdev</category>
      <category>testing</category>
    </item>
    <item>
      <title>Wrapping Libraries for Enhanced Flexibility: A Practical Approach</title>
      <dc:creator>Nico Vogel</dc:creator>
      <pubDate>Fri, 08 Sep 2023 14:36:54 +0000</pubDate>
      <link>https://dev.to/nicovogel/wrapping-libraries-for-enhanced-flexibility-a-practical-approach-4jln</link>
      <guid>https://dev.to/nicovogel/wrapping-libraries-for-enhanced-flexibility-a-practical-approach-4jln</guid>
      <description>&lt;p&gt;In the realm of software development, third-party libraries are invaluable tools for accelerating projects. &lt;br&gt;
However, these libraries might not perfectly align with evolving requirements. &lt;br&gt;
Enter the concept of wrapping libraries — an approach that adds a layer of control and adaptability.&lt;/p&gt;

&lt;p&gt;In this blog, we'll delve into library wrapping, its benefits, challenges, and provide an example. &lt;br&gt;
Let's explore how wrapping libraries empowers you to meet dynamic demands in the coding world.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code can be found here: &lt;a href="https://github.com/NicoVogel/logger-wrapper"&gt;NicoVogel/logger-wrapper&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  What Does it Mean to Wrap Libraries You Use?
&lt;/h1&gt;

&lt;p&gt;At its core, library wrapping involves crafting a bridge between your application code and external libraries.&lt;br&gt;
This bridge, often referred to as an interface, encapsulates the library's functionality, and your code exclusively interacts with this interface. &lt;br&gt;
Here's a breakdown of the process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Interface Creation&lt;/strong&gt;: Design an interface that exposes only the features you require from the library.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test Your Interface&lt;/strong&gt;: Develop a suite of tests for your interface to ensure that it behaves as expected. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Selective Usage&lt;/strong&gt;: In your application code, use the interface exclusively to interact with the library.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Library Usage&lt;/strong&gt;: The actual library is integrated into the implementation of your interface. \&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  Unveiling the Benefits
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Abstraction of Complexity&lt;/strong&gt;: Wrapping libraries allows you to abstract away unnecessary complexity. &lt;br&gt;
Your code interacts with a streamlined interface, making it easier to work with the library's features without being overwhelmed by its intricacies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flexible Library Swapping&lt;/strong&gt;: Should you need to switch libraries due to changes in requirements or the library's status, your application is less affected because you isolated all the library interaction to a single point inside the interface implementation.&lt;br&gt;
This is especially useful when dealing with unmaintained libraries or when you want to avoid vendor lock-in.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test-Driven Stability&lt;/strong&gt;: By testing your interface extensively, you ensure that your application's behavior remains consistent even if you decide to upgrade the library to a new major version. &lt;br&gt;
For this to work, you can't mock the library.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Feature Enhancement&lt;/strong&gt;: You can extend your interface with additional features that the original library might lack, tailoring the library's capabilities to your exact needs.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h1&gt;
  
  
  What Are the The Roadblocks to Be Aware Of
&lt;/h1&gt;

&lt;p&gt;As always, there are no silver bullets in software engineering.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Increased Code Complexity&lt;/strong&gt;: Creating and maintaining an interface requires additional code, potentially leading to increased complexity. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Abstraction Challenges&lt;/strong&gt;: Balancing abstraction and practicality can be challenging.&lt;br&gt;
Over-abstracting may lead to convoluted code, while under-abstracting might not provide the desired benefits like better generalization, less code duplication and isolation.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't underestimate the second downside. &lt;br&gt;
If you have never been a library/API maintainer, this can be incredibly hard.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Real-World Case: Creating a Logger Wrapper
&lt;/h1&gt;

&lt;p&gt;In my current project we have specific requirements for logging and we could not find a library that met them outright.&lt;br&gt;
The requirements are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logging needs to happen in frontend and backend.&lt;/li&gt;
&lt;li&gt;A log level defines what should be logged&lt;/li&gt;
&lt;li&gt;A logger hierarchy allows to increase/decrease logging in only parts of the application.&lt;/li&gt;
&lt;li&gt;Frontend logs are send to the backend.&lt;/li&gt;
&lt;li&gt;Frontend logs need to be accessible in the UI&lt;/li&gt;
&lt;li&gt;The backend stores frontend logs in a file.&lt;/li&gt;
&lt;li&gt;The backend logs are shown in the terminal and stored in a file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As usual, we did not need to meet all the requirements from the start, but it just grew over time.&lt;br&gt;
The general development flow was in essence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We started with &lt;code&gt;ts-log&lt;/code&gt; (after only a short search)&lt;/li&gt;
&lt;li&gt;As more requirements came in, we wrapped &lt;code&gt;ts-log&lt;/code&gt; in an interface to add more features&lt;/li&gt;
&lt;li&gt;The more we added, the uglier the wrapper became and we searched again for a library.
This time going with &lt;code&gt;pino&lt;/code&gt;.
We massively simplified the wrapper and there was no change at all for our application code.&lt;/li&gt;
&lt;li&gt;Again, &lt;code&gt;pino&lt;/code&gt; does not support everything we need out of the box, so we added some more capabilities in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This should give you a taste as to how a wrapper can form naturally over time.&lt;br&gt;
The code can be found here: &lt;a href="https://github.com/NicoVogel/logger-wrapper"&gt;NicoVogel/logger-wrapper&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The code does not reflect the full logger we use in production, but should give you a nice idea about how to wrap a library.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Don't Wrap Everything
&lt;/h1&gt;

&lt;p&gt;It's important to be selective when deciding whether to wrap a library or not. &lt;br&gt;
Consider the following decision tree:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Likelihood of Library Swap&lt;/strong&gt;: If switching libraries is foreseeable, creating a wrapper is a wise move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing Features&lt;/strong&gt;: If the library lacks essential features, consider creating a wrapper to address these gaps (The alternative is to not wrap it fully, but just provide a supporting class).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Library Support&lt;/strong&gt;: If the library was not updated for quite some time or is not used by many, consider creating a wrapper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evading Vendor Lock-in&lt;/strong&gt;: If you don't want a vendor lock-in, then you have to create a wrapper. But ask really hard why this is required (e.g. the database that we might change in the future, but never will). &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Remember, in the end the decision to wrap a library depends on your project requirements and team expertise.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Wrapping libraries through the creation of interfaces offers a practical approach to enhance flexibility and adaptability in your software projects. &lt;br&gt;
By abstracting away unnecessary complexities, tailoring functionality to your needs, and isolating potential library changes, you ensure a more stable and maintainable codebase. &lt;br&gt;
However, it's important to strike a balance and avoid over-abstracting. &lt;br&gt;
The decision to wrap a library should be based on careful consideration of factors such as potential library switches, missing features, and library support.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Onboarding New Team Members Simplified with Dev Containers</title>
      <dc:creator>Nico Vogel</dc:creator>
      <pubDate>Thu, 17 Aug 2023 20:46:23 +0000</pubDate>
      <link>https://dev.to/nicovogel/onboarding-new-team-members-simplified-with-dev-containers-5gag</link>
      <guid>https://dev.to/nicovogel/onboarding-new-team-members-simplified-with-dev-containers-5gag</guid>
      <description>&lt;h1&gt;
  
  
  Onboarding New Team Members Simplified with Dev Containers
&lt;/h1&gt;

&lt;p&gt;Ever grappled with the challenges of onboarding new team members? Initializing a development environment can often be a long-winded affair, especially when one needs to adjust to configurations from prior projects. What if I told you there's a strategy to alleviate such pain points and ensure a uniform development environment across diverse operating systems, including Linux, Windows, and Mac?&lt;/p&gt;

&lt;p&gt;In recent times, several IDEs have incorporated support for Dev Containers, and in this piece, I'll elaborate on this concept using Visual Studio Code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Dev Containers
&lt;/h2&gt;

&lt;p&gt;For those seeking a deep dive, Visual Studio Code's official documentation provides a comprehensive guide on Dev Containers and the various configuration possibilities. Check out the documentation &lt;a href="https://code.visualstudio.com/docs/devcontainers/containers"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In a nutshell, here's how Dev Containers work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define your environment via a &lt;code&gt;Dockerfile&lt;/code&gt; (optional) and &lt;code&gt;devcontainer.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Build and start the container via VS Code.&lt;/li&gt;
&lt;li&gt;Integrate the repository within the live container (automatic).&lt;/li&gt;
&lt;li&gt;A server instance is launched in the container by VS Code (automatic).&lt;/li&gt;
&lt;li&gt;The local VS Code instance connects to this server instance (automatic).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why Use Dev Containers?
&lt;/h2&gt;

&lt;p&gt;As you may already have noticed, defining environments using Dockerfile's has become a de facto standard for production services.&lt;br&gt;
We can now do the same for our development environments, defining them in code to gain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Consistency Assured&lt;/strong&gt;: With Dev Containers, uniformity across the team's development environment is a given, dispelling discrepancies in configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effortless Rollbacks&lt;/strong&gt;: Bumps on the road? Simply rebuild the current or revert to a former container state and resume your development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streamlined Maintenance&lt;/strong&gt;: The upkeep and update of the development environment are rendered straightforward and less prone to errors with Dev Containers.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Tangible Benefits Gained
&lt;/h2&gt;

&lt;p&gt;By adopting Dev Containers in my current project, we managed to reduce the onboarding for a new developer to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Permission for the git repository access.&lt;/li&gt;
&lt;li&gt;Having Git and Docker installed on their local machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We've managed to trim down the setup to approximately 2 minutes of hands-on work. &lt;br&gt;
This is succeeded by about 10 minutes of building time for the Docker Image, with an added 3 minutes for the installation of code dependencies within the container.&lt;/p&gt;

&lt;p&gt;So, for your project, we can guarantee a working setup within 15 minutes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This can be sped up further by pre-building the image and storing it in a repository.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Flip Side: Potential Limitations
&lt;/h2&gt;

&lt;p&gt;While Dev Containers offer numerous benefits, it's essential to acknowledge the potential downsides:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Overhead&lt;/strong&gt;: Owing to the containerization layer, Dev Containers introduce some overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restricted File Access&lt;/strong&gt;: On Windows, gaining access to files within the container is not straightforward (depending on your setup).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intermittent Rebuilding&lt;/strong&gt;: On rare occasions, a container rebuild becomes necessary for seamless functioning.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;We could not isolate the root cause yet, but as this occurs so rarely, we did not invest more resources in solving it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Setting Up Dev Containers
&lt;/h2&gt;

&lt;p&gt;To get started with a simplified Dev Container setup for my current project, you can use this template &lt;a href="https://github.com/NicoVogel/dev-container"&gt;NicoVogel/dev-container&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Template Setup At A Glance
&lt;/h3&gt;

&lt;p&gt;To grant you a brief overview of what the template includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use of &lt;code&gt;pnpm&lt;/code&gt; for improved performance.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;playwright&lt;/code&gt; for end-to-end testing.&lt;/li&gt;
&lt;li&gt;Enabled "docker in docker" via socket mount, allowing Docker to be used within the development environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To leverage the full potential of &lt;code&gt;pnpm&lt;/code&gt;, you need to combine it with Linux's filesystem instead of Windows' &lt;code&gt;ntfs&lt;/code&gt;.&lt;br&gt;
Dependencies are mostly small files, and Linux is just far superior for this workload.&lt;br&gt;
Therefore, we have opted to clone our code into the Linux's filesystem and mount it into the Dev Container. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This also speeds up &lt;code&gt;git&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For Windows users, two potential routes are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;WSL2&lt;/strong&gt;: Establish a personalized Linux and incorporate the repository therein.

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;+&lt;/code&gt; Easy to access files later on&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-&lt;/code&gt; Setup guide for Linux required&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clone Repository into Container Volume&lt;/strong&gt;: Use a feature from the Dev Containers extension to clone it directly into a docker volume.

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;+&lt;/code&gt; No setup needed whatsoever&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-&lt;/code&gt; You can access the files in the file explorer, but you need to find them first via docker commands.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Depending on the skill sets of your developers, one or the other may make more sense for you.&lt;/p&gt;

&lt;h4&gt;
  
  
  Features Omitted from the Template
&lt;/h4&gt;

&lt;p&gt;The main feature omitted is the artifactory integration.&lt;br&gt;
You can also extend your container setup to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store the Dev Container image in Artifactory

&lt;ul&gt;
&lt;li&gt;This includes proxy settings and custom company certificates.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Mirror npm packages in Artifactory.&lt;/li&gt;
&lt;li&gt;Provide a login script for Artifactory after startup.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;scripts/setup.py&lt;/code&gt; is the login script for Artifactory, in case anyone needs it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tailoring The Template
&lt;/h2&gt;

&lt;p&gt;To customize the template for your specific needs, follow these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Duplicate the template folder, renaming it &lt;code&gt;.devcontainer&lt;/code&gt;, ensuring its location is at your project's root.&lt;/li&gt;
&lt;li&gt;For those with a proxy, modify the parameters in &lt;code&gt;.devcontainer/devcontainer.env&lt;/code&gt;. If not, simply clear the file contents.&lt;/li&gt;
&lt;li&gt;Set a base image that contains your company's certificates (if you have any) and adjust the &lt;code&gt;FROM&lt;/code&gt; directive in &lt;code&gt;.devcontainer/Dockerfile&lt;/code&gt;. Alternatively, you can use any Ubuntu base image.&lt;/li&gt;
&lt;li&gt;Update the list of extensions that should be preinstalled automatically in &lt;code&gt;devcontainer/devcontainer.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With these adjustments in place, you're ready to use the VS Code command &lt;code&gt;Dev Containers: Reopen in Container&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  In Retrospect
&lt;/h2&gt;

&lt;p&gt;Dev Containers, as described using Visual Studio Code, offer a solution to simplify onboarding and ensure a consistent development environment across different operating systems.&lt;br&gt;
By defining the Dev Environment as Code, Dev Containers guarantee consistency, easy rollback, and simplified maintenance.&lt;br&gt;
This leads to a reproducible setup time and significantly reduced onboarding efforts.&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>docker</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
