Component visual testing with ViewCompontents and Appraise.qa

Photo by Markus Spiske on Unsplash

Component-driven (UI) Development is gaining track influenced by, among other things, the growing relevance of Design Systems and the increase of the complexity and number of the use cases any application has to be able to manage “looking right”. Storybook has emerged into development teams as one of the favourite Open Source tools to make it possible. In this post i’ll talk about RailsStorybookAppraise (or RSA xD), a Proof of Concept of Visual “component” Testing using Appraise.qa, Storybook, and the ViewComponent Rails engine.

Having a Design System lets us be recognized…

And every growing company finally need one for, among other things, ensure the maximum return on its advertising investment. In front of a new development project Storybook is one of the available tools to document and discuss internally the components between the different teams involved in their implementation:

https://storybook.js.org/cdb19b23da5e96c112ff3b8fded28a82/storybook-hero-video-optimized-lg.mp4

Considering that Storybook is an Open Source community-driven project that already supports most JS existing frameworks

…it is no wonder that Storybook is becoming the de facto standard to do that work. And Component-driven (User Interface) Development (componentdriven.org) the philosophy that comes behind it…

Visual Testing as a sentinel of our identity

Visual (regression or approval) Testing cares about what we are finally showing to those who are watching our app. It assumes that everything underneath is working (and hopefully being tested) correctly so that the image in question looks as desired.

This “carelessness” IMHO is very appropriate because for those pixels to be there are many “things” that have to be in their place. Very many in fact. That is the main reason why, when explaining a bug, we often start by saying something like “it was foolish” (“era una tontería”). The very name we give it, bug (insecto), already captures the essence of what I mean.

Therefore focusing on what we will finally show as if we were in a vacuum chamber to which no bug has access seems ideal to me. There are many good posts that have already been written on the subject, three of which I particularly liked and leave here for future reference:

Finally I’d like to add two more links: the first one, Happo.io, for being the first SaaS I tried when Mark Villacampa told me about it after Zeitwerk’s MadridRB (of the awesome Applitools and Percy already speak amply any of the three posts I put above), and the last link for being the Awesome List that has helped me to discover many other interesting resources on the subject:

For visual testing of components (not full pages) “the king” in the house is Chromatic.com, created and used by the Storybook team for Storybook’s own development, being StoryShots and Loki the alternatives related with this Proof of Concept using only Node.

Appraise.qa: painless Visual Testing

Painless visual testing (dixit) especially thanks to :

  • Start the build from sketches/drawings if needed,
  • Self-documented tests written in markdown in a free structure for its files and directories,
  • Two commands: test (our app) and approve (example/test).

Let’s see an example to better understand what i mean:

  • First (optional step): we can draw our awesome homepage and take a picture of it (which we save e.g. in test/examples/home.jpg):
  • Second: wrote our first test in markdown (which we save e.g. in test/examples/home.md):

When in next step we launch the build Appraise will extract from our .md files its “~~~yaml” fragments with attributes example (test name) and fixture (JS function), and will execute the test in three stages:

  1. Executes the JS function defined in the fixture file passing as parameter an object with the keys/values defined in the block (in our example it’ll call the url.js function passing { path: “/” } as parameter).
  2. That function should return anything that a browser is able to render (like for example an URL). Appraise will pass that value to the browser (by default a headless Chrome driven by Puppeteer) and will save the image rendered by the browser.
  3. Finally Appraise compares that image with the “desired” one (the one in our “base line”), and determines whether or not it is valid (we can set a couple of parameters such as the tolerance for considering a pixel to be distinct, and the total number of distinct pixels allowed).
  • Third: we launch our test… unsurprisingly Appraise tell us the images don’t match (or that no expected result has been defined yet).
  • And fourth: as soon as our app is rendering something that we consider valid we approve the example, making that image the first one generated by our app, our first real “base line”.

ViewComponent: ours Rails way

ViewComponent is a Rails engine that, as its own definition at viewcomponent.org, allows us to define Ruby objects with its presentation logic encapsulated. It’s an evolution of the Presenter design pattern inspired by React.

Was created by GitHub to refactor its huge amount of Ruby and Rails code, and are currently being used by some of the “big players” using Rails. Many of those publish the components definitions of their design systems making them available for the community, like for example the UK’S Department of Education, or GitHub itself.

In RailsConf 2021 Joel Hawksley (GitHub) finished his talk about ViewComponent calling on the Rails community to keep the framework relevant:

