Add test specs run with Safari, and workaround issues - it's still pretty useless on anything more than basic pages though (#2165)

Add test specs run with Safari, and workaround some issues - it's still pretty useless on anything more than basic pages though
This commit is contained in:
Thomas Walpole 2019-03-13 18:21:53 -07:00 committed by GitHub
parent 10cbe0583b
commit a61790eac2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 349 additions and 12 deletions

View File

@ -14,7 +14,7 @@ RSpec::Core::RakeTask.new(:spec_firefox) do |t|
t.pattern = './spec{,/*/**}/*{_spec.rb,_spec_firefox.rb}'
end
%w[chrome ie edge chrome_remote firefox_remote].each do |driver|
%w[chrome ie edge chrome_remote firefox_remote safari].each do |driver|
RSpec::Core::RakeTask.new(:"spec_#{driver}") do |t|
t.rspec_opts = rspec_opts
t.pattern = "./spec/*{_spec_#{driver}.rb}"

View File

@ -267,7 +267,11 @@ private
if @browser.respond_to? :session_storage
@browser.session_storage.clear
else
warn 'sessionStorage clear requested but is not supported by this driver' unless options[:clear_session_storage].nil?
begin
@browser&.execute_script('window.sessionStorage.clear()')
rescue # rubocop:disable Style/RescueStandardError
warn 'sessionStorage clear requested but is not supported by this driver' unless options[:clear_session_storage].nil?
end
end
end
@ -275,7 +279,11 @@ private
if @browser.respond_to? :local_storage
@browser.local_storage.clear
else
warn 'localStorage clear requested but is not supported by this driver' unless options[:clear_local_storage].nil?
begin
@browser&.execute_script('window.localStorage.clear()')
rescue # rubocop:disable Style/RescueStandardError
warn 'localStorage clear requested but is not supported by this driver' unless options[:clear_local_storage].nil?
end
end
end
@ -359,6 +367,8 @@ private
extend FirefoxDriver if sel_driver.capabilities.is_a?(::Selenium::WebDriver::Remote::W3C::Capabilities)
when :ie, :internet_explorer
extend InternetExplorerDriver
when :safari, :Safari_Technology_Preview
extend SafariDriver
end
end
@ -408,3 +418,4 @@ end
require 'capybara/selenium/driver_specializations/chrome_driver'
require 'capybara/selenium/driver_specializations/firefox_driver'
require 'capybara/selenium/driver_specializations/internet_explorer_driver'
require 'capybara/selenium/driver_specializations/safari_driver'

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'capybara/selenium/nodes/safari_node'
module Capybara::Selenium::Driver::SafariDriver
private
def build_node(native_node, initial_cache = {})
::Capybara::Selenium::SafariNode.new(self, native_node, initial_cache)
end
def bridge
browser.send(:bridge)
end
end

View File

@ -119,7 +119,7 @@ private
x.parent(:fieldset)[
x.attr(:disabled)
] + x.ancestor[
~x.self(:legned) |
~x.self(:legend) |
x.preceding_sibling(:legend)
][
x.parent(:fieldset)[

View File

@ -0,0 +1,145 @@
# frozen_string_literal: true
# require 'capybara/selenium/extensions/html5_drag'
class Capybara::Selenium::SafariNode < Capybara::Selenium::Node
# include Html5Drag
def click(keys = [], **options)
# driver.execute_script('arguments[0].scrollIntoViewIfNeeded({block: "center"})', self)
super
rescue ::Selenium::WebDriver::Error::ElementNotInteractableError
if tag_name == 'tr'
warn 'You are attempting to click a table row which has issues in safaridriver - '\
'Your test should probably be clicking on a table cell like a user would. '\
'Clicking the first cell in the row instead.'
return find_css('th:first-child,td:first-child')[0].click(keys, options)
end
raise
end
def select_option
driver.execute_script("arguments[0].closest('select').scrollIntoView()", self)
super
end
def unselect_option
driver.execute_script("arguments[0].closest('select').scrollIntoView()", self)
super
end
def visible_text
return '' unless visible?
vis_text = driver.execute_script('return arguments[0].innerText', self)
vis_text.gsub(/\ +/, ' ')
.gsub(/[\ \n]*\n[\ \n]*/, "\n")
.gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
.gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
.tr("\u00a0", ' ')
end
def disabled?
return true if super || (self[:disabled] == 'true')
# workaround for safaridriver reporting elements as enabled when they are nested in disabling elements
if %w[option optgroup].include? tag_name
find_xpath('parent::*[self::optgroup or self::select]')[0].disabled?
else
!find_xpath(DISABLED_BY_FIELDSET_XPATH).empty?
end
end
def set_file(value) # rubocop:disable Naming/AccessorMethodName
# By default files are appended so we have to clear here if its multiple and already set
native.clear if multiple? && driver.evaluate_script('arguments[0].files', self).any?
super
end
def send_keys(*args)
return super(*args.map { |arg| arg == :space ? ' ' : arg }) if args.none? { |arg| arg.is_a? Array }
native.click
_send_keys(args).perform
end
def set_text(value, clear: nil, **_unused)
value = value.to_s
if clear == :backspace
# Clear field by sending the correct number of backspace keys.
backspaces = [:backspace] * self.value.to_s.length
send_keys(*([[:control, 'e']] + backspaces + [value]))
else
super.tap do
# React doesn't see the safaridriver element clear
send_keys(:space, :backspace) if value.to_s.empty? && clear.nil?
end
end
end
private
def bridge
driver.browser.send(:bridge)
end
DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
x.parent(:fieldset)[
x.attr(:disabled)
] + x.ancestor[
~x.self(:legend) |
x.preceding_sibling(:legend)
][
x.parent(:fieldset)[
x.attr(:disabled)
]
]
end.to_s.freeze
def _send_keys(keys, actions = browser_action, down_keys = ModifierKeysStack.new)
case keys
when :control, :left_control, :right_control,
:alt, :left_alt, :right_alt,
:shift, :left_shift, :right_shift,
:meta, :left_meta, :right_meta,
:command
down_keys.press(keys)
actions.key_down(keys)
when String
keys = keys.upcase if down_keys&.include?(:shift)
actions.send_keys(keys)
when Symbol
actions.send_keys(keys)
when Array
down_keys.push
keys.each { |sub_keys| _send_keys(sub_keys, actions, down_keys) }
down_keys.pop.reverse_each { |key| actions.key_up(key) }
else
raise ArgumentError, 'Unknown keys type'
end
actions
end
class ModifierKeysStack
def initialize
@stack = []
end
def include?(key)
@stack.flatten.include?(key)
end
def press(key)
@stack.last.push(key)
end
def push
@stack.push []
end
def pop
@stack.pop
end
end
private_constant :ModifierKeysStack
end

View File

@ -84,7 +84,7 @@ Capybara::SpecHelper.spec '#attach_file' do
@session.attach_file('Multiple Documents',
[test_file_path, another_test_file_path].map { |f| with_os_path_separators(f) })
@session.click_button('Upload Multiple')
expect(@session.body).to include('2 | ') # number of files
expect(@session).to have_content('2 | ') # number of files
expect(@session.body).to include(File.read(test_file_path))
expect(@session.body).to include(File.read(another_test_file_path))
end
@ -98,7 +98,7 @@ Capybara::SpecHelper.spec '#attach_file' do
@session.attach_file 'Multiple Documents', with_os_path_separators(test_file_path)
@session.attach_file 'Multiple Documents', with_os_path_separators(another_test_file_path)
@session.click_button('Upload Multiple')
expect(@session.body).to include('1 | ') # number of files
expect(@session).to have_content('1 | ') # number of files
expect(@session.body).to include(File.read(another_test_file_path))
expect(@session.body).not_to include(File.read(test_file_path))
end

View File

@ -178,10 +178,10 @@ Capybara::SpecHelper.spec '#fill_in' do
it 'should only trigger onchange once' do
@session.visit('/with_js')
# Click somewhere on the page to ensure focus is acquired. Without this FF won't generate change events for some reason???
@session.find(:css, 'body').click
@session.find(:css, 'h1', text: 'FooBar').click
@session.fill_in('with_change_event', with: 'some value')
# click outside the field to trigger the change event
@session.find(:css, 'body').click
@session.find(:css, 'h1', text: 'FooBar').click
expect(@session.find(:css, '.change_event_triggered', match: :one)).to have_text 'some value'
end
@ -189,7 +189,7 @@ Capybara::SpecHelper.spec '#fill_in' do
@session.visit('/with_js')
@session.fill_in('with_change_event', with: '')
# click outside the field to trigger the change event
@session.find(:css, 'body').click
@session.find(:css, 'h1', text: 'FooBar').click
expect(@session).to have_selector(:css, '.change_event_triggered', match: :one)
end
end

View File

@ -23,7 +23,7 @@
et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation <a href="/foo" id="foo">ullamco</a> laboris nisi
ut aliquip ex ea commodo consequat.
<a href="/with_simple_html" aria-label="Go to simple"><img id="first_image" width="20" height="20" alt="awesome image" /></a>
<a href="/with_simple_html" aria-label="Go to simple"><img id="first_image" width="20" height="20" alt="awesome image" src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="/></a>
</p>
<p class="para" id="second" style="display: inline;">

View File

@ -0,0 +1,148 @@
# frozen_string_literal: true
require 'spec_helper'
require 'selenium-webdriver'
require 'shared_selenium_session'
require 'rspec/shared_spec_matchers'
SAFARI_DRIVER = :selenium_safari
::Selenium::WebDriver::Safari.driver_path = '/Applications/Safari Technology Preview.app/Contents/MacOS/safaridriver'
browser_options = ::Selenium::WebDriver::Safari::Options.new
# browser_options.headless! if ENV['HEADLESS']
# browser_options.add_option(:w3c, !!ENV['W3C'])
Capybara.register_driver :selenium_safari do |app|
Capybara::Selenium::Driver.new(app, browser: :safari, options: browser_options, timeout: 30).tap do |driver|
# driver.browser.download_path = Capybara.save_path
end
end
Capybara.register_driver :selenium_safari_not_clear_storage do |app|
safari_options = {
browser: :safari,
options: browser_options
}
Capybara::Selenium::Driver.new(app, safari_options.merge(clear_local_storage: false, clear_session_storage: false))
end
module TestSessions
Safari = Capybara::Session.new(SAFARI_DRIVER, TestApp)
end
skipped_tests = %i[response_headers status_code trigger windows drag]
$stdout.puts `#{Selenium::WebDriver::Safari.driver_path} --version` if ENV['CI']
Capybara::SpecHelper.run_specs TestSessions::Safari, SAFARI_DRIVER.to_s, capybara_skip: skipped_tests do |example|
case example.metadata[:full_description]
when /click_link can download a file/
skip "safaridriver doesn't provide a way to set the download directory"
when /Capybara::Session selenium_safari Capybara::Window#maximize/
pending "Safari headless doesn't support maximize" if ENV['HEADLESS']
when /Capybara::Session selenium_safari #visit without a server/,
/Capybara::Session selenium_safari #visit with Capybara.app_host set should override server/,
/Capybara::Session selenium_safari #reset_session! When reuse_server == false raises any standard errors caught inside the server during a second session/
skip "Safari webdriver doesn't support multiple sessions"
when /Capybara::Session selenium_safari #click_link with alternative text given to a contained image/,
'Capybara::Session selenium_safari #click_link_or_button with enable_aria_label should click on link'
pending 'safaridriver thinks these links are non-interactable for some unknown reason'
when /Capybara::Session selenium_safari #attach_file with a block can upload by clicking the file input/
skip "safaridriver doesn't allow clicking on file inputs"
when /Capybara::Session selenium_safari #attach_file with a block can upload by clicking the label/
skip 'hangs tests'
when /Capybara::Session selenium_safari #check when checkbox hidden with Capybara.automatic_label_click == false with allow_label_click == true should check via the label if input is visible but blocked by another element/,
'Capybara::Session selenium_safari node #click should not retry clicking when wait is disabled',
'Capybara::Session selenium_safari node #click should allow to retry longer',
'Capybara::Session selenium_safari node #click should retry clicking'
pending "safaridriver doesn't return a specific enough error to deal with this"
when /Capybara::Session selenium_safari #within_frame should find multiple nested frames/,
/Capybara::Session selenium_safari #within_frame works if the frame is closed/,
/Capybara::Session selenium_safari #switch_to_frame works if the frame is closed/
skip 'switch_to_frame(:parent) appears to go to the root in Safari rather than parent'
when /Capybara::Session selenium_safari #reset_session! removes ALL cookies/
skip 'Safari webdriver can only remove cookies for the current domain'
when /Capybara::Session selenium_safari #refresh it reposts/
skip "Safari opens an alert that can't be closed"
when 'Capybara::Session selenium_safari node #double_click should allow to adjust the offset',
'Capybara::Session selenium_safari node #double_click should double click an element'
pending "safardriver doesn't generate a double click event"
when 'Capybara::Session selenium_safari node #click should allow multiple modifiers',
/Capybara::Session selenium_safari node #(click|right_click|double_click) should allow modifiers/
pending "safaridriver doesn't take key state into account when clicking"
when 'Capybara::Session selenium_safari #fill_in on a pre-populated textfield with a reformatting onchange should trigger change when clearing field'
pending "safardriver clear doesn't generate change event"
when 'Capybara::Session selenium_safari #go_back should fetch a response from the driver from the previous page',
'Capybara::Session selenium_safari #go_forward should fetch a response from the driver from the previous page'
skip 'safaridriver loses the ability to find elements in the document after `go_back`'
when 'Capybara::Session selenium_safari node #send_keys should hold modifiers at top level'
skip 'Need to look into this'
end
end
RSpec.describe 'Capybara::Session with safari' do
include Capybara::SpecHelper
include_examples 'Capybara::Session', TestSessions::Safari, SAFARI_DRIVER
include_examples Capybara::RSpecMatchers, TestSessions::Safari, SAFARI_DRIVER
context 'storage' do
describe '#reset!' do
it 'clears storage by default' do
session = TestSessions::Safari
session.visit('/with_js')
session.find(:css, '#set-storage').click
session.reset!
session.visit('/with_js')
expect(session.evaluate_script('Object.keys(localStorage)')).to be_empty
expect(session.evaluate_script('Object.keys(sessionStorage)')).to be_empty
end
it 'does not clear storage when false' do
skip "Safari webdriver doesn't support multiple sessions"
session = Capybara::Session.new(:selenium_safari_not_clear_storage, TestApp)
session.visit('/with_js')
session.find(:css, '#set-storage').click
session.reset!
session.visit('/with_js')
expect(session.evaluate_script('Object.keys(localStorage)')).not_to be_empty
expect(session.evaluate_script('Object.keys(sessionStorage)')).not_to be_empty
end
end
end
context 'timeout' do
it 'sets the http client read timeout' do
expect(TestSessions::Safari.driver.browser.send(:bridge).http.read_timeout).to eq 30
end
end
describe 'filling in Safari-specific date and time fields with keystrokes' do
let(:datetime) { Time.new(1983, 6, 19, 6, 30) }
let(:session) { TestSessions::Safari }
before do
skip 'Too many other things broken currently'
session.visit('/form')
end
it 'should fill in a date input with a String' do
session.fill_in('form_date', with: '06/19/1983')
session.click_button('awesome')
expect(Date.parse(extract_results(session)['date'])).to eq datetime.to_date
end
it 'should fill in a time input with a String' do
session.fill_in('form_time', with: '06:30A')
session.click_button('awesome')
results = extract_results(session)['time']
expect(Time.parse(results).strftime('%r')).to eq datetime.strftime('%r')
end
it 'should fill in a datetime input with a String' do
session.fill_in('form_datetime', with: "06/19/1983\t06:30A")
session.click_button('awesome')
expect(Time.parse(extract_results(session)['datetime'])).to eq datetime
end
end
end

View File

@ -72,6 +72,7 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
context '#fill_in_with empty string and no options' do
it 'should trigger change when clearing a field' do
pending "safaridriver doesn't trigger change for clear" if safari?(session)
session.visit('/with_js')
session.fill_in('with_change_event', with: '')
# click outside the field to trigger the change event
@ -116,6 +117,7 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
it 'should only trigger onchange once' do
session.visit('/with_js')
sleep 2 if safari?(session) # Safari needs a delay (to load event handlers maybe ???)
session.fill_in('with_change_event',
with: 'some value',
fill_options: { clear: :backspace })
@ -140,13 +142,16 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
with: '',
fill_options: { clear: :backspace })
# click outside the field to trigger the change event
session.find(:css, 'body').click
# session.find(:css, 'body').click
session.find(:css, 'h1', text: 'FooBar').click
expect(session).to have_xpath('//p[@class="input_event_triggered"]', count: 13)
end
end
context '#fill_in with { clear: :none } fill_options' do
it 'should append to content in a field' do
pending 'Safari overwrites by default - need to figure out a workaround' if safari?(session)
session.visit('/form')
session.fill_in('form_first_name',
with: 'Harry',
@ -166,17 +171,20 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
});
JS
# work around weird FF issue where it would create an extra focus issue in some cases
session.find(:css, 'body').click
session.find(:css, 'h1', text: 'Form').click
# session.find(:css, 'body').click
end
it 'should generate standard events on changing value' do
pending "IE 11 doesn't support date input type" if ie?(session)
pending "Safari doesn't support date input type" if safari?(session)
session.fill_in('form_date', with: Date.today)
expect(session.evaluate_script('window.capybara_formDateFiredEvents')).to eq %w[focus input change]
end
it 'should not generate input and change events if the value is not changed' do
pending "IE 11 doesn't support date input type" if ie?(session)
pending "Safari doesn't support date input type" if safari?(session)
session.fill_in('form_date', with: Date.today)
session.fill_in('form_date', with: Date.today)
# Chrome adds an extra focus for some reason - ok for now
@ -264,6 +272,7 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
describe '#evaluate_async_script' do
it 'will timeout if the script takes too long' do
skip 'safaridriver returns the wrong error type' if safari?(session)
session.visit('/with_js')
expect do
session.using_wait_time(1) do
@ -298,6 +307,7 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
before do
skip "Firefox < 62 doesn't support a DataTransfer constuctor" if firefox_lt?(62.0, session)
skip "IE doesn't support a DataTransfer constuctor" if ie?(session)
skip "Safari doesn't support" if safari?(session)
end
it 'should HTML5 drag and drop an object' do
@ -351,6 +361,7 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
pending "Headless Chrome doesn't support directory upload - https://bugs.chromium.org/p/chromedriver/issues/detail?id=2521&q=directory%20upload&colspec=ID%20Status%20Pri%20Owner%20Summary" if chrome?(session) && ENV['HEADLESS']
pending "IE doesn't support uploading a directory" if ie?(session)
pending 'Chrome/chromedriver 73 breaks this' unless chrome_lt?(73, session)
pending "Safari doesn't support uploading a directory" if safari?(session)
session.visit('/form')
test_file_dir = File.expand_path('./fixtures', File.dirname(__FILE__))
@ -372,6 +383,7 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
describe 'Capybara#disable_animation' do
context 'when set to `true`' do
before(:context) do # rubocop:disable RSpec/BeforeAfterAll
skip "Safari doesn't support multiple sessions" if safari?(session)
# NOTE: Although Capybara.SpecHelper.reset! sets Capybara.disable_animation to false,
# it doesn't affect any of these tests because the settings are applied per-session
Capybara.disable_animation = true
@ -393,6 +405,7 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
context 'if we pass in css that matches elements' do
before(:context) do # rubocop:disable RSpec/BeforeAfterAll
skip "safaridriver doesn't support multiple sessions" if safari?(session)
# NOTE: Although Capybara.SpecHelper.reset! sets Capybara.disable_animation to false,
# it doesn't affect any of these tests because the settings are applied per-session
Capybara.disable_animation = '#with_animation a'
@ -414,6 +427,7 @@ RSpec.shared_examples 'Capybara::Session' do |session, mode|
context 'if we pass in css that does not match elements' do
before(:context) do # rubocop:disable RSpec/BeforeAfterAll
skip "Safari doesn't support multiple sessions" if safari?(session)
# NOTE: Although Capybara.SpecHelper.reset! sets Capybara.disable_animation to false,
# it doesn't affect any of these tests because the settings are applied per-session
Capybara.disable_animation = '.this-class-matches-nothing'

View File

@ -40,6 +40,10 @@ module Capybara
%i[internet_explorer ie].include?(browser_name(session))
end
def safari?(session)
%i[safari Safari Safari_Technology_Preview].include?(browser_name(session))
end
def browser_name(session)
session.driver.browser.browser if session.respond_to?(:driver)
end