# your-gem.gemspec
s.add_development_dependency 'reactive-ruby' # only if not already a dependency
s.add_development_dependency 'rake'
s.add_development_dependency 'rspec-rails', '3.3.3'
s.add_development_dependency 'timecop'
s.add_development_dependency 'opal-rspec', '0.4.3'
s.add_development_dependency 'sinatra'
# For Test Rails App
s.add_development_dependency 'rails', '4.2.4'
s.add_development_dependency 'react-rails', '1.3.1'
s.add_development_dependency 'opal-rails', '0.8.1'
if RUBY_PLATFORM == 'java'
s.add_development_dependency 'jdbc-sqlite3'
s.add_development_dependency 'activerecord-jdbcsqlite3-adapter'
s.add_development_dependency 'therubyrhino'
else
s.add_development_dependency 'sqlite3', '1.3.10'
s.add_development_dependency 'therubyracer', '0.12.2'
# The following allow react code to be tested from the server side
s.add_development_dependency "rspec-mocks"
s.add_development_dependency "rspec-expectations"
s.add_development_dependency "pry"
s.add_development_dependency 'pry-rescue'#, git: "https://github.com/joallard/pry-rescue.git"
s.add_development_dependency 'pry-stack_explorer'
#s.add_development_dependency "factory_girl_rails" # add this if you want to use factory girl
s.add_development_dependency 'shoulda'
s.add_development_dependency 'shoulda-matchers'
s.add_development_dependency 'rspec-its'
s.add_development_dependency 'rspec-collection_matchers'
s.add_development_dependency 'database_cleaner' #, git: "https://github.com/DatabaseCleaner/database_cleaner.git"
s.add_development_dependency 'capybara'
s.add_development_dependency 'selenium-webdriver'
s.add_development_dependency "poltergeist"
s.add_development_dependency 'spring-commands-rspec'
s.add_development_dependency 'chromedriver-helper'
s.add_development_dependency 'rspec-steps'
s.add_development_dependency 'parser'
s.add_development_dependency 'unparser'
s.add_development_dependency 'jquery-rails'
end
# Rakefile
require 'bundler'
Bundler.require
Bundler::GemHelper.install_tasks
require 'rspec/core/rake_task'
require 'opal/rspec/rake_task'
RSpec::Core::RakeTask.new('ruby:rspec')
Opal::RSpec::RakeTask.new('opal:rspec') do |s|
s.append_path 'spec/vendor'
s.index_path = 'spec/index.html.erb'
end
task :test do
Rake::Task['ruby:rspec'].invoke
Rake::Task['opal:rspec'].invoke
end
require 'generators/reactive_ruby/test_app/test_app_generator'
desc "Generates a dummy app for testing"
task :test_app do
ReactiveRuby::TestAppGenerator.start
puts "Setting up test app database..."
system("bundle exec rake db:drop db:create db:migrate > #{File::NULL}")
end
task default: [ :test ]
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules
# Ignore bundler config.
.bundle
spec/test_app
Gemfile.lock
# spec/spec_helper.rb
ENV["RAILS_ENV"] ||= 'test'
require 'opal'
require 'opal-rspec'
def opal?
RUBY_ENGINE == 'opal'
end
def ruby?
!opal?
end
if RUBY_ENGINE == 'opal'
require 'reactive-ruby'
require File.expand_path('../support/react/spec_helpers', __FILE__)
module Opal
module RSpec
module AsyncHelpers
module ClassMethods
def rendering(title, &block)
klass = Class.new do
include React::Component
def self.block
@block
end
def self.name
"dummy class"
end
def render
instance_eval &self.class.block
end
def self.should_generate(opts={}, &block)
sself = self
@self.async(@title, opts) do
expect_component_to_eventually(sself, &block)
end
end
def self.should_immediately_generate(opts={}, &block)
sself = self
@self.it(@title, opts) do
element = build_element sself, {}
context = block.arity > 0 ? self : element
expect((element and context.instance_exec(element, &block))).to be(true)
end
end
end
klass.instance_variable_set("@block", block)
klass.instance_variable_set("@self", self)
klass.instance_variable_set("@title", "it can render #{title}")
klass
end
end
end
end
end
RSpec.configure do |config|
config.include React::SpecHelpers
config.filter_run_including :opal => true
end
end
if RUBY_ENGINE != 'opal'
begin
require File.expand_path('../test_app/config/environment', __FILE__)
rescue LoadError
puts 'Could not load test application. Please ensure you have run `bundle exec rake test_app`'
end
require 'rspec/rails'
require 'timecop'
Dir["./spec/support/**/*.rb"].sort.each { |f| require f }
RSpec.configure do |config|
config.color = true
config.fail_fast = ENV['FAIL_FAST'] || false
config.fixture_path = File.join(File.expand_path(File.dirname(__FILE__)), "fixtures")
config.infer_spec_type_from_file_location!
config.mock_with :rspec
config.raise_errors_for_deprecations!
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, comment the following line or assign false
# instead of true.
config.use_transactional_fixtures = true
config.before :each do
Rails.cache.clear
end
config.filter_run_including focus: true
config.filter_run_excluding opal: true
config.run_all_when_everything_filtered = true
end
FACTORY_GIRL = false
#require 'rails_helper'
require 'rspec'
require 'rspec/expectations'
begin
require 'factory_girl_rails'
rescue LoadError
end
require 'shoulda/matchers'
require 'database_cleaner'
require 'capybara/rspec'
require 'capybara/rails'
require 'support/component_helpers'
require 'capybara/poltergeist'
require 'selenium-webdriver'
module React
module IsomorphicHelpers
def self.load_context(ctx, controller, name = nil)
@context = Context.new("#{controller.object_id}-#{Time.now.to_i}", ctx, controller, name)
end
end
end
module WaitForAjax
def wait_for_ajax
Timeout.timeout(Capybara.default_max_wait_time) do
begin
sleep 0.25
end until finished_all_ajax_requests?
end
end
def running?
result = page.evaluate_script("(function(active) {console.log('jquery is active? '+active); return active})(jQuery.active)")
result && !result.zero?
rescue Exception => e
puts "something wrong: #{e}"
end
def finished_all_ajax_requests?
unless running?
sleep 1
!running?
end
rescue Capybara::NotSupportedByDriverError
true
rescue Exception => e
e.message == "jQuery is not defined"
end
end
RSpec.configure do |config|
config.include WaitForAjax
end
RSpec.configure do |config|
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
# assertions if you prefer.
config.expect_with :rspec do |expectations|
# Enable only the newer, non-monkey-patching expect syntax.
# For more details, see:
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
expectations.syntax = [:should, :expect]
end
# rspec-mocks config goes here. You can use an alternate test double
# library (such as bogus or mocha) by changing the `mock_with` option here.
config.mock_with :rspec do |mocks|
# Enable only the newer, non-monkey-patching expect syntax.
# For more details, see:
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
mocks.syntax = :expect
# Prevents you from mocking or stubbing a method that does not exist on
# a real object. This is generally recommended.
mocks.verify_partial_doubles = true
end
config.include FactoryGirl::Syntax::Methods if defined? FactoryGirl
config.use_transactional_fixtures = false
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each) do |x|
puts " RUNNING #{x.full_description}"
end
config.before(:each, :js => true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
# Clear session data
Capybara.reset_sessions!
# Rollback transaction
DatabaseCleaner.clean
end
config.after(:all, :js => true) do
#size_window(:default)
end
config.after(:each, :js => true) do
#sleep(3)
end if ENV['DRIVER'] == 'ff'
config.include Capybara::DSL
Capybara.register_driver :chrome do |app|
#caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {"excludeSwitches" => [ "ignore-certificate-errors" ]})
caps = Selenium::WebDriver::Remote::Capabilities.chrome("chromeOptions" => {"args" => [ "--window-size=200,200" ]})
Capybara::Selenium::Driver.new(app, :browser => :chrome, :desired_capabilities => caps)
end
options = {js_errors: false,
timeout: 180,
phantomjs_logger: StringIO.new,
logger: StringIO.new,
inspector: true,
phantomjs_options: ['--load-images=no', '--ignore-ssl-errors=yes']}
Capybara.register_driver :poltergeist do |app|
Capybara::Poltergeist::Driver.new(app, options)
end
class Selenium::WebDriver::Firefox::Profile
def self.firebug_version
@firebug_version ||= '2.0.13-fx'
end
def self.firebug_version=(version)
@firebug_version = version
end
def frame_position
@frame_position ||= 'bottom'
end
def frame_position=(position)
@frame_position = ["left", "right", "top", "detached"].detect do |side|
position && position[0].downcase == side[0]
end || "bottom"
end
def enable_firebug(version = nil)
version ||= Selenium::WebDriver::Firefox::Profile.firebug_version
add_extension(File.expand_path("../bin/firebug-#{version}.xpi", __FILE__))
# For some reason, Firebug seems to trigger the Firefox plugin check
# (navigating to https://www.mozilla.org/en-US/plugincheck/ at startup).
# This prevents it. See http://code.google.com/p/selenium/issues/detail?id=4619.
self["extensions.blocklist.enabled"] = false
# Prevent "Welcome!" tab
self["extensions.firebug.showFirstRunPage"] = false
# Enable for all sites.
self["extensions.firebug.allPagesActivation"] = "on"
# Enable all features.
['console', 'net', 'script'].each do |feature|
self["extensions.firebug.#{feature}.enableSites"] = true
end
# Closed by default, will open detached.
self["extensions.firebug.framePosition"] = frame_position
self["extensions.firebug.previousPlacement"] = 3
# Disable native "Inspect Element" menu item.
self["devtools.inspector.enabled"] = false
self["extensions.firebug.hideDefaultInspector"] = true
end
end
Capybara.register_driver :selenium_with_firebug do |app|
profile = Selenium::WebDriver::Firefox::Profile.new
profile.frame_position = ENV['DRIVER'] && ENV['DRIVER'][2]
profile.enable_firebug
Capybara::Selenium::Driver.new(app, :browser => :firefox, :profile => profile)
end
Capybara.javascript_driver = :poltergeist
Capybara.default_max_wait_time = 2.seconds
Capybara.register_driver :chrome do |app|
Capybara::Selenium::Driver.new(app, :browser => :chrome)
end
if ENV['DRIVER'] =~ /^pg/
Capybara.javascript_driver = :poltergeist
elsif ENV['DRIVER'] == 'chrome'
Capybara.javascript_driver = :chrome
else
Capybara.javascript_driver = :selenium_with_firebug
end
config.include ComponentTestHelpers
end
FactoryGirl.define do
sequence :seq_number do |n|
" #{n}"
end
end if defined? FactoryGirl
end
# spec/support/react/spec_helpers.rb
module React
module SpecHelpers
`var ReactTestUtils = React.addons.TestUtils`
def renderToDocument(type, options = {})
element = React.create_element(type, options)
return renderElementToDocument(element)
end
def renderElementToDocument(element)
instance = Native(`ReactTestUtils.renderIntoDocument(#{element.to_n})`)
instance.class.include(React::Component::API)
return instance
end
def simulateEvent(event, element, params = {})
simulator = Native(`ReactTestUtils.Simulate`)
simulator[event.to_s].call(`#{element.to_n}.getDOMNode()`, params)
end
def isElementOfType(element, type)
`React.addons.TestUtils.isElementOfType(#{element.to_n}, #{type.cached_component_class})`
end
def build_element(type, options)
component = React.create_element(type, options)
element = `ReactTestUtils.renderIntoDocument(#{component.to_n})`
if `typeof React.findDOMNode === 'undefined'`
`$(element.getDOMNode())` # v0.12
else
`$(React.findDOMNode(element))` # v0.13
end
end
def expect_component_to_eventually(component_class, opts = {}, &block)
# Calls block after each update of a component until it returns true.
# When it does set the expectation to true. Uses the after_update
# callback of the component_class, then instantiates an element of that
# class The call back is only called on updates, so the call back is
# manually called right after the element is created. Because React.rb
# runs the callback inside the components context, we have to setup a
# lambda to get back to correct context before executing run_async.
# Because run_async can only be run once it is protected by clearing
# element once the test passes.
element = nil
check_block = lambda do
context = block.arity > 0 ? self : element
run_async do
element = nil; expect(true).to be(true)
end if element and context.instance_exec(element, &block)
end
component_class.after_update { check_block.call }
element = build_element component_class, opts
check_block.call
end
end
end
# spec/support/component_helpers.rb
require 'parser/current'
require 'unparser'
require 'pry'
module ComponentTestHelpers
def self.compile_to_opal(&block)
Opal.compile(block.source.split("\n")[1..-2].join("\n"))
end
TOP_LEVEL_COMPONENT_PATCH = lambda { |&block| Opal.compile(block.source.split("\n")[1..-2].join("\n"))}.call do #ComponentTestHelpers.compile_to_opal do
module React
class TopLevelRailsComponent
class << self
attr_accessor :event_history
def callback_history_for(proc_name)
event_history[proc_name]
end
def last_callback_for(proc_name)
event_history[proc_name].last
end
def clear_callback_history_for(proc_name)
event_history[proc_name] = []
end
def event_history_for(event_name)
event_history["_on#{event_name.event_camelize}"]
end
def last_event_for(event_name)
event_history["_on#{event_name.event_camelize}"].last
end
def clear_event_history_for(event_name)
event_history["_on#{event_name.event_camelize}"] = []
end
end
def component
return @component if @component
paths_searched = []
if params.component_name.start_with? "::"
paths_searched << params.component_name.gsub(/^\:\:/,"")
@component = params.component_name.gsub(/^\:\:/,"").split("::").inject(Module) { |scope, next_const| scope.const_get(next_const, false) } rescue nil
return @component if @component && @component.method_defined?(:render)
else
self.class.search_path.each do |path|
# try each path + params.controller + params.component_name
paths_searched << "#{path.name + '::' unless path == Module}#{params.controller}::#{params.component_name}"
@component = "#{params.controller}::#{params.component_name}".split("::").inject(path) { |scope, next_const| scope.const_get(next_const, false) } rescue nil
return @component if @component && @component.method_defined?(:render)
end
self.class.search_path.each do |path|
# then try each path + params.component_name
paths_searched << "#{path.name + '::' unless path == Module}#{params.component_name}"
@component = "#{params.component_name}".split("::").inject(path) { |scope, next_const| scope.const_get(next_const, false) } rescue nil
return @component if @component && @component.method_defined?(:render)
end
end
@component = nil
raise "Could not find component class '#{params.component_name}' for params.controller '#{params.controller}' in any component directory. Tried [#{paths_searched.join(", ")}]"
end
before_mount do
TopLevelRailsComponent.event_history = Hash.new {|h,k| h[k] = [] }
component.validator.rules.each do |name, rules|
if rules[:type] == Proc
TopLevelRailsComponent.event_history[name] = []
params.render_params[name] = lambda { |*args| TopLevelRailsComponent.event_history[name] << args.collect { |arg| Native(arg).to_n } }
end
end
end
def render
present component, params.render_params
end
end
end
end
def build_test_url_for(controller)
unless controller
Object.const_set("ReactTestController", Class.new(ActionController::Base)) unless defined?(::ReactTestController)
controller = ::ReactTestController
end
route_root = controller.name.gsub(/Controller$/,"").underscore
unless controller.method_defined? :test
controller.class_eval do
define_method(:test) do
route_root = self.class.name.gsub(/Controller$/,"").underscore
test_params = Rails.cache.read("/#{route_root}/#{params[:id]}")
@component_name = test_params[0]
@component_params = test_params[1]
render_params = test_params[2]
render_on = render_params.delete(:render_on) || :both
mock_time = render_params.delete(:mock_time)
style_sheet = render_params.delete(:style_sheet)
javascript = render_params.delete(:javascript)
code = render_params.delete(:code)
page = "<%= react_component @component_name, @component_params, { prerender: false } %>" # false should be: "#{render_on != :client_only} } %>" but its not working in the gem testing harness
page = "<script type='text/javascript'>\n//HELLO HELLO HELLO\n#{TOP_LEVEL_COMPONENT_PATCH}\n</script>\n"+page
if code
page = "<script type='text/javascript'>\n#{code}\n</script>\n"+page
end
#TODO figure out how to auto insert this line???? something like:
page = "<%= javascript_include_tag 'reactive-router' %>\n#{page}"
if (render_on != :server_only && !render_params[:layout]) || javascript
page = "<%= javascript_include_tag '#{javascript || 'application'}' %>\n"+page
end
if mock_time || (defined?(Timecop) && Timecop.top_stack_item)
unix_millis = ((mock_time || Time.now).to_f * 1000.0).to_i
page = "<%= javascript_include_tag 'spec/libs/lolex' %>\n"+
"<script type='text/javascript'>\n"+
" window.original_setInterval = setInterval;\n"+
" window.lolex_clock = lolex.install(#{unix_millis});\n"+
" window.original_setInterval(function() {window.lolex_clock.tick(10)}, 10);\n"+
"</script>\n"+page
end
if !render_params[:layout] || style_sheet
page = "<%= stylesheet_link_tag '#{style_sheet || 'application'}' %>\n"+page
end
if render_on == :server_only # so that test helper wait_for_ajax works
page = "<script type='text/javascript'>window.jQuery = {'active': 0}</script>\n#{page}"
else
page = "<%= javascript_include_tag 'jquery' %>\n<%= javascript_include_tag 'jquery_ujs' %>\n#{page}"
end
render_params[:inline] = page
render render_params
end
end
# test_routes = Proc.new do
# get "/#{route_root}/:id", to: "#{route_root}#test"
# end
# Rails.application.routes.eval_block(test_routes)
begin
routes = Rails.application.routes
routes.disable_clear_and_finalize = true
routes.clear!
routes.draw do
get "/#{route_root}/:id", to: "#{route_root}#test"
end
Rails.application.routes_reloader.paths.each{ |path| load(path) }
routes.finalize!
ActiveSupport.on_load(:action_controller) { routes.finalize! }
ensure
routes.disable_clear_and_finalize = false
end
end
"/#{route_root}/#{@test_id = (@test_id || 0) + 1}"
end
def on_client(&block)
@client_code = "#{@client_code}#{Unparser.unparse Parser::CurrentRuby.parse(block.source).children.last}\n"
end
def debugger
`debugger`
nil
end
def mount(component_name, params=nil, opts = {}, &block)
unless params
params = opts
opts = {}
end
test_url = build_test_url_for(opts.delete(:controller))
if block
block_with_helpers = <<-code
module ComponentHelpers
def self.js_eval(s)
`eval(s)`
end
def self.add_class(class_name, styles={})
style = styles.collect { |attr, value| "\#{attr.dasherize}:\#{value}"}.join("; ")
s = "<style type='text/css'> .\#{class_name}{ \#{style} } </style>"
`$(\#{s}).appendTo("head");`
end
end
#{@client_code}
#{Unparser.unparse Parser::CurrentRuby.parse(block.source).children.last}
code
opts[:code] = Opal.compile(block_with_helpers)
end
Rails.cache.write(test_url, [component_name, params, opts])
visit test_url
wait_for_ajax
end
[:callback_history_for, :last_callback_for, :clear_callback_history_for, :event_history_for, :last_event_for, :clear_event_history_for].each do |method|
define_method(method) { |event_name| evaluate_script("Opal.React.TopLevelRailsComponent.$#{method}('#{event_name}')") }
end
def run_on_client(&block)
script = Opal.compile(Unparser.unparse Parser::CurrentRuby.parse(block.source).children.last)
execute_script(script)
end
def open_in_chrome
`open http://#{page.server.host}:#{page.server.port}#{page.current_path}`
while true
sleep 1.hour
end
end
def size_window(width=nil, height=nil)
width, height = width if width.is_a? Array
portrait = true if height == :portrait
case width
when :small
width, height = [480, 320]
when :mobile
width, height = [640, 480]
when :tablet
width, height = [960, 640]
when :large
width, height = [1920, 6000]
when :default, nil
width, height = [1024, 768]
end
if portrait
width, height = [height, width]
end
if page.driver.browser.respond_to?(:manage)
page.driver.browser.manage.window.resize_to(width, height)
elsif page.driver.respond_to?(:resize)
page.driver.resize(width, height)
end
end
end
# config.ru
require 'bundler'
Bundler.require
require "opal-rspec"
# add additional requires here as needed
Opal.append_path File.expand_path('../spec', __FILE__)
run Opal::Server.new { |s|
s.main = 'opal/rspec/sprockets_runner'
s.append_path 'spec'
# append any additional paths as needed:
#i.e. s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js"))
s.debug = true
s.index_path = 'spec/index.html.erb'
}
# spec/index.html.erb
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<%= javascript_include_tag 'vendor/es5-shim.min' %>
<%= javascript_include_tag @server.main %>
<div id="placeholder" style="display: none"></div>
<div id="render_here"></div>
</body>
</html>
Specs go as normal in files like specs/xxx/something_spec.rb
where xxx is whatever organization you like.
Specs can be run in three environments:
- In the browser, with opal-rspec running the tests inside the browser
- In the browser, with rspec/capybara loading and running the tests
- As a normal server side rspec test.
specs that can be driven directly on the client using opal-rspec with the :opal => true
tag like this:
require 'spec_helper'
describe 'React DSL', :opal => true do
it "will turn the last string in a block into a element" do
stub_const 'Foo', Class.new
Foo.class_eval do
include React::Component
def render
div { "hello" }
end
end
expect(React.render_to_static_markup(React.create_element(Foo))).to eq('<div>hello</div>')
end
end
Opal-Rspec tests can also be run in the browser (good for debug) by running bundle exec rackup
and then pointing your browser to localhost:9292
If you want the test to run on the client, but be driven from the server, then add the :js => true
tag to the test like this:
describe "A component", js: true do
it "can be created and mounted from rspec" do
test_message = "I AM FOO!"
mount "Hello", message: test_message do
class Hello < React::Component::Base
param :message
def render
params.message
end
end
end
page.should have_content(test_message)
end
end
Note the use of the mount
method which will mount the component named by the first parameter, passing it the optional params hash.
mount
takes a block which can be used to define any client side code, useful for creating stub components, or modifying component behaviors for testing purposes.
Finally any spec that is not tagged with :js
, or :opal
will run as normal.