From 40e3abdd50aaf58c45e765c2249c45cc37bdc1e7 Mon Sep 17 00:00:00 2001 From: Thomas Walpole Date: Sat, 1 Jun 2019 18:17:59 -0700 Subject: [PATCH] Update edge testing for Chrome based Edge on MacOS --- gemfiles/Gemfile.chrome_edge | 13 ++ gemfiles/Gemfile.edge-firefox | 4 +- lib/capybara/selenium/driver.rb | 1 + .../driver_specializations/edge_driver.rb | 119 ++++++++++++++++++ lib/capybara/selenium/nodes/edge_node.rb | 92 ++++++++++++++ .../spec/session/window/window_spec.rb | 5 +- spec/selenium_spec_edge.rb | 15 ++- spec/shared_selenium_session.rb | 3 + spec/spec_helper.rb | 17 ++- 9 files changed, 261 insertions(+), 8 deletions(-) create mode 100644 gemfiles/Gemfile.chrome_edge create mode 100644 lib/capybara/selenium/driver_specializations/edge_driver.rb create mode 100644 lib/capybara/selenium/nodes/edge_node.rb diff --git a/gemfiles/Gemfile.chrome_edge b/gemfiles/Gemfile.chrome_edge new file mode 100644 index 0000000000..8be8d41c67 --- /dev/null +++ b/gemfiles/Gemfile.chrome_edge @@ -0,0 +1,13 @@ +source "https://rubygems.org" + +gem 'bundler', '< 3.0' +gemspec path: '..' + +gem 'xpath', github: 'teamcapybara/xpath' + +gem 'selenium-webdriver', github: 'seleniumhq/selenium', glob: 'rb/*.gemspec' +gem 'webdrivers', github: 'twalpole/webdrivers', branch: 'selenium_4' +gem 'rack', github: 'rack/rack' +gem 'sinatra', github: 'sinatra/sinatra', branch: 'master' + +gem 'puma', github: 'puma/puma' diff --git a/gemfiles/Gemfile.edge-firefox b/gemfiles/Gemfile.edge-firefox index 267f560262..a081fa31da 100644 --- a/gemfiles/Gemfile.edge-firefox +++ b/gemfiles/Gemfile.edge-firefox @@ -6,8 +6,8 @@ gemspec path: '..' gem 'xpath', github: 'teamcapybara/xpath' gem 'selenium-webdriver', :path => '../../selenium/build/rb' -gem 'webdrivers', github: 'twalpole/webdrivers', branch: 'selenium_4' if ENV['CI'] +gem 'webdrivers', 'twalpole/webdrivers', branch: 'selenium_4' if ENV['CI'] gem 'rack', github: 'rack/rack' gem 'sinatra', github: 'sinatra/sinatra', branch: 'master' -gem 'puma', github: 'puma/puma' \ No newline at end of file +gem 'puma', github: 'puma/puma' diff --git a/lib/capybara/selenium/driver.rb b/lib/capybara/selenium/driver.rb index 0a0934ea69..02e229842e 100644 --- a/lib/capybara/selenium/driver.rb +++ b/lib/capybara/selenium/driver.rb @@ -475,3 +475,4 @@ def accept_unhandled_reset_alert require 'capybara/selenium/driver_specializations/firefox_driver' require 'capybara/selenium/driver_specializations/internet_explorer_driver' require 'capybara/selenium/driver_specializations/safari_driver' +require 'capybara/selenium/driver_specializations/edge_driver' diff --git a/lib/capybara/selenium/driver_specializations/edge_driver.rb b/lib/capybara/selenium/driver_specializations/edge_driver.rb new file mode 100644 index 0000000000..ab90fe9a11 --- /dev/null +++ b/lib/capybara/selenium/driver_specializations/edge_driver.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'capybara/selenium/nodes/edge_node' + +module Capybara::Selenium::Driver::EdgeDriver + def fullscreen_window(handle) + return super if edgedriver_version < 75 + + within_given_window(handle) do + begin + super + rescue NoMethodError => e + raise unless e.message.match?(/full_screen_window/) + + result = bridge.http.call(:post, "session/#{bridge.session_id}/window/fullscreen", {}) + result['value'] + end + end + end + + def resize_window_to(handle, width, height) + super + rescue Selenium::WebDriver::Error::UnknownError => e + raise unless e.message.match?(/failed to change window state/) + + # Chromedriver doesn't wait long enough for state to change when coming out of fullscreen + # and raises unnecessary error. Wait a bit and try again. + sleep 0.25 + super + end + + def reset! + return super if edgedriver_version < 75 + # Use instance variable directly so we avoid starting the browser just to reset the session + return unless @browser + + switch_to_window(window_handles.first) + window_handles.slice(1..-1).each { |win| close_window(win) } + + timer = Capybara::Helpers.timer(expire_in: 10) + begin + @browser.navigate.to('about:blank') + clear_storage unless uniform_storage_clear? + wait_for_empty_page(timer) + rescue *unhandled_alert_errors + accept_unhandled_reset_alert + retry + end + + execute_cdp('Storage.clearDataForOrigin', origin: '*', storageTypes: storage_types_to_clear) + end + + def download_path=(path) + if @browser.respond_to?(:download_path=) + @browser.download_path = path + else + # Not yet implemented in seleniun-webdriver for edge so do it ourselves + execute_cdp('Page.setDownloadBehavior', behavior: 'allow', downloadPath: path) + end + end + +private + + def storage_types_to_clear + types = ['cookies'] + types << 'local_storage' if clear_all_storage? + types.join(',') + end + + def clear_all_storage? + options.values_at(:clear_session_storage, :clear_local_storage).none? { |s| s == false } + end + + def uniform_storage_clear? + clear = options.values_at(:clear_session_storage, :clear_local_storage) + clear.all? { |s| s == false } || clear.none? { |s| s == false } + end + + def clear_storage + # Chrome errors if attempt to clear storage on about:blank + # In W3C mode it crashes chromedriver + url = current_url + super unless url.nil? || url.start_with?('about:') + end + + def delete_all_cookies + execute_cdp('Network.clearBrowserCookies') + rescue *cdp_unsupported_errors + # If the CDP clear isn't supported do original limited clear + super + end + + def cdp_unsupported_errors + @cdp_unsupported_errors ||= [Selenium::WebDriver::Error::WebDriverError] + end + + def execute_cdp(cmd, params = {}) + args = { cmd: cmd, params: params } + result = bridge.http.call(:post, "session/#{bridge.session_id}/goog/cdp/execute", args) + result['value'] + end + + def build_node(native_node, initial_cache = {}) + ::Capybara::Selenium::EdgeNode.new(self, native_node, initial_cache) + end + + def bridge + browser.send(:bridge) + end + + def edgedriver_version + @edgedriver_version ||= begin + caps = browser.capabilities + caps['chrome']&.fetch('chromedriverVersion', nil).to_f + end + end +end + +Capybara::Selenium::Driver.register_specialization :edge, Capybara::Selenium::Driver::EdgeDriver diff --git a/lib/capybara/selenium/nodes/edge_node.rb b/lib/capybara/selenium/nodes/edge_node.rb new file mode 100644 index 0000000000..33bd287d08 --- /dev/null +++ b/lib/capybara/selenium/nodes/edge_node.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'capybara/selenium/extensions/html5_drag' + +class Capybara::Selenium::EdgeNode < Capybara::Selenium::Node + include Html5Drag + + def set_text(value, clear: nil, **_unused) + return super unless chrome_edge? + + super.tap do + # React doesn't see the chromedriver element clear + send_keys(:space, :backspace) if value.to_s.empty? && clear.nil? + end + end + + def set_file(value) # rubocop:disable Naming/AccessorMethodName + # In Chrome 75+ files are appended (due to WebDriver spec - why?) so we have to clear here if its multiple and already set + if chrome_edge? + driver.execute_script(<<~JS, self) + if (arguments[0].multiple && (arguments[0].files.length > 0)){ + arguments[0].value = null; + } + JS + end + super + rescue *file_errors => e + raise ArgumentError, "Selenium < 3.14 with remote Chrome doesn't support multiple file upload" if e.message.match?(/File not found : .+\n.+/m) + + raise + end + + def drag_to(element) + return super unless chrome_edge? && html5_draggable? + + html5_drag_to(element) + end + + def drop(*args) + return super unless chrome_edge? + + html5_drop(*args) + end + + # def click(*) + # super + # rescue ::Selenium::WebDriver::Error::WebDriverError => e + # # chromedriver 74 (at least on mac) raises the wrong error for this + # raise ::Selenium::WebDriver::Error::ElementClickInterceptedError, e.message if e.message.match?(/element click intercepted/) + # + # raise + # end + + def disabled? + return super unless chrome_edge? + + driver.evaluate_script("arguments[0].matches(':disabled, select:disabled *')", self) + end + + def select_option + return super unless chrome_edge? + + # To optimize to only one check and then click + selected_or_disabled = driver.evaluate_script(<<~JS, self) + arguments[0].matches(':disabled, select:disabled *, :checked') + JS + click unless selected_or_disabled + end + +private + + def file_errors + @file_errors = ::Selenium::WebDriver.logger.suppress_deprecations do + [::Selenium::WebDriver::Error::ExpectedError] + end + end + + def bridge + driver.browser.send(:bridge) + end + + def browser_version + @browser_version ||= begin + caps = driver.browser.capabilities + (caps[:browser_version] || caps[:version]).to_f + end + end + + def chrome_edge? + browser_version >= 75 + end +end diff --git a/lib/capybara/spec/session/window/window_spec.rb b/lib/capybara/spec/session/window/window_spec.rb index 709b258e87..9ef12e3422 100644 --- a/lib/capybara/spec/session/window/window_spec.rb +++ b/lib/capybara/spec/session/window/window_spec.rb @@ -130,11 +130,12 @@ def win_size other_window = @session.window_opened_by do @session.find(:css, '#openWindow').click end - other_window.resize_to(600, 300) + + other_window.resize_to(600, 400) expect(@session.current_window).to eq(orig_window) @session.within_window(other_window) do - expect(@session.current_window.size).to eq([600, 300]) + expect(@session.current_window.size).to eq([600, 400]) end end end diff --git a/spec/selenium_spec_edge.rb b/spec/selenium_spec_edge.rb index d2cdeca126..f1cf7ac0d1 100644 --- a/spec/selenium_spec_edge.rb +++ b/spec/selenium_spec_edge.rb @@ -6,16 +6,23 @@ require 'shared_selenium_node' require 'rspec/shared_spec_matchers' +Selenium::WebDriver::Edge::Service.driver_path = '/usr/local/bin/msedgedriver' +# Not yet implemented in the selenium-webdriver edge driver +# Selenium::WebDriver::Edge.path = '/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary' + Capybara.register_driver :selenium_edge do |app| # ::Selenium::WebDriver.logger.level = "debug" - Capybara::Selenium::Driver.new(app, browser: :edge) + Capybara::Selenium::Driver.new(app, browser: :edge).tap do |driver| + driver.browser + driver.download_path = Capybara.save_path + end end module TestSessions SeleniumEdge = Capybara::Session.new(:selenium_edge, TestApp) end -skipped_tests = %i[response_headers status_code trigger modals] +skipped_tests = %i[response_headers status_code trigger] Capybara::SpecHelper.log_selenium_driver_version(Selenium::WebDriver::Edge) if ENV['CI'] @@ -23,6 +30,10 @@ module TestSessions case example.metadata[:description] when /#refresh it reposts$/ skip 'Edge insists on prompting without providing a way to suppress' + when /should be able to open non-http url/ + skip 'Crashes' + when /when Capybara.always_include_port is true/ + skip 'Crashes' end end diff --git a/spec/shared_selenium_session.rb b/spec/shared_selenium_session.rb index a5b306e231..3af53cd2ba 100644 --- a/spec/shared_selenium_session.rb +++ b/spec/shared_selenium_session.rb @@ -41,6 +41,7 @@ it 'should have return code 1 when running selenium_driver_rspec_failure.rb' do skip 'only setup for local non-headless' if headless_or_remote? + skip 'Not setup for edge' if edge?(session) system(env, 'rspec spec/fixtures/selenium_driver_rspec_failure.rb', out: File::NULL, err: File::NULL) expect($CHILD_STATUS.exitstatus).to eq(1) @@ -48,6 +49,7 @@ it 'should have return code 0 when running selenium_driver_rspec_success.rb' do skip 'only setup for local non-headless' if headless_or_remote? + skip 'Not setup for edge' if edge?(session) system(env, 'rspec spec/fixtures/selenium_driver_rspec_success.rb', out: File::NULL, err: File::NULL) expect($CHILD_STATUS.exitstatus).to eq(0) @@ -311,6 +313,7 @@ pending "IE doesn't support uploading a directory" if ie?(session) pending 'Chrome/chromedriver 73 breaks this' if chrome?(session) && !chrome_lt?(73, session) pending "Safari doesn't support uploading a directory" if safari?(session) + # pending "Edge/msedgedriver doesn't support directory upload" if edge?(session) && edge_gte?(75, session) session.visit('/form') test_file_dir = File.expand_path('./fixtures', File.dirname(__FILE__)) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 84c2149646..6c1a566a18 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,18 +32,31 @@ def chrome?(session) browser_name(session) == :chrome end + def chrome_version(session) + (session.driver.browser.capabilities[:browser_version] || + session.driver.browser.capabilities[:version]).to_f + end + def chrome_lt?(version, session) - chrome?(session) && (session.driver.browser.capabilities[:version].to_f < version) + chrome?(session) && (chrome_version(session) < version) end def chrome_gte?(version, session) - chrome?(session) && (session.driver.browser.capabilities[:version].to_f >= version) + chrome?(session) && (chrome_version(session) < version) end def edge?(session) browser_name(session) == :edge end + def edge_lt?(version, session) + edge?(session) && (chrome_version(session) < version) + end + + def edge_gte?(version, session) + edge?(session) && (chrome_version(session) >= version) + end + def ie?(session) %i[internet_explorer ie].include?(browser_name(session)) end