capybarbecue 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8fa420c19a4cf57eb1a6fb4d9122e1a01f7355cf
4
+ data.tar.gz: f3456b9851b38b602da77a2a542bfb78d71cf602
5
+ SHA512:
6
+ metadata.gz: c50a76bc67c480cddab96a0aa8a55459b6522ec022e6961ae369d7dc7c14953394b08c37b9501be7fc34437b44da186a6b1efc72f725014e7da1ab4db5fd065a
7
+ data.tar.gz: 329541d5aabcf0be4d796721aca4156fc52971d64c94f6b6113a0cf4cf09dbd463dc12c482487c18020e865f18773e89f2d38b9e22c556e088d8fd7a4ae14371
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development do
4
+ gem "rspec"
5
+ gem "rr", require: false
6
+ gem "yard"
7
+ gem "rdoc"
8
+ gem "bundler"
9
+ gem "jeweler"
10
+ gem "poltergeist"
11
+ gem "launchy"
12
+ end
13
+
14
+ gem "activesupport"
15
+ gem "capybara", "~>2.1.0"
@@ -0,0 +1,78 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activesupport (4.0.0)
5
+ i18n (~> 0.6, >= 0.6.4)
6
+ minitest (~> 4.2)
7
+ multi_json (~> 1.3)
8
+ thread_safe (~> 0.1)
9
+ tzinfo (~> 0.3.37)
10
+ addressable (2.3.5)
11
+ atomic (1.1.10)
12
+ capybara (2.1.0)
13
+ mime-types (>= 1.16)
14
+ nokogiri (>= 1.3.3)
15
+ rack (>= 1.0.0)
16
+ rack-test (>= 0.5.4)
17
+ xpath (~> 2.0)
18
+ diff-lcs (1.2.4)
19
+ eventmachine (1.0.3)
20
+ faye-websocket (0.4.7)
21
+ eventmachine (>= 0.12.0)
22
+ git (1.2.5)
23
+ http_parser.rb (0.5.3)
24
+ i18n (0.6.4)
25
+ jeweler (1.8.4)
26
+ bundler (~> 1.0)
27
+ git (>= 1.2.5)
28
+ rake
29
+ rdoc
30
+ json (1.8.0)
31
+ launchy (2.3.0)
32
+ addressable (~> 2.3)
33
+ mime-types (1.23)
34
+ mini_portile (0.5.1)
35
+ minitest (4.7.5)
36
+ multi_json (1.7.7)
37
+ nokogiri (1.6.0)
38
+ mini_portile (~> 0.5.0)
39
+ poltergeist (1.3.0)
40
+ capybara (~> 2.1.0)
41
+ faye-websocket (>= 0.4.4, < 0.5.0)
42
+ http_parser.rb (~> 0.5.3)
43
+ rack (1.5.2)
44
+ rack-test (0.6.2)
45
+ rack (>= 1.0)
46
+ rake (10.1.0)
47
+ rdoc (4.0.1)
48
+ json (~> 1.4)
49
+ rr (1.1.1)
50
+ rspec (2.14.1)
51
+ rspec-core (~> 2.14.0)
52
+ rspec-expectations (~> 2.14.0)
53
+ rspec-mocks (~> 2.14.0)
54
+ rspec-core (2.14.4)
55
+ rspec-expectations (2.14.0)
56
+ diff-lcs (>= 1.1.3, < 2.0)
57
+ rspec-mocks (2.14.2)
58
+ thread_safe (0.1.2)
59
+ atomic
60
+ tzinfo (0.3.37)
61
+ xpath (2.0.0)
62
+ nokogiri (~> 1.3)
63
+ yard (0.8.7)
64
+
65
+ PLATFORMS
66
+ ruby
67
+
68
+ DEPENDENCIES
69
+ activesupport
70
+ bundler
71
+ capybara (~> 2.1.0)
72
+ jeweler
73
+ launchy
74
+ poltergeist
75
+ rdoc
76
+ rr
77
+ rspec
78
+ yard
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Andrew DiMichele
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,37 @@
1
+ = Rails Thread =
2
+ Put a proxy class in front of Capybara::Driver::Base and Capybara::Driver::Node
3
+ Capybara:Session#driver is the instance of driver
4
+ Probably need to create a new driver which is a proxy for another driver
5
+ Proxy class puts method_missing args on MQ to be sent to Driver thread
6
+ Needs to yield to Driver thread and then execute the RackRunner
7
+ RackRunner reads rack requests from a MQ, calling the rack endpoint with those; returns when done
8
+ This should probably use the rack-test gem? This probably isn't what we want since this simulates a browser
9
+
10
+ = Driver Thread =
11
+ Reads RPCs off a MQ, calls the commands in the appropriate class, and puts the response on another MQ
12
+ Part of the new driver passed to Capybara
13
+ Should be able to use the Capybara driver of your choice
14
+ How is a Node passed back to the Rails thread? Maybe it's OK for Node queries to go straight through to the webdriver?
15
+ These could trigger web requests (like a click) that would not be processed... maybe wrap in the Barbecue driver
16
+ so that the RackRunner is triggered appropriately? That still causes a problem when clicking a link since the
17
+ webdriver might block... need to think about this some more
18
+
19
+ = Capybara::Server =
20
+ Rewrite this class entirely
21
+ Should spawn a simple Rack webserver that just take the #call Args and puts them on the MQ
22
+ Needs to block until MQ response is received
23
+ Webserver could be Puma or EventMachine
24
+
25
+
26
+ Notes:
27
+ Queue class provided by 'thread' library
28
+
29
+ ----- Latest Notes -----
30
+ * Interface with Session instead
31
+ * Don't try to mimic interfaces - just ferry calls over
32
+ * Add field to the base node class so we can tell it's a node
33
+ * If return value is a Node then wrap it
34
+ * If return value is enumerable, wrap its Node values via #map
35
+
36
+ ---- TODO ----
37
+ * Raise errors during timeouts if Time is frozen
@@ -0,0 +1,19 @@
1
+ = capybarbecue
2
+
3
+ Description goes here.
4
+
5
+ == Contributing to capybarbecue
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
9
+ * Fork the project.
10
+ * Start a feature/bugfix branch.
11
+ * Commit and push until you are happy with your contribution.
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2013 Andrew DiMichele. See LICENSE.txt for
18
+ further details.
19
+
@@ -0,0 +1,44 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ require './lib/capybarbecue/version'
16
+ Jeweler::Tasks.new do |gem|
17
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
18
+ gem.name = "capybarbecue"
19
+ gem.homepage = "http://github.com/adimichele/capybarbecue"
20
+ gem.license = "MIT"
21
+ gem.summary = %Q{Makes your Capybara test suite work better}
22
+ gem.description = %Q{Makes fundamental changes to Capybara's threading architecture so you can write stable tests with a shared database connection.}
23
+ gem.email = "backflip@gmail.com"
24
+ gem.authors = ["Andrew DiMichele"]
25
+ gem.version = Capybarbecue::VERSION
26
+ # dependencies defined in Gemfile
27
+ end
28
+ Jeweler::RubygemsDotOrgTasks.new
29
+
30
+ require 'rspec/core'
31
+ require 'rspec/core/rake_task'
32
+ RSpec::Core::RakeTask.new(:spec) do |spec|
33
+ spec.pattern = FileList['spec/**/*_spec.rb']
34
+ end
35
+
36
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
37
+ spec.pattern = 'spec/**/*_spec.rb'
38
+ spec.rcov = true
39
+ end
40
+
41
+ task :default => :spec
42
+
43
+ require 'yard'
44
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,44 @@
1
+ require 'active_support/all'
2
+
3
+ module Capybarbecue
4
+ class << self
5
+ def activated?
6
+ Capybara.respond_to? :original_session
7
+ end
8
+
9
+ def activate!
10
+ return if activated?
11
+ require 'capybarbecue/async_call'
12
+ require 'capybarbecue/async_delegate_class'
13
+ require 'capybarbecue/rack_runner'
14
+ require 'capybarbecue/server'
15
+
16
+ (class << Capybara; self end).instance_eval do
17
+ alias_method :original_session, :current_session
18
+ define_method :current_session do
19
+ Capybarbecue::AsyncDelegateClass.new(original_session) do
20
+ Capybara.app.handle_requests
21
+ end
22
+ end
23
+ end
24
+
25
+ Capybara.send(:session_pool).clear
26
+
27
+ # Swap out Capybara's server for the one that waits
28
+ # Do this by deleting everything in Capybara#session_pool (private) and swapping Capybara.app
29
+ new_app = Capybarbecue::Server.new(Capybara.app)
30
+ Capybara.app = new_app
31
+ end
32
+
33
+ def deactivate!
34
+ # Make sure this kills all threads too?
35
+ return unless activated?
36
+ (class << Capybara; self end).instance_eval do
37
+ alias_method :current_session, :original_session
38
+ remove_method :original_session
39
+ end
40
+ Capybara.send(:session_pool).clear
41
+ Capybara.app = Capybara.app.app
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,63 @@
1
+ require 'thread'
2
+
3
+ class AsyncCall # Asynchronously calls a method
4
+ DEFAULT_TIMEOUT = 5.0
5
+ attr_reader :thread
6
+ attr_reader :ready
7
+ attr_reader :to_s
8
+ alias :ready? :ready
9
+
10
+ def initialize(obj, method, *args, &block)
11
+ @obj, @method, @args, @block = obj, method, args, block
12
+ @ready = false
13
+ @response = nil
14
+ @exception = nil
15
+ @thread = Thread.new do
16
+ begin
17
+ respond_with obj.send(method, *args, &block)
18
+ rescue Exception => e
19
+ respond_with_exception e
20
+ end
21
+ end
22
+ end
23
+
24
+ # If a block is passed, it will be called repeatedly until a response comes in
25
+ def wait_for_response(timeout=DEFAULT_TIMEOUT)
26
+ started_at = Time.now
27
+ while Time.now - started_at < timeout.seconds && !ready?
28
+ yield if block_given?
29
+ # It feels dangerous not to sleep here... keep a pulse on this (sleep causes performance problems)
30
+ Thread.pass
31
+ end
32
+ kill! and raise Timeout::Error.new('Timeout expired before response received') unless ready?
33
+ if @exception.present?
34
+ # Add the backtrace from this thread to make it useful
35
+ backtrace = @exception.backtrace + Kernel.caller
36
+ @exception.set_backtrace(backtrace)
37
+ raise @exception
38
+ end
39
+ @response
40
+ end
41
+
42
+ def kill!
43
+ @thread.kill
44
+ end
45
+
46
+ def to_s
47
+ s = "#{@obj.class.name}##{@method}(#{@args.map{ |a| a.class.name }.join(', ')})"
48
+ s += ' { ... }' if @block.present?
49
+ s
50
+ end
51
+
52
+ private
53
+
54
+ def respond_with(value)
55
+ @response = value
56
+ @ready = true
57
+ end
58
+
59
+ def respond_with_exception(e)
60
+ @exception = e
61
+ @ready = true
62
+ end
63
+ end
@@ -0,0 +1,30 @@
1
+ module Capybarbecue
2
+ class AsyncDelegateClass
3
+ attr_reader :instance
4
+
5
+ def initialize(instance, &wait_proc)
6
+ @instance = instance
7
+ @wait_proc = wait_proc # Called repeatedly while waiting
8
+ end
9
+
10
+ def method_missing(method, *args, &block)
11
+ call = AsyncCall.new(@instance, method, *args, &block)
12
+ wrap_response(call.wait_for_response(&@wait_proc))
13
+ end
14
+
15
+ private
16
+
17
+ def respond_to_missing?(method, include_all=false)
18
+ @instance.respond_to?(method, include_all)
19
+ end
20
+
21
+ # Wrap anything that looks like Capybara
22
+ def wrap_response(value)
23
+ if value.class.name.include?('Capybara')
24
+ AsyncDelegateClass.new(value, &@wait_proc)
25
+ else
26
+ value
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ module Capybarbecue
2
+ class RackRunner
3
+ # Rack runner portion of main thread
4
+ # Reads rack requests of a message queue, runs them, and puts the result on another MQ
5
+ # Should have some sort of delay or signaling system to eliminate race conditions when waiting for requests
6
+ end
7
+ end
@@ -0,0 +1,64 @@
1
+ require 'thread'
2
+
3
+ module Capybarbecue
4
+ class Server
5
+ DEFAULT_TIMEOUT = 30
6
+ attr_accessor :wait_for_response # For testing - set to false to keep #call from blocking
7
+ attr_accessor :timeout, :app
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ @requestmq = Queue.new
12
+ @wait_for_response = true
13
+ @timeout = DEFAULT_TIMEOUT
14
+ end
15
+
16
+ def call(env)
17
+ queue_and_wait(env)
18
+ end
19
+
20
+ # Should be run by another thread - respond to all queued requests
21
+ def handle_requests
22
+ until @requestmq.empty?
23
+ request = @requestmq.deq(true)
24
+ begin
25
+ request.response = @app.call(request.env)
26
+ rescue Exception => e
27
+ request.exception = e
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def queue_and_wait(env)
35
+ request = QueuedRequest.new(env)
36
+ @requestmq.enq(request)
37
+ return unless wait_for_response
38
+ started_at = Time.now
39
+ while !request.ready? && Time.now - started_at < timeout.seconds
40
+ # It feels dangerous not to sleep here... keep a pulse on this (sleep causes performance problems)
41
+ Thread.pass
42
+ end
43
+ raise Timeout::Error.new('Timeout expired before response received') unless request.ready?
44
+ if request.exception.present?
45
+ # Add the backtrace from this thread to make it useful
46
+ backtrace = request.exception.backtrace + Kernel.caller
47
+ request.exception.set_backtrace(backtrace)
48
+ raise request.exception
49
+ end
50
+ request.response
51
+ end
52
+
53
+ class QueuedRequest
54
+ attr_reader :env
55
+ attr_accessor :response, :exception
56
+ def initialize(env)
57
+ @env = env
58
+ end
59
+ def ready?
60
+ @response.present? || @exception.present?
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ module Capybarbecue
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,94 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe AsyncCall do
4
+ let(:obj) { Object.new }
5
+ let(:method) { :nil? }
6
+ let(:args) { [] }
7
+ let(:block) { nil }
8
+ subject{ AsyncCall.new(obj, method, *args, &block) }
9
+ after{ subject.kill! }
10
+ describe '#thread' do
11
+ it 'should be a thread' do
12
+ expect(subject.thread).to be_a Thread
13
+ end
14
+ end
15
+ describe '#ready' do
16
+ let(:method){ :test }
17
+ before do
18
+ stub(obj).test do
19
+ sleep 0.1
20
+ end
21
+ end
22
+ it 'should be true once the response is ready' do
23
+ expect(subject).to_not be_ready
24
+ sleep 0.11
25
+ expect(subject).to be_ready
26
+ end
27
+ end
28
+ describe '#wait_for_response' do
29
+ let(:obj){ Object }
30
+ let(:method){ :name }
31
+ it 'returns the value' do
32
+ expect(subject.wait_for_response).to eq 'Object'
33
+ end
34
+ context 'with a block' do
35
+ before do
36
+ stub(obj).name { sleep 0.3 }
37
+ end
38
+ it 'calls the block repeatedly while waiting' do
39
+ mock(obj).foo.at_least(2)
40
+ subject.wait_for_response { obj.foo }
41
+ end
42
+ end
43
+ context 'when the timeout expires' do
44
+ let(:method){ :test }
45
+ before do
46
+ stub(obj).test do
47
+ sleep 1
48
+ end
49
+ end
50
+ it 'raises an error and kills the thread' do
51
+ expect{ subject.wait_for_response(0.01) }.to raise_error Timeout::Error
52
+ sleep 0.1
53
+ expect(subject.thread).to_not be_alive
54
+ end
55
+ end
56
+ context 'when the call raises an exception' do
57
+ let(:method){ :test }
58
+ before do
59
+ stub(obj).test do
60
+ 1 / 0
61
+ end
62
+ end
63
+ it 'also raises the exception' do
64
+ expect{ subject.wait_for_response }.to raise_error ZeroDivisionError
65
+ end
66
+ end
67
+ end
68
+ describe '#kill!' do
69
+ let(:method){ :test }
70
+ before do
71
+ stub(obj).test do
72
+ sleep 1
73
+ end
74
+ end
75
+ it 'kills the thread' do
76
+ expect{ subject.kill!; sleep 0.01 }.to change{ subject.thread.stop? }.from(false).to(true)
77
+ end
78
+ end
79
+ describe '#to_s' do
80
+ let(:obj) { Object.new }
81
+ let(:method) { :foo }
82
+ let(:args) { [1, Object.new] }
83
+ before{ stub(obj).foo.with_any_args }
84
+ it 'returns the method as a string' do
85
+ expect(subject.to_s).to eq 'Object#foo(Fixnum, Object)'
86
+ end
87
+ context 'with a block' do
88
+ let(:block){ Proc.new do end }
89
+ it 'returns the method as a string and indicates a block' do
90
+ expect(subject.to_s).to eq 'Object#foo(Fixnum, Object) { ... }'
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,64 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
2
+
3
+ describe Capybarbecue::AsyncDelegateClass do
4
+ let(:obj){ Object.new }
5
+ subject{ Capybarbecue::AsyncDelegateClass.new(obj) }
6
+
7
+ it 'calls the method on the session asynchronously' do
8
+ mock(obj).foo{ 'cats' }
9
+ expect(subject.foo).to eq 'cats'
10
+ end
11
+
12
+ context 'when the object returned is not a Capybara object' do
13
+ before{ mock(obj).foo{ 'cats' } }
14
+ it 'should be itself' do
15
+ expect(subject.foo).to be_an_instance_of String
16
+ end
17
+ end
18
+
19
+ context 'when the object returned is a Capybara object' do
20
+ before { mock(obj).foo{ Capybara::Driver::Base.new } }
21
+ it 'wraps the return value in AsyncDelegateClass' do
22
+ expect(subject.foo).to be_an_instance_of Capybarbecue::AsyncDelegateClass
23
+ end
24
+ context 'when a block is given' do
25
+ subject do
26
+ Capybarbecue::AsyncDelegateClass.new(obj) do
27
+ obj.wait_func
28
+ end
29
+ end
30
+ it 'preserves the wait_proc' do
31
+ mock(obj).wait_func.twice
32
+ resp = subject.foo
33
+ stub(resp.instance).foo
34
+ resp.foo
35
+ end
36
+ end
37
+ end
38
+
39
+ describe '#instance' do
40
+ it 'should be the instance' do
41
+ subject.instance.should be obj
42
+ end
43
+ end
44
+
45
+ describe '#respond_to?' do
46
+ it 'returns @instance#respond_to?' do
47
+ stub(obj).foo
48
+ expect(subject).to respond_to :foo
49
+ end
50
+ end
51
+
52
+ context 'when a block is given' do
53
+ subject do
54
+ Capybarbecue::AsyncDelegateClass.new(obj) do
55
+ obj.wait_func
56
+ end
57
+ end
58
+ it 'runs the block before returning' do
59
+ mock(obj).wait_func.at_least(2)
60
+ stub(obj).foo { sleep 0.3 }
61
+ subject.foo
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,4 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Capybarbecue::RackRunner do
4
+ end
@@ -0,0 +1,55 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Capybarbecue::Server do
4
+ let(:app) { Object.new }
5
+ subject{ Capybarbecue::Server.new(app) }
6
+ before{ subject.timeout = 5 } # This makes tests fail a little quicker
7
+ describe '#app' do
8
+ its(:app){ should be app }
9
+ end
10
+ describe '#handle_requests' do
11
+ before{ subject.wait_for_response = false }
12
+ it 'responds to all queued requests' do
13
+ request1 = Object.new
14
+ request2 = Object.new
15
+ mock(app).call(request1)
16
+ mock(app).call(request2)
17
+ subject.call(request1)
18
+ subject.call(request2)
19
+ subject.handle_requests
20
+ end
21
+ end
22
+ describe '#call' do
23
+ context 'when another thread handles the request' do
24
+ before do
25
+ stub(app).call.with_any_args { |arg| arg }
26
+ end
27
+ let!(:thread) do
28
+ @thread = Thread.new do
29
+ while true
30
+ subject.handle_requests
31
+ sleep 0.05
32
+ end
33
+ end
34
+ end
35
+ after { thread.kill }
36
+ it 'gives the response' do
37
+ expect(subject.call('avacado')).to eq 'avacado'
38
+ end
39
+ context 'when there is an exception' do
40
+ before do
41
+ stub(app).call.with_any_args { raise Exception.new }
42
+ end
43
+ it 'raises the exception' do
44
+ expect{ subject.call('avacado') }.to raise_error
45
+ end
46
+ end
47
+ end
48
+ context 'when the timeout expires' do
49
+ before{ subject.timeout = 0.01 }
50
+ it 'raises an exception' do
51
+ expect{ subject.call(nil) }.to raise_error Timeout::Error
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,69 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Capybarbecue do
4
+ describe '#activated' do
5
+ subject{ Capybarbecue.activated? }
6
+ it 'is true because spec_helper activates it' do
7
+ expect(subject).to be_true
8
+ end
9
+ context 'when deactivated' do
10
+ before{ Capybarbecue.deactivate! }
11
+ it 'is false' do
12
+ expect(subject).to be_false
13
+ end
14
+ end
15
+ end
16
+ describe '#activate' do
17
+ before do
18
+ Capybarbecue.deactivate!
19
+ end
20
+ subject{ Capybarbecue.activate! }
21
+ after{ Capybarbecue.deactivate! }
22
+ it 'redefines Capybara#current_session' do
23
+ subject
24
+ expect(Capybara.current_session).to be_an_instance_of Capybarbecue::AsyncDelegateClass
25
+ expect(Capybara.current_session.instance).to be Capybara.original_session
26
+ end
27
+ it 'saves old Capybara#current_session Capybara#original_session' do
28
+ subject
29
+ expect(Capybara.original_session).to be_an_instance_of Capybara::Session
30
+ end
31
+ it 'clears the session pool' do
32
+ Capybara.current_session
33
+ Capybara.send(:session_pool).should_not be_empty
34
+ subject
35
+ Capybara.send(:session_pool).should be_empty
36
+ end
37
+ it 'inserts Capybarbecue::Server as the app' do
38
+ old_app = Capybara.app
39
+ subject
40
+ expect(Capybara.app).to be_an_instance_of Capybarbecue::Server
41
+ expect(Capybara.app.app).to be old_app
42
+ end
43
+ end
44
+
45
+ describe '#deactivate' do
46
+ before { Capybarbecue.activate! }
47
+ subject { Capybarbecue.deactivate! }
48
+ after { Capybarbecue.activate! }
49
+ it 'restores #current_session' do
50
+ subject
51
+ expect(Capybarbecue).to_not be_activated
52
+ expect(Capybara.current_session).to be_an_instance_of Capybara::Session
53
+ end
54
+ it 'deletes #original_session' do
55
+ subject
56
+ expect(Capybara.methods).to_not include :original_session
57
+ end
58
+ it 'restores the server app' do
59
+ subject
60
+ expect(Capybara.app).to_not be_an_instance_of Capybarbecue::Server
61
+ end
62
+ it 'clears the session pool' do
63
+ Capybara.current_session
64
+ Capybara.send(:session_pool).should_not be_empty
65
+ subject
66
+ Capybara.send(:session_pool).should be_empty
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,91 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
2
+
3
+ describe 'With a real life app', :type => :feature, :capybara_feature => true do
4
+ # Test that operations on nodes work as expected
5
+ let(:path){ '/test/path'}
6
+ before{ visit path }
7
+ describe Capybara::Node::Element do
8
+ subject{ find("##{id}") }
9
+ describe '#text' do
10
+ let(:id){ 'hidden_text' }
11
+ its(:text){ should include 'I am text' }
12
+ end
13
+ describe '#[]' do
14
+ let(:id){ 'hidden_text' }
15
+ its(['id']){ should eq id }
16
+ end
17
+ describe '#value' do
18
+ let(:id){ 'volvo' }
19
+ its(:value){ should eq 'volvo' }
20
+ end
21
+ describe '#set' do
22
+ let(:id){ 'text_field' }
23
+ it 'sets the value of the field' do
24
+ expect{ subject.set 'peanuts' }.to change{ subject.value }.to 'peanuts'
25
+ end
26
+ end
27
+ describe '#select_option, #unselect_option, #selected?, #checked?' do
28
+ let(:id){ 'volvo' }
29
+ it 'selects and unselects an option' do
30
+ expect{ subject.select_option }.to change{ subject.selected? }.from(false).to(true)
31
+ expect{ subject.unselect_option }.to change{ subject.selected? }.from(true).to(false)
32
+ expect(subject).to_not be_checked
33
+ end
34
+ end
35
+ describe '#click' do
36
+ let(:id){ 'link1' }
37
+ it 'follows the link' do
38
+ expect{ subject.click }.to change{ page.current_path }.to '/link/1'
39
+ end
40
+ end
41
+ describe '#hover' do
42
+ let(:id){ 'div1' }
43
+ before do
44
+ page.execute_script("document.getElementById('#{id}').onmouseover = function(){ document.write('hovered') }")
45
+ end
46
+ it 'triggers the hover js' do
47
+ subject.hover
48
+ page.should have_text 'hovered'
49
+ end
50
+ end
51
+ describe '#tag_name' do
52
+ let(:id){ 'div1' }
53
+ its(:tag_name){ should eq 'div' }
54
+ end
55
+ describe '#visible?' do
56
+ let(:id){ 'div1' }
57
+ it{ should be_visible }
58
+ end
59
+ describe '#disabled?' do
60
+ let(:id){ 'disabled_text' }
61
+ it{ should be_disabled }
62
+ end
63
+ describe '#path' do
64
+ let(:id){ 'text_field' }
65
+ pending 'is not supported in some drivers'
66
+ end
67
+ describe '#trigger' do
68
+ let(:id){ 'div1' }
69
+ before do
70
+ page.execute_script("document.getElementById('#{id}').onclick = function(){ document.write('sandwiches') }")
71
+ end
72
+ it 'triggers a click event' do
73
+ subject.trigger 'click'
74
+ expect(page).to have_text 'sandwiches'
75
+ end
76
+ end
77
+ describe '#drag_to' do
78
+ let(:id){ 'text_field' }
79
+ let(:target){ find('#disabled_text') }
80
+ it 'works?' do
81
+ subject.drag_to target
82
+ end
83
+ end
84
+ describe '#reload' do
85
+ let(:id){ 'text_field' }
86
+ it 'reloads the node' do
87
+ expect(subject.reload.value).to eq 'monkeys'
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,133 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '../../spec_helper')
2
+ require 'fileutils'
3
+
4
+ # Ideally want to test everything in Capybara::Session::DSL_METHODS
5
+ # These methods haven't been tested yet:
6
+ #NODE_METHODS = [
7
+ # :resolve, :query # These don't appear to be actual methods?
8
+ #]
9
+ #SESSION_METHODS = [
10
+ #]
11
+ #
12
+ # As methods are tested, add them to this list... it will help keep track of test coverage as Capybara changes
13
+ #NODE_METHODS = [
14
+ # :find, :all, :first, :find_by_id, :find_button, :find_link, :find_field, :has_css?, :has_no_css?, :has_link?, :text,
15
+ # :has_no_link?, :has_button?, :has_no_button?, :has_field?, :has_no_field?, :has_xpath?, :has_no_xpath?,
16
+ # :has_checked_field?, :has_unchecked_field?, :has_selector?, :has_no_selector?, :assert_selector, :assert_no_selector,
17
+ # :has_no_checked_field?, :has_no_unchecked_field?, :has_select?, :has_no_select?, :has_no_table?, :has_table?,
18
+ # :has_content?, :has_text?, :has_no_content?, :has_no_text?, :field_labeled,
19
+ # :click_link_or_button, :click_button, :click_link, :click_on, :fill_in, :choose, :check, :uncheck, :select, :unselect, :attach_file
20
+ #]
21
+ #SESSION_METHODS = [
22
+ # :current_url, :current_host, :current_path, :visit, :body, :html, :source,
23
+ # :title, :has_title?, :has_no_title?, :status_code, :response_headers,
24
+ # :reset_session!, :current_scope, :save_page, :save_and_open_page, :save_screenshot,
25
+ # :within, :within_fieldset, :within_table, :within_frame, :within_window,
26
+ # :evaluate_script, :execute_script
27
+ #]
28
+ # Notes: current_scope is used extensively within Capybara
29
+
30
+ describe 'With a real life app', :type => :feature, :capybara_feature => true do
31
+ let(:path){ '/test/path'}
32
+ before{ visit path }
33
+ scenario 'page properties all work' do
34
+ expect(title).to eq 'Test Rack App'
35
+ expect(page).to have_title 'Test Rack App'
36
+ expect(page).to have_no_title 'Test Crack App'
37
+ expect(current_url).to end_with '/test/path'
38
+ expect(current_host).to include '127.0.0.1'
39
+ expect(current_path).to eq '/test/path'
40
+ expect(status_code).to eq 200
41
+ expect(response_headers['Content-Type']).to eq 'text/html'
42
+ end
43
+ context 'with a temp dir' do
44
+ let(:path){ './tmp/test' }
45
+ before{ FileUtils.mkdir_p path }
46
+ after{ FileUtils.rm_rf path }
47
+ scenario 'we can save page screenshots and pages' do
48
+ save_page File.join(path, 'save.html')
49
+ save_and_open_page File.join(path, 'save_and_open.html')
50
+ save_screenshot File.join(path, 'screenshot.png')
51
+ end
52
+ end
53
+ scenario 'we can check page content' do
54
+ expect(page).to have_content '/test/path'
55
+ expect(page).to have_no_content '/another/path'
56
+ expect(page).to have_text '/test/path'
57
+ expect(page).to have_no_text '/another/path'
58
+ expect(body).to include '/test/path'
59
+ expect(html).to include '/test/path'
60
+ expect(source).to include '/test/path'
61
+ expect(text).to include '/test/path'
62
+ end
63
+ scenario 'we can find elements on the page' do
64
+ expect(all('div')).to be_present
65
+ expect(first('div')).to be_present
66
+ expect(find('#div1')).to be_present
67
+ expect(find_by_id('div1')).to be_present
68
+ expect(find_button('Imma button!')).to be_present
69
+ expect(find_link('click1')).to be_present
70
+ expect(find_field('text_field')).to be_present
71
+ end
72
+ scenario 'we can check for elements on the page' do
73
+ expect(page).to have_selector('div#div1')
74
+ expect(page).to have_no_selector('a#div1')
75
+ page.assert_selector 'div#div1'
76
+ page.assert_no_selector 'a#div1'
77
+ expect(page).to have_xpath('//div/div/a')
78
+ expect(page).to have_no_xpath('//div/oops/a')
79
+ expect(page).to have_css('div#div1')
80
+ expect(page).to have_no_css('a#div1')
81
+ expect(page).to have_link('click1')
82
+ expect(page).to have_no_link('dontclick1')
83
+ expect(page).to have_button('Imma button!')
84
+ expect(page).to have_no_button('I aint no button')
85
+ expect(page).to have_field('text_field')
86
+ expect(page).to have_no_field('no_text_field')
87
+ expect(page).to have_checked_field('checkbox_field2')
88
+ expect(page).to have_no_checked_field('checkbox_field')
89
+ expect(page).to have_unchecked_field('checkbox_field')
90
+ expect(page).to have_no_unchecked_field('checkbox_field2')
91
+ expect(page).to have_select('select_field')
92
+ expect(page).to have_no_select('nota_select_field')
93
+ expect(page).to have_table('table1')
94
+ expect(page).to have_no_table('div1')
95
+ end
96
+ scenario 'we can interact with an element on the page' do
97
+ expect{ click_link_or_button 'link2' }.to change{ current_path }.to '/link/2'
98
+ expect{ click_on 'link1' }.to change{ current_path }.to '/link/1'
99
+ expect{ click_link 'link2' }.to change{ current_path }.to '/link/2'
100
+ expect{ click_button 'button1' }.to change{ current_path }.to '/button/1'
101
+ expect{ fill_in 'text_field', with: 'red sox' }.to change{ find_field('text_field').value }.to 'red sox'
102
+ expect{ choose 'male' }.to change{ find_field('male').checked? }.to true
103
+ expect{ check 'checkbox_field' }.to change{ find_field('checkbox_field').checked? }.to true
104
+ expect{ uncheck 'checkbox_field' }.to change{ find_field('checkbox_field').checked? }.to false
105
+ expect{ page.select 'Volvo', from: 'select_field' }.to change{ find('#select_field').value }.to %w{volvo}
106
+ expect{ unselect 'Volvo', from: 'select_field' }.to change{ find('#select_field').value }.to []
107
+ attach_file 'file_field', File.expand_path(File.dirname(__FILE__) + '../../spec_helper.rb')
108
+ expect(find('#file_field').value).to end_with 'spec_helper.rb'
109
+ end
110
+ scenario 'we can search within elements' do
111
+ within 'div#div1' do
112
+ expect(page).to have_css 'div.divclass2'
113
+ end
114
+ within_fieldset 'fieldset1' do
115
+ expect(page).to have_field 'file_field'
116
+ end
117
+ within_table 'table1' do
118
+ expect(page).to have_content 'cell 1,1'
119
+ end
120
+ within_frame 'myframe' do
121
+ expect(page).to have_css '#inframe'
122
+ end
123
+ page.execute_script("window.open(null,'window1').document.write('i am an open window')") # create a window
124
+ within_window 'window1' do
125
+ expect(page.html).to have_content 'i am an open window'
126
+ end
127
+ end
128
+ scenario 'we can execute javascript' do
129
+ page.execute_script("document.write('it worked')")
130
+ expect(html).to match /it worked/
131
+ expect(evaluate_script('4 + 4')).to eq 8
132
+ end
133
+ end
@@ -0,0 +1,19 @@
1
+ #$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ #$LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'capybara/rspec'
5
+ require 'capybara/poltergeist'
6
+ require 'capybarbecue'
7
+ require 'rr'
8
+
9
+ # Requires supporting files with custom matchers and macros, etc,
10
+ # in ./support/ and its subdirectories.
11
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
12
+
13
+ Capybara.default_driver = :poltergeist
14
+ Capybara.app = TestRackApp.new
15
+ Capybarbecue.activate!
16
+
17
+ RSpec.configure do |config|
18
+ config.mock_with :rr
19
+ end
@@ -0,0 +1,64 @@
1
+ require 'rack'
2
+
3
+ class TestRackApp
4
+ def get_html(request)
5
+ <<-HTML % request.path
6
+ <html>
7
+ <head><title>Test Rack App</title></head>
8
+ <body>
9
+ <h1>%s</h1>
10
+ <div class='divclass1' id='div1'>
11
+ <div class='divclass2' id='div2'>
12
+ <a href='/link/1' id='link1'>click1</a>
13
+ </div>
14
+ </div>
15
+ <div class='divclass1' id='div3'>
16
+ <div class='divclass2' id='div4'>
17
+ <a href='/link/2' id='link2'>click2</a>
18
+ </div>
19
+ </div>
20
+ <div id='hidden_text'>I am <span id='hidden_portion' style='display:none;'>hidden </span>text</div>
21
+ <form id='testform'>
22
+ <input type='text' name='text_field' id='text_field' value='monkeys'></input>
23
+ <input type='text' name='disabled_text' id='disabled_text' disabled='1'></input>
24
+ <select name='select_field' id='select_field' multiple='1'>
25
+ <option value="volvo" id='volvo'>Volvo</option>
26
+ <option value="saab" id='saab'>Saab</option>
27
+ <option value="mercedes" id='mercedes'>Mercedes</option>
28
+ <option value="audi" id='audi'>Audi</option>
29
+ </select>
30
+ <fieldset id='fieldset1'>
31
+ <input type='checkbox' id='checkbox_field' name='checkbox_field' value='raisins'></input>
32
+ <input type='checkbox' id='checkbox_field2' name='checkbox_field2' value='peanuts' checked='1'></input>
33
+ <input type='radio' id='male' name='sex' value='male'>Male</input>
34
+ <input type='radio' id='female' name='sex' value='female'>Female</input>
35
+ <input type='file' id='file_field' name='file_field'></input>
36
+ </fieldset>
37
+ <button type="button" name='button1' id='button1' onclick='window.location="/button/1"'>Imma button!</button>
38
+ </form>
39
+ <table id='table1'>
40
+ <tr>
41
+ <td>cell 1,1</td>
42
+ <td>cell 1,2</td>
43
+ </tr>
44
+ <tr>
45
+ <td>cell 2,1</td>
46
+ <td>cell 2,2</td>
47
+ </tr>
48
+ </table>
49
+ <iframe id='myframe' name='myframe' src='/iframe'>
50
+ </iframe>
51
+ </body>
52
+ </html>
53
+ HTML
54
+ end
55
+
56
+ def call(env)
57
+ request = Rack::Request.new(env)
58
+ if request.path == '/iframe'
59
+ [200, {"Content-Type" => "text/html"}, ["<html><body><div id='inframe'></div></body></html>"]]
60
+ else
61
+ [200, {"Content-Type" => "text/html"}, [get_html(request)]]
62
+ end
63
+ end
64
+ end
metadata ADDED
@@ -0,0 +1,210 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: capybarbecue
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andrew DiMichele
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: capybara
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 2.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 2.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rr
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rdoc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: jeweler
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: poltergeist
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: launchy
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - '>='
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: Makes fundamental changes to Capybara's threading architecture so you
154
+ can write stable tests with a shared database connection.
155
+ email: backflip@gmail.com
156
+ executables: []
157
+ extensions: []
158
+ extra_rdoc_files:
159
+ - LICENSE.txt
160
+ - README.rdoc
161
+ files:
162
+ - .document
163
+ - .rspec
164
+ - Gemfile
165
+ - Gemfile.lock
166
+ - LICENSE.txt
167
+ - NOTES.txt
168
+ - README.rdoc
169
+ - Rakefile
170
+ - lib/capybarbecue.rb
171
+ - lib/capybarbecue/async_call.rb
172
+ - lib/capybarbecue/async_delegate_class.rb
173
+ - lib/capybarbecue/rack_runner.rb
174
+ - lib/capybarbecue/server.rb
175
+ - lib/capybarbecue/version.rb
176
+ - spec/capybarbecue/async_call_spec.rb
177
+ - spec/capybarbecue/async_delegate_class_spec.rb
178
+ - spec/capybarbecue/rack_runner_spec.rb
179
+ - spec/capybarbecue/server_spec.rb
180
+ - spec/capybarbecue_spec.rb
181
+ - spec/integration/element_spec.rb
182
+ - spec/integration/session_spec.rb
183
+ - spec/spec_helper.rb
184
+ - spec/support/test_rack_app.rb
185
+ homepage: http://github.com/adimichele/capybarbecue
186
+ licenses:
187
+ - MIT
188
+ metadata: {}
189
+ post_install_message:
190
+ rdoc_options: []
191
+ require_paths:
192
+ - lib
193
+ required_ruby_version: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - '>='
196
+ - !ruby/object:Gem::Version
197
+ version: '0'
198
+ required_rubygems_version: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - '>='
201
+ - !ruby/object:Gem::Version
202
+ version: '0'
203
+ requirements: []
204
+ rubyforge_project:
205
+ rubygems_version: 2.0.3
206
+ signing_key:
207
+ specification_version: 4
208
+ summary: Makes your Capybara test suite work better
209
+ test_files: []
210
+ has_rdoc: