Keep screenshots of your app update with RSpec

#automated-test, #dev, #ruby

If you know me, you know me for one thing:

ALWAYS write automated test for your software, ALWAYS. Period.

And not only unit test but all kind of test you can.

But I also share this idea:

If you are doing the same thing again and again, why not make a machine do it?

With these two things together you can come with a solution for documenting your app.

  1. You have the automated test already, and
  2. You can take screenshots within the tests

I will show here what I did in one of my projects.

The Doc model

First thing I needed was a model to call all, I came up with this content:

class Doc
  attr_reader :doc_id

  def initialize(doc_id)
    @doc_id = doc_id
  end

  def self.all
    list.keys.map { |doc_id| Doc.new(doc_id) }
  end

  def self.list
    if Rails.env.production?
      @list ||= Psych.load(File.read(Rails.root.join(BASE_DIR, "index.yml")))
    else
      Psych.load(File.read(Rails.root.join(BASE_DIR, "index.yml")))
    end
  end

  def title
    list.fetch(@doc_id) { { "title" => "Not found" } }["title"]
  end

  def description
    list.fetch(@doc_id) { { "description" => "" } }["description"]
  end

  def path
    return not_found if invalid?

    @path ||= Rails.root.join(BASE_DIR, @doc_id + ".html.erb")
  end

  def url
    "/#{BASE_DIR}/#{@doc_id}"
  end

  def invalid?
    !valid?
  end

  private

  BASE_DIR = "docs".freeze

  def not_found
    Rails.root.join(BASE_DIR, "not_found.html.erb")
  end

  def whitelist
    list.keys
  end

  def valid?
    whitelist.include?(@doc_id)
  end

  def list
    self.class.list
  end
end

You can make it DRYer, but for now, it is working.

The DocsController

The controller is simpler:

class DocsController < ApplicationController
  layout "docs"

  def index
    @docs = Doc.all
    @title = "Docs"
    @description = ""
  end

  def show
    @doc = Doc.new(params[:id])

    @title = @doc.title
    @description = @doc.description

    if @doc.invalid?
      render "show", status: 404
    end
  end
end

It is mandatory to set @title and @description because the docs layout needs them to correct set the metatags.

The route for this is very simple:

resources :docs, only: %i[index show]

Views

app/views/layouts/docs.html.erb:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>MyApp - <%= @title %></title>
    <meta name="description" content="<%= @description %>">
  </head>
  <body>
    <%= yield %>
  </body>
</html>

This is a proof of concept layout, so expand it as you need.

app/views/docs/index.html.erb:

<ul>
  <% @docs.each do |doc| %>
    <li><%= link_to doc.title, doc.url %></li>
  <% end %>
</ul>

And app/views/docs/show.html.erb:

<%= render "header" %>

<h2><%= @doc.title %></h2>

<%= render file: @doc.path %>

Writing the documentation

First, you need the screenshots. Change your RSpec code to have something like this:

page.driver.browser.manage.window.resize_to(1024, 768)
page.driver.save_screenshot Rails.root.join("app/assets/images/screenshots/create_account_01.png"), full: true

Remember: you have to set the browser size only once. And you can make this code even DRYer by creating a helper or something in spec/support.

WARNING: The first time you run the test you will have to create the directory app/assets/images/screenshots.

If you had paid attention to my code you noticed that all the documentation is inside the docs directory. But besides that, it is also required an index.yml file.

I did not want to have another dependency to set a structure like Jekyll which you have the metadata on the top of the file and I did not want to have to much work doing the parser.

The index.yml looks like this:

create_account:
  title: "Create an Account"
  description: "How to create an Account"

You can have an extra lang attribute if your app is multi-language. Just make sure to change everything else.

Writing the documentation. Why I had so much work to do this?

<p>
After loggining click in <em><%= Account.model_name.human(count: 2) %></em>:
</p>

<%= image_tag "screenshots/create_account_01.png" %>

[ ... ]

As you can see the best part is to write the documentation, you can use the regular helpers, models, and even access the database (just make sure you have a caching mechanism in front of it).

That is all. I already have this structure in one of my apps, but I also created a draft of this feature in my organize2 repo, check this commit.

Final notes

  • As I mentioned before, if your app is multi-language it is pretty easy to add support to that
  • It is up to you when to update the screenshots. When you run your build RSpec will regenerate the image even if you have not changed anything at all
  • If you are too bored to manually update the screenshots add it to your CI, and upload it to S3 or other hosting services. OR as part of your deploy, you can set write all the documentation test in spec/features/docs for example and run only that tests when deploying
  • Add this to your config/initializers/assets.rb:
Rails.application.config.assets.paths << Rails.root.join("app/assets/images/screenshots")
Rails.application.config.assets.precompile << Rails.root.join("app/assets/images/screenshots")
  .entries
  .map(&:to_s)
  .select { |path| path.match(%r[png]) }