Using Blade (JavaScript) Runner on Rails App

Motivation

Você pode ver esse post em português

Looking for a javascript test runner for Rails, I found some options, so I decided to choose Blade, as this is not a comparison (I'm not even experienced to do this), here are the reasons why I preferred Blade:

  • Easy to configure on standalone javascript libs
  • Sprockets support
  • Actively used on Turbolinks, Trix, and ActionCable
  • I believe it can become Rails default javascript runner in the future

So I started to configure as usual on a javascript lib.

# Gemfile
group :development, :test do
  gem 'blade'
end

After editing Gemfile, as usual:

bundle install

And added a blade.yml

load_paths:
  - test/javascripts/src
  - test/javascripts/vendor

logical_paths:
  - application.js
  - test.js

As this is a Rails application, my idea is set load paths to test/javascripts/{src/vendor} so Blade load my test and support libraries, test.js is the entry point for the test suite, and application.js is the javascript entry file on Rails.

I got blade running as expected, but it didn't load application.js, asking @javan on Twitter, he replied there's no built-in Rails integration, so we are on our own.

The first trial was add app/assets/javascripts to load_path, it worked, but no javascript asset that is loaded using a gem was found (like jquery-rails).

So why not hook on blade bin file, make Rails load the assets path and configure on blade?

After some time figuring out how to get things working, I got into this bin for blade, let comment each step:

#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'blade' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
  Pathname.new(__FILE__).realpath)

require "rubygems"
require "bundler/setup"

# COMMENT: here finishes bundler prelude

# COMMENT: Initialize rails application
require_relative File.join('..', 'config/application.rb')
Rails.application.initialize!

# COMMENT: Initialize Rails assets paths...
paths = Rails.application.assets.paths
cwd = `pwd`.chomp + "/"
paths = paths.map { |x| x.gsub(cwd, '') }

# COMMENT: ...and concatenate with specified on .blade.yml
blade_config = YAML::load_file(File.join(__dir__, '..', '.blade.yml'))
paths.concat(blade_config["load_paths"])
Blade.initialize!(interface: 'runner', load_paths: paths)

# COMMENT: bundler run blade
load Gem.bin_path("blade", "blade")

I'm not sure this is the better approach; I got positive feedback from @javan so next step is to make this a blade plugin :)

Bonus: Fixtures

One of my primary motivation on getting a javascript runner was make sure my fixtures were always updated, my first idea was to process the erb and... no way!

So I had an enlightenment, it's a standard approach to test the (view) output on a Rails test controller (ok, the idea is not if this is the approach, but it's possible) because we have response.body, so why not write this to fixtures?

The rationale behind is:

  1. We can access test/javascripts/src/fixtures on blade via http://localhost:9876/fixtures
  2. We have access to response.body on Rails test controller
  3. Let's write to files on Rails test suite
  4. Run Javascript test suite with updated fixtures

Edited test_helper.rb and added

require 'fileutils'

class ActiveSupport::TestCase
  def save_fixture(name, selector = nil)
    fixture_dir = File.dirname(name)
    fixture_dir = File.join(fixtures_dir, fixture_dir)
    fixture_path = File.join(fixture_dir, File.basename(name) + ".html")

    unless File.directory?(fixture_dir)
      FileUtils.mkdir_p(fixture_dir)
    end

    # Get only body for fixture (or selector if set)
    selector = selector.nil? ? 'body' : selector
    output = Nokogiri::HTML(response.body)
    if (selector == "body")
      output = output.css(selector)[0].children.to_s
    else
      output = output.css(selector)[0].to_s
    end
    File.write(fixture_path, output)
  end
end

# Setup JavaScript Fixtures Dir
def fixtures_dir
  test_dir = File.dirname(File.expand_path(__FILE__))
  File.join(test_dir, 'javascripts', 'src', 'fixtures')
end

puts fixtures_dir
unless File.directory?(fixtures_dir)
  FileUtils.mkdir_p(fixtures_dir)
end

On a controller test in only needed:

require 'test_helper'

class ThingControllerTest < ActionController::TestCase
  test "index" do
    get :index
    assert :success
    # do not specify second parameter if you want the whole body
    save_fixture('things/table', 'table#things')
  end
end

This way we get the response.body written to test/javascripts/src/fixtures/things/table.html

And you are ready to write you JS test

QUnit.test( "ThingsController#index", function( assert ) {
  assert.expect(1);
  var done = assert.async();

  $.get( "/fixtures/things/table.html", function( data ) {
    $( "#qunit-fixture" ).html( data );
    length = $("table#things tbody").find('tr').length;
    assert.equal(length, 2, "Two things listed");
    done();
  });
});

The result is excellent:

blade test suite

If the view is changed, the fixture will be updated, and if JS is broken, the test will fail.

Finishing

For me this is reached all my expectations for a javascript test runner for Rails because it is a powerful setup on the following points:

  1. Easy to install
  2. Run tests on many browsers
  3. Can run on CI using sauce labs (maybe a blade-phantomjs in the future?)

And what do you think about this approach, you have any tips on testing JS on Rails that you'd like to share?

ps: This apporach became a Blade plugin, please give feedback!

Happy Testing on Rails!