mirror of
https://github.com/teamcapybara/capybara.git
synced 2022-11-09 12:08:07 -05:00
Add smaller isDisplayed and getAttribute atoms
This commit is contained in:
parent
62230c7e0b
commit
7a410b5256
18 changed files with 710 additions and 24 deletions
14
Rakefile
14
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?"
|
||||
|
|
|
@ -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'
|
||||
|
|
1
lib/capybara/selenium/atoms/getAttribute.min.js
vendored
Normal file
1
lib/capybara/selenium/atoms/getAttribute.min.js
vendored
Normal 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}})()
|
1
lib/capybara/selenium/atoms/isDisplayed.min.js
vendored
Normal file
1
lib/capybara/selenium/atoms/isDisplayed.min.js
vendored
Normal 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)}})()
|
161
lib/capybara/selenium/atoms/src/getAttribute.js
Normal file
161
lib/capybara/selenium/atoms/src/getAttribute.js
Normal 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;
|
||||
};
|
||||
})()
|
454
lib/capybara/selenium/atoms/src/isDisplayed.js
Normal file
454
lib/capybara/selenium/atoms/src/isDisplayed.js
Normal 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);
|
||||
};
|
||||
})()
|
|
@ -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/)
|
||||
|
|
|
@ -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)
|
||||
|
|
18
lib/capybara/selenium/patches/atoms.rb
Normal file
18
lib/capybara/selenium/patches/atoms.rb
Normal 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']
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
29
spec/shared_selenium_node.rb
Normal file
29
spec/shared_selenium_node.rb
Normal 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
|
Loading…
Reference in a new issue