HyperSpec

Build Status

With HyperSpec you can run isomorphic specs for all your Hyperloop code using RSpec. Everything runs as standard RSpec test specs.

For example if you have a component like this:

class SayHello < React::Component::Base
  param :name
  render(DIV) do
    "Hello there #{params.name}"
  end
end

Your test spec would look like this:

describe 'SayHello', js: true do
  it 'has the correct content' do
    mount "SayHello", name: 'Fred'
    expect(page).to have_content('Hello there Fred')
  end
end

The mount method will setup a blank client window, and mount the named component in the window, passing any parameters.

Notice that the spec will need a client environment so we must set js: true.

The mount method can also take a block which will be recompiled and set to the client before mounting the component. You can place any client side code in the mount block including the definition of components.

describe "the mount's code block", js: true do
  it 'will be recompiled on the client' do
    mount 'ShowOff' do
      class ShowOff < React::Component::Base
        render(DIV) { 'Now how cool is that???' }
      end
    end
    expect(page).to have_content('Now how cool is that???' )
  end
end

Why?

Hyperloop wants to make the server-client divide as transparent to the developer as practical. Given this, it makes sense that the testing should also be done with as little concern for client versus server.

HyperSpec allows you to directly use tools like FactoryBot (or Hyperloop Operations) to setup some test data, then run a spec to make sure that a component correctly displays, or modifies that data. You can use Timecop to manipulate time and keep in sync between the server and client. This makes testing easier and more realistic without writing a lot of redundant code.

Installation

Add this line to your application's Gemfile in the test section:

gem 'hyper-spec'

Execute:

$ bundle install

and then in your spec_helper.rb file

require 'hyper-spec'

You will also need to install selenium, poltergeist and firefox version 46.0.1 (ff latest still does not play well with selenium).

Sadly at this time the selenium chrome driver does not play nicely with Opal, so you can't use Chrome. We are working on getting rid of the whole selenium business. Stay tuned.

Environment Variables

You can set DRIVER to ff to run the client in Firefox and see what is going on. By default tests will run in poltergeist which is quicker, but harder to debug problems.

DRIVER=ff bundle exec rspec

Spec Helpers

HyperSpec adds the following spec helpers to your test environment

  • mount
  • client_option and client_options
  • on_client
  • isomorphic
  • evaluate_ruby
  • expect_evaluate_ruby
  • expect_promise
  • call back and event history methods
  • pause
  • attributes_on_client
  • size_window
  • add_class

The mount Method

mount takes the name of a component, prepares an empty test window, and mounts the named component in the window.
You may give a block to mount which will be recompiled on the client, and run before mounting. This means that the component mounted may be actually defined in the block, which is useful for setting up top level wrapper components, which will invoke your component under test. You can also modify existing components for white box testing, or local fixture data, constants, etc.

mount may also be given a hash of the parameters to be passed to the component.

mount 'Display', test: 123 do
  class Display < React::Component::Base
    param :test
    render(DIV) { params.test.to_s }
  end
end

The client_option Method

There are several options that control the mounting process. Use client_option (or client_options) before accessing any client side to set any of these options:

  • render_on: :server_only, :client_only, or :both, default is client_only.
  • layout: specify the layout to be used. Default is :none.
  • style_sheet: specify the name of the style sheet to be loaded. Defaults to the application stylesheet.
  • javascript: specify the name of the javascript asset file to be loaded. Defaults to the application js file.

For example:

it "can be rendered server side only" do
  client_option render_on: :server_only
  mount 'SayHello', name: 'George'
  expect(page).to have_content('Hello there George')
  # Server only means no code is downloaded to the client
  expect(evaluate_script('typeof React')).to eq('undefined')
end

If you need to pull in alternative style sheets and javascript files, the recommended way to do this is to

  1. Add them to a specs/assets/stylesheets and specs/assets/javascripts directory and
  2. Add the following line to your config/environment/test.rb file:
    ruby config.assets.paths << ::Rails.root.join('spec', 'assets', 'stylesheets').to_s config.assets.paths << ::Rails.root.join('spec', 'assets', 'javascripts').to_s

This way you will not pollute your application with these 'test only' files.

The javascript spec asset files can be .rb files and contain ruby code as well. See the specs for examples!

The on_client Method

on_client takes a block and compiles and runs it on the client. This is useful in setting up test constants and client only fixtures.

Note that on_client needs to proceed any calls to mount, evaluate_ruby, expect_evaluate_ruby or expect_promise as these methods will initiate the client load process.

The isomorphic Method

Similar to on_client but the block is also run on the server. This is useful for setting constants shared by both client and server, and modifying behavior of isomorphic classes such as ActiveRecord models, and HyperOperations.

isomorphic do
  class SomeModel < ActiveRecord::Base
    def fake_attribute
      12
    end
  end
end

The evaluate_ruby Method

Takes either a string or a block, dynamically compiles it, downloads it to the client and runs it.

evaluate_ruby do
  i = 12
  i * 2
end
# returns 24

isomorphic do
  def factorial(n)
    n == 1 ? 1 : n * factorial(n-1)
  end
end

expect(evaluate_ruby("factorial(5)")).to eq(factorial(5))

evaluate_ruby can also be very useful for debug. Set a breakpoint in your test, then use evaluate_ruby to interrogate the state of the client.

