├── .gitignore ├── .rspec ├── .ruby-version ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── transactional_capybara.rb └── transactional_capybara │ ├── ajax_helpers.rb │ ├── rspec.rb │ ├── shared_connection.rb │ ├── shared_connection │ ├── active_record.rb │ └── sequel.rb │ └── version.rb ├── spec ├── .travis.config.yml ├── ajax_spec.rb ├── capybara_integration_spec.rb ├── config.yml.example ├── spec_helper.rb └── support │ ├── active_record_setup.rb │ ├── db_config.rb │ ├── migration.rb │ ├── model │ ├── active_record.rb │ └── sequel.rb │ ├── sequel_setup.rb │ ├── server.rb │ ├── vendor │ ├── angular.js │ └── jquery.js │ └── views │ ├── angular.erb │ ├── basic.erb │ └── jquery.erb └── transactional_capybara.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | InstalledFiles 7 | _yardoc 8 | coverage 9 | doc/ 10 | lib/bundler/man 11 | pkg 12 | rdoc 13 | spec/reports 14 | test/tmp 15 | test/version_tmp 16 | tmp 17 | spec/config.yml 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.0.0-p195 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - "2.2" 4 | - "2.3" 5 | - "2.4" 6 | env: 7 | - MOZ_HEADLESS=1 8 | addons: 9 | firefox: latest 10 | before_install: 11 | - wget https://github.com/mozilla/geckodriver/releases/download/v0.18.0/geckodriver-v0.18.0-linux64.tar.gz 12 | - mkdir geckodriver 13 | - tar -xzf geckodriver*.tar.gz -C geckodriver 14 | - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 15 | - export PATH=$PATH:$PWD/geckodriver 16 | - export DISPLAY=:99.0 17 | before_script: 18 | - mysql -e "create database IF NOT EXISTS transactional_capybara_test;" -uroot 19 | - psql -c 'create database transactional_capybara_test;' -U postgres 20 | - "cp spec/{.travis.,}config.yml" 21 | script: bundle exec rake test:all 22 | cache: bundler 23 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in transactional_capybara.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | transactional_capybara (0.2.0) 5 | capybara (~> 2.12) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | activemodel (4.2.10) 11 | activesupport (= 4.2.10) 12 | builder (~> 3.1) 13 | activerecord (4.2.10) 14 | activemodel (= 4.2.10) 15 | activesupport (= 4.2.10) 16 | arel (~> 6.0) 17 | activesupport (4.2.10) 18 | i18n (~> 0.7) 19 | minitest (~> 5.1) 20 | thread_safe (~> 0.3, >= 0.3.4) 21 | tzinfo (~> 1.1) 22 | addressable (2.5.2) 23 | public_suffix (>= 2.0.2, < 4.0) 24 | arel (6.0.4) 25 | builder (3.2.3) 26 | capybara (2.13.0) 27 | addressable 28 | mime-types (>= 1.16) 29 | nokogiri (>= 1.3.3) 30 | rack (>= 1.0.0) 31 | rack-test (>= 0.5.4) 32 | xpath (~> 2.0) 33 | capybara-webkit (1.14.0) 34 | capybara (>= 2.3.0, < 2.14.0) 35 | json 36 | childprocess (0.8.0) 37 | ffi (~> 1.0, >= 1.0.11) 38 | cliver (0.3.2) 39 | diff-lcs (1.2.5) 40 | ffi (1.9.18) 41 | i18n (0.7.0) 42 | json (1.8.6) 43 | mime-types (3.1) 44 | mime-types-data (~> 3.2015) 45 | mime-types-data (3.2016.0521) 46 | mini_portile (0.6.2) 47 | minitest (5.8.0) 48 | multi_json (1.12.2) 49 | mustermann (1.0.1) 50 | mysql2 (0.3.20) 51 | nokogiri (1.6.6.2) 52 | mini_portile (~> 0.6.0) 53 | pg (0.18.2) 54 | poltergeist (1.6.0) 55 | capybara (~> 2.1) 56 | cliver (~> 0.3.1) 57 | multi_json (~> 1.0) 58 | websocket-driver (>= 0.2.0) 59 | public_suffix (2.0.5) 60 | rack (2.0.3) 61 | rack-protection (2.0.0) 62 | rack 63 | rack-test (0.6.3) 64 | rack (>= 1.0) 65 | rake (10.4.2) 66 | rspec (3.3.0) 67 | rspec-core (~> 3.3.0) 68 | rspec-expectations (~> 3.3.0) 69 | rspec-mocks (~> 3.3.0) 70 | rspec-core (3.3.1) 71 | rspec-support (~> 3.3.0) 72 | rspec-expectations (3.3.0) 73 | diff-lcs (>= 1.2.0, < 2.0) 74 | rspec-support (~> 3.3.0) 75 | rspec-mocks (3.3.1) 76 | diff-lcs (>= 1.2.0, < 2.0) 77 | rspec-support (~> 3.3.0) 78 | rspec-support (3.3.0) 79 | rubyzip (1.2.1) 80 | selenium-webdriver (3.7.0) 81 | childprocess (~> 0.5) 82 | rubyzip (~> 1.0) 83 | sequel (4.42.1) 84 | sinatra (2.0.0) 85 | mustermann (~> 1.0) 86 | rack (~> 2.0) 87 | rack-protection (= 2.0.0) 88 | tilt (~> 2.0) 89 | sqlite3 (1.3.10) 90 | thread_safe (0.3.6) 91 | tilt (2.0.8) 92 | tzinfo (1.2.2) 93 | thread_safe (~> 0.1) 94 | websocket-driver (0.6.2) 95 | websocket-extensions (>= 0.1.0) 96 | websocket-extensions (0.1.2) 97 | xpath (2.1.0) 98 | nokogiri (~> 1.3) 99 | 100 | PLATFORMS 101 | ruby 102 | 103 | DEPENDENCIES 104 | activerecord 105 | bundler (~> 1.3) 106 | capybara-webkit 107 | mysql2 108 | pg 109 | poltergeist 110 | rake 111 | rspec 112 | selenium-webdriver 113 | sequel 114 | sinatra 115 | sqlite3 116 | transactional_capybara! 117 | 118 | BUNDLED WITH 119 | 1.16.0 120 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Ian Young 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Database Transactions 💜 Capybara 2 | 3 | [![Build Status]](https://travis-ci.org/iangreenleaf/transactional_capybara) 4 | 5 | You want your specs to use transactions for speed 🐎🐎🐎 6 | 7 | But as soon as you try it with Capybara, things go wrong 💻💥 8 | 9 | Don't flip tables. 10 | Use this instead. 11 | 12 | For a detailed explanation of how this works, refer to the [introductory blog post]. 13 | 14 | ## Support ## 15 | 16 | Right now this gem automatically handles the following things: 17 | 18 | * ActiveRecord 19 | * Sequel 20 | * jQuery 21 | * Angular 22 | 23 | Tested on Capybara 2.4.x, may not work on other major versions. 24 | 25 | Don't see something you want? 26 | I'd love a pull request, or even just a friendly inquiry! 27 | 28 | ## Setup ## 29 | 30 | Add it to your Gemfile, of course: 31 | 32 | ```ruby 33 | group :test do 34 | gem 'transactional_capybara' 35 | end 36 | ``` 37 | 38 | And then initialize it in your tests… 39 | 40 | ### RSpec ### 41 | 42 | In `rails_helper.rb` (or `spec_helper.rb`): 43 | 44 | ```ruby 45 | require "transactional_capybara/rspec" 46 | ``` 47 | 48 | If you're using `ActiveRecord::Base.maintain_test_schema!` in your `rails_helper.rb` (or `spec_helper.rb`), make sure it is invoked before requiring transactional_capybara. Failure to do so might cause some non-deterministic connection failures. 49 | 50 | Your database connection is automatically shared between threads, and all specs tagged with `js: true` will wait for AJAX requests to finish before continuing. 51 | 52 | Wow. Much convenience. So relax. 53 | 54 | ### All other test frameworks ### 55 | 56 | Somewhere near the beginning of your test initialization: 57 | 58 | ```ruby 59 | TransactionalCapybara.share_connection 60 | ``` 61 | 62 | And then make sure to define a hook that will run after each Capybara test: 63 | 64 | ```ruby 65 | after :each do 66 | TransactionalCapybara::AjaxHelpers.wait_for_ajax(page) 67 | end 68 | ``` 69 | 70 | ## Manually waiting for AJAX ## 71 | 72 | You might have situations where you need to wait for AJAX calls to complete at times other than teardown. 73 | For example, you might have a pattern like this if you access models directly for either setup or verification of results: 74 | 75 | ```ruby 76 | visit '/page-that-fires-ajax' 77 | Model.where(whatever).first 78 | ``` 79 | 80 | This can still fail! 81 | The ideal solution is to avoid direct database manipulation in integration tests. 82 | However, if you insist on doing this, you can stay safe by waiting for AJAX to complete before continuing: 83 | 84 | ```ruby 85 | visit '/page-that-fires-ajax' 86 | TransactionalCapybara::AjaxHelpers.wait_for_ajax(page) 87 | Model.where(whatever).first 88 | ``` 89 | 90 | If you'd like the helper more easily accessible, just mix the `AjaxHelpers` module into your test suite. 91 | In RSpec, you can do this in the config block: 92 | 93 | ```ruby 94 | RSpec.configure do |config| 95 | config.include TransactionalCapybara::AjaxHelpers 96 | end 97 | ``` 98 | Or in any example group: 99 | 100 | ```ruby 101 | describe 'awesome web stuff' do 102 | include TransactionalCapybara::AjaxHelpers 103 | end 104 | ``` 105 | 106 | Now the helper is easily available: 107 | 108 | ```ruby 109 | visit '/page-that-fires-ajax' 110 | wait_for_ajax 111 | Model.where(whatever).first 112 | ``` 113 | 114 | ## DatabaseCleaner ## 115 | 116 | For this gem to be able to help with AJAX, it needs to be invoked *before* DatabaseCleaner rolls back the transaction. 117 | 118 | You should be good to go if your setup looks like this: 119 | 120 | ```ruby 121 | config.around(:each) do |example| 122 | DatabaseCleaner.cleaning do 123 | example.run 124 | end 125 | end 126 | ``` 127 | 128 | But if you're using before/after hooks to clean, like: 129 | 130 | ```ruby 131 | before :each do 132 | DatabaseCleaner.start 133 | end 134 | 135 | after :each do 136 | DatabaseCleaner.clean 137 | end 138 | ``` 139 | 140 | Then you need to make sure the AJAX hook runs first. 141 | Declare it before the DatabaseCleaner cleanup and you should be set: 142 | 143 | ```ruby 144 | before :each do 145 | DatabaseCleaner.start 146 | end 147 | 148 | after :each do 149 | TransactionalCapybara::AjaxHelpers.wait_for_ajax(page) 150 | DatabaseCleaner.clean 151 | end 152 | ``` 153 | 154 | ## Sequel ## 155 | If you want to have shared database connections with sequel just 156 | add the option `single_threaded: true` to your sequel connection in test. 157 | 158 | ## Contributing 159 | 160 | 1. Fork it 161 | 2. Create your feature branch (`git checkout -b my-new-feature`) 162 | 3. Make your changes. Don't forget to add a test! 163 | 3. Commit your changes (`git commit -am 'Add some feature'`) 164 | 4. Push to the branch (`git push origin my-new-feature`) 165 | 6. Create new Pull Request 166 | 167 | ### Running the tests ### 168 | 169 | ``` 170 | cp spec/support/config.yml{.example,} # Edit as necessary 171 | DB=sqlite rspec spec 172 | ``` 173 | 174 | You can run the specs with other databases as well, but you will have to create the databases and users manually first. 175 | 176 | You can run the tests against all combinations of supported databases, web drivers, and ORMs with: 177 | 178 | ``` 179 | rake test:all 180 | ``` 181 | 182 | That rake task may be examined to find all the options currently under test, as well. 183 | 184 | [Build Status]: https://travis-ci.org/iangreenleaf/transactional_capybara.svg?branch=master 185 | [introductory blog post]: http://technotes.iangreenleaf.com/posts/the-one-true-guide-to-database-transactions-with-capybara.html 186 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "bundler/cli" 3 | require 'rspec/core/rake_task' 4 | require 'yaml' 5 | require 'active_support' 6 | 7 | namespace :test do 8 | RSpec::Core::RakeTask.new(:rspec) 9 | 10 | desc "Run tests against all drivers and all databases listed in the config" 11 | task :all do 12 | db_config = YAML.load_file(File.join(File.dirname(__FILE__), "spec/config.yml")) 13 | configs = db_config["database"].keys.product( 14 | %w[selenium webkit poltergeist], 15 | %w[active_record sequel] 16 | ) 17 | configs.each do |db_name, driver, orm| 18 | ENV['DB'] = db_name 19 | ENV['DRIVER'] = driver 20 | ENV['ORM'] = orm 21 | puts ENV.to_hash.slice 'DRIVER', 'DB', 'ORM' 22 | Rake::Task['test:rspec'].reenable 23 | Rake::Task['test:rspec'].invoke 24 | end 25 | end 26 | 27 | desc "Run the test server for manual debugging" 28 | task :server do 29 | db_type = ENV['DB'] || 'sqlite' 30 | db_config = YAML.load_file(File.join(File.dirname(__FILE__), "spec/config.yml")) 31 | db = db_config["database"][db_type] 32 | require 'active_record' 33 | ActiveRecord::Base.establish_connection(db) 34 | load File.join(File.dirname(__FILE__), "spec/support/schema.rb") 35 | require_relative './spec/support/server' 36 | require_relative './spec/support/model' 37 | TestValue.create! content: "This is a server response" 38 | AjaxServer.should_return_from_ajax = true 39 | AjaxServer.run! 40 | end 41 | end 42 | 43 | task :bundle do 44 | Bundler::CLI.start(['install']) 45 | end 46 | 47 | if Rake::Task.task_defined? 'release:guard_clean' 48 | Rake::Task['release:guard_clean'].enhance [:bundle] 49 | end 50 | -------------------------------------------------------------------------------- /lib/transactional_capybara.rb: -------------------------------------------------------------------------------- 1 | require "transactional_capybara/version" 2 | require "transactional_capybara/ajax_helpers" 3 | require "transactional_capybara/shared_connection" 4 | -------------------------------------------------------------------------------- /lib/transactional_capybara/ajax_helpers.rb: -------------------------------------------------------------------------------- 1 | module TransactionalCapybara 2 | module AjaxHelpers 3 | class PageWaiting 4 | def initialize(page) 5 | @page = page 6 | end 7 | 8 | def wait_for_ajax 9 | wait_until { finished_all_ajax_requests? } 10 | end 11 | 12 | def finished_all_ajax_requests? 13 | capybara_sessions.all? do |name, session| 14 | if is_session_touched?(session) 15 | PageWaiting.new(session).finished_ajax_requests? 16 | else 17 | true 18 | end 19 | end 20 | end 21 | 22 | def finished_ajax_requests? 23 | ( angular_requests + jquery_requests ).zero? 24 | end 25 | 26 | # TODO: timeout each individual session 27 | def wait_until(timeout=default_timeout) 28 | Timeout.timeout(timeout) do 29 | until yield 30 | sleep(0.01) 31 | end 32 | end 33 | rescue Timeout::Error 34 | # Oh well, hopefully it finished! 35 | end 36 | 37 | private 38 | 39 | # Hack into Capybara's private interface to get access to all sessions 40 | def capybara_sessions 41 | Capybara.send :session_pool 42 | end 43 | 44 | # Handle Capybara.default_wait_time deprecation 45 | # https://github.com/jnicklas/capybara/pull/1502 46 | def default_timeout 47 | @default_timeout ||= begin 48 | Capybara.respond_to?(:default_max_wait_time) ? 49 | Capybara.default_max_wait_time : 50 | Capybara.default_wait_time 51 | end 52 | end 53 | 54 | # Another hack, to see if Capybara sessions have been used 55 | def is_session_touched?(session) 56 | session.instance_variable_get(:@touched) 57 | end 58 | 59 | def run_js(expr) 60 | @page.evaluate_script(expr) 61 | end 62 | 63 | def angular? 64 | run_js("!!window.angular") && run_js("!!document.querySelector('[ng-app]')") 65 | end 66 | 67 | def angular_requests 68 | if angular? 69 | run_js("angular.element(document.querySelector('[ng-app]')).injector().get('$http').pendingRequests.length") 70 | else 71 | 0 72 | end 73 | end 74 | 75 | def jquery? 76 | run_js("!!window.jQuery") 77 | end 78 | 79 | def jquery_requests 80 | if jquery? 81 | run_js("jQuery.active") 82 | else 83 | 0 84 | end 85 | end 86 | end 87 | 88 | def wait_for_ajax 89 | TransactionalCapybara::AjaxHelpers.wait_for_ajax(page) 90 | end 91 | 92 | def self.wait_for_ajax(page) 93 | PageWaiting.new(page).wait_for_ajax 94 | end 95 | 96 | def ajax_finished? 97 | TransactionalCapybara::AjaxHelpers.ajax_finished?(page) 98 | end 99 | 100 | def self.ajax_finished?(page) 101 | PageWaiting.new(page).finished_all_ajax_requests? 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/transactional_capybara/rspec.rb: -------------------------------------------------------------------------------- 1 | require 'transactional_capybara' 2 | 3 | RSpec.configure do |config| 4 | TransactionalCapybara.share_connection 5 | config.after(:each, js: true) do 6 | TransactionalCapybara::AjaxHelpers.wait_for_ajax(page) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/transactional_capybara/shared_connection.rb: -------------------------------------------------------------------------------- 1 | module TransactionalCapybara 2 | module_function 3 | def share_connection 4 | #noop is default 5 | end 6 | end 7 | if defined?(ActiveRecord::Base) 8 | require_relative './shared_connection/active_record' 9 | end 10 | if defined?(Sequel::Model) 11 | require_relative './shared_connection/sequel' 12 | end 13 | -------------------------------------------------------------------------------- /lib/transactional_capybara/shared_connection/active_record.rb: -------------------------------------------------------------------------------- 1 | class ActiveRecord::Base 2 | mattr_accessor :shared_connection 3 | @@shared_connection = nil 4 | 5 | def self.connection 6 | @@shared_connection || retrieve_connection 7 | end 8 | end 9 | 10 | module TransactionalCapybara 11 | module_function 12 | def share_connection 13 | ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/transactional_capybara/shared_connection/sequel.rb: -------------------------------------------------------------------------------- 1 | module TransactionalCapybara 2 | module_function 3 | def share_connection 4 | warn 'WARNING: No database connection sharing enabled! You propably want to use the sequel connection option single_threaded: true' unless Sequel::Model.db.single_threaded? 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/transactional_capybara/version.rb: -------------------------------------------------------------------------------- 1 | module TransactionalCapybara 2 | VERSION = "0.2.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/.travis.config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | database: 3 | mysql: 4 | adapter: mysql2 5 | host: localhost 6 | username: travis 7 | database: transactional_capybara_test 8 | sqlite: 9 | adapter: 10 | active_record: sqlite3 11 | sequel: sqlite 12 | database: ":memory:" 13 | postgres: 14 | adapter: postgresql 15 | username: postgres 16 | database: transactional_capybara_test 17 | -------------------------------------------------------------------------------- /spec/ajax_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'support/server' 2 | 3 | RSpec.describe "server with AJAX", type: :feature, js: true do 4 | before do 5 | Capybara.app = AjaxServer 6 | AjaxServer.should_return_from_ajax = false 7 | @expected_message = "foobar" 8 | TestValue.create! content: @expected_message 9 | end 10 | 11 | it "waits for AJAX from jQuery" do 12 | visit "/ajax/jquery" 13 | expect(page).to have_content("Hello") 14 | click_link "Do AJAX" 15 | Thread.fork do 16 | sleep 0.5 17 | AjaxServer.should_return_from_ajax = true 18 | end 19 | expect(find(".message").text).not_to eq(@expected_message) 20 | TransactionalCapybara::AjaxHelpers.wait_for_ajax(page) 21 | expect(find(".message").text).to eq(@expected_message) 22 | end 23 | 24 | it "waits for AJAX from Angular" do 25 | visit "/ajax/angular" 26 | expect(page).to have_content("Hello") 27 | click_link "Do AJAX" 28 | Thread.fork do 29 | sleep 0.5 30 | AjaxServer.should_return_from_ajax = true 31 | end 32 | expect(find(".message").text).not_to eq(@expected_message) 33 | TransactionalCapybara::AjaxHelpers.wait_for_ajax(page) 34 | expect(find(".message").text).to eq(@expected_message) 35 | end 36 | 37 | context "mixed in" do 38 | include TransactionalCapybara::AjaxHelpers 39 | it "provides wait_for_ajax helper" do 40 | visit "/ajax/jquery" 41 | expect(page).to have_content("Hello") 42 | click_link "Do AJAX" 43 | Thread.fork do 44 | sleep 0.5 45 | AjaxServer.should_return_from_ajax = true 46 | end 47 | expect(find(".message").text).not_to eq(@expected_message) 48 | wait_for_ajax 49 | expect(find(".message").text).to eq(@expected_message) 50 | end 51 | end 52 | 53 | context "after hook", check_result_after: true do 54 | it "automatically waits for AJAX" do 55 | visit "/ajax/jquery" 56 | expect(page).to have_content("Hello") 57 | click_link "Do AJAX" 58 | Thread.fork do 59 | sleep 0.5 60 | AjaxServer.should_return_from_ajax = true 61 | end 62 | expect(find(".message").text).not_to eq(@expected_message) 63 | end 64 | end 65 | 66 | it "waits on all sessions" do 67 | using_session :foo do 68 | visit "/ajax/jquery" 69 | expect(page).to have_content("Hello") 70 | click_link "Do AJAX" 71 | end 72 | 73 | using_session :bar do 74 | visit "/boring_page" 75 | expect(page).to have_content("Hi") 76 | end 77 | 78 | Thread.fork do 79 | sleep 0.5 80 | AjaxServer.should_return_from_ajax = true 81 | end 82 | TransactionalCapybara::AjaxHelpers.wait_for_ajax(page) 83 | 84 | using_session :foo do 85 | expect(find(".message").text).to eq(@expected_message) 86 | end 87 | 88 | AjaxServer.should_return_from_ajax = false 89 | 90 | using_session :bar do 91 | visit "/ajax/jquery" 92 | expect(page).to have_content("Hello") 93 | click_link "Do AJAX" 94 | end 95 | 96 | Thread.fork do 97 | sleep 0.5 98 | AjaxServer.should_return_from_ajax = true 99 | end 100 | TransactionalCapybara::AjaxHelpers.wait_for_ajax(page) 101 | 102 | using_session :bar do 103 | expect(find(".message").text).to eq(@expected_message) 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/capybara_integration_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative 'support/server' 2 | 3 | RSpec.describe "integration with Capybara" do 4 | let(:page) { double } 5 | 6 | def simulate_page_wait(expected_timeout) 7 | page_handler = TransactionalCapybara::AjaxHelpers::PageWaiting.new(page) 8 | expect(Timeout).to receive(:timeout).with(expected_timeout).at_least(:once) 9 | page_handler.wait_until { true } 10 | end 11 | 12 | context "version >= 2.5", if: capybara_version_matching(">= 2.5.0") do 13 | it "uses Capybara.default_max_wait_time if available" do 14 | Capybara.default_max_wait_time = 3 15 | simulate_page_wait(3) 16 | end 17 | end 18 | 19 | context "version < 2.5", if: capybara_version_matching("< 2.5.0") do 20 | it "fallbacks on deprecated Capybara.default_wait_time" do 21 | Capybara.default_wait_time = 6 22 | simulate_page_wait(6) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/config.yml.example: -------------------------------------------------------------------------------- 1 | --- 2 | database: 3 | mysql: 4 | adapter: mysql2 5 | host: localhost 6 | username: transactional_capybara 7 | password: abcd1234 8 | database: transactional_capybara 9 | sqlite: 10 | adapter: 11 | active_record: sqlite3 12 | sequel: sqlite 13 | host: localhost 14 | database: tmp/test.db 15 | postgres: 16 | adapter: postgresql 17 | host: localhost 18 | username: transactional_capybara 19 | password: abcd1234 20 | database: transactional_capybara 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | ENV['DB'] ||= 'sqlite' 3 | ENV['ORM'] ||= 'active_record' 4 | 5 | require_relative "support/#{ENV['ORM']}_setup.rb" 6 | 7 | require 'capybara/rspec' 8 | require 'capybara/poltergeist' 9 | require 'capybara-webkit' 10 | 11 | capybara_driver = ENV['DRIVER'] || 'selenium' 12 | Capybara.javascript_driver = capybara_driver.to_sym 13 | 14 | RSpec.configure do |config| 15 | config.expect_with :rspec do |expectations| 16 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 17 | end 18 | 19 | config.after(:each, check_result_after: true) do 20 | expect(find(".message").text).to eq(@expected_message) 21 | end 22 | 23 | def capybara_version_matching(spec) 24 | Gem::Dependency.new('capybara', spec).match?('capybara', Capybara::VERSION) 25 | end 26 | end 27 | 28 | require 'transactional_capybara/rspec' 29 | -------------------------------------------------------------------------------- /spec/support/active_record_setup.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require_relative 'db_config' 3 | 4 | config = db_config('active_record') 5 | ActiveRecord::Base.establish_connection(config) 6 | 7 | ActiveRecord::Schema.define do 8 | create_table :test_values, :force => true do |t| 9 | t.string :content 10 | end 11 | end 12 | 13 | require_relative 'model/active_record' 14 | -------------------------------------------------------------------------------- /spec/support/db_config.rb: -------------------------------------------------------------------------------- 1 | def db_config orm 2 | db_config = YAML.load_file(File.join(File.dirname(__FILE__), "../config.yml")) 3 | db = db_config["database"][ENV['DB']] 4 | db["adapter"] = db["adapter"][ENV['ORM']] unless db["adapter"].is_a? String 5 | db 6 | end 7 | -------------------------------------------------------------------------------- /spec/support/migration.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Migration.create_table :test_values do |t| 2 | t.string :content 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/model/active_record.rb: -------------------------------------------------------------------------------- 1 | class TestValue < ActiveRecord::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/support/model/sequel.rb: -------------------------------------------------------------------------------- 1 | class TestValue < Sequel::Model 2 | def self.create!(data) 3 | new(data).save(raise_on_failure: true) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/support/sequel_setup.rb: -------------------------------------------------------------------------------- 1 | require 'sequel' 2 | require_relative 'db_config' 3 | 4 | config = db_config('sequel').merge(single_threaded: true) 5 | db = Sequel.connect(config) 6 | 7 | db.create_table! :test_values do 8 | primary_key :id 9 | String :content 10 | end 11 | require_relative 'model/sequel' 12 | -------------------------------------------------------------------------------- /spec/support/server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'tilt/erb' 3 | 4 | class AjaxServer < Sinatra::Base 5 | set :public_folder, File.dirname(__FILE__) + '/vendor' 6 | 7 | class << self 8 | attr_accessor :should_return_from_ajax 9 | end 10 | 11 | get "/boring_page" do 12 | erb "Hi", layout: :basic 13 | end 14 | 15 | get "/ajax/jquery" do 16 | erb :jquery, layout: :basic 17 | end 18 | 19 | get "/ajax/angular" do 20 | erb :angular, layout: :basic 21 | end 22 | 23 | get "/ajax/endpoint" do 24 | until AjaxServer.should_return_from_ajax do 25 | sleep 0.01 26 | end 27 | TestValue.last.content 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/support/views/angular.erb: -------------------------------------------------------------------------------- 1 |