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:
- We can access
test/javascripts/src/fixtures
on blade viahttp://localhost:9876/fixtures
- We have access to
response.body
on Rails test controller - Let’s write to files on Rails test suite
- 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:
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:
- Easy to install
- Run tests on many browsers
- 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!