From 7a410b52565b4855ac59cc9f2aa1f997902a1ef0 Mon Sep 17 00:00:00 2001 From: Thomas Walpole Date: Thu, 2 May 2019 18:57:14 -0700 Subject: [PATCH] Add smaller isDisplayed and getAttribute atoms --- Rakefile | 14 + capybara.gemspec | 1 + .../selenium/atoms/getAttribute.min.js | 1 + .../selenium/atoms/isDisplayed.min.js | 1 + .../selenium/atoms/src/getAttribute.js | 161 +++++++ .../selenium/atoms/src/isDisplayed.js | 454 ++++++++++++++++++ lib/capybara/selenium/driver.rb | 1 + lib/capybara/selenium/node.rb | 2 +- lib/capybara/selenium/patches/atoms.rb | 18 + spec/sauce_spec_chrome.rb | 1 + spec/selenium_spec_chrome.rb | 6 +- spec/selenium_spec_chrome_remote.rb | 6 +- spec/selenium_spec_edge.rb | 6 +- spec/selenium_spec_firefox.rb | 15 +- spec/selenium_spec_firefox_remote.rb | 6 +- spec/selenium_spec_ie.rb | 6 +- spec/selenium_spec_safari.rb | 6 +- spec/shared_selenium_node.rb | 29 ++ 18 files changed, 710 insertions(+), 24 deletions(-) create mode 100644 lib/capybara/selenium/atoms/getAttribute.min.js create mode 100644 lib/capybara/selenium/atoms/isDisplayed.min.js create mode 100644 lib/capybara/selenium/atoms/src/getAttribute.js create mode 100644 lib/capybara/selenium/atoms/src/isDisplayed.js create mode 100644 lib/capybara/selenium/patches/atoms.rb create mode 100644 spec/shared_selenium_node.rb diff --git a/Rakefile b/Rakefile index de398733..bbebb917 100644 --- a/Rakefile +++ b/Rakefile @@ -65,6 +65,20 @@ task :travis do Rake::Task[:cucumber].invoke end +task :build_js do + require 'uglifier' + Dir.glob('./lib/capybara/selenium/atoms/src/*.js').each do |fn| + js = ::Uglifier.compile( + File.read(fn), + compress: { + negate_iife: false, # Negate immediately invoked function expressions to avoid extra parens + side_effects: false # Pass false to disable potentially dropping functions marked as "pure" + } + )[0...-1] + File.write("./lib/capybara/selenium/atoms/#{File.basename(fn).gsub('.js', '.min.js')}", js) + end +end + task :release do version = Capybara::VERSION puts "Releasing #{version}, y/n?" diff --git a/capybara.gemspec b/capybara.gemspec index 66fa9487..7ad9b0ab 100644 --- a/capybara.gemspec +++ b/capybara.gemspec @@ -33,6 +33,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency('rack', ['>= 1.6.0']) s.add_runtime_dependency('rack-test', ['>= 0.6.3']) s.add_runtime_dependency('regexp_parser', ['~>1.2']) + s.add_runtime_dependency('uglifier') s.add_runtime_dependency('xpath', ['~>3.2']) s.add_development_dependency('byebug') unless RUBY_PLATFORM == 'java' diff --git a/lib/capybara/selenium/atoms/getAttribute.min.js b/lib/capybara/selenium/atoms/getAttribute.min.js new file mode 100644 index 00000000..07dfca5d --- /dev/null +++ b/lib/capybara/selenium/atoms/getAttribute.min.js @@ -0,0 +1 @@ +(function(){function u(e){var t=e.tagName.toUpperCase();if("OPTION"==t)return!0;if("INPUT"!=t)return!1;var r=e.type.toLowerCase();return"checkbox"==r||"radio"==r}function s(e){var t="selected",r=e.type&&e.type.toLowerCase();return"checkbox"!=r&&"radio"!=r||(t="checked"),!!e[t]}function c(e,t){var r=e.getAttributeNode(t);return r&&r.specified?r.value:null}var i=["allowfullscreen","allowpaymentrequest","allowusermedia","async","autofocus","autoplay","checked","compact","complete","controls","declare","default","defaultchecked","defaultselected","defer","disabled","ended","formnovalidate","hidden","indeterminate","iscontenteditable","ismap","itemscope","loop","multiple","muted","nohref","nomodule","noresize","noshade","novalidate","nowrap","open","paused","playsinline","pubdate","readonly","required","reversed","scoped","seamless","seeking","selected","truespeed","typemustmatch","willvalidate"],d={"class":"className",readonly:"readOnly"};return function f(e,t){var r=null,a=t.toLowerCase();if("style"==a)return(r=e.style)&&"string"!=typeof r&&(r=r.cssText),r;if(("selected"==a||"checked"==a)&&u(e))return s(e)?"true":null;if(tagName=e.tagName.toUpperCase(),"IMG"==tagName&&"src"==a||"A"==tagName&&"href"==a)return(r=c(e,a))&&(r=e[a]),r;if("spellcheck"==a){if(null===!(r=c(e,a))){if("false"==r.toLowerCase())return"false";if("true"==r.toLowerCase())return"true"}return e[a]+""}var l,n=d[t]||t;if(i.some(function(e){e==a}))return(r=!(null===(r=c(e,a)))||e[n])?"true":null;try{l=e[n]}catch(o){}return null!=(r=null==l||"object"==typeof l||"function"==typeof l?c(e,t):l)?r.toString():null}})() \ No newline at end of file diff --git a/lib/capybara/selenium/atoms/isDisplayed.min.js b/lib/capybara/selenium/atoms/isDisplayed.min.js new file mode 100644 index 00000000..fa5405f8 --- /dev/null +++ b/lib/capybara/selenium/atoms/isDisplayed.min.js @@ -0,0 +1 @@ +(function(){function f(t,e,n){function r(t){var e=x(t);if(0=s.left+s.width,m=o.top>=s.top+s.height;if(N&&"hidden"==h.x||m&&"hidden"==h.y)return T.HIDDEN;if(N&&"visible"!=h.x||m&&"visible"!=h.y){if(i){var y=r(f);if(o.left>=u.scrollWidth-y.x||o.right>=u.scrollHeight-y.y)return T.HIDDEN}return C(f)==T.HIDDEN?T.HIDDEN:T.SCROLL}}}return T.NONE}function o(t){var e=t.document.documentElement;return{width:e.clientWidth,height:e.clientHeight}}function p(t,e,n,r){return{left:t,top:e,width:n,height:r}}function x(t){var e,n=c(t);if(n)return n.rect;if("HTML"==t.tagName.toUpperCase()){t.ownerDocument;var r=o(window);return p(0,0,r.width,r.height)}try{e=t.getBoundingClientRect()}catch(i){return p(0,0,0,0)}return p(e.left,e.top,e.right-e.left,e.bottom-e.top)}function h(t){var e=1,n=window.getComputedStyle(t).opacity;n&&(e=Number(n));var r=D(t);return r&&r.nodeType==Node.ELEMENT_NODE&&(e*=h(r)),e}function s(t){var e=t.shape.toLowerCase(),n=t.coords.split(",");if("rect"==e&&4==n.length){var r=n[0],i=n[1];return p(r,i,n[2]-r,n[3]-i)}if("circle"==e&&3==n.length){var o=n[0],a=n[1],u=n[2];return p(o-u,a-u,2*u,2*u)}if("poly"==e&&2 0 && imageMap.rect.height > 0 && + isShown_(imageMap.image, ignoreOpacity, parentsDisplayedFn); + } + + // Any hidden input is not shown. + if ((elemTagName == "INPUT") && (elem.type.toLowerCase() == "hidden")) { + return false; + } + + // Any NOSCRIPT element is not shown. + if (elemTagName == "NOSCRIPT") { + return false; + } + + // Any element with hidden/collapsed visibility is not shown. + var visibility = window.getComputedStyle(elem)["visibility"]; + if (visibility == "collapse" || visibility == "hidden") { + return false; + } + + if (!parentsDisplayedFn(elem)) { + return false; + } + + // Any transparent element is not shown. + if (!ignoreOpacity && getOpacity(elem) == 0) { + return false; + } + + // Any element without positive size dimensions is not shown. + function positiveSize(e) { + var rect = getClientRect(e); + if (rect.height > 0 && rect.width > 0) { + return true; + } + // A vertical or horizontal SVG Path element will report zero width or + // height but is "shown" if it has a positive stroke-width. + if ((e.tagName.toUpperCase() == "PATH") && (rect.height > 0 || rect.width > 0)) { + var strokeWidth = window.getComputedStyle(e)["stroke-width"]; + return !!strokeWidth && (parseInt(strokeWidth, 10) > 0); + } + // Zero-sized elements should still be considered to have positive size + // if they have a child element or text node with positive size, unless + // the element has an 'overflow' style of "hidden". + return window.getComputedStyle(e)["overflow"] != "hidden" && + Array.prototype.slice.call(e.childNodes).some(function(n) { + return (n.nodeType == Node.TEXT_NODE) || + ((n.nodeType == Node.ELEMENT_NODE) && positiveSize(n)); + }); + } + + if (!positiveSize(elem)) { + return false; + } + + // Elements that are hidden by overflow are not shown. + function hiddenByOverflow(e) { + return getOverflowState(e) == OverflowState.HIDDEN && + Array.prototype.slice.call(e.childNodes).every(function(n) { + return (n.nodeType != Node.ELEMENT_NODE) || hiddenByOverflow(n) || + !positiveSize(n); + }); + } + return !hiddenByOverflow(elem); + } + + function getClientRegion(elem) { + var region = getClientRect(elem); + return { left: region.left, + right: region.left + region.width, + top: region.top, + bottom: region.top + region.height }; + } + + function getParentElement(node) { + return node.parentElement + } + + function getOverflowState(elem) { + var region = getClientRegion(elem); + var ownerDoc = elem.ownerDocument; + var htmlElem = ownerDoc.documentElement; + var bodyElem = ownerDoc.body; + var htmlOverflowStyle = window.getComputedStyle(htmlElem)["overflow"]; + var treatAsFixedPosition; + + // Return the closest ancestor that the given element may overflow. + function getOverflowParent(e) { + function canBeOverflowed(container) { + // The HTML element can always be overflowed. + if (container == htmlElem) { + return true; + } + var containerStyle = window.getComputedStyle(container); + // An element cannot overflow an element with an inline or contents display style. + var containerDisplay = containerStyle["display"]; + if ((containerDisplay.indexOf("inline") == 0) || + (containerDisplay == "contents")) { + return false; + } + // An absolute-positioned element cannot overflow a static-positioned one. + if ((position == "absolute") && (containerStyle["position"] == "static")) { + return false; + } + return true; + } + + var position = window.getComputedStyle(e)["position"]; + if (position == "fixed") { + treatAsFixedPosition = true; + // Fixed-position element may only overflow the viewport. + return e == htmlElem ? null : htmlElem; + } else { + var parent = getParentElement(e); + while (parent && !canBeOverflowed(parent)) { + parent = getParentElement(parent); + } + return parent; + } + }; + + // Return the x and y overflow styles for the given element. + function getOverflowStyles(e) { + // When the element has an overflow style of 'visible', it assumes + // the overflow style of the body, and the body is really overflow:visible. + var overflowElem = e; + if (htmlOverflowStyle == "visible") { + // Note: bodyElem will be null/undefined in SVG documents. + if (e == htmlElem && bodyElem) { + overflowElem = bodyElem; + } else if (e == bodyElem) { + return {x: "visible", y: "visible"}; + } + } + var overflowElemStyle = window.getComputedStyle(overflowElem); + var overflow = { + x: overflowElemStyle["overflow-x"], + y: overflowElemStyle["overflow-y"] + }; + // The element cannot have a genuine 'visible' overflow style, + // because the viewport can't expand; 'visible' is really 'auto'. + if (e == htmlElem) { + overflow.x = overflow.x == "visible" ? "auto" : overflow.x; + overflow.y = overflow.y == "visible" ? "auto" : overflow.y; + } + return overflow; + }; + + // Returns the scroll offset of the given element. + function getScroll(e) { + if (e == htmlElem) { + return { x: window.scrollX, y: window.scrollY } + } + return { x: e.scrollLeft, y: e.scrollTop } + } + + // Check if the element overflows any ancestor element. + for (var container = getOverflowParent(elem); + !!container; + container = getOverflowParent(container)) { + var containerOverflow = getOverflowStyles(container); + + // If the container has overflow:visible, the element cannot overflow it. + if (containerOverflow.x == "visible" && containerOverflow.y == "visible") { + continue; + } + + var containerRect = getClientRect(container); + + // Zero-sized containers without overflow:visible hide all descendants. + if (containerRect.width == 0 || containerRect.height == 0) { + return OverflowState.HIDDEN; + } + + // Check "underflow": if an element is to the left or above the container + var underflowsX = region.right < containerRect.left; + var underflowsY = region.bottom < containerRect.top; + if ((underflowsX && containerOverflow.x == "hidden") || + (underflowsY && containerOverflow.y == "hidden")) { + return OverflowState.HIDDEN; + } else if ((underflowsX && containerOverflow.x != "visible") || + (underflowsY && containerOverflow.y != "visible")) { + // When the element is positioned to the left or above a container, we + // have to distinguish between the element being completely outside the + // container and merely scrolled out of view within the container. + var containerScroll = getScroll(container); + var unscrollableX = region.right < containerRect.left - containerScroll.x; + var unscrollableY = region.bottom < containerRect.top - containerScroll.y; + if ((unscrollableX && containerOverflow.x != "visible") || + (unscrollableY && containerOverflow.x != "visible")) { + return OverflowState.HIDDEN; + } + var containerState = getOverflowState(container); + return containerState == OverflowState.HIDDEN ? + OverflowState.HIDDEN : OverflowState.SCROLL; + } + + // Check "overflow": if an element is to the right or below a container + var overflowsX = region.left >= containerRect.left + containerRect.width; + var overflowsY = region.top >= containerRect.top + containerRect.height; + if ((overflowsX && containerOverflow.x == "hidden") || + (overflowsY && containerOverflow.y == "hidden")) { + return OverflowState.HIDDEN; + } else if ((overflowsX && containerOverflow.x != "visible") || + (overflowsY && containerOverflow.y != "visible")) { + // If the element has fixed position and falls outside the scrollable area + // of the document, then it is hidden. + if (treatAsFixedPosition) { + var docScroll = getScroll(container); + if ((region.left >= htmlElem.scrollWidth - docScroll.x) || + (region.right >= htmlElem.scrollHeight - docScroll.y)) { + return OverflowState.HIDDEN; + } + } + // If the element can be scrolled into view of the parent, it has a scroll + // state; unless the parent itself is entirely hidden by overflow, in + // which it is also hidden by overflow. + var containerState = getOverflowState(container); + return containerState == OverflowState.HIDDEN ? + OverflowState.HIDDEN : OverflowState.SCROLL; + } + } + + // Does not overflow any ancestor. + return OverflowState.NONE; + } + + function getViewportSize(win) { + var el = win.document.documentElement; + return { width: el.clientWidth, height: el.clientHeight }; + } + + function rect_(x, y, w, h){ + return { left: x, top: y, width: w, height: h }; + } + + function getClientRect(elem) { + var imageMap = maybeFindImageMap_(elem); + if (imageMap) { + return imageMap.rect; + } else if (elem.tagName.toUpperCase() == "HTML") { + // Define the client rect of the element to be the viewport. + var doc = elem.ownerDocument; + // TODO: Is this too simplified??? + var viewportSize = getViewportSize(window); + return rect_(0, 0, viewportSize.width, viewportSize.height); + } else { + var nativeRect; + try { + nativeRect = elem.getBoundingClientRect(); + } catch (e) { + return rect_(0, 0, 0, 0); + } + + return rect_(nativeRect.left, nativeRect.top, + nativeRect.right - nativeRect.left, nativeRect.bottom - nativeRect.top); + } + } + + function getOpacity(elem) { + // By default the element is opaque. + var elemOpacity = 1; + + var opacityStyle = window.getComputedStyle(elem)["opacity"]; + if (opacityStyle) { + elemOpacity = Number(opacityStyle); + } + + // Let's apply the parent opacity to the element. + var parentElement = getParentElement(elem); + if (parentElement && parentElement.nodeType == Node.ELEMENT_NODE) { + elemOpacity = elemOpacity * getOpacity(parentElement); + } + return elemOpacity; + } + + function getAreaRelativeRect_(area) { + var shape = area.shape.toLowerCase(); + var coords = area.coords.split(","); + if (shape == "rect" && coords.length == 4) { + var x = coords[0], y = coords[1]; + return rect_(x, y, coords[2] - x, coords[3] - y); + } else if (shape == "circle" && coords.length == 3) { + var centerX = coords[0], centerY = coords[1], radius = coords[2]; + return rect_(centerX - radius, centerY - radius, 2 * radius, 2 * radius); + } else if (shape == "poly" && coords.length > 2) { + var minX = coords[0], minY = coords[1], maxX = minX, maxY = minY; + for (var i = 2; i + 1 < coords.length; i += 2) { + minX = Math.min(minX, coords[i]); + maxX = Math.max(maxX, coords[i]); + minY = Math.min(minY, coords[i + 1]); + maxY = Math.max(maxY, coords[i + 1]); + } + return rect_(minX, minY, maxX - minX, maxY - minY); + } + return rect_(0, 0, 0, 0); + } + + function maybeFindImageMap_(elem) { + // If not a or , return null indicating so. + var elemTagName = elem.tagName.toUpperCase(); + var isMap = elemTagName == "MAP"; + if (!isMap && (elemTagName != "AREA")) { + return null; + } + + // Get the associated with this element, or null if none. + var map = isMap ? elem : + ((getParentElement(elem).tagName.toUpperCase() == "MAP") ? + getParentElement(elem) : null); + + var image = null, rect = null; + if (map && map.name) { + var mapDoc = map.ownerDocument; + + image = mapDoc.querySelector("*[usemap='#" + map.name + "']"); + + if (image) { + rect = getClientRect(image); + if (!isMap && elem.shape.toLowerCase() != "default") { + // Shift and crop the relative area rectangle to the map. + var relRect = getAreaRelativeRect_(elem); + var relX = Math.min(Math.max(relRect.left, 0), rect.width); + var relY = Math.min(Math.max(relRect.top, 0), rect.height); + var w = Math.min(relRect.width, rect.width - relX); + var h = Math.min(relRect.height, rect.height - relY); + rect = rect_(relX + rect.left, relY + rect.top, w, h); + } + } + } + + return {image: image, rect: rect || rect_(0, 0, 0, 0)}; + } + + function getAncestor(element, matcher) { + if (element) { + element = getParentElement(element); + } + while (element) { + if (matcher(element)) { + return element; + } + element = getParentElement(element); + } + // Reached the root of the DOM without a match + return null; + } + + + function isElement(node, opt_tagName) { + // because we call this with deprecated tags such as SHADOW + if (opt_tagName && (typeof opt_tagName !== "string")) { + opt_tagName = opt_tagName.toString(); + } + return !!node && node.nodeType == Node.ELEMENT_NODE && + (!opt_tagName || node.tagName.toUpperCase() == opt_tagName); + } + + function getParentNodeInComposedDom(node) { + var /**@type {Node}*/ parent = node.parentNode; + + // Shadow DOM v1 + if (parent && parent.shadowRoot && node.assignedSlot !== undefined) { + // Can be null on purpose, meaning it has no parent as + // it hasn't yet been slotted + return node.assignedSlot ? node.assignedSlot.parentNode : null; + } + + // Shadow DOM V0 (deprecated) + if (node.getDestinationInsertionPoints) { + var destinations = node.getDestinationInsertionPoints(); + if (destinations.length > 0) { + return destinations[destinations.length - 1]; + } + } + + return parent; + } + + return function isShown(elem, opt_ignoreOpacity) { + /** + * Determines whether an element or its parents have `display: none` set + * @param {!Node} e the element + * @return {boolean} + */ + function displayed(e) { + if (window.getComputedStyle(e)["display"] == "none"){ + return false; + } + + var parent = getParentNodeInComposedDom(e); + + if ((typeof ShadowRoot === "function") && (parent instanceof ShadowRoot)) { + if (parent.host.shadowRoot !== parent) { + // There is a younger shadow root, which will take precedence over + // the shadow this element is in, thus this element won't be + // displayed. + return false; + } else { + parent = parent.host; + } + } + + if (parent && (parent.nodeType == Node.DOCUMENT_NODE || + parent.nodeType == Node.DOCUMENT_FRAGMENT_NODE)) { + return true; + } + + return parent && displayed(parent); + } + + return isShown_(elem, !!opt_ignoreOpacity, displayed); + }; +})() diff --git a/lib/capybara/selenium/driver.rb b/lib/capybara/selenium/driver.rb index 4c2b45c7..0a0934ea 100644 --- a/lib/capybara/selenium/driver.rb +++ b/lib/capybara/selenium/driver.rb @@ -18,6 +18,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base def load_selenium require 'selenium-webdriver' require 'capybara/selenium/logger_suppressor' + require 'capybara/selenium/patches/atoms' warn "Warning: You're using an unsupported version of selenium-webdriver, please upgrade." if Gem.loaded_specs['selenium-webdriver'].version < Gem::Version.new('3.5.0') rescue LoadError => e raise e unless e.message.match?(/selenium-webdriver/) diff --git a/lib/capybara/selenium/node.rb b/lib/capybara/selenium/node.rb index 7136061f..a832b9bd 100644 --- a/lib/capybara/selenium/node.rb +++ b/lib/capybara/selenium/node.rb @@ -155,7 +155,7 @@ class Capybara::Selenium::Node < Capybara::Driver::Node end def content_editable? - native.attribute('isContentEditable') + native.attribute('isContentEditable') == 'true' end def ==(other) diff --git a/lib/capybara/selenium/patches/atoms.rb b/lib/capybara/selenium/patches/atoms.rb new file mode 100644 index 00000000..bf32386f --- /dev/null +++ b/lib/capybara/selenium/patches/atoms.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module CapybaraAtoms +private # rubocop:disable Layout/IndentationWidth + + def read_atom(function) + @atoms ||= Hash.new do |hash, key| + hash[key] = begin + File.read(File.expand_path("../../atoms/#{key}.min.js", __FILE__)) + rescue Errno::ENOENT + super + end + end + @atoms[function] + end +end + +::Selenium::WebDriver::Remote::Bridge.prepend CapybaraAtoms unless ENV['DISABLE_CAPYBARA_SELENIUM_OPTIMIZATIONS'] diff --git a/spec/sauce_spec_chrome.rb b/spec/sauce_spec_chrome.rb index aec5fea4..85e8bc11 100644 --- a/spec/sauce_spec_chrome.rb +++ b/spec/sauce_spec_chrome.rb @@ -5,6 +5,7 @@ require 'selenium-webdriver' require 'sauce_whisk' # require 'shared_selenium_session' +# require 'shared_selenium_node' # require 'rspec/shared_spec_matchers' Capybara.register_driver :sauce_chrome do |app| diff --git a/spec/selenium_spec_chrome.rb b/spec/selenium_spec_chrome.rb index d91cabe5..0cbccf53 100644 --- a/spec/selenium_spec_chrome.rb +++ b/spec/selenium_spec_chrome.rb @@ -3,6 +3,7 @@ require 'spec_helper' require 'selenium-webdriver' require 'shared_selenium_session' +require 'shared_selenium_node' require 'rspec/shared_spec_matchers' CHROME_DRIVER = :selenium_chrome @@ -51,8 +52,9 @@ end RSpec.describe 'Capybara::Session with chrome' do include Capybara::SpecHelper - include_examples 'Capybara::Session', TestSessions::Chrome, CHROME_DRIVER - include_examples Capybara::RSpecMatchers, TestSessions::Chrome, CHROME_DRIVER + ['Capybara::Session', 'Capybara::Node', Capybara::RSpecMatchers].each do |examples| + include_examples examples, TestSessions::Chrome, CHROME_DRIVER + end context 'storage' do describe '#reset!' do diff --git a/spec/selenium_spec_chrome_remote.rb b/spec/selenium_spec_chrome_remote.rb index 7c9a8f6b..411bdd35 100644 --- a/spec/selenium_spec_chrome_remote.rb +++ b/spec/selenium_spec_chrome_remote.rb @@ -3,6 +3,7 @@ require 'spec_helper' require 'selenium-webdriver' require 'shared_selenium_session' +require 'shared_selenium_node' require 'rspec/shared_spec_matchers' def selenium_host @@ -72,8 +73,9 @@ end RSpec.describe 'Capybara::Session with remote Chrome' do include Capybara::SpecHelper - include_examples 'Capybara::Session', TestSessions::Chrome, CHROME_REMOTE_DRIVER - include_examples Capybara::RSpecMatchers, TestSessions::Chrome, CHROME_REMOTE_DRIVER + ['Capybara::Session', 'Capybara::Node', Capybara::RSpecMatchers].each do |examples| + include_examples examples, TestSessions::Chrome, CHROME_REMOTE_DRIVER + end it 'is considered to be chrome' do expect(session.driver.browser.browser).to eq :chrome diff --git a/spec/selenium_spec_edge.rb b/spec/selenium_spec_edge.rb index ab2fedc7..d2cdeca1 100644 --- a/spec/selenium_spec_edge.rb +++ b/spec/selenium_spec_edge.rb @@ -3,6 +3,7 @@ require 'spec_helper' require 'selenium-webdriver' require 'shared_selenium_session' +require 'shared_selenium_node' require 'rspec/shared_spec_matchers' Capybara.register_driver :selenium_edge do |app| @@ -27,6 +28,7 @@ end RSpec.describe 'Capybara::Session with Edge', capybara_skip: skipped_tests do include Capybara::SpecHelper - include_examples 'Capybara::Session', TestSessions::SeleniumEdge, :selenium_edge - include_examples Capybara::RSpecMatchers, TestSessions::SeleniumEdge, :selenium_edge + ['Capybara::Session', 'Capybara::Node', Capybara::RSpecMatchers].each do |examples| + include_examples examples, TestSessions::SeleniumEdge, :selenium_edge + end end diff --git a/spec/selenium_spec_firefox.rb b/spec/selenium_spec_firefox.rb index 31b9f8f9..174905cd 100644 --- a/spec/selenium_spec_firefox.rb +++ b/spec/selenium_spec_firefox.rb @@ -3,6 +3,7 @@ require 'spec_helper' require 'selenium-webdriver' require 'shared_selenium_session' +require 'shared_selenium_node' require 'rspec/shared_spec_matchers' browser_options = ::Selenium::WebDriver::Firefox::Options.new @@ -68,8 +69,9 @@ end RSpec.describe 'Capybara::Session with firefox' do # rubocop:disable RSpec/MultipleDescribes include Capybara::SpecHelper - include_examples 'Capybara::Session', TestSessions::SeleniumFirefox, :selenium_firefox - include_examples Capybara::RSpecMatchers, TestSessions::SeleniumFirefox, :selenium_firefox + ['Capybara::Session', 'Capybara::Node', Capybara::RSpecMatchers].each do |examples| + include_examples examples, TestSessions::SeleniumFirefox, :selenium_firefox + end describe 'filling in Firefox-specific date and time fields with keystrokes' do let(:datetime) { Time.new(1983, 6, 19, 6, 30) } @@ -198,13 +200,4 @@ RSpec.describe Capybara::Selenium::Node do expect(session).to have_link('Has been alt control meta') end end - - context '#send_keys' do - it 'should process space' do - session = TestSessions::SeleniumFirefox - session.visit('/form') - session.find(:css, '#address1_city').send_keys('ocean', [:shift, :space, 'side']) - expect(session.find(:css, '#address1_city').value).to eq 'ocean SIDE' - end - end end diff --git a/spec/selenium_spec_firefox_remote.rb b/spec/selenium_spec_firefox_remote.rb index 4c8c8a97..9a77fba7 100644 --- a/spec/selenium_spec_firefox_remote.rb +++ b/spec/selenium_spec_firefox_remote.rb @@ -3,6 +3,7 @@ require 'spec_helper' require 'selenium-webdriver' require 'shared_selenium_session' +require 'shared_selenium_node' require 'rspec/shared_spec_matchers' def selenium_host @@ -77,8 +78,9 @@ end RSpec.describe 'Capybara::Session with remote firefox' do include Capybara::SpecHelper - include_examples 'Capybara::Session', TestSessions::RemoteFirefox, FIREFOX_REMOTE_DRIVER - include_examples Capybara::RSpecMatchers, TestSessions::RemoteFirefox, FIREFOX_REMOTE_DRIVER + ['Capybara::Session', 'Capybara::Node', Capybara::RSpecMatchers].each do |examples| + include_examples examples, TestSessions::RemoteFirefox, FIREFOX_REMOTE_DRIVER + end it 'is considered to be firefox' do expect(session.driver.browser.browser).to eq :firefox diff --git a/spec/selenium_spec_ie.rb b/spec/selenium_spec_ie.rb index edd1e86a..4ba82de9 100644 --- a/spec/selenium_spec_ie.rb +++ b/spec/selenium_spec_ie.rb @@ -3,6 +3,7 @@ require 'spec_helper' require 'selenium-webdriver' require 'shared_selenium_session' +require 'shared_selenium_node' require 'rspec/shared_spec_matchers' if ENV['CI'] @@ -114,8 +115,9 @@ end RSpec.describe 'Capybara::Session with Internet Explorer', capybara_skip: skipped_tests do # rubocop:disable RSpec/MultipleDescribes include Capybara::SpecHelper - include_examples 'Capybara::Session', TestSessions::SeleniumIE, :selenium_ie - include_examples Capybara::RSpecMatchers, TestSessions::SeleniumIE, :selenium_ie + ['Capybara::Session', 'Capybara::Node', Capybara::RSpecMatchers].each do |examples| + include_examples examples, TestSessions::SeleniumIE, :selenium_ie + end end RSpec.describe Capybara::Selenium::Node do diff --git a/spec/selenium_spec_safari.rb b/spec/selenium_spec_safari.rb index e9328a1d..c797299f 100644 --- a/spec/selenium_spec_safari.rb +++ b/spec/selenium_spec_safari.rb @@ -3,6 +3,7 @@ require 'spec_helper' require 'selenium-webdriver' require 'shared_selenium_session' +require 'shared_selenium_node' require 'rspec/shared_spec_matchers' SAFARI_DRIVER = :selenium_safari @@ -77,8 +78,9 @@ 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 + ['Capybara::Session', 'Capybara::Node', Capybara::RSpecMatchers].each do |examples| + include_examples examples, TestSessions::Safari, SAFARI_DRIVER + end context 'storage' do describe '#reset!' do diff --git a/spec/shared_selenium_node.rb b/spec/shared_selenium_node.rb new file mode 100644 index 00000000..04040028 --- /dev/null +++ b/spec/shared_selenium_node.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'selenium-webdriver' + +RSpec.shared_examples 'Capybara::Node' do |session, _mode| + let(:session) { session } + + context '#content_editable?' do + it 'returns true when the element is content editable' do + session.visit('/with_js') + expect(session.find(:css, '#existing_content_editable').base.content_editable?).to be true + expect(session.find(:css, '#existing_content_editable_child').base.content_editable?).to be true + end + + it 'returns false when the element is not content editable' do + session.visit('/with_js') + expect(session.find(:css, '#drag').base.content_editable?).to be false + end + end + + context '#send_keys' do + it 'should process space' do + session.visit('/form') + session.find(:css, '#address1_city').send_keys('ocean', [:shift, :space, 'side']) + expect(session.find(:css, '#address1_city').value).to eq 'ocean SIDE' + end + end +end