Shortly before that Joel referred to the need to innovate and explore potentially disruptive roads, such as the technology he had just presented.

One of the main advantages of using ViewComponent is the possibility of having unit tests, which are several orders of magnitude faster than integration or acceptance tests (e2e).

Although full page visual (regression or approval) testing are closer to the latter (and its scary slowness), component visual testing would be closer to the former, unit tests, making it IMHO a very attractive road to be explored by the community.

I owe the Rails community a lot, much more than just this (lousy xD) little post (and talk!!), but this Proof of Concept is my small grain of sand to explore that road. 🧐

RailsStorybookAppraise (RSA): a PoC

RSA (github.com/nando/RSA) is a Proof of Concept of components visual testing using RailsViewComponents an Appraise.qa.

To perform this PoC locally we need to have Rails, Node, and Yarn installed. Although the fastest way to do it is to clone the RSA repo, we can also do it step by step by following, in reverse, their self-explanatory commit messages (they use the gitmoji convention and, when needed, have an extra description of what they add to the project):

🔧 Run “rails new RSA” and complete .gitignore

We create our Rails application with “rails new AwesomeSite” and add to .gitignore the files we know we don’t want to have in the repository (*/**/*.stories.json, node_modules, public/storybook/ y test/appraise/results/).

➕ $ bundle add view_component view_component_storybook

➕ $ yarn add storybook/server — dev…

In this commit we made three changes to incorporate Storybook into the Node project:

  • We add two of its packages (server and addon-controls):
$ yarn add @storybook/server @storybook/addon-controls --dev
  • We add two scripts in our package.json: one to run Storybook server in the development environment, and the other to generate it with static files in public/storybook:
{ 
"scripts": {
"storybook": "start-storybook",
"storybook:build": "build-storybook -o public/storybook"
},
[...]
}
  • We add the .storybook folder with its first two files: main.js (to tell Storybook where we have the definition of our stories), and preview.js (to tell it where is the server to which it can request the referenced components)..

➕ $ bundle install rack-cors

🔧 Require storybook/engine & rack-cors cfg.

We add the Storybook engine in our config/application.rb file:

[...]
require "rails/all"
require "view_component/storybook/engine"
[...]

In config/environments/development.rb we add the location of the assets server and a basic CORS configuration:

[...]
config.action_controller.asset_host = 'http://localhost:3000'
config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '/rails/stories/*',
:headers => :any,
:methods => [:get]
end
end
[...]

✨ Our button component first two stories are (almost) here!!

This commit code was generated by our first component, with:

$ rails generate component Button title --sidecar
create app/components/button_component.rb
invoke test_unit
create test/components/button_component_test.rb
invoke erb
create app/components/button_component/button_component.html.erb
$

And replacing its empty test with the first two stories of our button that will be visually tested soon…

class ButtonComponentStories < ViewComponent::Storybook::Stories
story(:with_short_text) do
constructor(title: "Title")
end
story(:with_long_text) do
constructor(title: "THE REALLY BIG BUTTON THAT DOESN'T DO ANYTHING")
end
end

Finally to give some colour to our buttons we are going to add some minimal styling (courtesy of w3schools.com, thanks!) to our application.css file:

button {
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}

To view Storybook in our development environment we first need to generate our component stories in its JSON format (running the view_component_storybook:write_stories_json Rake task) and start the Rails server:

At this point we can, either raise a local Storybook server by launching “npm run storybook”, or deploy it with static files in public/storybook using its storybook:build script instead:

Whether we do the latter and visit http://localhost:3000/storybook/, or do the former and launch Storybook locally, we should see the first two stories on our brand new button:

And without further ado let’s add the visual tests (or “examples”, as Appraise calls them) of our first two stories to our project…

➕ $ yarn add appraise — dev (+ npm scripts)

We add the Appraise dependency:

$ yarn add appraise --dev

And we edit again the package.json file to add the two scripts needed to launch and approve the examples:

{
"scripts": {
"examples": "./node_modules/.bin/appraise run [...]",
"approve": "./node_modules/.bin/appraise approve [...]",
[...]
},
[...]
}

🧪 Add the first two “examples” (aka tests) of our button

