From 12c10059709346bfeadb2cea18f510174d842303 Mon Sep 17 00:00:00 2001 From: Thomas Walpole Date: Wed, 3 May 2017 11:43:42 -0700 Subject: [PATCH] Workaround chromedriver lack of support for system modals when running in headless mode --- lib/capybara/selenium/driver.rb | 117 ++++++++++++++++-- .../spec/session/dismiss_confirm_spec.rb | 6 +- .../spec/session/dismiss_prompt_spec.rb | 2 +- spec/shared_selenium_session.rb | 1 + 4 files changed, 110 insertions(+), 16 deletions(-) diff --git a/lib/capybara/selenium/driver.rb b/lib/capybara/selenium/driver.rb index bc729f52..3090d979 100644 --- a/lib/capybara/selenium/driver.rb +++ b/lib/capybara/selenium/driver.rb @@ -14,7 +14,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base def browser unless @browser - if options[:browser].to_s == "firefox" + if firefox? options[:desired_capabilities] ||= Selenium::WebDriver::Remote::Capabilities.firefox options[:desired_capabilities].merge!({ unexpectedAlertBehaviour: "ignore" }) end @@ -232,20 +232,32 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base end def accept_modal(_type, options={}) - yield if block_given? - modal = find_modal(options) - modal.send_keys options[:with] if options[:with] - message = modal.text - modal.accept - message + if headless_chrome? + insert_modal_handlers(true, options[:with], options[:text]) + yield if block_given? + find_headless_modal(options) + else + yield if block_given? + modal = find_modal(options) + modal.send_keys options[:with] if options[:with] + message = modal.text + modal.accept + message + end end def dismiss_modal(_type, options={}) - yield if block_given? - modal = find_modal(options) - message = modal.text - modal.dismiss - message + if headless_chrome? + insert_modal_handlers(false, options[:with], options[:text]) + yield if block_given? + find_headless_modal(options) + else + yield if block_given? + modal = find_modal(options) + message = modal.text + modal.dismiss + message + end end def quit @@ -304,6 +316,49 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base private + def insert_modal_handlers(accept, response_text, expected_text=nil) + script = <<-JS + if (typeof window.capybara === 'undefined') { + window.capybara = { + modal_handlers: [], + current_modal_status: function() { + return [this.modal_handlers[0].called, this.modal_handlers[0].modal_text]; + }, + add_handler: function(handler) { + this.modal_handlers.unshift(handler); + }, + remove_handler: function(handler) { + window.alert = handler.alert; + window.confirm = handler.confirm; + window.prompt = handler.prompt; + }, + handler_called: function(handler, str) { + handler.called = true; + handler.modal_text = str; + this.remove_handler(handler); + } + }; + }; + + var modal_handler = { + prompt: window.prompt, + confirm: window.confirm, + alert: window.alert, + } + window.capybara.add_handler(modal_handler); + + window.alert = window.confirm = function(str) { + window.capybara.handler_called(modal_handler, str); + return #{accept ? 'true' : 'false'}; + }; + window.prompt = function(str) { + window.capybara.handler_called(modal_handler, str); + return #{accept ? "'#{response_text}'" : 'null'}; + } + JS + execute_script script + end + def within_given_window(handle) original_handle = self.current_window_handle if handle == original_handle @@ -333,6 +388,32 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base end end + def find_headless_modal(options={}) + # Selenium has its own built in wait (2 seconds)for a modal to show up, so this wait is really the minimum time + # Actual wait time may be longer than specified + wait = Selenium::WebDriver::Wait.new( + timeout: options.fetch(:wait, session_options.default_max_wait_time) || 0 , + ignore: Selenium::WebDriver::Error::NoAlertPresentError) + begin + wait.until do + called, alert_text = evaluate_script('window.capybara.current_modal_status()') + if called + execute_script('window.capybara.modal_handlers.shift()') + regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text].to_s) + if alert_text.match(regexp) + alert_text + else + raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}") + end + else + nil + end + end + rescue Selenium::WebDriver::Error::TimeOutError + raise Capybara::ModalNotFound.new("Unable to find modal dialog#{" with #{options[:text]}" if options[:text]}") + end + end + def silenced_unknown_error_message?(msg) silenced_unknown_error_messages.any? { |r| msg =~ r } end @@ -353,4 +434,16 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base arg end end + + def firefox? + options[:browser].to_s == "firefox" + end + + def chrome? + options[:browser].to_s == "chrome" + end + + def headless_chrome? + chrome? && (options[:args] || []).include?("headless") + end end diff --git a/lib/capybara/spec/session/dismiss_confirm_spec.rb b/lib/capybara/spec/session/dismiss_confirm_spec.rb index 7068951b..064ddc24 100644 --- a/lib/capybara/spec/session/dismiss_confirm_spec.rb +++ b/lib/capybara/spec/session/dismiss_confirm_spec.rb @@ -10,14 +10,14 @@ Capybara::SpecHelper.spec '#dismiss_confirm', requires: [:modals] do end expect(@session).to have_xpath("//a[@id='open-confirm' and @confirmed='false']") end - + it "should dismiss the confirm if the message matches" do @session.dismiss_confirm 'Confirm opened' do @session.click_link('Open confirm') end expect(@session).to have_xpath("//a[@id='open-confirm' and @confirmed='false']") end - + it "should not dismiss the confirm if the message doesn't match" do expect do @session.dismiss_confirm 'Incorrect Text' do @@ -25,7 +25,7 @@ Capybara::SpecHelper.spec '#dismiss_confirm', requires: [:modals] do end end.to raise_error(Capybara::ModalNotFound) end - + it "should return the message presented" do message = @session.dismiss_confirm do diff --git a/lib/capybara/spec/session/dismiss_prompt_spec.rb b/lib/capybara/spec/session/dismiss_prompt_spec.rb index 0758ad26..b600d532 100644 --- a/lib/capybara/spec/session/dismiss_prompt_spec.rb +++ b/lib/capybara/spec/session/dismiss_prompt_spec.rb @@ -10,7 +10,7 @@ Capybara::SpecHelper.spec '#dismiss_prompt', requires: [:modals] do end expect(@session).to have_xpath("//a[@id='open-prompt' and @response='dismissed']") end - + it "should return the message presented" do message = @session.dismiss_prompt do @session.click_link('Open prompt') diff --git a/spec/shared_selenium_session.rb b/spec/shared_selenium_session.rb index d28976b9..7bcc48b1 100644 --- a/spec/shared_selenium_session.rb +++ b/spec/shared_selenium_session.rb @@ -55,6 +55,7 @@ RSpec.shared_examples "Capybara::Session" do |session, mode| describe "#accept_alert" do it "supports a blockless mode" do + skip "Headless Chrome doesn't support blockless modal methods" if @session.driver.send(:headless_chrome?) @session.visit('/with_js') @session.click_link('Open alert') @session.accept_alert