├── .github └── workflows │ └── test_and_release.yml ├── .gitignore ├── .rspec ├── Gemfile ├── LICENSE ├── README.mdown ├── Rakefile ├── capybara-mechanize.gemspec ├── gemfiles ├── Gemfile.capybara_master ├── puma5.gemfile ├── puma5_capybara36.gemfile └── puma6.gemfile ├── lib └── capybara │ ├── mechanize.rb │ ├── mechanize │ ├── browser.rb │ ├── cucumber.rb │ ├── driver.rb │ ├── form.rb │ ├── node.rb │ └── version.rb │ └── spec │ └── extended_test_app.rb └── spec ├── driver ├── mechanize_driver_spec.rb └── remote_mechanize_driver_spec.rb ├── session ├── mechanize_spec.rb └── remote_mechanize_spec.rb ├── spec_helper.rb └── support ├── disable_external_tests.rb ├── extended_test_app_setup.rb └── remote_test_url.rb /.github/workflows/test_and_release.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | ruby-version: ['2.6', '2.7', '3.0', '3.1'] 9 | gemfile: 10 | - puma5 11 | - puma5_capybara36 12 | - puma6 13 | exclude: 14 | - ruby-version: '2.6' 15 | gemfile: puma6 16 | - ruby-version: '2.6' 17 | gemfile: puma5 18 | env: 19 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby-version }} 26 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 27 | - name: Run tests 28 | run: bundle exec rake 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## MAC OS 2 | .DS_Store 3 | .bundle 4 | ## TEXTMATE 5 | *.tmproj 6 | tmtags 7 | 8 | ## EMACS 9 | *~ 10 | \#* 11 | .\#* 12 | 13 | ## VIM 14 | *.swp 15 | 16 | ## PROJECT::GENERAL 17 | coverage 18 | rdoc 19 | pkg 20 | 21 | ## PROJECT::SPECIFIC 22 | Gemfile.lock 23 | gemfiles/*.lock 24 | 25 | save_path_tmp -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color --backtrace 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'http://rubygems.org' 4 | 5 | gemspec 6 | 7 | gem 'puma', '~> 6.0' 8 | gem 'sinatra', '~> 2.0' 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2011 Jeroen van Dijk and contributors 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. 21 | -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | Capybara-mechanize 2 | ================== 3 | 4 | [![.github/workflows/test_and_release.yml](https://github.com/phillbaker/capybara-mechanize/actions/workflows/test_and_release.yml/badge.svg)](https://github.com/phillbaker/capybara-mechanize/actions/workflows/test_and_release.yml) 5 | 6 | This gem makes it possible to use Capybara for (partially) remote testing. It inherits most functionality from the RackTest driver and only uses [Mechanize](https://github.com/sparklemotion/mechanize) for remote requests. 7 | 8 | It is currently in use to test the integration between a Rails application and Twitter authorization and sharing. 9 | 10 | This gem is a [Capybara](http://github.com/jnicklas/capybara) extension. I have been inspired by the Capybara driver and some earlier efforts for a Mechanize driver. 11 | 12 | Thanks to [Pinkelstar](http://www.pinkelstar.com) for giving me the time and the need to develop this gem. 13 | 14 | ### Installation 15 | 16 | gem install capybara-mechanize 17 | 18 | ### Compatibility 19 | 20 | For support with: 21 | * Capybara 2.x, use versions of this gem less than or equal 1.11.0 22 | * Capybara 3.x, use versions of this gem greater than or equal to 1.12.0 23 | 24 | ### Usage without Cucumber 25 | 26 | require 'capybara/mechanize' 27 | 28 | ### Further configuration of Mechanize 29 | 30 | Upon instantiation of a `Driver`, you can configure [`mechanize` agent 31 | options](http://www.rubydoc.info/gems/mechanize/Mechanize) in a `#configure` block: 32 | 33 | ```ruby 34 | Capybara.register_driver :mechanize do |app| 35 | driver = Capybara::Mechanize::Driver.new(app) 36 | driver.configure do |agent| 37 | # Configure other Mechanize options here. 38 | agent.log = Logger.new "mech.log" 39 | agent.user_agent_alias = 'Mac Safari' 40 | end 41 | driver 42 | end 43 | ``` 44 | ### Usage without rack app 45 | 46 | You can configure it to use for external servers. Until this issue https://github.com/jeroenvandijk/capybara-mechanize/issues/66 is resolved, you can configure with 47 | ``` 48 | Capybara.register_driver :mechanize do |app| 49 | Capybara::Mechanize::Driver.new(proc {}) 50 | end 51 | ``` 52 | and use like this 53 | ``` 54 | session = Capybara::Session.new :mechanize 55 | session.visit 'https://github.com' 56 | ``` 57 | ### Usage with Cucumber and tags 58 | 59 | A @mechanize tag is added to your hooks when you add the following line to your env.rb 60 | 61 | require 'capybara/mechanize/cucumber' 62 | 63 | The following scenario will then be using the Mechanize driver 64 | 65 | @mechanize 66 | Scenario: do something remote 67 | When I click the remote link 68 | 69 | ### Remote testing 70 | 71 | When you want to use this driver to test a remote application. You have to set the app_host: 72 | 73 | Capybara.app_host = "http://www.yourapp.com" 74 | 75 | Note that I haven't tested this case for my self yet. The Capybara tests pass for this situation though so it should work! Please provide me with feedback if it doesn't. 76 | 77 | ### HTTP errors 78 | 79 | If you receive the error `Net::HTTP::Persistent::Error: too many connection resets`, try setting a timeout value: `page.driver.browser.agent.idle_timeout = 0.4`. 80 | 81 | ## Running tests 82 | 83 | Run bundler 84 | 85 | bundle install 86 | 87 | Then you are ready to run the test like so 88 | 89 | bundle exec rake spec 90 | 91 | Todo 92 | ---- 93 | * Test this driver with non-rack/non-ruby projects 94 | 95 | Note on Patches/Pull Requests 96 | ----------------------------- 97 | 98 | * Fork the project. 99 | * Make your feature addition or bug fix. 100 | * Add tests for it. This is important so I don't break it in a 101 | future version unintentionally. 102 | * Commit, do not mess with rakefile, version, or history. 103 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 104 | * Send me a pull request. Bonus points for topic branches. 105 | 106 | Copyright 107 | --------- 108 | Copyright (c) 2010-2013 Jeroen van Dijk. See LICENSE for details. 109 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | 5 | require 'rspec/core/rake_task' 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | 10 | require 'rdoc/task' 11 | RDoc::Task.new do |rdoc| 12 | version = File.exist?('VERSION') ? File.read('VERSION') : '' 13 | 14 | rdoc.rdoc_dir = 'rdoc' 15 | rdoc.title = "capybara-mechanize #{version}" 16 | rdoc.rdoc_files.include('README*') 17 | rdoc.rdoc_files.include('lib/**/*.rb') 18 | end 19 | -------------------------------------------------------------------------------- /capybara-mechanize.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $:.push File.expand_path('lib', __dir__) 4 | require 'capybara/mechanize/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'capybara-mechanize' 8 | s.version = Capybara::Mechanize::VERSION 9 | s.required_ruby_version = '>= 2.6.0' 10 | 11 | s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version= 12 | s.authors = ['Jeroen van Dijk'] 13 | s.summary = 'RackTest driver for Capybara with remote request support' 14 | s.description = 'RackTest driver for Capybara, but with remote request support thanks to mechanize' 15 | 16 | s.email = 'jeroen@jeevidee.nl' 17 | s.files = Dir.glob('{lib,spec}/**/*') + %w[README.mdown] 18 | s.homepage = 'https://github.com/jeroenvandijk/capybara-mechanize' 19 | s.rdoc_options = ['--charset=UTF-8'] 20 | s.require_paths = ['lib'] 21 | s.rubygems_version = '1.3.7' 22 | 23 | s.add_runtime_dependency('capybara', ['>= 3.0.0', '< 4']) 24 | s.add_runtime_dependency('mechanize', ['~> 2.8.5']) 25 | 26 | s.add_development_dependency('launchy', ['>= 2.0.4']) 27 | s.add_development_dependency('rake') 28 | s.add_development_dependency('rdoc') 29 | s.add_development_dependency('rspec', ['~>3.5']) 30 | end 31 | -------------------------------------------------------------------------------- /gemfiles/Gemfile.capybara_master: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec :path => '..' 4 | 5 | gem 'capybara', github: 'jnicklas/capybara' 6 | 7 | gem 'sinatra', '~> 2.0' 8 | gem 'puma' 9 | -------------------------------------------------------------------------------- /gemfiles/puma5.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'http://rubygems.org' 4 | 5 | gemspec path: '..' 6 | 7 | gem 'capybara', '> 3.37' 8 | gem 'puma', '~> 5.0' 9 | gem 'sinatra', '~> 2.0' 10 | -------------------------------------------------------------------------------- /gemfiles/puma5_capybara36.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'http://rubygems.org' 4 | 5 | gemspec path: '..' 6 | 7 | gem 'capybara', '= 3.36' 8 | gem 'puma', '~> 5.0' 9 | gem 'sinatra', '~> 2.0' 10 | -------------------------------------------------------------------------------- /gemfiles/puma6.gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'http://rubygems.org' 4 | 5 | gemspec path: '..' 6 | 7 | gem 'capybara', '> 3.37' 8 | gem 'puma', '~> 5.0' 9 | gem 'sinatra', '~> 2.0' 10 | -------------------------------------------------------------------------------- /lib/capybara/mechanize.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara' 4 | 5 | module Capybara::Mechanize 6 | class << self 7 | # Host that should be considered local (includes default_host) 8 | def local_hosts 9 | @local_hosts ||= begin 10 | default_host = URI.parse(Capybara.default_host || '').host || Capybara.default_host 11 | [default_host].compact 12 | end 13 | end 14 | 15 | attr_writer :local_hosts 16 | end 17 | end 18 | 19 | require 'capybara/mechanize/driver' 20 | 21 | Capybara.register_driver :mechanize do |app| 22 | Capybara::Mechanize::Driver.new(app) 23 | end 24 | -------------------------------------------------------------------------------- /lib/capybara/mechanize/browser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ostruct' 4 | require 'capybara/rack_test/driver' 5 | require 'mechanize' 6 | require 'capybara/mechanize/node' 7 | require 'capybara/mechanize/form' 8 | 9 | class Capybara::Mechanize::Browser < Capybara::RackTest::Browser 10 | extend Forwardable 11 | 12 | def_delegator :agent, :scheme_handlers 13 | def_delegator :agent, :scheme_handlers= 14 | 15 | def initialize(driver) 16 | @agent = ::Mechanize.new 17 | @agent.redirect_ok = false 18 | @agent.user_agent = default_user_agent 19 | super 20 | end 21 | 22 | def reset_host! 23 | @last_remote_uri = nil 24 | @last_request_remote = nil 25 | @errored_remote_response = nil 26 | super 27 | end 28 | 29 | def current_url 30 | last_request_remote? ? remote_response.current_url : super 31 | end 32 | 33 | def last_response 34 | last_request_remote? ? remote_response : super 35 | end 36 | 37 | def last_request 38 | last_request_remote? ? OpenStruct.new(request_method: @last_method, params: @last_params, url: remote_response.current_url) : super 39 | end 40 | 41 | # For each of these http methods, we want to intercept the method call. 42 | # Then we determine if the call is remote or local. 43 | # Remote: Handle it with our process_remote_request method. 44 | # Local: Register the local request and call super to let RackTest get it. 45 | %i[get post put delete].each do |method| 46 | define_method(method) do |path, params = {}, env = {}, &block| 47 | path = @last_path if path.nil? || path.empty? 48 | 49 | if remote?(path) 50 | process_remote_request(method, path, params, env, &block) 51 | else 52 | register_local_request 53 | super(path, params, env, &block) 54 | end 55 | 56 | @last_path = path 57 | @last_method = method 58 | @last_params = params 59 | @last_env = env 60 | end 61 | end 62 | 63 | def refresh 64 | if last_request_remote? 65 | process_remote_request(@last_method, @last_remote_uri.to_s, @last_params, @last_env) 66 | else 67 | register_local_request 68 | super 69 | end 70 | end 71 | 72 | def remote?(url) 73 | if Capybara.app_host 74 | true 75 | else 76 | host = URI.parse(url).host 77 | 78 | if host.nil? 79 | last_request_remote? 80 | else 81 | !Capybara::Mechanize.local_hosts.include?(host) 82 | end 83 | end 84 | end 85 | 86 | def find(format, selector) 87 | if format == :css 88 | dom.css(selector, Capybara::RackTest::CSSHandlers.new) 89 | else 90 | dom.xpath(selector) 91 | end.map { |node| Capybara::Mechanize::Node.new(self, node) } 92 | end 93 | 94 | attr_reader :agent, :errored_remote_response 95 | 96 | private 97 | 98 | def last_request_remote? 99 | !!@last_request_remote 100 | end 101 | 102 | def register_local_request 103 | @last_remote_uri = nil 104 | @last_request_remote = false 105 | end 106 | 107 | def remote_request_path 108 | @last_remote_uri.nil? ? nil : @last_remote_uri.path 109 | end 110 | 111 | def request_path 112 | last_request_remote? ? remote_request_path : super 113 | end 114 | 115 | def process_remote_request(method, url, attributes, headers) 116 | return unless remote?(url) 117 | 118 | uri = URI.parse(url) 119 | @last_remote_uri = uri 120 | url = uri.to_s 121 | 122 | reset_cache! 123 | begin 124 | if method == :post 125 | if attributes.is_a? Mechanize::Form 126 | submit_mechanize_form(url, attributes, headers) 127 | else 128 | @agent.send(method, url, attributes, headers) 129 | end 130 | elsif method == :get 131 | if attributes.is_a? Mechanize::Form 132 | submit_mechanize_form(url, attributes, headers) 133 | else 134 | referer = headers['HTTP_REFERER'] 135 | @agent.send(method, url, attributes, referer, headers) 136 | end 137 | else 138 | @agent.send(method, url, attributes, headers) 139 | end 140 | @errored_remote_response = nil 141 | rescue Mechanize::ResponseCodeError => e 142 | @errored_remote_response = e.page 143 | 144 | raise "Received the following error for a #{method.to_s.upcase} request to #{url}: '#{e.message}'" if Capybara.raise_server_errors 145 | end 146 | @last_request_remote = true 147 | end 148 | 149 | def submit_mechanize_form(url, form, headers) 150 | form.action = url 151 | @agent.submit(form, nil, headers) 152 | end 153 | 154 | def remote_response 155 | if errored_remote_response 156 | ResponseProxy.new(errored_remote_response) 157 | elsif @agent.current_page 158 | ResponseProxy.new(@agent.current_page, current_fragment: @current_fragment) 159 | end 160 | end 161 | 162 | def default_user_agent 163 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.853.0 Safari/535.2' 164 | end 165 | 166 | class ResponseProxy 167 | extend Forwardable 168 | 169 | def_delegator :page, :body 170 | 171 | attr_reader :page 172 | 173 | def initialize(page, current_fragment: nil) 174 | @page = page 175 | @current_fragment = current_fragment 176 | end 177 | 178 | def current_url 179 | uri = page.uri.dup 180 | uri.fragment = @current_fragment if @current_fragment 181 | uri.to_s 182 | end 183 | 184 | def headers 185 | # Hax the content-type contains utf8, so Capybara specs are failing, need to ask mailinglist 186 | headers = page.response 187 | headers['content-type']&.gsub!(';charset=utf-8', '') 188 | headers 189 | end 190 | 191 | def [](key) 192 | headers[key] 193 | end 194 | 195 | def status 196 | page.code.to_i 197 | end 198 | 199 | def redirect? 200 | status >= 300 && status < 400 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /lib/capybara/mechanize/cucumber.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara/mechanize' 4 | 5 | Before('@mechanize') do 6 | Capybara.current_driver = :mechanize 7 | end 8 | -------------------------------------------------------------------------------- /lib/capybara/mechanize/driver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara/mechanize/browser' 4 | 5 | class Capybara::Mechanize::Driver < Capybara::RackTest::Driver 6 | def initialize(app, **options) 7 | raise ArgumentError, 'mechanize requires a rack application, but none was given' unless app 8 | 9 | super 10 | end 11 | 12 | def remote?(url) 13 | browser.remote?(url) 14 | end 15 | 16 | def configure 17 | yield(browser.agent) if block_given? 18 | end 19 | 20 | def browser 21 | @browser ||= Capybara::Mechanize::Browser.new(self) 22 | end 23 | 24 | def reset! 25 | @browser.agent.shutdown 26 | super 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/capybara/mechanize/form.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Capybara::Mechanize::Form < Capybara::RackTest::Form 4 | def params(button) 5 | return super unless use_mechanize? 6 | 7 | node = {} 8 | # Create a fake form 9 | class << node 10 | def search(*_args); []; end 11 | end 12 | 13 | node['method'] = button && button['formmethod'] 14 | 15 | node['method'] ||= (respond_to?(:request_method, true) ? request_method : method).to_s.upcase 16 | 17 | node['enctype'] = multipart? ? 'multipart/form-data' : 'application/x-www-form-urlencoded' 18 | 19 | @m_form = Mechanize::Form.new(node, nil, form_referer) 20 | 21 | super 22 | 23 | @m_form 24 | end 25 | 26 | private 27 | 28 | def merge_param!(params, key, value) 29 | return super unless use_mechanize? 30 | 31 | # Adding a nil value here will result in the form element existing with the empty string as its value. 32 | # Instead don't add the form element at all. 33 | return params if value.is_a? NilUploadedFile 34 | 35 | if value.is_a? Rack::Test::UploadedFile 36 | @m_form.enctype = 'multipart/form-data' 37 | 38 | ul = Mechanize::Form::FileUpload.new({ 'name' => key.to_s }, value.original_filename) 39 | ul.mime_type = value.content_type 40 | ul.file_data = (value.rewind; value.read) 41 | 42 | @m_form.file_uploads << ul 43 | 44 | return params 45 | end 46 | 47 | @m_form.fields << Mechanize::Form::Field.new({ 'name' => key.to_s }, value) 48 | 49 | params 50 | end 51 | 52 | def use_mechanize? 53 | driver.remote?(native['action'].to_s) 54 | end 55 | 56 | def form_referer 57 | Mechanize::Page.new URI(driver.current_url) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/capybara/mechanize/node.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Capybara::Mechanize::Node < Capybara::RackTest::Node 4 | def click(keys = [], **options) 5 | options.delete(:offset) 6 | raise ArgumentError, 'The mechanize driver does not support click options' unless keys.empty? && options.empty? 7 | 8 | submits = respond_to?(:submits?) ? submits? : 9 | ((tag_name == 'input' and %w[submit image].include?(type)) or 10 | ((tag_name == 'button') and type.nil? or type == 'submit')) 11 | 12 | if tag_name == 'a' or tag_name == 'label' or 13 | (tag_name == 'input' and %w[checkbox radio].include?(type)) 14 | Capybara::VERSION > '3.0.0' ? super : super() 15 | elsif submits 16 | associated_form = form 17 | Capybara::Mechanize::Form.new(driver, form).submit(self) if associated_form 18 | else 19 | super 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/capybara/mechanize/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Capybara 4 | module Mechanize 5 | VERSION = '1.13.0' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/capybara/spec/extended_test_app.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara/spec/test_app' 4 | 5 | class ExtendedTestApp < TestApp 6 | set :environment, :production # so we don't get debug info that makes our test pass! 7 | disable :protection 8 | 9 | get %r{/redirect_to/(.*)} do 10 | redirect params[:captures].first 11 | end 12 | 13 | get '/form_with_relative_action_to_host' do 14 | %(
15 | 16 |
) 17 | end 18 | 19 | get '/request_info/form_with_no_action' do 20 | %(
21 | 22 |
) 23 | end 24 | 25 | get '/relative_link_to_host' do 26 | %(host) 27 | end 28 | 29 | get '/request_info/user_agent' do 30 | request.user_agent 31 | end 32 | 33 | get '/request_info/*' do 34 | current_request_info 35 | end 36 | 37 | post '/request_info/*' do 38 | current_request_info 39 | end 40 | 41 | get '/subsite/relative_link_to_host' do 42 | %(host) 43 | end 44 | 45 | get '/subsite/local_link_to_host' do 46 | %(host) 47 | end 48 | 49 | get '/subsite/request_info2/*' do 50 | 'subsite: ' + current_request_info 51 | end 52 | 53 | get '/redirect_with_http_param' do 54 | redirect '/redirect_target?foo=http' 55 | end 56 | 57 | get '/redirect_target' do 58 | %(correct redirect) 59 | end 60 | 61 | get %r{/form_posts_to/(.*)} do 62 | %( 63 |
64 | 65 |
66 | ) 67 | end 68 | 69 | post '/get_referer' do 70 | request.referer.nil? ? 'No referer' : "Got referer: #{request.referer}" 71 | end 72 | 73 | @form_post_count = 0 74 | 75 | private 76 | 77 | def current_request_info 78 | "Current host is #{request.url}, method #{request.request_method.downcase}" 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/driver/mechanize_driver_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Capybara::Mechanize::Driver, 'local' do 6 | let(:driver) { Capybara::Mechanize::Driver.new(ExtendedTestApp) } 7 | 8 | describe '#configure' do 9 | it 'allows extended configuration of the agent' do 10 | expect_any_instance_of(::Mechanize).to receive(:foo=).with('test') 11 | driver.configure do |agent| 12 | agent.foo = 'test' 13 | end 14 | end 15 | end 16 | 17 | describe ':headers option' do 18 | it 'should always set headers' do 19 | driver = Capybara::RackTest::Driver.new(TestApp, headers: { 'HTTP_FOO' => 'foobar' }) 20 | driver.visit('/get_header') 21 | expect(driver.html).to include('foobar') 22 | end 23 | 24 | it 'should keep headers on link clicks' do 25 | driver = Capybara::RackTest::Driver.new(TestApp, headers: { 'HTTP_FOO' => 'foobar' }) 26 | driver.visit('/header_links') 27 | driver.find_xpath('.//a').first.click 28 | expect(driver.html).to include('foobar') 29 | end 30 | 31 | it 'should keep headers on form submit' do 32 | driver = Capybara::RackTest::Driver.new(TestApp, headers: { 'HTTP_FOO' => 'foobar' }) 33 | driver.visit('/header_links') 34 | driver.find_xpath('.//input').first.click 35 | expect(driver.html).to include('foobar') 36 | end 37 | 38 | it 'should keep headers on redirects' do 39 | driver = Capybara::RackTest::Driver.new(TestApp, headers: { 'HTTP_FOO' => 'foobar' }) 40 | driver.visit('/get_header_via_redirect') 41 | expect(driver.html).to include('foobar') 42 | end 43 | end 44 | 45 | describe ':follow_redirects option' do 46 | it 'defaults to following redirects' do 47 | driver = Capybara::RackTest::Driver.new(TestApp) 48 | 49 | driver.visit('/redirect') 50 | expect(driver.response.header['Location']).to be_nil 51 | expect(driver.browser.current_url).to match %r{/landed$} 52 | end 53 | 54 | it 'is possible to not follow redirects' do 55 | driver = Capybara::RackTest::Driver.new(TestApp, follow_redirects: false) 56 | 57 | driver.visit('/redirect') 58 | expect(driver.response.header['Location']).to match %r{/redirect_again$} 59 | expect(driver.browser.current_url).to match %r{/redirect$} 60 | end 61 | end 62 | 63 | describe ':redirect_limit option' do 64 | context 'with default redirect limit' do 65 | let(:driver) { Capybara::RackTest::Driver.new(TestApp) } 66 | 67 | it 'should follow 5 redirects' do 68 | driver.visit('/redirect/5/times') 69 | expect(driver.html).to include('redirection complete') 70 | end 71 | 72 | it 'should not follow more than 6 redirects' do 73 | expect do 74 | driver.visit('/redirect/6/times') 75 | end.to raise_error(Capybara::InfiniteRedirectError) 76 | end 77 | end 78 | 79 | context 'with 21 redirect limit' do 80 | let(:driver) { Capybara::RackTest::Driver.new(TestApp, redirect_limit: 21) } 81 | 82 | it 'should follow 21 redirects' do 83 | driver.visit('/redirect/21/times') 84 | expect(driver.html).to include('redirection complete') 85 | end 86 | 87 | it 'should not follow more than 21 redirects' do 88 | expect do 89 | driver.visit('/redirect/22/times') 90 | end.to raise_error(Capybara::InfiniteRedirectError) 91 | end 92 | end 93 | end 94 | 95 | it 'should default to local mode for relative paths' do 96 | expect(driver).not_to be_remote('/') 97 | end 98 | 99 | it 'should default to local mode for the default host' do 100 | expect(driver).not_to be_remote('http://www.example.com') 101 | end 102 | 103 | context 'with an app_host' do 104 | before do 105 | Capybara.app_host = 'http://www.remote.com' 106 | end 107 | 108 | after do 109 | Capybara.app_host = nil 110 | end 111 | 112 | it 'should treat urls as remote' do 113 | expect(driver).to be_remote('http://www.remote.com') 114 | end 115 | end 116 | 117 | context 'with a default url, no app host' do 118 | before do 119 | Capybara.default_host = 'http://www.local.com' 120 | end 121 | 122 | after do 123 | Capybara.default_host = CAPYBARA_DEFAULT_HOST 124 | end 125 | 126 | context 'local hosts' do 127 | before do 128 | Capybara::Mechanize.local_hosts = ['subdomain.local.com'] 129 | end 130 | 131 | after do 132 | Capybara::Mechanize.local_hosts = nil 133 | end 134 | 135 | it 'should allow local hosts to be set' do 136 | expect(driver).not_to be_remote('http://subdomain.local.com') 137 | end 138 | end 139 | 140 | it 'should treat urls with the same host names as local' do 141 | expect(driver).not_to be_remote('http://www.local.com') 142 | end 143 | 144 | it 'should treat other urls as remote' do 145 | expect(driver).to be_remote('http://www.remote.com') 146 | end 147 | 148 | it 'should treat relative paths as remote if the previous request was remote' do 149 | driver.visit(remote_test_url) 150 | expect(driver).to be_remote('/some_relative_link') 151 | end 152 | 153 | it 'should treat relative paths as local if the previous request was local' do 154 | driver.visit('http://www.local.com') 155 | expect(driver).not_to be_remote('/some_relative_link') 156 | end 157 | 158 | it 'should receive the right host' do 159 | driver.visit('http://www.local.com/host') 160 | should_be_a_local_get 161 | end 162 | 163 | it 'should consider relative paths to be local when the previous request was local' do 164 | driver.visit('http://www.local.com/host') 165 | driver.visit('/host') 166 | 167 | should_be_a_local_get 168 | expect(driver).not_to be_remote('/first_local') 169 | end 170 | 171 | it 'should consider relative paths to be remote when the previous request was remote' do 172 | driver.visit("#{remote_test_url}/host") 173 | driver.get('/host') 174 | 175 | should_be_a_remote_get 176 | expect(driver).to be_remote('/second_remote') 177 | end 178 | 179 | it 'should always switch to the right context' do 180 | driver.visit('http://www.local.com/host') 181 | driver.get('/host') 182 | driver.get("#{remote_test_url}/host") 183 | driver.get('/host') 184 | driver.get('http://www.local.com/host') 185 | 186 | should_be_a_local_get 187 | expect(driver).not_to be_remote('/second_local') 188 | end 189 | 190 | it 'should follow redirects from local to remote' do 191 | driver.visit("http://www.local.com/redirect_to/#{remote_test_url}/host") 192 | should_be_a_remote_get 193 | end 194 | 195 | it 'should follow redirects from remote to local' do 196 | driver.visit("#{remote_test_url}/redirect_to/http://www.local.com/host") 197 | should_be_a_local_get 198 | end 199 | 200 | it 'passes the status code of remote calls back to be validated' do 201 | quietly do 202 | driver.visit(remote_test_url) 203 | driver.get('/asdfafadfsdfs') 204 | expect(driver.response.status).to be >= 400 205 | end 206 | end 207 | 208 | context 'when errors are set to true' do 209 | it 'raises an useful error because it is probably a misconfiguration' do 210 | quietly do 211 | original = Capybara.raise_server_errors 212 | 213 | expect do 214 | driver.visit(remote_test_url) 215 | Capybara.raise_server_errors = true 216 | driver.get('/asdfafadfsdfs') 217 | end.to raise_error(%r{Received the following error for a GET request to /asdfafadfsdfs:}) 218 | Capybara.raise_server_errors = original 219 | end 220 | end 221 | end 222 | end 223 | 224 | it 'should include the right host when remote' do 225 | driver.visit("#{remote_test_url}/host") 226 | should_be_a_remote_get 227 | end 228 | 229 | describe '#reset!' do 230 | before do 231 | Capybara.default_host = 'http://www.local.com' 232 | end 233 | 234 | after do 235 | Capybara.default_host = CAPYBARA_DEFAULT_HOST 236 | end 237 | 238 | it 'should reset remote host' do 239 | driver.visit("#{remote_test_url}/host") 240 | should_be_a_remote_get 241 | driver.reset! 242 | driver.visit('/host') 243 | should_be_a_local_get 244 | end 245 | end 246 | 247 | def should_be_a_remote_get 248 | expect(driver.current_url).to include(remote_test_url) 249 | end 250 | 251 | def should_be_a_local_get 252 | expect(driver.current_url).to include('www.local.com') 253 | end 254 | end 255 | -------------------------------------------------------------------------------- /spec/driver/remote_mechanize_driver_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Capybara::Mechanize::Driver, 'remote' do 6 | before do 7 | Capybara.app_host = remote_test_url 8 | end 9 | 10 | after do 11 | Capybara.app_host = nil 12 | end 13 | 14 | let(:driver) { Capybara::Mechanize::Driver.new(ExtendedTestApp) } 15 | 16 | context 'in remote mode' do 17 | it 'should pass arguments through to a get request' do 18 | driver.visit("#{remote_test_url}/form/get", form: { value: 'success' }) 19 | expect(driver.html).to include('success') 20 | end 21 | 22 | it 'should pass arguments through to a post request' do 23 | driver.post("#{remote_test_url}/form", form: { value: 'success' }) 24 | expect(driver.html).to include('success') 25 | end 26 | 27 | describe 'redirect' do 28 | it 'should handle redirects with http-params' do 29 | driver.visit "#{remote_test_url}/redirect_with_http_param" 30 | expect(driver.html).to include('correct redirect') 31 | end 32 | end 33 | 34 | context 'for a post request' do 35 | it 'transforms nested map in post data' do 36 | driver.post("#{remote_test_url}/form", form: { key: 'value' }) 37 | expect(driver.html).to match(/:key=>"value"|key: value/) 38 | end 39 | end 40 | 41 | context 'process remote request' do 42 | it 'transforms nested map in post data' do 43 | driver.submit(:post, "#{remote_test_url}/form", form: { key: 'value' }) 44 | expect(driver.html).to match(/:key=>"value"|key: value/) 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/session/mechanize_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | module TestSessions 6 | Mechanize = Capybara::Session.new(:mechanize, TestApp) 7 | end 8 | 9 | skipped_tests = %i[ 10 | about_scheme 11 | active_element 12 | css 13 | download 14 | frames 15 | hover 16 | html_validation 17 | js 18 | modals 19 | screenshot 20 | scroll 21 | send_keys 22 | server 23 | shadow_dom 24 | spatial 25 | windows 26 | ] 27 | 28 | Capybara::SpecHelper.run_specs(TestSessions::Mechanize, 'Mechanize', capybara_skip: skipped_tests) do |example| 29 | case example.metadata[:full_description] 30 | when /has_css\? should support case insensitive :class and :id options/ 31 | skip "Nokogiri doesn't support case insensitive CSS attribute matchers" 32 | end 33 | end 34 | 35 | describe Capybara::Session do 36 | context 'with mechanize driver' do 37 | let(:session) { Capybara::Session.new(:mechanize, ExtendedTestApp) } 38 | 39 | before do 40 | Capybara.default_host = 'http://www.local.com' 41 | end 42 | 43 | after do 44 | Capybara.default_host = CAPYBARA_DEFAULT_HOST 45 | end 46 | 47 | describe '#driver' do 48 | it 'should be a mechanize driver' do 49 | expect(session.driver).to be_an_instance_of(Capybara::Mechanize::Driver) 50 | end 51 | end 52 | 53 | describe '#mode' do 54 | it 'should remember the mode' do 55 | expect(session.mode).to eq(:mechanize) 56 | end 57 | end 58 | 59 | describe '#click_link' do 60 | it 'should use data-method if option is true' do 61 | session.driver.options[:respect_data_method] = true 62 | session.visit '/with_html' 63 | session.click_link 'A link with data-method' 64 | expect(session.html).to include('The requested object was deleted') 65 | end 66 | 67 | it 'should not use data-method if option is false' do 68 | session.driver.options[:respect_data_method] = false 69 | session.visit '/with_html' 70 | session.click_link 'A link with data-method' 71 | expect(session.html).to include('Not deleted') 72 | end 73 | 74 | it "should use data-method if available even if it's capitalized" do 75 | session.driver.options[:respect_data_method] = true 76 | session.visit '/with_html' 77 | session.click_link 'A link with capitalized data-method' 78 | expect(session.html).to include('The requested object was deleted') 79 | end 80 | 81 | after do 82 | session.driver.options[:respect_data_method] = false 83 | end 84 | end 85 | 86 | describe '#attach_file' do 87 | context 'with multipart form' do 88 | it 'should submit an empty form-data section if no file is submitted' do 89 | session.visit('/form') 90 | session.click_button('Upload Empty') 91 | expect(session.html).to include('Successfully ignored empty file field.') 92 | end 93 | end 94 | end 95 | 96 | it 'should use the last remote url when following relative links' do 97 | session.visit("#{remote_test_url}/relative_link_to_host") 98 | session.click_link 'host' 99 | expect(session.body).to include("Current host is #{remote_test_url}/request_info/host, method get") 100 | end 101 | 102 | it 'should use the last remote url when submitting a form with a relative action' do 103 | session.visit("#{remote_test_url}/form_with_relative_action_to_host") 104 | session.click_button 'submit' 105 | expect(session.body).to include("Current host is #{remote_test_url}/request_info/host, method post") 106 | end 107 | 108 | it 'should use the last url when submitting a form with no action' do 109 | session.visit("#{remote_test_url}/request_info/form_with_no_action") 110 | session.click_button 'submit' 111 | expect(session.body).to include("Current host is #{remote_test_url}/request_info/form_with_no_action, method post") 112 | end 113 | 114 | it 'should send correct user agent' do 115 | session.visit("#{remote_test_url}/request_info/user_agent") 116 | expect(session.body).to include('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.853.0 Safari/535.2') 117 | end 118 | 119 | context 'form referer when switching from local to remote' do 120 | it 'sends the referer' do 121 | session.visit "/form_posts_to/#{remote_test_url}/get_referer" 122 | session.click_button 'submit' 123 | expect(session.body).to include 'Got referer' 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /spec/session/remote_mechanize_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | module TestSessions 6 | RemoteMechanize = Capybara::Session.new(:mechanize, TestApp) 7 | end 8 | 9 | shared_context 'remote tests' do 10 | before do 11 | Capybara.app_host = remote_test_url 12 | end 13 | 14 | after do 15 | Capybara.app_host = nil 16 | end 17 | end 18 | 19 | skipped_tests = %i[ 20 | about_scheme 21 | active_element 22 | css 23 | download 24 | frames 25 | hover 26 | html_validation 27 | js 28 | modals 29 | screenshot 30 | scroll 31 | send_keys 32 | server 33 | shadow_dom 34 | spatial 35 | windows 36 | ] 37 | 38 | Capybara::SpecHelper.run_specs(TestSessions::RemoteMechanize, 'RemoteMechanize', capybara_skip: skipped_tests) do |example| 39 | case example.metadata[:full_description] 40 | when /has_css\? should support case insensitive :class and :id options/ 41 | skip "Nokogiri doesn't support case insensitive CSS attribute matchers" 42 | end 43 | end 44 | 45 | describe Capybara::Session do 46 | context 'with remote mechanize driver' do 47 | include_context 'remote tests' 48 | 49 | let(:session) { Capybara::Session.new(:mechanize, ExtendedTestApp) } 50 | 51 | describe '#driver' do 52 | it 'should be a mechanize driver' do 53 | expect(session.driver).to be_an_instance_of(Capybara::Mechanize::Driver) 54 | end 55 | end 56 | 57 | describe '#mode' do 58 | it 'should remember the mode' do 59 | expect(session.mode).to eq(:mechanize) 60 | end 61 | end 62 | 63 | describe '#click_link' do 64 | it 'should use data-method if option is true' do 65 | session.driver.options[:respect_data_method] = true 66 | session.visit '/with_html' 67 | session.click_link 'A link with data-method' 68 | expect(session.html).to include('The requested object was deleted') 69 | end 70 | 71 | it 'should not use data-method if option is false' do 72 | session.driver.options[:respect_data_method] = false 73 | session.visit '/with_html' 74 | session.click_link 'A link with data-method' 75 | expect(session.html).to include('Not deleted') 76 | end 77 | 78 | it "should use data-method if available even if it's capitalized" do 79 | session.driver.options[:respect_data_method] = true 80 | session.visit '/with_html' 81 | session.click_link 'A link with capitalized data-method' 82 | expect(session.html).to include('The requested object was deleted') 83 | end 84 | 85 | after do 86 | session.driver.options[:respect_data_method] = false 87 | end 88 | end 89 | 90 | describe '#attach_file' do 91 | context 'with multipart form' do 92 | it 'should submit an empty form-data section if no file is submitted' do 93 | session.visit('/form') 94 | session.click_button('Upload Empty') 95 | expect(session.html).to include('Successfully ignored empty file field.') 96 | end 97 | end 98 | end 99 | 100 | context 'remote app in a sub-path' do 101 | it 'follows relative link correctly' do 102 | session.visit '/subsite/relative_link_to_host' 103 | session.click_link 'host' 104 | expect(session.body).to include('request_info2/host') 105 | end 106 | 107 | it 'follows local link correctly' do 108 | session.visit '/subsite/local_link_to_host' 109 | session.click_link 'host' 110 | expect(session.body).to include('request_info2/host') 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'capybara/spec/spec_helper' 4 | require 'capybara/mechanize' 5 | require 'capybara/spec/extended_test_app' 6 | 7 | PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..')).freeze 8 | 9 | $LOAD_PATH << File.join(PROJECT_ROOT, 'lib') 10 | 11 | Dir[File.join(PROJECT_ROOT, 'spec', 'support', '**', '*.rb')].each { |file| require(file) } 12 | 13 | RSpec.configure do |config| 14 | config.filter_run focus: true unless ENV['CI'] 15 | config.run_all_when_everything_filtered = true 16 | 17 | # Used with DisableExternalTests 18 | config.filter_run_excluding :external_test_disabled 19 | 20 | config.include RemoteTestUrl 21 | config.extend RemoteTestUrl 22 | config.include Capybara::SpecHelper 23 | 24 | config.after do 25 | Capybara::Mechanize.local_hosts = nil 26 | end 27 | 28 | Capybara::SpecHelper.configure(config) 29 | 30 | config.order = 'random' 31 | 32 | CAPYBARA_DEFAULT_HOST = Capybara.default_host 33 | end 34 | -------------------------------------------------------------------------------- /spec/support/disable_external_tests.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DisableExternalTests 4 | attr_accessor :tests_to_disable 5 | 6 | def disable(top_level_example_group) 7 | tests_to_disable.each do |to_disable| 8 | example_group = top_level_example_group 9 | 10 | example_description = to_disable.pop 11 | 12 | to_disable.each do |description| 13 | example_group = example_group.children.find { |g| g.description == description } 14 | end 15 | 16 | example = example_group.examples.find { |e| e.description == example_description } 17 | 18 | example.metadata[:external_test_disabled] = true unless example.nil? 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/support/extended_test_app_setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This class works around some weirdness with Capybara's test suite and sinatra's behavior. 4 | # We need to make sure that sinatra uses TestApp for at least one request before the Capybara session 5 | # specs run. Without this we get errors from sinatra trying to handle requests with TestApp.clone 6 | class ExtendedTestAppSetup 7 | include Capybara::DSL 8 | 9 | attr_reader :remote_test_url 10 | 11 | def boot 12 | boot_test_app 13 | boot_remote_app 14 | Capybara.raise_server_errors = false 15 | 16 | self 17 | end 18 | 19 | def boot_test_app 20 | Capybara.app = TestApp 21 | dummy_server = Capybara::Server.new(TestApp) 22 | dummy_server.boot 23 | 24 | # Boot TestApp's Sinatra 25 | visit '/' 26 | end 27 | 28 | def boot_remote_app 29 | remote_server = Capybara::Server.new(ExtendedTestApp) 30 | remote_server.boot 31 | @remote_test_url = "http://localhost:#{remote_server.port}" 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/support/remote_test_url.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RemoteTestUrl 4 | def remote_test_url 5 | ExtendedTestAppSetup.new.boot.remote_test_url 6 | end 7 | end 8 | --------------------------------------------------------------------------------