Skip to content

Instantly share code, notes, and snippets.

@catmando
Last active May 24, 2016 13:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save catmando/bf05f13b1315a53ab1236571ddae69b4 to your computer and use it in GitHub Desktop.
Save catmando/bf05f13b1315a53ab1236571ddae69b4 to your computer and use it in GitHub Desktop.
Adding Specs to Opal / React.rb Gems
/*!
* https://github.com/es-shims/es5-shim
* @license es5-shim Copyright 2009-2014 by contributors, MIT License
* see https://github.com/es-shims/es5-shim/blob/v4.1.0/LICENSE
*/
(function(t,e){"use strict";if(typeof define==="function"&&define.amd){define(e)}else if(typeof exports==="object"){module.exports=e()}else{t.returnExports=e()}})(this,function(){var t=Array.prototype;var e=Object.prototype;var r=Function.prototype;var n=String.prototype;var i=Number.prototype;var a=t.slice;var o=t.splice;var u=t.push;var l=t.unshift;var f=r.call;var s=e.toString;var c=Array.isArray||function ye(t){return s.call(t)==="[object Array]"};var p=typeof Symbol==="function"&&typeof Symbol.toStringTag==="symbol";var h;var v=Function.prototype.toString,g=function de(t){try{v.call(t);return true}catch(e){return false}},y="[object Function]",d="[object GeneratorFunction]";h=function me(t){if(typeof t!=="function"){return false}if(p){return g(t)}var e=s.call(t);return e===y||e===d};var m;var b=RegExp.prototype.exec,w=function be(t){try{b.call(t);return true}catch(e){return false}},T="[object RegExp]";m=function we(t){if(typeof t!=="object"){return false}return p?w(t):s.call(t)===T};var x;var O=String.prototype.valueOf,j=function Te(t){try{O.call(t);return true}catch(e){return false}},S="[object String]";x=function xe(t){if(typeof t==="string"){return true}if(typeof t!=="object"){return false}return p?j(t):s.call(t)===S};var E=function Oe(t){var e=s.call(t);var r=e==="[object Arguments]";if(!r){r=!c(t)&&t!==null&&typeof t==="object"&&typeof t.length==="number"&&t.length>=0&&h(t.callee)}return r};var N=function(t){var e=Object.defineProperty&&function(){try{Object.defineProperty({},"x",{});return true}catch(t){return false}}();var r;if(e){r=function(t,e,r,n){if(!n&&e in t){return}Object.defineProperty(t,e,{configurable:true,enumerable:false,writable:true,value:r})}}else{r=function(t,e,r,n){if(!n&&e in t){return}t[e]=r}}return function n(e,i,a){for(var o in i){if(t.call(i,o)){r(e,o,i[o],a)}}}}(e.hasOwnProperty);function I(t){var e=typeof t;return t===null||e==="undefined"||e==="boolean"||e==="number"||e==="string"}var D={ToInteger:function je(t){var e=+t;if(e!==e){e=0}else if(e!==0&&e!==1/0&&e!==-(1/0)){e=(e>0||-1)*Math.floor(Math.abs(e))}return e},ToPrimitive:function Se(t){var e,r,n;if(I(t)){return t}r=t.valueOf;if(h(r)){e=r.call(t);if(I(e)){return e}}n=t.toString;if(h(n)){e=n.call(t);if(I(e)){return e}}throw new TypeError},ToObject:function(t){if(t==null){throw new TypeError("can't convert "+t+" to object")}return Object(t)},ToUint32:function Ee(t){return t>>>0}};var M=function Ne(){};N(r,{bind:function Ie(t){var e=this;if(!h(e)){throw new TypeError("Function.prototype.bind called on incompatible "+e)}var r=a.call(arguments,1);var n;var i=function(){if(this instanceof n){var i=e.apply(this,r.concat(a.call(arguments)));if(Object(i)===i){return i}return this}else{return e.apply(t,r.concat(a.call(arguments)))}};var o=Math.max(0,e.length-r.length);var u=[];for(var l=0;l<o;l++){u.push("$"+l)}n=Function("binder","return function ("+u.join(",")+"){ return binder.apply(this, arguments); }")(i);if(e.prototype){M.prototype=e.prototype;n.prototype=new M;M.prototype=null}return n}});var F=f.bind(e.hasOwnProperty);var R=function(){var t=[1,2];var e=t.splice();return t.length===2&&c(e)&&e.length===0}();N(t,{splice:function De(t,e){if(arguments.length===0){return[]}else{return o.apply(this,arguments)}}},!R);var U=function(){var e={};t.splice.call(e,0,0,1);return e.length===1}();N(t,{splice:function Me(t,e){if(arguments.length===0){return[]}var r=arguments;this.length=Math.max(D.ToInteger(this.length),0);if(arguments.length>0&&typeof e!=="number"){r=a.call(arguments);if(r.length<2){r.push(this.length-t)}else{r[1]=D.ToInteger(e)}}return o.apply(this,r)}},!U);var k=[].unshift(0)!==1;N(t,{unshift:function(){l.apply(this,arguments);return this.length}},k);N(Array,{isArray:c});var A=Object("a");var C=A[0]!=="a"||!(0 in A);var P=function Fe(t){var e=true;var r=true;if(t){t.call("foo",function(t,r,n){if(typeof n!=="object"){e=false}});t.call([1],function(){"use strict";r=typeof this==="string"},"x")}return!!t&&e&&r};N(t,{forEach:function Re(t){var e=D.ToObject(this),r=C&&x(this)?this.split(""):e,n=arguments[1],i=-1,a=r.length>>>0;if(!h(t)){throw new TypeError}while(++i<a){if(i in r){t.call(n,r[i],i,e)}}}},!P(t.forEach));N(t,{map:function Ue(t){var e=D.ToObject(this),r=C&&x(this)?this.split(""):e,n=r.length>>>0,i=Array(n),a=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var o=0;o<n;o++){if(o in r){i[o]=t.call(a,r[o],o,e)}}return i}},!P(t.map));N(t,{filter:function ke(t){var e=D.ToObject(this),r=C&&x(this)?this.split(""):e,n=r.length>>>0,i=[],a,o=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var u=0;u<n;u++){if(u in r){a=r[u];if(t.call(o,a,u,e)){i.push(a)}}}return i}},!P(t.filter));N(t,{every:function Ae(t){var e=D.ToObject(this),r=C&&x(this)?this.split(""):e,n=r.length>>>0,i=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var a=0;a<n;a++){if(a in r&&!t.call(i,r[a],a,e)){return false}}return true}},!P(t.every));N(t,{some:function Ce(t){var e=D.ToObject(this),r=C&&x(this)?this.split(""):e,n=r.length>>>0,i=arguments[1];if(!h(t)){throw new TypeError(t+" is not a function")}for(var a=0;a<n;a++){if(a in r&&t.call(i,r[a],a,e)){return true}}return false}},!P(t.some));var Z=false;if(t.reduce){Z=typeof t.reduce.call("es5",function(t,e,r,n){return n})==="object"}N(t,{reduce:function Pe(t){var e=D.ToObject(this),r=C&&x(this)?this.split(""):e,n=r.length>>>0;if(!h(t)){throw new TypeError(t+" is not a function")}if(!n&&arguments.length===1){throw new TypeError("reduce of empty array with no initial value")}var i=0;var a;if(arguments.length>=2){a=arguments[1]}else{do{if(i in r){a=r[i++];break}if(++i>=n){throw new TypeError("reduce of empty array with no initial value")}}while(true)}for(;i<n;i++){if(i in r){a=t.call(void 0,a,r[i],i,e)}}return a}},!Z);var J=false;if(t.reduceRight){J=typeof t.reduceRight.call("es5",function(t,e,r,n){return n})==="object"}N(t,{reduceRight:function Ze(t){var e=D.ToObject(this),r=C&&x(this)?this.split(""):e,n=r.length>>>0;if(!h(t)){throw new TypeError(t+" is not a function")}if(!n&&arguments.length===1){throw new TypeError("reduceRight of empty array with no initial value")}var i,a=n-1;if(arguments.length>=2){i=arguments[1]}else{do{if(a in r){i=r[a--];break}if(--a<0){throw new TypeError("reduceRight of empty array with no initial value")}}while(true)}if(a<0){return i}do{if(a in r){i=t.call(void 0,i,r[a],a,e)}}while(a--);return i}},!J);var z=Array.prototype.indexOf&&[0,1].indexOf(1,2)!==-1;N(t,{indexOf:function Je(t){var e=C&&x(this)?this.split(""):D.ToObject(this),r=e.length>>>0;if(!r){return-1}var n=0;if(arguments.length>1){n=D.ToInteger(arguments[1])}n=n>=0?n:Math.max(0,r+n);for(;n<r;n++){if(n in e&&e[n]===t){return n}}return-1}},z);var $=Array.prototype.lastIndexOf&&[0,1].lastIndexOf(0,-3)!==-1;N(t,{lastIndexOf:function ze(t){var e=C&&x(this)?this.split(""):D.ToObject(this),r=e.length>>>0;if(!r){return-1}var n=r-1;if(arguments.length>1){n=Math.min(n,D.ToInteger(arguments[1]))}n=n>=0?n:r-Math.abs(n);for(;n>=0;n--){if(n in e&&t===e[n]){return n}}return-1}},$);var B=!{toString:null}.propertyIsEnumerable("toString"),G=function(){}.propertyIsEnumerable("prototype"),H=!F("x","0"),L=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],X=L.length;N(Object,{keys:function $e(t){var e=h(t),r=E(t),n=t!==null&&typeof t==="object",i=n&&x(t);if(!n&&!e&&!r){throw new TypeError("Object.keys called on a non-object")}var a=[];var o=G&&e;if(i&&H||r){for(var u=0;u<t.length;++u){a.push(String(u))}}if(!r){for(var l in t){if(!(o&&l==="prototype")&&F(t,l)){a.push(String(l))}}}if(B){var f=t.constructor,s=f&&f.prototype===t;for(var c=0;c<X;c++){var p=L[c];if(!(s&&p==="constructor")&&F(t,p)){a.push(p)}}}return a}});var Y=Object.keys&&function(){return Object.keys(arguments).length===2}(1,2);var q=Object.keys;N(Object,{keys:function Be(e){if(E(e)){return q(t.slice.call(e))}else{return q(e)}}},!Y);var K=-621987552e5;var Q="-000001";var V=Date.prototype.toISOString&&new Date(K).toISOString().indexOf(Q)===-1;N(Date.prototype,{toISOString:function Ge(){var t,e,r,n,i;if(!isFinite(this)){throw new RangeError("Date.prototype.toISOString called on non-finite value.")}n=this.getUTCFullYear();i=this.getUTCMonth();n+=Math.floor(i/12);i=(i%12+12)%12;t=[i+1,this.getUTCDate(),this.getUTCHours(),this.getUTCMinutes(),this.getUTCSeconds()];n=(n<0?"-":n>9999?"+":"")+("00000"+Math.abs(n)).slice(0<=n&&n<=9999?-4:-6);e=t.length;while(e--){r=t[e];if(r<10){t[e]="0"+r}}return n+"-"+t.slice(0,2).join("-")+"T"+t.slice(2).join(":")+"."+("000"+this.getUTCMilliseconds()).slice(-3)+"Z"}},V);var W=false;try{W=Date.prototype.toJSON&&new Date(NaN).toJSON()===null&&new Date(K).toJSON().indexOf(Q)!==-1&&Date.prototype.toJSON.call({toISOString:function(){return true}})}catch(_){}if(!W){Date.prototype.toJSON=function He(t){var e=Object(this),r=D.ToPrimitive(e),n;if(typeof r==="number"&&!isFinite(r)){return null}n=e.toISOString;if(typeof n!=="function"){throw new TypeError("toISOString property is not callable")}return n.call(e)}}var te=Date.parse("+033658-09-27T01:46:40.000Z")===1e15;var ee=!isNaN(Date.parse("2012-04-04T24:00:00.500Z"))||!isNaN(Date.parse("2012-11-31T23:59:59.000Z"));var re=isNaN(Date.parse("2000-01-01T00:00:00.000Z"));if(!Date.parse||re||ee||!te){Date=function(t){function e(r,n,i,a,o,u,l){var f=arguments.length;if(this instanceof t){var s=f===1&&String(r)===r?new t(e.parse(r)):f>=7?new t(r,n,i,a,o,u,l):f>=6?new t(r,n,i,a,o,u):f>=5?new t(r,n,i,a,o):f>=4?new t(r,n,i,a):f>=3?new t(r,n,i):f>=2?new t(r,n):f>=1?new t(r):new t;s.constructor=e;return s}return t.apply(this,arguments)}var r=new RegExp("^"+"(\\d{4}|[+-]\\d{6})"+"(?:-(\\d{2})"+"(?:-(\\d{2})"+"(?:"+"T(\\d{2})"+":(\\d{2})"+"(?:"+":(\\d{2})"+"(?:(\\.\\d{1,}))?"+")?"+"("+"Z|"+"(?:"+"([-+])"+"(\\d{2})"+":(\\d{2})"+")"+")?)?)?)?"+"$");var n=[0,31,59,90,120,151,181,212,243,273,304,334,365];function i(t,e){var r=e>1?1:0;return n[e]+Math.floor((t-1969+r)/4)-Math.floor((t-1901+r)/100)+Math.floor((t-1601+r)/400)+365*(t-1970)}function a(e){return Number(new t(1970,0,1,0,0,0,e))}for(var o in t){e[o]=t[o]}e.now=t.now;e.UTC=t.UTC;e.prototype=t.prototype;e.prototype.constructor=e;e.parse=function u(e){var n=r.exec(e);if(n){var o=Number(n[1]),u=Number(n[2]||1)-1,l=Number(n[3]||1)-1,f=Number(n[4]||0),s=Number(n[5]||0),c=Number(n[6]||0),p=Math.floor(Number(n[7]||0)*1e3),h=Boolean(n[4]&&!n[8]),v=n[9]==="-"?1:-1,g=Number(n[10]||0),y=Number(n[11]||0),d;if(f<(s>0||c>0||p>0?24:25)&&s<60&&c<60&&p<1e3&&u>-1&&u<12&&g<24&&y<60&&l>-1&&l<i(o,u+1)-i(o,u)){d=((i(o,u)+l)*24+f+g*v)*60;d=((d+s+y*v)*60+c)*1e3+p;if(h){d=a(d)}if(-864e13<=d&&d<=864e13){return d}}return NaN}return t.parse.apply(this,arguments)};return e}(Date)}if(!Date.now){Date.now=function Le(){return(new Date).getTime()}}var ne=i.toFixed&&(8e-5.toFixed(3)!=="0.000"||.9.toFixed(0)!=="1"||1.255.toFixed(2)!=="1.25"||0xde0b6b3a7640080.toFixed(0)!=="1000000000000000128");var ie={base:1e7,size:6,data:[0,0,0,0,0,0],multiply:function Xe(t,e){var r=-1;while(++r<ie.size){e+=t*ie.data[r];ie.data[r]=e%ie.base;e=Math.floor(e/ie.base)}},divide:function Ye(t){var e=ie.size,r=0;while(--e>=0){r+=ie.data[e];ie.data[e]=Math.floor(r/t);r=r%t*ie.base}},numToString:function qe(){var t=ie.size;var e="";while(--t>=0){if(e!==""||t===0||ie.data[t]!==0){var r=String(ie.data[t]);if(e===""){e=r}else{e+="0000000".slice(0,7-r.length)+r}}}return e},pow:function Ke(t,e,r){return e===0?r:e%2===1?Ke(t,e-1,r*t):Ke(t*t,e/2,r)},log:function Qe(t){var e=0;while(t>=4096){e+=12;t/=4096}while(t>=2){e+=1;t/=2}return e}};N(i,{toFixed:function Ve(t){var e,r,n,i,a,o,u,l;e=Number(t);e=e!==e?0:Math.floor(e);if(e<0||e>20){throw new RangeError("Number.toFixed called with invalid number of decimals")}r=Number(this);if(r!==r){return"NaN"}if(r<=-1e21||r>=1e21){return String(r)}n="";if(r<0){n="-";r=-r}i="0";if(r>1e-21){a=ie.log(r*ie.pow(2,69,1))-69;o=a<0?r*ie.pow(2,-a,1):r/ie.pow(2,a,1);o*=4503599627370496;a=52-a;if(a>0){ie.multiply(0,o);u=e;while(u>=7){ie.multiply(1e7,0);u-=7}ie.multiply(ie.pow(10,u,1),0);u=a-1;while(u>=23){ie.divide(1<<23);u-=23}ie.divide(1<<u);ie.multiply(1,1);ie.divide(2);i=ie.numToString()}else{ie.multiply(0,o);ie.multiply(1<<-a,0);i=ie.numToString()+"0.00000000000000000000".slice(2,2+e)}}if(e>0){l=i.length;if(l<=e){i=n+"0.0000000000000000000".slice(0,e-l+2)+i}else{i=n+i.slice(0,l-e)+"."+i.slice(l-e)}}else{i=n+i}return i}},ne);var ae=n.split;if("ab".split(/(?:ab)*/).length!==2||".".split(/(.?)(.?)/).length!==4||"tesst".split(/(s)*/)[1]==="t"||"test".split(/(?:)/,-1).length!==4||"".split(/.?/).length||".".split(/()()/).length>1){(function(){var t=typeof/()??/.exec("")[1]==="undefined";n.split=function(e,r){var n=this;if(typeof e==="undefined"&&r===0){return[]}if(!m(e)){return ae.call(this,e,r)}var i=[],a=(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.extended?"x":"")+(e.sticky?"y":""),o=0,l,f,s,c;e=new RegExp(e.source,a+"g");n+="";if(!t){l=new RegExp("^"+e.source+"$(?!\\s)",a)}r=typeof r==="undefined"?-1>>>0:D.ToUint32(r);f=e.exec(n);while(f){s=f.index+f[0].length;if(s>o){i.push(n.slice(o,f.index));if(!t&&f.length>1){f[0].replace(l,function(){for(var t=1;t<arguments.length-2;t++){if(typeof arguments[t]==="undefined"){f[t]=void 0}}})}if(f.length>1&&f.index<n.length){u.apply(i,f.slice(1))}c=f[0].length;o=s;if(i.length>=r){break}}if(e.lastIndex===f.index){e.lastIndex++}f=e.exec(n)}if(o===n.length){if(c||!e.test("")){i.push("")}}else{i.push(n.slice(o))}return i.length>r?i.slice(0,r):i}})()}else if("0".split(void 0,0).length){n.split=function We(t,e){if(typeof t==="undefined"&&e===0){return[]}return ae.call(this,t,e)}}var oe=n.replace;var ue=function(){var t=[];"x".replace(/x(.)?/g,function(e,r){t.push(r)});return t.length===1&&typeof t[0]==="undefined"}();if(!ue){n.replace=function _e(t,e){var r=h(e);var n=m(t)&&/\)[*?]/.test(t.source);if(!r||!n){return oe.call(this,t,e)}else{var i=function(r){var n=arguments.length;var i=t.lastIndex;t.lastIndex=0;var a=t.exec(r)||[];t.lastIndex=i;a.push(arguments[n-2],arguments[n-1]);return e.apply(this,a)};return oe.call(this,t,i)}}}var le=n.substr;var fe="".substr&&"0b".substr(-1)!=="b";N(n,{substr:function tr(t,e){return le.call(this,t<0?(t=this.length+t)<0?0:t:t,e)}},fe);var se=" \n \f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003"+"\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028"+"\u2029\ufeff";var ce="\u200b";var pe="["+se+"]";var he=new RegExp("^"+pe+pe+"*");var ve=new RegExp(pe+pe+"*$");var ge=n.trim&&(se.trim()||!ce.trim());N(n,{trim:function er(){if(typeof this==="undefined"||this===null){throw new TypeError("can't convert "+this+" to object")}return String(this).replace(he,"").replace(ve,"")}},ge);if(parseInt(se+"08")!==8||parseInt(se+"0x16")!==22){parseInt=function(t){var e=/^0[xX]/;return function r(n,i){n=String(n).trim();if(!Number(i)){i=e.test(n)?16:10}return t(n,i)}}(parseInt)}});

Add development dependencies to .gemspec

  # 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

Add Tasks to Rake File

# 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 ]

update .gitignore

# 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

update / add spec_helper

# 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

add spec_helpers.rb to spec/support/react/ Note its spec_helpers plural!

# 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

Add /spec/support/component_helpers.rb

# 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

Add /spec/vendor/es5-shim.min.js (see file in this gist) - for running tests in the browser

Add a config.ru file (this lets you run the specs in the browser)

# 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'
}

add index.html.erb to specs directory (for running tests in the browser)

# 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>

Add Your Specs

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:

  1. In the browser, with opal-rspec running the tests inside the browser
  2. In the browser, with rspec/capybara loading and running the tests
  3. As a normal server side rspec test.

Using opal-rspec to drive the tests (:opal => true)

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

Running tests using server rspec and capybara to drive the tests (:js => true)

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.

Vanilla RSPEC

Finally any spec that is not tagged with :js, or :opal will run as normal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment