Friday, August 26, 2011

Testing a Rails 3.1 Engine's Javascript with Jasmine

At work, we've got a Rails engine that provides some complex drop-in forms to various applications. The forms have associated Javascript which, on Rails 3.0, we had to copy and paste between applications. That's both annoying and fragile, since things can get out of sync.

To test our engine, we had created a dummy app with the Enginex gem, located in spec/dummy.

In upgrading the engine to 3.1 (currently on release candidate 6), we wanted to move the associated JS into the engine, so that they would be versioned together in a single gem.

That wasn't so hard, but we also needed our associated Jasmine tests to run. For that, we needed to precompile our Javascript before rake jasmine ran so that Jasmine could load and test it.

Precompile Problems


And that's where things got sticky. According to Ryan Biggs' documentation, the default matcher for precompiling files is:

[ /\w+\.(?!js|css).+/, /application.(css|js)$/ ]

Basically, besides application.js and application.css, any js (or css) files with multiple dots in it, whether it's foo.js.coffee or jquery.cookie.js, will get compiled to its own file.

We had two problems with that:

  • It didn't match our engine's manifest file, because it's not named application.js. (It's not named that because in the host application, we will already have an application.js and we don't want a conflict.)
  • It did match a bunch of other JS files which didn't need to be compiled separately. For example, the manifest requires jquery.cookie.js, so that file gets compiled and added into the manifest file. It doesn't need to also be compiled as jquery-aaff723a97d782d30e3fc2f0dee5d849.cookie.js; we don't plan to serve it separately.

To solve the first problem, in Engine.rb, we added:

initializer "myEngine.asset_pipeline" do |app|
  app.config.assets.precompile << 'myEngine-manifest.js'
end

To solve the second problem, we... did nothing. We don't care. We're letting it create extra compiled files that we don't need.

We could have changed the match regex to only match files with endings like in .js.something or .css.something, instead of the broad rule that will also match .min.js. But doing that in an engine might have broken a host application if it were compiling something we didn't foresee. Maybe at some point a host app will want to compile .csv.erb files or something; we don't want to preclude that.

So we chose "be unobtrusive to the host app" over "prevent unnecessary compilation".

(We also could have prevented jquery.cookie.js from being compiled separately by renaming it to jquery-cookie.js, but we think that's annoying.)

Jasmine Setup


Now that our myEngine-manifest.js was being compiled to myEngine-manifest-20309309d9309330309390.js, we needed to let Jasmine know to load that single file, containing all our compiled Javascript, rather than all the separate files it had to load previously. So, in jasmine.yml, we now have:

src_files:
  - spec/dummy/public/assets/myEngine-manifest-*.js

The * will match whatever hash fingerprint is added to the filename.

Now, before we can run rake jasmine, we needed to make sure everything was compiled appropriately. So we created this simple Rake task in the engine's Rakefile:

task :precompile_jasmine => ['assets:clean', 'assets:precompile', 'jasmine']

Engine Rake tasks don't necessarily need access to Rails tasks, so to get the task above to work, we had to put the following above it so that we'd have access to those asset-related Rails tasks:

load File.expand_path('../spec/dummy/Rakefile', FILE)


Turning on Sprockets


To get Sprockets working in our dummy application, we had to make two changes to spec/dummy/config/application.rb:

  • Below the other railties requirements, add require sprockets/railtie
  • Within the app settings block, add config.assets.enabled = true

You may not need to do this; it's a side-effect of the fact that we generated our engine with a version of the Enginex gem that preceded Rails 3.1.

Other setup


  • To get Jasmine working with Rails 3.1, make sure you've got a new enough version. Jasmine 1.0.2.1 worked for us.
  • You don't want to check in all your compiled assets, so be sure to add this to your .gitignore: spec/dummy/public/assets/*

Weirdness


Something weird about assets:precompile makes it, and any task that runs after it, run twice. This means that `rake jasmine` requires two interrupts to shut down properly in our current setup. (In a previous attempt, it tried to run again while it was running, and got an error because it couldn't re-bind to the same port.)

Addendum


There's no way I could have figured this out by myself, and I'm not sure that I'll be able to answer questions about it. Adam Hunter and I worked on it together, and he contributed more brainpower than I did. But after all the blind alleys and frustration, I was determined to write up what we did, and got Adam's help doing so. So: good luck replicating this. :)

Also, if you're looking to use Jasmine in a Rails 3.1 app with coffeescript, this blog post from Pivotal Labs may be more helpful. We got some ideas from it, too.