Visual Regression Testing Rails Mailers
Recently in work we’ve started using Percy.io to catch visual regressions in our UI. It’s a great tool - it allows us to catch regressions in CSS or HTML that we would have only caught previously with manual testing.
The service costs us $100 a month for 3 developers - compare that to the salary of a dedicated QA and it’s a no brainer. One missing piece of the puzzle which doesn’t come out of the box is testing Rails mailers. We have quite a few HTML emails that we send out which are prone to the same visual regression problems that our UI is. The set up is relatively easy using action mailer previews.
ActionMailer::Previews are only mounted in your development environment by default. We need to mount them in the test environment in order to take screenshots with Percy:
config.action_mailer.show_previews = true
# the path to the directory of your ActionMailer::Previews
config.action_mailer.preview_path = Rails.root.join("spec/mailers/previews")
Percy doesn’t load iframes by default and mailer previews run inside an iframe so we need to change the default Percy settings in rails_helper.rb
:
config.before(:suite) do
Percy::Capybara.use_loader(::Percy::Capybara::Loaders::SprocketsLoader, include_iframes: true)
Percy::Capybara.initialize_build
end
Lastly, we can write an integration spec that uses meta programming to iterate over all of our mailers:
require 'spec_helper'
describe 'Mail Preview' do
it 'previews all the mails', js: true do
# set up any data required for the mailer previews
create(:user)
# The action mailer preview classes don't load unless we visit a mailer url to begin with
visit '/rails/mailers/'
ActionMailer::Preview.subclasses.each do |preview_class|
# build the urls for the previews
(preview_class.instance_methods - Object.methods).each do |method|
slug = "#{preview_class.name.underscore.gsub('_preview', '')}/#{method}"
begin
visit "/rails/mailers/#{slug}"
within_frame(find('[name="messageBody"]')) do
expect(page).to have_selector('table.body')
Percy::Capybara.snapshot(page, name: slug)
end
rescue AbstractController::ActionNotFound
puts "Couldnt visit #{slug}"
end
end
end
end
end