# My Awesome Components ExamplesHere we talk about our components awesomeness (if needed!! xDDD).## Button component### With short text
~~~yaml example="Button w short text" fixture="url.js"
path: /rails/stories/button_component/with_short_text
~~~
### With long text
~~~yaml example="Button w long text" fixture="url.js"
path: /rails/stories/button_component/with_long_text
~~~

We launch the application tests by running our “examples” script and Appraise tells us that it has no images to compare the result of the executions with (i.e. it has no “base line”):

If we go to the folder where Appraise generates the results of its execution (tests/appraise/results) we can navigate to see the images of the two buttons that our component is returning at this time:

In the pages with the results, when these are negative, Appraise offers us the command that we should execute if we want the image/s obtained by the last execution to become our “base line” for the “example/s” (as our scripts use npm to make these commands work we have to add “npm run” in front of them, and two dashes in front of the parameters).

In the previous page we can see that Appraise offers us the command to approve all the images of the component, but instead of approving all of them we will approve only the first one (we can navigate to that particular failure to copy&paste its approval parameters):

Tras aprobar la imagen el test/ejemplo pasa a ser válido.

For the second button we have other plans…

¡¡We want the second button image to look like the one in TRBBTDDA!!

To achieve our goal we download the TRBBTDDA button image and put it in the folder test/appraise/examples where we have, on one side the markdown with the definition of our unique example, and now also the image of the button that we have just validated that Appraise stored next to it:

Along with the definition of our tests/examples Appraise adds its “base line”.

Let’s open my_awesome_components.md to see what Appraise has added to it when we approve the short text button example:

# My Awesome Components ExamplesHere we talk about our components awesomeness (if needed!! xDDD).## Button component### With short text
~~~yaml example="Button w short text" fixture="url.js"
path: /rails/stories/button_component/with_short_text
~~~
### With long text
~~~yaml example="Button w long text" fixture="url.js"
path: /rails/stories/button_component/with_long_text
~~~
## Button w short text![Button w short text](Buttonwshorttext-ae761028-977e-43f8-809c-956fedde8669.png)(generated: 8/1/2022 16:53:09)

It has added a new section with the name of the example we have just validated in which it stores the name of the associated image and the date it was generated. To add the TRBBTDDA image, we duplicate this section and then modify its name and the name of its associated image:

[...]## Button w long text![Button w long text](button-that-doesnt-do-anything.png)

Now when we launch the tests Appraise tells us that the images have different dimensions:

And in the results of the story of the button with long text we can see the current situation:

We copy button-that-doesnt-do-anything.png in app/assets/images and modify our view code to show it if the title is long:

<div data-controller="button-component--button-component">
<%- if title.length > 30 -%>
<%= image_tag("button-that-doesnt-do-anything.png",
onclick: "alert('Hi there!! Nothing here!!');") %>
<%- else -%>
<%= button_to title %>
<%- end -%>
</div>

Now, if we go to our Storybook and click on the button of his second story… DOES NOTHING!! xDDD

Let’s see what happen when we launch our visual test:

Appraise it continues failing because the dimensions of the original button in our “base line” (320x200px) do not match those of the one generated by the browser when rederderized as part of a webpage (1024x220px):

The width of the image generated by the browser can be set using the initial_width parameter of Appraise. In our case we have it set in the definition of our “examples” script to 1024px:

As a matter of fact, for our visual validation requirements (that to say, a Proof of Concept of a “Chromatic DIY tool”) the image generated from Rails for our button is perfectly valid. And if we approve this exampleee… we will have created our first real “base line”!!! ¡¡chispas!!*

(*) “Sparks!!” is a classic joke for first time achievements that comes from a funny teenage perfume commercial on Spanish TV. By the way, this could also be my first talk in Madrid-rb!! ✨✨✨

--

--

--

Ex-@colgado (Apuntes Colgados). Opinions are my own.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

The people behind the tech: Amal’s story

Partition Deletion With fdisk Command On Linux Machine

Programming Language Predictions 2020

Java lessons

Dagger 2 for Android Beginners — Advanced part I

Fastly launch an EKS cluster with eksctl deploying a docker Flask web app with cluster autoscaler…

How To Use $GLOBALS[] In PHP

Effective DevOps for Enterprise Websites

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
nando

nando

Ex-@colgado (Apuntes Colgados). Opinions are my own.

More from Medium

How to: Common imports for all your Jest test files

What I’ve learnt about Cypress this month (as a newcomer) 🤖

What’s Your Route?

Farewell mouse/touch events, welcome pointer events