Add smaller isDisplayed and getAttribute atoms

This commit is contained in:
Thomas Walpole 2019-05-02 18:57:14 -07:00
parent 62230c7e0b
commit 7a410b5256
18 changed files with 710 additions and 24 deletions

View File

@ -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?"

View File

@ -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'

View File

@ -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}})()

View File

@ -0,0 +1 @@
(function(){function f(t,e,n){function r(t){var e=x(t);if(0<e.height&&0<e.width)return!0;if("PATH"==t.tagName.toUpperCase()&&(0<e.height||0<e.width)){var n=window.getComputedStyle(t)["stroke-width"];return!!n&&0<parseInt(n,10)}return"hidden"!=window.getComputedStyle(t).overflow&&Array.prototype.slice.call(t.childNodes).some(function(t){return t.nodeType==Node.TEXT_NODE||t.nodeType==Node.ELEMENT_NODE&&r(t)})}function i(t){return C(t)==T.HIDDEN&&Array.prototype.slice.call(t.childNodes).every(function(t){return t.nodeType!=Node.ELEMENT_NODE||i(t)||!r(t)})}var o=t.tagName.toUpperCase();if("BODY"==o)return!0;var a=D(t);if(a&&a.tagName&&"DETAILS"==a.tagName.toUpperCase()&&!a.open&&"SUMMARY"!=o)return!1;if("OPTION"==o||"OPTGROUP"==o){var u=v(t,function(t){return"SELECT"==t.tagName.toUpperCase()});return!!u&&f(u,!0,n)}var l=c(t);if(l)return!!l.image&&0<l.rect.width&&0<l.rect.height&&f(l.image,e,n);if("INPUT"==o&&"hidden"==t.type.toLowerCase())return!1;if("NOSCRIPT"==o)return!1;var d=window.getComputedStyle(t).visibility;return"collapse"!=d&&"hidden"!=d&&(!!n(t)&&(!(!e&&0==h(t))&&(!!r(t)&&!i(t))))}function E(t){var e=x(t);return{left:e.left,right:e.left+e.width,top:e.top,bottom:e.top+e.height}}function D(t){return t.parentElement}function C(t){function e(t){function e(t){if(t==u)return!0;var e=window.getComputedStyle(t),n=e.display;return 0!=n.indexOf("inline")&&"contents"!=n&&("absolute"!=r||"static"!=e.position)}var r=window.getComputedStyle(t).position;if("fixed"==r)return i=!0,t==u?null:u;for(var n=D(t);n&&!e(n);)n=D(n);return n}function n(t){var e=t;if("visible"==d)if(t==u&&l)e=l;else if(t==l)return{x:"visible",y:"visible"};var n=window.getComputedStyle(e),r={x:n["overflow-x"],y:n["overflow-y"]};return t==u&&(r.x="visible"==r.x?"auto":r.x,r.y="visible"==r.y?"auto":r.y),r}function r(t){return t==u?{x:window.scrollX,y:window.scrollY}:{x:t.scrollLeft,y:t.scrollTop}}for(var i,o=E(t),a=t.ownerDocument,u=a.documentElement,l=a.body,d=window.getComputedStyle(u).overflow,f=e(t);f;f=e(f)){var h=n(f);if("visible"!=h.x||"visible"!=h.y){var s=x(f);if(0==s.width||0==s.height)return T.HIDDEN;var p=o.right<s.left,c=o.bottom<s.top;if(p&&"hidden"==h.x||c&&"hidden"==h.y)return T.HIDDEN;if(p&&"visible"!=h.x||c&&"visible"!=h.y){var v=r(f),g=o.right<s.left-v.x,w=o.bottom<s.top-v.y;return g&&"visible"!=h.x||w&&"visible"!=h.x?T.HIDDEN:C(f)==T.HIDDEN?T.HIDDEN:T.SCROLL}var N=o.left>=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<n.length){for(var l=n[0],d=n[1],f=l,h=d,s=2;s+1<n.length;s+=2)l=Math.min(l,n[s]),f=Math.max(f,n[s]),d=Math.min(d,n[s+1]),h=Math.max(h,n[s+1]);return p(l,d,f-l,h-d)}return p(0,0,0,0)}function c(t){var e=t.tagName.toUpperCase(),n="MAP"==e;if(!n&&"AREA"!=e)return null;var r=n?t:"MAP"==D(t).tagName.toUpperCase()?D(t):null,i=null,o=null;if(r&&r.name&&((i=r.ownerDocument.querySelector("*[usemap='#"+r.name+"']"))&&(o=x(i),!n&&"default"!=t.shape.toLowerCase()))){var a=s(t),u=Math.min(Math.max(a.left,0),o.width),l=Math.min(Math.max(a.top,0),o.height),d=Math.min(a.width,o.width-u),f=Math.min(a.height,o.height-l);o=p(u+o.left,l+o.top,d,f)}return{image:i,rect:o||p(0,0,0,0)}}function v(t,e){for(t&&(t=D(t));t;){if(e(t))return t;t=D(t)}return null}function r(t){var e=t.parentNode;if(e&&e.shadowRoot&&t.assignedSlot!==undefined)return t.assignedSlot?t.assignedSlot.parentNode:null;if(t.getDestinationInsertionPoints){var n=t.getDestinationInsertionPoints();if(0<n.length)return n[n.length-1]}return e}var T={NONE:"none",HIDDEN:"hidden",SCROLL:"scroll"};return function i(t,e){function n(t){if("none"==window.getComputedStyle(t).display)return!1;var e=r(t);if("function"==typeof ShadowRoot&&e instanceof ShadowRoot){if(e.host.shadowRoot!==e)return!1;e=e.host}return!(!e||e.nodeType!=Node.DOCUMENT_NODE&&e.nodeType!=Node.DOCUMENT_FRAGMENT_NODE)||e&&n(e)}return f(t,!!e,n)}})()

View File

@ -0,0 +1,161 @@
(function(){
var BOOLEAN_PROPERTIES = [
"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"
];
var PROPERTY_ALIASES = {
"class": "className",
"readonly": "readOnly"
};
function isSelectable(element){
var tagName = element.tagName.toUpperCase();
if (tagName == "OPTION"){
return true;
}
if (tagName == "INPUT") {
var type = element.type.toLowerCase();
return type == "checkbox" || type == "radio";
}
return false;
}
function isSelected(element){
var propertyName = "selected";
var type = element.type && element.type.toLowerCase();
if ("checkbox" == type || "radio" == type) {
propertyName = "checked";
}
return !!element[propertyName];
}
function getAttributeValue(element, name){
var attr = element.getAttributeNode(name);
return (attr && attr.specified) ? attr.value : null;
}
return function get(element, attribute){
var value = null;
var name = attribute.toLowerCase();
if ("style" == name) {
value = element.style;
if (value && (typeof value != "string")) {
value = value.cssText;
}
return value;
}
if (("selected" == name || "checked" == name) &&
isSelectable(element)) {
return isSelected(element) ? "true" : null;
}
tagName = element.tagName.toUpperCase();
// The property is consistent. Return that in preference to the attribute for links and images.
if (((tagName == "IMG") && name == "src") ||
((tagName == "A") && name == "href")) {
value = getAttributeValue(element, name);
if (value) {
// We want the full URL if present
value = element[name];
}
return value;
}
if ("spellcheck" == name) {
value = getAttributeValue(element, name);
if (!value === null) {
if (value.toLowerCase() == "false") {
return "false";
} else if (value.toLowerCase() == "true") {
return "true";
}
}
// coerce the property value to a string
return element[name] + "";
}
var propName = PROPERTY_ALIASES[attribute] || attribute;
if (BOOLEAN_PROPERTIES.some(function(prop){ prop == name })) {
value = getAttributeValue(element, name);
value = !(value === null) || element[propName];
return value ? "true" : null;
}
var property;
try {
property = element[propName]
} catch (e) {
// Leaves property undefined or null
}
// 1- Call getAttribute if getProperty fails,
// i.e. property is null or undefined.
// This happens for event handlers in Firefox.
// For example, calling getProperty for 'onclick' would
// fail while getAttribute for 'onclick' will succeed and
// return the JS code of the handler.
//
// 2- When property is an object we fall back to the
// actual attribute instead.
// See issue http://code.google.com/p/selenium/issues/detail?id=966
if ((property == null) || (typeof property == "object") || (typeof property == "function")) {
value = getAttributeValue(element, attribute);
} else {
value = property;
};
// The empty string is a valid return value.
return value != null ? value.toString() : null;
};
})()

View File

@ -0,0 +1,454 @@
(function(){
var OverflowState = {
NONE: "none",
HIDDEN: "hidden",
SCROLL: "scroll"
};
function isShown_(elem, ignoreOpacity, parentsDisplayedFn) {
// By convention, BODY element is always shown: BODY represents the document
// and even if there's nothing rendered in there, user can always see there's
// the document.
var elemTagName = elem.tagName.toUpperCase();
if (elemTagName == "BODY") {
return true;
}
// Child of DETAILS element is not shown unless the DETAILS element is open
// or the child is a SUMMARY element.
var parent = getParentElement(elem);
if (parent && parent.tagName && (parent.tagName.toUpperCase() == "DETAILS") &&
!parent.open && !(elemTagName == "SUMMARY")) {
return false;
}
// Option or optgroup is shown if enclosing select is shown (ignoring the
// select's opacity).
if ((elemTagName == "OPTION") ||
(elemTagName == "OPTGROUP")) {
var select = getAncestor(elem, function(e) {
return e.tagName.toUpperCase() == "SELECT";
});
return !!select && isShown_(select, true, parentsDisplayedFn);
}
// Image map elements are shown if image that uses it is shown, and
// the area of the element is positive.
var imageMap = maybeFindImageMap_(elem);
if (imageMap) {
return !!imageMap.image &&
imageMap.rect.width > 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 <html> 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 <html> 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 <html> 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 <map> or <area>, return null indicating so.
var elemTagName = elem.tagName.toUpperCase();
var isMap = elemTagName == "MAP";
if (!isMap && (elemTagName != "AREA")) {
return null;
}
// Get the <map> 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);
};
})()

View File

@ -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/)

View File

@ -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)

View File

@ -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']

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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