The expect_evaluate_ruby Method

Combines expect and evaluate methods:

expect_evaluate_ruby do
  i = 1
  5.times { |n| i = i*n }
  i
end.to eq(120)

The expect_promise Method

Works like expect_evaluate_ruby but is used with promises. expect_promise will hang until the promise resolves and then return to the results.

expect_promise do
  Promise.new.tap do |p|
    after(2) { p.resolve('hello') }
  end
end.to eq('hello')

Call Back and Event History Methods

HyperReact components can generate events and perform callbacks. HyperSpec provides methods to test if an event or callback was made.

mount 'CallBackOnEveryThirdClick' do
  class CallBackOnEveryThirdClick < React::Component::Base
    param :click3, type: Proc
    def increment_click
      @clicks ||= 0
      @clicks = (@clicks + 1)
      params.click3(@clicks) if @clicks % 3 == 0
    end
    render do
      DIV(class: :tp_clicker) { "click me" }
      .on(:click) { increment_click }
    end
  end
end

7.times { page.click('#tp_clicker') }
expect(callback_history_for(:click3)).to eq([[3], [6]])

Note that for things to work, the param must be declared as a type: Proc.

  • callback_history_for: the entire history given as an array of arrays
  • last_callback_for: same as callback_history_for(xxx).last
  • clear_callback_history_for: clears the array (userful for repeating test variations without remounting)
  • event_history_for, last_event_for, clear_event_history_for: same but for events.

The pause Method

For debugging. Everything stops, until you type go() in the client console. Running binding.pry also has this effect, and is often sufficient, however it will also block the server from responding unless you have a multithreaded server.

The attributes_on_client Method

This feature is currently untested - use at your own risk.

This reads the value of active record model attributes on the client.

In other words the method attributes_on_client is added to all ActiveRecord models. You then take a model you have instance of on the server, and by passing the Capybara page object, you get back the attributes for that same model instance, currently on the client.

expect(some_record_on_server.attributes_on_client(page)[:fred]).to eq(12)

Note that after persisting a record the client and server will be synced so this is mainly useful for debug or in rare cases where it is important to interrogate the value on the client before its persisted.

The size_window Method

Sets the size of the test window. You can say: size_window(width, height) or pass one of the following standard sizes: to one of the following standard sizes:

  • small: 480 X 320
  • mobile: 640 X 480
  • tablet: 960 X 640
  • large: 1920 X 6000
  • default: 1024 X 768

example: size_window(:mobile)

You can also modify the standard sizes with :portrait

example: size_window(:table, :portrait)

You can also specify the size by providing the width and height.

example: size_window(600, 600)

size_window with no parameters is the same as size_window(:default)

Typically you will use this in a before(:each) or before(:step) block

The add_class Method

Sometimes it's useful to change styles during testing (mainly for debug so that changes on screen are visible.)

The add_class method takes a class name (as a symbol or string), and hash representing the style.

it "can add classes during testing" do
  add_class :some_class, borderStyle: :solid
  mount 'StyledDiv' do
    class StyledDiv < React::Component::Base
      render(DIV, id: 'hello', class: 'some_class') do
        'Hello!'
      end
    end
  end
  expect(page.find('#hello').native.css_value('border-right-style')).to eq('solid')
end

Integration with the Steps gem

The rspec-steps gem can be useful in doing client side testing. Without rspec-steps, each test spec will cause a reload of the browser window. While this insures that each test runs in a clean environment, it is typically not necessary and can really slow down testing.

The rspec-steps gem will run each test without reloading the window, which is usually fine.

Checkout the rspec-steps example in the hyper_spec.rb file for an example.

Note that hopefully in the near future we are going to build a custom capybara driver that will just directly talk to Hyperloop on the client side. Once this is in place these troubles should go away! - Volunteers welcome to help!

Timecop Integration

HyperSpec is integrated with Timecop to freeze, move and speed up time. The client and server times will be kept in sync when you use any these Timecop methods:

  • freeze: Freezes time at the specified point in time (default is Time.now)
  • travel: Time runs normally forward from the point specified.
  • scale: Like travel but times runs faster.
  • return: Return to normal system time.

For example:

Timecop.freeze # freeze time at current time
# ... test some stuff
Timecop.freeze Time.now+10.minutes # move time forward 10 minutes
# ... check to see if expected events happened etc
Timecop.return
Timecop.scale 60, Time.now-1.year do
  # Time will begin 1 year ago but advance 60 times faster than normal
  sleep 10
  # still sleeps for 10 seconds YOUR time, but server and client will
  # think 10 minutes have passed
end
# no need for Timecop.return if using the block style

See the Timecop README for more details.

There is one confusing thing to note: On the server if you sleep then you will sleep for the specified number of seconds when viewed outside of the test. However inside the test environment if you look at Time.now, you will see it advancing according to the scale factor. Likewise if you have a after or every block on the client, you will wait according to simulated time.

Common Problems

If you are getting failures on Poltergeist but not Firefox, make sure you are not requiring browser in your components.rb. Requiring browser/interval or browser/delay is okay.

Development

After checking out the repo, run bundle install and you should be good to go.

Tests are run either by running rake or for more control:

DRIVER=ff bundle exec rspec spec/hyper_spec.rb

where DRIVER can be either ff (firefox) or pg (poltergeist - default).

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/hyper-spec. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.