diff --git a/packages/driver/src/dom/coordinates.js b/packages/driver/src/dom/coordinates.js index 17445b7fde47..45a3df7b83d3 100644 --- a/packages/driver/src/dom/coordinates.js +++ b/packages/driver/src/dom/coordinates.js @@ -1,204 +1,224 @@ -$window = require("./window") +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS205: Consider reworking code to avoid use of IIFEs + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const $window = require("./window"); -getElementAtPointFromViewport = (doc, x, y) -> - doc.elementFromPoint(x, y) +const getElementAtPointFromViewport = (doc, x, y) => doc.elementFromPoint(x, y); -getElementPositioning = ($el) -> - el = $el[0] +const getElementPositioning = function($el) { + const el = $el[0]; - win = $window.getWindowByElement(el) + const win = $window.getWindowByElement(el); - ## properties except for width / height - ## are relative to the top left of the viewport - rect = el.getBoundingClientRect() + //# properties except for width / height + //# are relative to the top left of the viewport + const rect = el.getBoundingClientRect(); - center = getCenterCoordinates(rect) + const center = getCenterCoordinates(rect); - ## add the center coordinates - ## because its useful to any caller - topCenter = center.y - leftCenter = center.x + //# add the center coordinates + //# because its useful to any caller + const topCenter = center.y; + const leftCenter = center.x; return { - scrollTop: el.scrollTop - scrollLeft: el.scrollLeft - width: rect.width - height: rect.height + scrollTop: el.scrollTop, + scrollLeft: el.scrollLeft, + width: rect.width, + height: rect.height, fromViewport: { - top: rect.top - left: rect.left - right: rect.right - bottom: rect.bottom - topCenter + top: rect.top, + left: rect.left, + right: rect.right, + bottom: rect.bottom, + topCenter, leftCenter - } + }, fromWindow: { - top: rect.top + win.pageYOffset - left: rect.left + win.pageXOffset - topCenter: topCenter + win.pageYOffset + top: rect.top + win.pageYOffset, + left: rect.left + win.pageXOffset, + topCenter: topCenter + win.pageYOffset, leftCenter: leftCenter + win.pageXOffset } - } - -getCoordsByPosition = (left, top, xPosition = "center", yPosition = "center") -> - left = switch xPosition - when "left" then Math.ceil(left) - when "center" then Math.floor(left) - when "right" then Math.floor(left) - 1 - - top = switch yPosition - when "top" then Math.ceil(top) - when "center" then Math.floor(top) - when "bottom" then Math.floor(top) - 1 - - ## returning x/y here because this is - ## about the target position we want - ## to fire the event at based on what - ## the desired xPosition and yPosition is + }; +}; + +const getCoordsByPosition = function(left, top, xPosition = "center", yPosition = "center") { + left = (() => { switch (xPosition) { + case "left": return Math.ceil(left); + case "center": return Math.floor(left); + case "right": return Math.floor(left) - 1; + } })(); + + top = (() => { switch (yPosition) { + case "top": return Math.ceil(top); + case "center": return Math.floor(top); + case "bottom": return Math.floor(top) - 1; + } })(); + + //# returning x/y here because this is + //# about the target position we want + //# to fire the event at based on what + //# the desired xPosition and yPosition is return { - x: left + x: left, y: top - } - -getTopLeftCoordinates = (rect) -> - x = rect.left - y = rect.top - getCoordsByPosition(x, y, "left", "top") - -getTopCoordinates = (rect) -> - x = rect.left + rect.width / 2 - y = rect.top - getCoordsByPosition(x, y, "center", "top") - -getTopRightCoordinates = (rect) -> - x = rect.left + rect.width - y = rect.top - getCoordsByPosition(x, y, "right", "top") - -getLeftCoordinates = (rect) -> - x = rect.left - y = rect.top + rect.height / 2 - getCoordsByPosition(x, y, "left", "center") - -getCenterCoordinates = (rect) -> - x = rect.left + rect.width / 2 - y = rect.top + rect.height / 2 - getCoordsByPosition(x, y, "center", "center") - -getRightCoordinates = (rect) -> - x = rect.left + rect.width - y = rect.top + rect.height / 2 - getCoordsByPosition(x, y, "right", "center") - -getBottomLeftCoordinates = (rect) -> - x = rect.left - y = rect.top + rect.height - getCoordsByPosition(x, y, "left", "bottom") - -getBottomCoordinates = (rect) -> - x = rect.left + rect.width / 2 - y = rect.top + rect.height - getCoordsByPosition(x, y, "center", "bottom") - -getBottomRightCoordinates = (rect) -> - x = rect.left + rect.width - y = rect.top + rect.height - getCoordsByPosition(x, y, "right", "bottom") - -getElementCoordinatesByPositionRelativeToXY = ($el, x, y) -> - positionProps = getElementPositioning($el) - - { fromViewport, fromWindow } = positionProps - - fromViewport.left += x - fromViewport.top += y - - fromWindow.left += x - fromWindow.top += y - - viewportTargetCoords = getTopLeftCoordinates(fromViewport) - windowTargetCoords = getTopLeftCoordinates(fromWindow) - - fromViewport.x = viewportTargetCoords.x - fromViewport.y = viewportTargetCoords.y - - fromWindow.x = windowTargetCoords.x - fromWindow.y = windowTargetCoords.y - - return positionProps - -getElementCoordinatesByPosition = ($el, position = "center") -> - positionProps = getElementPositioning($el) - - ## get the coordinates from the window - ## but also from the viewport so - ## whoever is calling us can use it - ## however they'd like - { width, height, fromViewport, fromWindow } = positionProps - - ## dynamically call the function by transforming the name - ## bottom -> getBottomCoordinates - ## topLeft -> getTopLeftCoordinates - capitalizedPosition = position.charAt(0).toUpperCase() + position.slice(1) - - fnName = "get" + capitalizedPosition + "Coordinates" - - fn = calculations[fnName] - - ## get the desired x/y coords based on - ## what position we're trying to target - viewportTargetCoords = fn({ - width - height - top: fromViewport.top + }; +}; + +const getTopLeftCoordinates = function(rect) { + const x = rect.left; + const y = rect.top; + return getCoordsByPosition(x, y, "left", "top"); +}; + +const getTopCoordinates = function(rect) { + const x = rect.left + (rect.width / 2); + const y = rect.top; + return getCoordsByPosition(x, y, "center", "top"); +}; + +const getTopRightCoordinates = function(rect) { + const x = rect.left + rect.width; + const y = rect.top; + return getCoordsByPosition(x, y, "right", "top"); +}; + +const getLeftCoordinates = function(rect) { + const x = rect.left; + const y = rect.top + (rect.height / 2); + return getCoordsByPosition(x, y, "left", "center"); +}; + +var getCenterCoordinates = function(rect) { + const x = rect.left + (rect.width / 2); + const y = rect.top + (rect.height / 2); + return getCoordsByPosition(x, y, "center", "center"); +}; + +const getRightCoordinates = function(rect) { + const x = rect.left + rect.width; + const y = rect.top + (rect.height / 2); + return getCoordsByPosition(x, y, "right", "center"); +}; + +const getBottomLeftCoordinates = function(rect) { + const x = rect.left; + const y = rect.top + rect.height; + return getCoordsByPosition(x, y, "left", "bottom"); +}; + +const getBottomCoordinates = function(rect) { + const x = rect.left + (rect.width / 2); + const y = rect.top + rect.height; + return getCoordsByPosition(x, y, "center", "bottom"); +}; + +const getBottomRightCoordinates = function(rect) { + const x = rect.left + rect.width; + const y = rect.top + rect.height; + return getCoordsByPosition(x, y, "right", "bottom"); +}; + +const getElementCoordinatesByPositionRelativeToXY = function($el, x, y) { + const positionProps = getElementPositioning($el); + + const { fromViewport, fromWindow } = positionProps; + + fromViewport.left += x; + fromViewport.top += y; + + fromWindow.left += x; + fromWindow.top += y; + + const viewportTargetCoords = getTopLeftCoordinates(fromViewport); + const windowTargetCoords = getTopLeftCoordinates(fromWindow); + + fromViewport.x = viewportTargetCoords.x; + fromViewport.y = viewportTargetCoords.y; + + fromWindow.x = windowTargetCoords.x; + fromWindow.y = windowTargetCoords.y; + + return positionProps; +}; + +const getElementCoordinatesByPosition = function($el, position = "center") { + const positionProps = getElementPositioning($el); + + //# get the coordinates from the window + //# but also from the viewport so + //# whoever is calling us can use it + //# however they'd like + const { width, height, fromViewport, fromWindow } = positionProps; + + //# dynamically call the function by transforming the name + //# bottom -> getBottomCoordinates + //# topLeft -> getTopLeftCoordinates + const capitalizedPosition = position.charAt(0).toUpperCase() + position.slice(1); + + const fnName = `get${capitalizedPosition}Coordinates`; + + const fn = calculations[fnName]; + + //# get the desired x/y coords based on + //# what position we're trying to target + const viewportTargetCoords = fn({ + width, + height, + top: fromViewport.top, left: fromViewport.left - }) - - ## get the desired x/y coords based on - ## what position we're trying to target - windowTargetCoords = fn({ - width - height - top: fromWindow.top + }); + + //# get the desired x/y coords based on + //# what position we're trying to target + const windowTargetCoords = fn({ + width, + height, + top: fromWindow.top, left: fromWindow.left - }) + }); - fromViewport.x = viewportTargetCoords.x - fromViewport.y = viewportTargetCoords.y + fromViewport.x = viewportTargetCoords.x; + fromViewport.y = viewportTargetCoords.y; - fromWindow.x = windowTargetCoords.x - fromWindow.y = windowTargetCoords.y + fromWindow.x = windowTargetCoords.x; + fromWindow.y = windowTargetCoords.y; - ## return an object with both sets - ## of normalized coordinates for both - ## the window and the viewport + //# return an object with both sets + //# of normalized coordinates for both + //# the window and the viewport return { - width - height - fromViewport + width, + height, + fromViewport, fromWindow - } - -calculations = { - getTopCoordinates - getTopLeftCoordinates - getTopRightCoordinates - getLeftCoordinates - getCenterCoordinates - getRightCoordinates - getBottomLeftCoordinates - getBottomCoordinates + }; +}; + +var calculations = { + getTopCoordinates, + getTopLeftCoordinates, + getTopRightCoordinates, + getLeftCoordinates, + getCenterCoordinates, + getRightCoordinates, + getBottomLeftCoordinates, + getBottomCoordinates, getBottomRightCoordinates -} +}; module.exports = { - getCoordsByPosition + getCoordsByPosition, - getElementPositioning + getElementPositioning, - getElementAtPointFromViewport + getElementAtPointFromViewport, - getElementCoordinatesByPosition + getElementCoordinatesByPosition, getElementCoordinatesByPositionRelativeToXY -} +}; diff --git a/packages/driver/src/dom/document.js b/packages/driver/src/dom/document.js index 7d747f913b9f..3b789d6da2f4 100644 --- a/packages/driver/src/dom/document.js +++ b/packages/driver/src/dom/document.js @@ -1,30 +1,41 @@ -$jquery = require("./jquery") - -docNode = Node.DOCUMENT_NODE - -isDocument = (obj) -> - try - if $jquery.isJquery(obj) - obj = obj[0] - - Boolean(obj and obj.nodeType is docNode) - catch - false - -hasActiveWindow = (doc) -> - ## does this document have a currently active window (defaultView) - return !!doc.defaultView - -getDocumentFromElement = (el) -> - if isDocument(el) - return el - - el.ownerDocument +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const $jquery = require("./jquery"); + +const docNode = Node.DOCUMENT_NODE; + +const isDocument = function(obj) { + try { + if ($jquery.isJquery(obj)) { + obj = obj[0]; + } + + return Boolean(obj && (obj.nodeType === docNode)); + } catch (error) { + return false; + } +}; + +const hasActiveWindow = doc => + //# does this document have a currently active window (defaultView) + !!doc.defaultView +; + +const getDocumentFromElement = function(el) { + if (isDocument(el)) { + return el; + } + + return el.ownerDocument; +}; module.exports = { - isDocument + isDocument, - hasActiveWindow + hasActiveWindow, getDocumentFromElement -} +}; diff --git a/packages/driver/src/dom/elements.js b/packages/driver/src/dom/elements.js index 2658e53cc5a1..7a565f069dce 100644 --- a/packages/driver/src/dom/elements.js +++ b/packages/driver/src/dom/elements.js @@ -1,606 +1,682 @@ -_ = require("lodash") -$ = require("jquery") -$jquery = require("./jquery") -$window = require("./window") -$document = require("./document") -$utils = require("../cypress/utils") - -fixedOrStickyRe = /(fixed|sticky)/ - -focusable = "body,a[href],link[href],button,select,[tabindex],input,textarea,[contenteditable]" - -inputTypeNeedSingleValueChangeRe = /^(date|time|month|week)$/ -canSetSelectionRangeElementRe = /^(text|search|URL|tel|password)$/ - -## rules for native methods and props -## if a setter or getter or function then add a native method -## if a traversal, don't - -descriptor = (klass, prop) -> - Object.getOwnPropertyDescriptor(window[klass].prototype, prop) - -_getValue = -> - switch - when isInput(this) - descriptor("HTMLInputElement", "value").get - when isTextarea(this) - descriptor("HTMLTextAreaElement", "value").get - when isSelect(this) - descriptor("HTMLSelectElement", "value").get - when isButton(this) - descriptor("HTMLButtonElement", "value").get - else - ## is an option element - descriptor("HTMLOptionElement", "value").get - -_setValue = -> - switch - when isInput(this) - descriptor("HTMLInputElement", "value").set - when isTextarea(this) - descriptor("HTMLTextAreaElement", "value").set - when isSelect(this) - descriptor("HTMLSelectElement", "value").set - when isButton(this) - descriptor("HTMLButtonElement", "value").set - else - ## is an options element - descriptor("HTMLOptionElement", "value").set - -_getSelectionStart = -> - switch - when isInput(this) - descriptor('HTMLInputElement', 'selectionStart').get - when isTextarea(this) - descriptor('HTMLTextAreaElement', 'selectionStart').get - -_getSelectionEnd = -> - switch - when isInput(this) - descriptor('HTMLInputElement', 'selectionEnd').get - when isTextarea(this) - descriptor('HTMLTextAreaElement', 'selectionEnd').get - -_nativeFocus = -> - switch - when $window.isWindow(this) - window.focus - when isSvg(this) - window.SVGElement.prototype.focus - else - window.HTMLElement.prototype.focus - -_nativeBlur = -> - switch - when $window.isWindow(this) - window.blur - when isSvg(this) - window.SVGElement.prototype.blur - else - window.HTMLElement.prototype.blur - -_nativeSetSelectionRange = -> - switch - when isInput(this) - window.HTMLInputElement.prototype.setSelectionRange - else - ## is textarea - window.HTMLTextAreaElement.prototype.setSelectionRange - -_nativeSelect = -> - switch - when isInput(this) - window.HTMLInputElement.prototype.select - else - ## is textarea - window.HTMLTextAreaElement.prototype.select - -_isContentEditable = -> - switch - when isSvg(this) - false - else - descriptor("HTMLElement", "isContentEditable").get - -_setType = -> - switch - when isInput(this) - descriptor("HTMLInputElement", "type").set - when isButton(this) - descriptor("HTMLButtonElement", "type").set - - -_getType = -> - switch - when isInput(this) - descriptor("HTMLInputElement", "type").get - when isButton(this) - descriptor("HTMLButtonElement", "type").get - -nativeGetters = { - value: _getValue - selectionStart: descriptor("HTMLInputElement", "selectionStart").get - isContentEditable: _isContentEditable - isCollapsed: descriptor("Selection", 'isCollapsed').get - selectionStart: _getSelectionStart - selectionEnd: _getSelectionEnd +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); +const $ = require("jquery"); +const $jquery = require("./jquery"); +const $window = require("./window"); +const $document = require("./document"); +const $utils = require("../cypress/utils"); + +const fixedOrStickyRe = /(fixed|sticky)/; + +const focusable = "body,a[href],link[href],button,select,[tabindex],input,textarea,[contenteditable]"; + +const inputTypeNeedSingleValueChangeRe = /^(date|time|month|week)$/; +const canSetSelectionRangeElementRe = /^(text|search|URL|tel|password)$/; + +//# rules for native methods and props +//# if a setter or getter or function then add a native method +//# if a traversal, don't + +const descriptor = (klass, prop) => Object.getOwnPropertyDescriptor(window[klass].prototype, prop); + +const _getValue = function() { + switch (false) { + case !isInput(this): + return descriptor("HTMLInputElement", "value").get; + case !isTextarea(this): + return descriptor("HTMLTextAreaElement", "value").get; + case !isSelect(this): + return descriptor("HTMLSelectElement", "value").get; + case !isButton(this): + return descriptor("HTMLButtonElement", "value").get; + default: + //# is an option element + return descriptor("HTMLOptionElement", "value").get; + } +}; + +const _setValue = function() { + switch (false) { + case !isInput(this): + return descriptor("HTMLInputElement", "value").set; + case !isTextarea(this): + return descriptor("HTMLTextAreaElement", "value").set; + case !isSelect(this): + return descriptor("HTMLSelectElement", "value").set; + case !isButton(this): + return descriptor("HTMLButtonElement", "value").set; + default: + //# is an options element + return descriptor("HTMLOptionElement", "value").set; + } +}; + +const _getSelectionStart = function() { + switch (false) { + case !isInput(this): + return descriptor('HTMLInputElement', 'selectionStart').get; + case !isTextarea(this): + return descriptor('HTMLTextAreaElement', 'selectionStart').get; + } +}; + +const _getSelectionEnd = function() { + switch (false) { + case !isInput(this): + return descriptor('HTMLInputElement', 'selectionEnd').get; + case !isTextarea(this): + return descriptor('HTMLTextAreaElement', 'selectionEnd').get; + } +}; + +const _nativeFocus = function() { + switch (false) { + case !$window.isWindow(this): + return window.focus; + case !isSvg(this): + return window.SVGElement.prototype.focus; + default: + return window.HTMLElement.prototype.focus; + } +}; + +const _nativeBlur = function() { + switch (false) { + case !$window.isWindow(this): + return window.blur; + case !isSvg(this): + return window.SVGElement.prototype.blur; + default: + return window.HTMLElement.prototype.blur; + } +}; + +const _nativeSetSelectionRange = function() { + switch (false) { + case !isInput(this): + return window.HTMLInputElement.prototype.setSelectionRange; + default: + //# is textarea + return window.HTMLTextAreaElement.prototype.setSelectionRange; + } +}; + +const _nativeSelect = function() { + switch (false) { + case !isInput(this): + return window.HTMLInputElement.prototype.select; + default: + //# is textarea + return window.HTMLTextAreaElement.prototype.select; + } +}; + +const _isContentEditable = function() { + switch (false) { + case !isSvg(this): + return false; + default: + return descriptor("HTMLElement", "isContentEditable").get; + } +}; + +const _setType = function() { + switch (false) { + case !isInput(this): + return descriptor("HTMLInputElement", "type").set; + case !isButton(this): + return descriptor("HTMLButtonElement", "type").set; + } +}; + + +const _getType = function() { + switch (false) { + case !isInput(this): + return descriptor("HTMLInputElement", "type").get; + case !isButton(this): + return descriptor("HTMLButtonElement", "type").get; + } +}; + +const nativeGetters = { + value: _getValue, + selectionStart: descriptor("HTMLInputElement", "selectionStart").get, + isContentEditable: _isContentEditable, + isCollapsed: descriptor("Selection", 'isCollapsed').get, + selectionStart: _getSelectionStart, + selectionEnd: _getSelectionEnd, type: _getType -} +}; -nativeSetters = { - value: _setValue +const nativeSetters = { + value: _setValue, type: _setType -} - -nativeMethods = { - addEventListener: window.EventTarget.prototype.addEventListener - removeEventListener: window.EventTarget.prototype.removeEventListener - createRange: window.document.createRange - getSelection: window.document.getSelection - removeAllRanges: window.Selection.prototype.removeAllRanges - addRange: window.Selection.prototype.addRange - execCommand: window.document.execCommand - getAttribute: window.Element.prototype.getAttribute - setSelectionRange: _nativeSetSelectionRange - modify: window.Selection.prototype.modify - focus: _nativeFocus - blur: _nativeBlur +}; + +const nativeMethods = { + addEventListener: window.EventTarget.prototype.addEventListener, + removeEventListener: window.EventTarget.prototype.removeEventListener, + createRange: window.document.createRange, + getSelection: window.document.getSelection, + removeAllRanges: window.Selection.prototype.removeAllRanges, + addRange: window.Selection.prototype.addRange, + execCommand: window.document.execCommand, + getAttribute: window.Element.prototype.getAttribute, + setSelectionRange: _nativeSetSelectionRange, + modify: window.Selection.prototype.modify, + focus: _nativeFocus, + blur: _nativeBlur, select: _nativeSelect -} - -tryCallNativeMethod = -> - try - callNativeMethod.apply(null, arguments) - catch err - return - -callNativeMethod = (obj, fn, args...) -> - if not nativeFn = nativeMethods[fn] - fns = _.keys(nativeMethods).join(", ") - throw new Error("attempted to use a native fn called: #{fn}. Available fns are: #{fns}") - - retFn = nativeFn.apply(obj, args) - - if _.isFunction(retFn) - retFn = retFn.apply(obj, args) - - return retFn - -getNativeProp = (obj, prop) -> - if not nativeProp = nativeGetters[prop] - props = _.keys(nativeGetters).join(", ") - throw new Error("attempted to use a native getter prop called: #{prop}. Available props are: #{props}") - - retProp = nativeProp.call(obj, prop) - - if _.isFunction(retProp) - ## if we got back another function - ## then invoke it again - retProp = retProp.call(obj, prop) - - return retProp - -setNativeProp = (obj, prop, val) -> - if not nativeProp = nativeSetters[prop] - fns = _.keys(nativeSetters).join(", ") - throw new Error("attempted to use a native setter prop called: #{fn}. Available props are: #{fns}") - - retProp = nativeProp.call(obj, val) - - if _.isFunction(retProp) - retProp = retProp.call(obj, val) - - return retProp - -isNeedSingleValueChangeInputElement = (el) -> - if !isInput(el) - return false - - return inputTypeNeedSingleValueChangeRe.test(el.type) - -canSetSelectionRangeElement = (el) -> - isTextarea(el) or (isInput(el) and canSetSelectionRangeElementRe.test(getNativeProp(el, 'type'))) - -getTagName = (el) -> - tagName = el.tagName or "" - tagName.toLowerCase() - -isContentEditable = (el) -> - ## this property is the tell-all for contenteditable - ## should be true for elements: - ## - with [contenteditable] - ## - with document.designMode = 'on' +}; + +const tryCallNativeMethod = function() { + try { + return callNativeMethod.apply(null, arguments); + } catch (err) { + return; + } +}; + +var callNativeMethod = function(obj, fn, ...args) { + let nativeFn; + if (!(nativeFn = nativeMethods[fn])) { + const fns = _.keys(nativeMethods).join(", "); + throw new Error(`attempted to use a native fn called: ${fn}. Available fns are: ${fns}`); + } + + let retFn = nativeFn.apply(obj, args); + + if (_.isFunction(retFn)) { + retFn = retFn.apply(obj, args); + } + + return retFn; +}; + +const getNativeProp = function(obj, prop) { + let nativeProp; + if (!(nativeProp = nativeGetters[prop])) { + const props = _.keys(nativeGetters).join(", "); + throw new Error(`attempted to use a native getter prop called: ${prop}. Available props are: ${props}`); + } + + let retProp = nativeProp.call(obj, prop); + + if (_.isFunction(retProp)) { + //# if we got back another function + //# then invoke it again + retProp = retProp.call(obj, prop); + } + + return retProp; +}; + +const setNativeProp = function(obj, prop, val) { + let nativeProp; + if (!(nativeProp = nativeSetters[prop])) { + const fns = _.keys(nativeSetters).join(", "); + throw new Error(`attempted to use a native setter prop called: ${fn}. Available props are: ${fns}`); + } + + let retProp = nativeProp.call(obj, val); + + if (_.isFunction(retProp)) { + retProp = retProp.call(obj, val); + } + + return retProp; +}; + +const isNeedSingleValueChangeInputElement = function(el) { + if (!isInput(el)) { + return false; + } + + return inputTypeNeedSingleValueChangeRe.test(el.type); +}; + +const canSetSelectionRangeElement = el => isTextarea(el) || (isInput(el) && canSetSelectionRangeElementRe.test(getNativeProp(el, 'type'))); + +const getTagName = function(el) { + const tagName = el.tagName || ""; + return tagName.toLowerCase(); +}; + +const isContentEditable = el => + //# this property is the tell-all for contenteditable + //# should be true for elements: + //# - with [contenteditable] + //# - with document.designMode = 'on' getNativeProp(el, "isContentEditable") +; -isTextarea = (el) -> - getTagName(el) is 'textarea' +var isTextarea = el => getTagName(el) === 'textarea'; -isInput = (el) -> - getTagName(el) is 'input' +var isInput = el => getTagName(el) === 'input'; -isButton = (el) -> - getTagName(el) is 'button' +var isButton = el => getTagName(el) === 'button'; -isSelect = (el) -> - getTagName(el) is 'select' +var isSelect = el => getTagName(el) === 'select'; -isOption = (el) -> - getTagName(el) is 'option' +const isOption = el => getTagName(el) === 'option'; -isBody = (el) -> - getTagName(el) is 'body' +const isBody = el => getTagName(el) === 'body'; -isSvg = (el) -> - try - "ownerSVGElement" of el - catch - false - -isElement = (obj) -> - try - if $jquery.isJquery(obj) - obj = obj[0] - - Boolean(obj and _.isElement(obj)) - catch - false - -isFocusable = ($el) -> - $el.is(focusable) - -isType = ($el, type) -> - el = [].concat($jquery.unwrap($el))[0] - ## NOTE: use DOMElement.type instead of getAttribute('type') since - ## will have type="text", and behaves like text type - elType = (getNativeProp(el, 'type') or "").toLowerCase() - - if _.isArray(type) - return _.includes(type, elType) - - elType is type - -isScrollOrAuto = (prop) -> - prop is "scroll" or prop is "auto" - -isAncestor = ($el, $maybeAncestor) -> - $el.parents().index($maybeAncestor) >= 0 - -isSelector = ($el, selector) -> - $el.is(selector) - -isDetached = ($el) -> - not isAttached($el) - -isAttached = ($el) -> - ## if we're being given window - ## then these are automaticallyed attached - if $window.isWindow($el) - ## there is a code path when forcing focus and - ## blur on the window where this check is necessary. - return true - - ## if this is a document we can simply check - ## whether or not it has a defaultView (window). - ## documents which are part of stale pages - ## will have this property null'd out - if $document.isDocument($el) - return $document.hasActiveWindow($el) - - ## normalize into an array - els = [].concat($jquery.unwrap($el)) - - ## we could be passed an empty array here - ## which in that case it is not attached - if els.length is 0 - return false - - ## get the document from the first element - doc = $document.getDocumentFromElement(els[0]) - - ## TODO: i guess its possible each element - ## is technically bound to a differnet document - ## but c'mon - isIn = (el) -> - $.contains(doc, el) - - ## make sure the document is currently - ## active (it has a window) and - ## make sure every single element - ## is attached to this document - return $document.hasActiveWindow(doc) and _.every(els, isIn) - -isSame = ($el1, $el2) -> - el1 = $jquery.unwrap($el1) - el2 = $jquery.unwrap($el2) - - el1 and el2 and _.isEqual(el1, el2) - -isTextLike = ($el) -> - sel = (selector) -> isSelector($el, selector) - type = (type) -> isType($el, type) - - isContentEditableElement = isContentEditable($el.get(0)) - - _.some([ - isContentEditableElement - sel("textarea") - sel(":text") - type("text") - type("password") - type("email") - type("number") - type("date") - type("week") - type("month") - type("time") - type("datetime") - type("datetime-local") - type("search") - type("url") +var isSvg = function(el) { + try { + return "ownerSVGElement" in el; + } catch (error) { + return false; + } +}; + +const isElement = function(obj) { + try { + if ($jquery.isJquery(obj)) { + obj = obj[0]; + } + + return Boolean(obj && _.isElement(obj)); + } catch (error) { + return false; + } +}; + +const isFocusable = $el => $el.is(focusable); + +const isType = function($el, type) { + const el = [].concat($jquery.unwrap($el))[0]; + //# NOTE: use DOMElement.type instead of getAttribute('type') since + //# will have type="text", and behaves like text type + const elType = (getNativeProp(el, 'type') || "").toLowerCase(); + + if (_.isArray(type)) { + return _.includes(type, elType); + } + + return elType === type; +}; + +const isScrollOrAuto = prop => (prop === "scroll") || (prop === "auto"); + +const isAncestor = ($el, $maybeAncestor) => $el.parents().index($maybeAncestor) >= 0; + +const isSelector = ($el, selector) => $el.is(selector); + +const isDetached = $el => !isAttached($el); + +var isAttached = function($el) { + //# if we're being given window + //# then these are automaticallyed attached + if ($window.isWindow($el)) { + //# there is a code path when forcing focus and + //# blur on the window where this check is necessary. + return true; + } + + //# if this is a document we can simply check + //# whether or not it has a defaultView (window). + //# documents which are part of stale pages + //# will have this property null'd out + if ($document.isDocument($el)) { + return $document.hasActiveWindow($el); + } + + //# normalize into an array + const els = [].concat($jquery.unwrap($el)); + + //# we could be passed an empty array here + //# which in that case it is not attached + if (els.length === 0) { + return false; + } + + //# get the document from the first element + const doc = $document.getDocumentFromElement(els[0]); + + //# TODO: i guess its possible each element + //# is technically bound to a differnet document + //# but c'mon + const isIn = el => $.contains(doc, el); + + //# make sure the document is currently + //# active (it has a window) and + //# make sure every single element + //# is attached to this document + return $document.hasActiveWindow(doc) && _.every(els, isIn); +}; + +const isSame = function($el1, $el2) { + const el1 = $jquery.unwrap($el1); + const el2 = $jquery.unwrap($el2); + + return el1 && el2 && _.isEqual(el1, el2); +}; + +const isTextLike = function($el) { + const sel = selector => isSelector($el, selector); + const type = type => isType($el, type); + + const isContentEditableElement = isContentEditable($el.get(0)); + + return _.some([ + isContentEditableElement, + sel("textarea"), + sel(":text"), + type("text"), + type("password"), + type("email"), + type("number"), + type("date"), + type("week"), + type("month"), + type("time"), + type("datetime"), + type("datetime-local"), + type("search"), + type("url"), type("tel") - ]) - -isScrollable = ($el) -> - checkDocumentElement = (win, documentElement) -> - ## Check if body height is higher than window height - return true if win.innerHeight < documentElement.scrollHeight - - ## Check if body width is higher than window width - return true if win.innerWidth < documentElement.scrollWidth - - ## else return false since the window is not scrollable - return false - - ## if we're the window, we want to get the document's - ## element and check its size against the actual window - switch - when $window.isWindow($el) - win = $el - - checkDocumentElement(win, win.document.documentElement) - else - ## if we're any other element, we do some css calculations - ## to see that the overflow is correct and the scroll - ## area is larger than the actual height or width - el = $el[0] - - {overflow, overflowY, overflowX} = window.getComputedStyle(el) - - ## y axis - ## if our content height is less than the total scroll height - if el.clientHeight < el.scrollHeight - ## and our element has scroll or auto overflow or overflowX - return true if isScrollOrAuto(overflow) or isScrollOrAuto(overflowY) - - ## x axis - if el.clientWidth < el.scrollWidth - return true if isScrollOrAuto(overflow) or isScrollOrAuto(overflowX) - - return false - -isDescendent = ($el1, $el2) -> - return false if not $el2 - - !!(($el1.get(0) is $el2.get(0)) or $el1.has($el2).length) - -## in order to simulate actual user behavior we need to do the following: -## 1. take our element and figure out its center coordinate -## 2. check to figure out the element listed at those coordinates -## 3. if this element is ourself or our descendants, click whatever was returned -## 4. else throw an error because something is covering us up -getFirstFocusableEl = ($el) -> - return $el if isFocusable($el) - - parent = $el.parent() - - ## if we have no parent then just return - ## the window since that can receive focus - if not parent.length - win = $window.getWindowByElement($el.get(0)) - - return $(win) - - getFirstFocusableEl($el.parent()) - -getFirstFixedOrStickyPositionParent = ($el) -> - ## return null if we're at body/html - ## cuz that means nothing has fixed position - return null if not $el or $el.is("body,html") - - ## if we have fixed position return ourselves - if fixedOrStickyRe.test($el.css("position")) - return $el - - ## else recursively continue to walk up the parent node chain - getFirstFixedOrStickyPositionParent($el.parent()) - -getFirstStickyPositionParent = ($el) -> - ## return null if we're at body/html - ## cuz that means nothing has sticky position - return null if not $el or $el.is("body,html") - - ## if we have sticky position return ourselves - if $el.css("position") == "sticky" - return $el - - ## else recursively continue to walk up the parent node chain - getFirstStickyPositionParent($el.parent()) - -getFirstScrollableParent = ($el) -> - # doc = $el.prop("ownerDocument") - - # win = getWindowFromDoc(doc) - - ## this may be null or not even defined in IE - # scrollingElement = doc.scrollingElement - - search = ($el) -> - $parent = $el.parent() - - ## we have no more parents - if not ($parent or $parent.length) - return null - - ## we match the scrollingElement - # if $parent[0] is scrollingElement - # return $parent - - ## instead of fussing with scrollingElement - ## we'll simply return null here and let our - ## caller deal with situations where they're - ## needing to scroll the window or scrollableElement - if $parent.is("html,body") or $document.isDocument($parent) - return null - - if isScrollable($parent) - return $parent - - return search($parent) - - return search($el) - -getElements = ($el) -> - return if not $el?.length - - ## unroll the jquery object - els = $jquery.unwrap($el) - - if els.length is 1 - els[0] - else - els - -getContainsSelector = (text, filter = "") -> - escapedText = $utils.escapeQuotes(text) - "#{filter}:not(script):contains('#{escapedText}'), #{filter}[type='submit'][value~='#{escapedText}']" - -priorityElement = "input[type='submit'], button, a, label" - -getFirstDeepestElement = (elements, index = 0) -> - ## iterate through all of the elements in pairs - ## and check if the next item in the array is a - ## descedent of the current. if it is continue - ## to recurse. if not, or there is no next item - ## then return the current - $current = elements.slice(index, index + 1) - $next = elements.slice(index + 1, index + 2) - - return $current if not $next - - ## does current contain next? - if $.contains($current.get(0), $next.get(0)) - getFirstDeepestElement(elements, index + 1) - else - ## return the current if it already is a priority element - return $current if $current.is(priorityElement) - - ## else once we find the first deepest element then return its priority - ## parent if it has one and it exists in the elements chain - $priorities = elements.filter $current.parents(priorityElement) - if $priorities.length then $priorities.last() else $current - -## short form css-inlines the element -## long form returns the outerHTML -stringify = (el, form = "long") -> - ## if we are formatting the window object - if $window.isWindow(el) - return "" - - ## if we are formatting the document object - if $document.isDocument(el) - return "" - - ## convert this to jquery if its not already one - $el = $jquery.wrap(el) - - switch form - when "long" - text = _.chain($el.text()).clean().truncate({length: 10 }).value() - children = $el.children().length - str = $el.clone().empty().prop("outerHTML") - switch - when children then str.replace(">...#{text} 1 - "[ <#{str}>, #{$el.length - 1} more... ]" - else - "<#{str}>" + ]); +}; + +const isScrollable = function($el) { + const checkDocumentElement = function(win, documentElement) { + //# Check if body height is higher than window height + if (win.innerHeight < documentElement.scrollHeight) { return true; } + + //# Check if body width is higher than window width + if (win.innerWidth < documentElement.scrollWidth) { return true; } + + //# else return false since the window is not scrollable + return false; + }; + + //# if we're the window, we want to get the document's + //# element and check its size against the actual window + switch (false) { + case !$window.isWindow($el): + var win = $el; + + return checkDocumentElement(win, win.document.documentElement); + default: + //# if we're any other element, we do some css calculations + //# to see that the overflow is correct and the scroll + //# area is larger than the actual height or width + var el = $el[0]; + + var {overflow, overflowY, overflowX} = window.getComputedStyle(el); + + //# y axis + //# if our content height is less than the total scroll height + if (el.clientHeight < el.scrollHeight) { + //# and our element has scroll or auto overflow or overflowX + if (isScrollOrAuto(overflow) || isScrollOrAuto(overflowY)) { return true; } + } + + //# x axis + if (el.clientWidth < el.scrollWidth) { + if (isScrollOrAuto(overflow) || isScrollOrAuto(overflowX)) { return true; } + } + + return false; + } +}; + +const isDescendent = function($el1, $el2) { + if (!$el2) { return false; } + + return !!(($el1.get(0) === $el2.get(0)) || $el1.has($el2).length); +}; + +//# in order to simulate actual user behavior we need to do the following: +//# 1. take our element and figure out its center coordinate +//# 2. check to figure out the element listed at those coordinates +//# 3. if this element is ourself or our descendants, click whatever was returned +//# 4. else throw an error because something is covering us up +var getFirstFocusableEl = function($el) { + if (isFocusable($el)) { return $el; } + + const parent = $el.parent(); + + //# if we have no parent then just return + //# the window since that can receive focus + if (!parent.length) { + const win = $window.getWindowByElement($el.get(0)); + + return $(win); + } + + return getFirstFocusableEl($el.parent()); +}; + +var getFirstFixedOrStickyPositionParent = function($el) { + //# return null if we're at body/html + //# cuz that means nothing has fixed position + if (!$el || $el.is("body,html")) { return null; } + + //# if we have fixed position return ourselves + if (fixedOrStickyRe.test($el.css("position"))) { + return $el; + } + + //# else recursively continue to walk up the parent node chain + return getFirstFixedOrStickyPositionParent($el.parent()); +}; + +var getFirstStickyPositionParent = function($el) { + //# return null if we're at body/html + //# cuz that means nothing has sticky position + if (!$el || $el.is("body,html")) { return null; } + + //# if we have sticky position return ourselves + if ($el.css("position") === "sticky") { + return $el; + } + + //# else recursively continue to walk up the parent node chain + return getFirstStickyPositionParent($el.parent()); +}; + +const getFirstScrollableParent = function($el) { + // doc = $el.prop("ownerDocument") + + // win = getWindowFromDoc(doc) + + //# this may be null or not even defined in IE + // scrollingElement = doc.scrollingElement + + var search = function($el) { + const $parent = $el.parent(); + + //# we have no more parents + if (!($parent || $parent.length)) { + return null; + } + + //# we match the scrollingElement + // if $parent[0] is scrollingElement + // return $parent + + //# instead of fussing with scrollingElement + //# we'll simply return null here and let our + //# caller deal with situations where they're + //# needing to scroll the window or scrollableElement + if ($parent.is("html,body") || $document.isDocument($parent)) { + return null; + } + + if (isScrollable($parent)) { + return $parent; + } + + return search($parent); + }; + + return search($el); +}; + +const getElements = function($el) { + if (!($el != null ? $el.length : undefined)) { return; } + + //# unroll the jquery object + const els = $jquery.unwrap($el); + + if (els.length === 1) { + return els[0]; + } else { + return els; + } +}; + +const getContainsSelector = function(text, filter = "") { + const escapedText = $utils.escapeQuotes(text); + return `${filter}:not(script):contains('${escapedText}'), ${filter}[type='submit'][value~='${escapedText}']`; +}; + +const priorityElement = "input[type='submit'], button, a, label"; + +var getFirstDeepestElement = function(elements, index = 0) { + //# iterate through all of the elements in pairs + //# and check if the next item in the array is a + //# descedent of the current. if it is continue + //# to recurse. if not, or there is no next item + //# then return the current + const $current = elements.slice(index, index + 1); + const $next = elements.slice(index + 1, index + 2); + + if (!$next) { return $current; } + + //# does current contain next? + if ($.contains($current.get(0), $next.get(0))) { + return getFirstDeepestElement(elements, index + 1); + } else { + //# return the current if it already is a priority element + if ($current.is(priorityElement)) { return $current; } + + //# else once we find the first deepest element then return its priority + //# parent if it has one and it exists in the elements chain + const $priorities = elements.filter($current.parents(priorityElement)); + if ($priorities.length) { return $priorities.last(); } else { return $current; } + } +}; + +//# short form css-inlines the element +//# long form returns the outerHTML +const stringify = function(el, form = "long") { + //# if we are formatting the window object + let id, klass; + if ($window.isWindow(el)) { + return ""; + } + + //# if we are formatting the document object + if ($document.isDocument(el)) { + return ""; + } + + //# convert this to jquery if its not already one + const $el = $jquery.wrap(el); + + switch (form) { + case "long": + var text = _.chain($el.text()).clean().truncate({length: 10 }).value(); + var children = $el.children().length; + var str = $el.clone().empty().prop("outerHTML"); + switch (false) { + case !children: return str.replace(">...${text} 1) { + return `[ <${str}>, ${$el.length - 1} more... ]`; + } else { + return `<${str}>`; + } + } +}; module.exports = { - isElement + isElement, - isSelector + isSelector, - isScrollOrAuto + isScrollOrAuto, - isFocusable + isFocusable, - isAttached + isAttached, - isDetached + isDetached, - isAncestor + isAncestor, - isScrollable + isScrollable, - isTextLike + isTextLike, - isDescendent + isDescendent, - isContentEditable + isContentEditable, - isSame + isSame, - isBody + isBody, - isInput + isInput, - isTextarea + isTextarea, - isType + isType, - isNeedSingleValueChangeInputElement + isNeedSingleValueChangeInputElement, - canSetSelectionRangeElement + canSetSelectionRangeElement, - stringify + stringify, - getNativeProp + getNativeProp, - setNativeProp + setNativeProp, - callNativeMethod + callNativeMethod, - tryCallNativeMethod + tryCallNativeMethod, - getElements + getElements, - getFirstFocusableEl + getFirstFocusableEl, - getContainsSelector + getContainsSelector, - getFirstDeepestElement + getFirstDeepestElement, - getFirstFixedOrStickyPositionParent + getFirstFixedOrStickyPositionParent, - getFirstStickyPositionParent + getFirstStickyPositionParent, getFirstScrollableParent -} +}; diff --git a/packages/driver/src/dom/index.js b/packages/driver/src/dom/index.js index 636bc3f6e0d7..8fc8ef69bf3e 100644 --- a/packages/driver/src/dom/index.js +++ b/packages/driver/src/dom/index.js @@ -1,88 +1,92 @@ -$jquery = require("./jquery") -$window = require("./window") -$document = require("./document") -$elements = require("./elements") -$visibility = require("./visibility") -$coordinates = require("./coordinates") - -{ isWindow, getWindowByElement } = $window -{ isDocument } = $document -{ wrap, unwrap, isJquery, query } = $jquery -{ isVisible, isHidden, getReasonIsHidden } = $visibility -{ isType, isFocusable, isElement, isScrollable, stringify, getElements, getContainsSelector, getFirstDeepestElement, isDetached, isAttached, isTextLike, isSelector, isDescendent, getFirstFixedOrStickyPositionParent, getFirstStickyPositionParent, getFirstScrollableParent } = $elements -{ getCoordsByPosition, getElementPositioning, getElementCoordinatesByPosition, getElementAtPointFromViewport, getElementCoordinatesByPositionRelativeToXY } = $coordinates - -isDom = (obj) -> - isElement(obj) or isWindow(obj) or isDocument(obj) - -## we are exposing these publicly to be used -## by our own internal code, but also for -## our users. They can use them for debugging -## purposes or for overriding. Everything else -## can be tucked away behind these interfaces. +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const $jquery = require("./jquery"); +const $window = require("./window"); +const $document = require("./document"); +const $elements = require("./elements"); +const $visibility = require("./visibility"); +const $coordinates = require("./coordinates"); + +const { isWindow, getWindowByElement } = $window; +const { isDocument } = $document; +const { wrap, unwrap, isJquery, query } = $jquery; +const { isVisible, isHidden, getReasonIsHidden } = $visibility; +const { isType, isFocusable, isElement, isScrollable, stringify, getElements, getContainsSelector, getFirstDeepestElement, isDetached, isAttached, isTextLike, isSelector, isDescendent, getFirstFixedOrStickyPositionParent, getFirstStickyPositionParent, getFirstScrollableParent } = $elements; +const { getCoordsByPosition, getElementPositioning, getElementCoordinatesByPosition, getElementAtPointFromViewport, getElementCoordinatesByPositionRelativeToXY } = $coordinates; + +const isDom = obj => isElement(obj) || isWindow(obj) || isDocument(obj); + +//# we are exposing these publicly to be used +//# by our own internal code, but also for +//# our users. They can use them for debugging +//# purposes or for overriding. Everything else +//# can be tucked away behind these interfaces. module.exports = { - wrap + wrap, - query + query, - unwrap + unwrap, - isDom + isDom, - isType + isType, - isVisible + isVisible, - isHidden + isHidden, - isFocusable + isFocusable, - isTextLike + isTextLike, - isScrollable + isScrollable, - isDetached + isDetached, - isAttached + isAttached, - isSelector + isSelector, - isDescendent + isDescendent, - isElement + isElement, - isDocument + isDocument, - isWindow + isWindow, - isJquery + isJquery, - stringify + stringify, - getElements + getElements, - getContainsSelector + getContainsSelector, - getFirstDeepestElement + getFirstDeepestElement, - getWindowByElement + getWindowByElement, - getReasonIsHidden + getReasonIsHidden, - getFirstScrollableParent + getFirstScrollableParent, - getFirstFixedOrStickyPositionParent + getFirstFixedOrStickyPositionParent, - getFirstStickyPositionParent + getFirstStickyPositionParent, - getCoordsByPosition + getCoordsByPosition, - getElementPositioning + getElementPositioning, - getElementAtPointFromViewport + getElementAtPointFromViewport, - getElementCoordinatesByPosition + getElementCoordinatesByPosition, getElementCoordinatesByPositionRelativeToXY -} +}; diff --git a/packages/driver/src/dom/jquery.js b/packages/driver/src/dom/jquery.js index 7173df102150..95478b9059bf 100644 --- a/packages/driver/src/dom/jquery.js +++ b/packages/driver/src/dom/jquery.js @@ -1,34 +1,40 @@ -$ = require("jquery") -_ = require("lodash") - -## wrap the object in jquery -wrap = (obj) -> - $(obj) - -query = (selector, context) -> - new $.fn.init(selector, context) - -## pull out the raw elements if this is wrapped -unwrap = (obj) -> - if isJquery(obj) - ## return an array of elements - obj.toArray() - else - obj - -isJquery = (obj) -> - ## does it have the jquery property and is the - ## constructor a function? - !!(obj and obj.jquery and _.isFunction(obj.constructor)) - -## doing a little jiggle wiggle here -## to avoid circular dependencies +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const $ = require("jquery"); +const _ = require("lodash"); + +//# wrap the object in jquery +const wrap = obj => $(obj); + +const query = (selector, context) => new $.fn.init(selector, context); + +//# pull out the raw elements if this is wrapped +const unwrap = function(obj) { + if (isJquery(obj)) { + //# return an array of elements + return obj.toArray(); + } else { + return obj; + } +}; + +var isJquery = obj => + //# does it have the jquery property and is the + //# constructor a function? + !!(obj && obj.jquery && _.isFunction(obj.constructor)) +; + +//# doing a little jiggle wiggle here +//# to avoid circular dependencies module.exports = { - wrap + wrap, - query + query, - unwrap + unwrap, isJquery -} +}; diff --git a/packages/driver/src/dom/selection.js b/packages/driver/src/dom/selection.js index d8b167564c50..ac69b87ef45e 100644 --- a/packages/driver/src/dom/selection.js +++ b/packages/driver/src/dom/selection.js @@ -1,471 +1,539 @@ -$document = require("./document") -$elements = require("./elements") -$ = require("jquery") - -INTERNAL_STATE = "__Cypress_state__" - -_getSelectionBoundsFromTextarea = (el) -> - { - start: $elements.getNativeProp(el, "selectionStart") +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const $document = require("./document"); +const $elements = require("./elements"); +const $ = require("jquery"); + +const INTERNAL_STATE = "__Cypress_state__"; + +const _getSelectionBoundsFromTextarea = el => + ({ + start: $elements.getNativeProp(el, "selectionStart"), end: $elements.getNativeProp(el, "selectionEnd") - } + }) +; -_getSelectionBoundsFromInput = (el) -> - if $elements.canSetSelectionRangeElement(el) +const _getSelectionBoundsFromInput = function(el) { + let internalState; + if ($elements.canSetSelectionRangeElement(el)) { return { - start: $elements.getNativeProp(el, "selectionStart") + start: $elements.getNativeProp(el, "selectionStart"), end: $elements.getNativeProp(el, "selectionEnd") - } + }; + } - if internalState = el[INTERNAL_STATE] + if (internalState = el[INTERNAL_STATE]) { return { - start: internalState.start + start: internalState.start, end: internalState.end - } + }; + } return { - start: 0 + start: 0, end: 0 - } - -_getSelectionBoundsFromContentEditable = (el) -> - doc = $document.getDocumentFromElement(el) - - if doc.getSelection - ## global selection object - sel = doc.getSelection() - ## selection has at least one range (most always 1; only 0 at page load) - if sel.rangeCount - ## get the first (usually only) range obj - range = sel.getRangeAt(0) - - ## if div[contenteditable] > text - hostContenteditable = _getHostContenteditable(range.commonAncestorContainer) - if hostContenteditable is el + }; +}; + +const _getSelectionBoundsFromContentEditable = function(el) { + const doc = $document.getDocumentFromElement(el); + + if (doc.getSelection) { + //# global selection object + const sel = doc.getSelection(); + //# selection has at least one range (most always 1; only 0 at page load) + if (sel.rangeCount) { + //# get the first (usually only) range obj + const range = sel.getRangeAt(0); + + //# if div[contenteditable] > text + const hostContenteditable = _getHostContenteditable(range.commonAncestorContainer); + if (hostContenteditable === el) { return { - start: range.startOffset + start: range.startOffset, end: range.endOffset - } + }; + } + } + } return { - start: null + start: null, end: null + }; +}; + + //# TODO get ACTUAL caret position in contenteditable, not line + +const _replaceSelectionContentsContentEditable = function(el, text) { + const doc = $document.getDocumentFromElement(el); + //# NOTE: insertText will also handle '\n', and render newlines + $elements.callNativeMethod(doc, "execCommand", "insertText", true, text); +}; + + //# Keeping around native implementation + //# for same reasons as listed below + //# + // if text is "\n" + // return _insertNewlineIntoContentEditable(el) + // doc = $document.getDocumentFromElement(el) + // range = _getSelectionRangeByEl(el) + // ## delete anything in the selection + // startNode = range.startContainer + // range.deleteContents() + // newTextNode + // if text is ' ' + // newTextNode = doc.createElement('p') + // else + // newTextNode = doc.createTextNode(text) + // if $elements.isElement(startNode) + // if startNode.firstChild?.tagName is "BR" + // range.selectNode(startNode.firstChild) + // range.deleteContents() + // ## else startNode is el, so just insert the node + // startNode.appendChild(newTextNode) + // if text is ' ' + // newTextNode.outerHTML = ' ' + // range.selectNodeContents(startNode.lastChild) + // range.collapse() + // return + // else + // # nodeOffset = range.startOffset + // # oldValue = startNode.nodeValue || "" + // range.insertNode(newTextNode) + // range.selectNodeContents(newTextNode) + // range.collapse() + // if text is ' ' + // newTextNode.outerHTML = ' ' + // # updatedValue = _insertSubstring(oldValue, text, [nodeOffset, nodeOffset]) + // # newNodeOffset = nodeOffset + text.length + // # startNode.nodeValue = updatedValue + // el.normalize() + +const _insertSubstring = (curText, newText, [start, end]) => curText.substring(0, start) + newText + curText.substring(end); + +var _getHostContenteditable = function(el) { + let curEl = el; + + while (curEl.parentElement && !$elements.tryCallNativeMethod(curEl, "getAttribute", "contenteditable")) { + curEl = curEl.parentElement; + } + //# if there's no host contenteditable, we must be in designmode + //# so act as if the original element is the host contenteditable + //# TODO: remove this when we no longer click before type and move + //# cursor to the end + if (!$elements.callNativeMethod(curEl, "getAttribute", "contenteditable")) { + return el; + } + + return curEl; +}; + +const _getInnerLastChild = function(el) { + while (el.lastChild) { + el = el.lastChild; } - ## TODO get ACTUAL caret position in contenteditable, not line - -_replaceSelectionContentsContentEditable = (el, text) -> - doc = $document.getDocumentFromElement(el) - ## NOTE: insertText will also handle '\n', and render newlines - $elements.callNativeMethod(doc, "execCommand", "insertText", true, text) - return - - ## Keeping around native implementation - ## for same reasons as listed below - ## - # if text is "\n" - # return _insertNewlineIntoContentEditable(el) - # doc = $document.getDocumentFromElement(el) - # range = _getSelectionRangeByEl(el) - # ## delete anything in the selection - # startNode = range.startContainer - # range.deleteContents() - # newTextNode - # if text is ' ' - # newTextNode = doc.createElement('p') - # else - # newTextNode = doc.createTextNode(text) - # if $elements.isElement(startNode) - # if startNode.firstChild?.tagName is "BR" - # range.selectNode(startNode.firstChild) - # range.deleteContents() - # ## else startNode is el, so just insert the node - # startNode.appendChild(newTextNode) - # if text is ' ' - # newTextNode.outerHTML = ' ' - # range.selectNodeContents(startNode.lastChild) - # range.collapse() - # return - # else - # # nodeOffset = range.startOffset - # # oldValue = startNode.nodeValue || "" - # range.insertNode(newTextNode) - # range.selectNodeContents(newTextNode) - # range.collapse() - # if text is ' ' - # newTextNode.outerHTML = ' ' - # # updatedValue = _insertSubstring(oldValue, text, [nodeOffset, nodeOffset]) - # # newNodeOffset = nodeOffset + text.length - # # startNode.nodeValue = updatedValue - # el.normalize() - -_insertSubstring = (curText, newText, [start, end]) -> - curText.substring(0, start) + newText + curText.substring(end) - -_getHostContenteditable = (el) -> - curEl = el - - while curEl.parentElement && !$elements.tryCallNativeMethod(curEl, "getAttribute", "contenteditable") - curEl = curEl.parentElement - ## if there's no host contenteditable, we must be in designmode - ## so act as if the original element is the host contenteditable - ## TODO: remove this when we no longer click before type and move - ## cursor to the end - if !$elements.callNativeMethod(curEl, "getAttribute", "contenteditable") - return el - - return curEl - -_getInnerLastChild = (el) -> - while (el.lastChild) - el = el.lastChild - - return el - -_getSelectionByEl = (el) -> - doc = $document.getDocumentFromElement(el) - doc.getSelection() - -_getSelectionRangeByEl = (el) -> - sel = _getSelectionByEl(el) - if sel.rangeCount > 0 - sel.getRangeAt(0) - else throw new Error("No selection in document") - -deleteSelectionContents = (el) -> - if $elements.isContentEditable(el) - doc = $document.getDocumentFromElement(el) - $elements.callNativeMethod(doc, "execCommand", 'delete', false, null) - return - - ## for input and textarea, update selected text with empty string - replaceSelectionContents(el, "") - -setSelectionRange = (el, start, end) -> - - if $elements.canSetSelectionRangeElement(el) - $elements.callNativeMethod(el, "setSelectionRange", start, end) - return - - ## NOTE: Some input elements have mobile implementations - ## and thus may not always have a cursor, so calling setSelectionRange will throw. - ## we are assuming desktop here, so we store our own internal state. + return el; +}; + +const _getSelectionByEl = function(el) { + const doc = $document.getDocumentFromElement(el); + return doc.getSelection(); +}; + +const _getSelectionRangeByEl = function(el) { + const sel = _getSelectionByEl(el); + if (sel.rangeCount > 0) { + return sel.getRangeAt(0); + } else { throw new Error("No selection in document"); } +}; + +const deleteSelectionContents = function(el) { + if ($elements.isContentEditable(el)) { + const doc = $document.getDocumentFromElement(el); + $elements.callNativeMethod(doc, "execCommand", 'delete', false, null); + return; + } + + //# for input and textarea, update selected text with empty string + return replaceSelectionContents(el, ""); +}; + +const setSelectionRange = function(el, start, end) { + + if ($elements.canSetSelectionRangeElement(el)) { + $elements.callNativeMethod(el, "setSelectionRange", start, end); + return; + } + + //# NOTE: Some input elements have mobile implementations + //# and thus may not always have a cursor, so calling setSelectionRange will throw. + //# we are assuming desktop here, so we store our own internal state. el[INTERNAL_STATE] = { - start + start, end + }; + +}; + +const deleteRightOfCursor = function(el) { + if ($elements.isTextarea(el) || $elements.isInput(el)) { + const {start, end} = getSelectionBounds(el); + + if (start === $elements.getNativeProp(el, "value").length) { + //# nothing to delete, nothing to right of selection + return false; + } + + setSelectionRange(el, start, end + 1); + deleteSelectionContents(el); + //# successful delete + return true; + } + + if ($elements.isContentEditable(el)) { + const selection = _getSelectionByEl(el); + $elements.callNativeMethod(selection, "modify", "extend", "forward", "character"); + + if ($elements.getNativeProp(selection, "isCollapsed")) { + //# there's nothing to delete + return false; + } + + deleteSelectionContents(el); + //# successful delete + return true; } +}; - return +const deleteLeftOfCursor = function(el) { + if ($elements.isTextarea(el) || $elements.isInput(el)) { + const {start, end} = getSelectionBounds(el); -deleteRightOfCursor = (el) -> - if $elements.isTextarea(el) || $elements.isInput(el) - {start, end} = getSelectionBounds(el) + if (start === 0) { + //# there's nothing to delete, nothing before cursor + return false; + } + + setSelectionRange(el, start - 1, end); + deleteSelectionContents(el); + //# successful delete + return true; + } + + if ($elements.isContentEditable(el)) { + //# there is no 'backwardDelete' command for execCommand, so use the Selection API + const selection = _getSelectionByEl(el); + $elements.callNativeMethod(selection, "modify", "extend", "backward", "character"); + + if (selection.isCollapsed) { + //# there's nothing to delete + //# since extending the selection didn't do anything + return false; + } - if start is $elements.getNativeProp(el, "value").length - ## nothing to delete, nothing to right of selection - return false - - setSelectionRange(el, start, end + 1) - deleteSelectionContents(el) - ## successful delete - return true - - if $elements.isContentEditable(el) - selection = _getSelectionByEl(el) - $elements.callNativeMethod(selection, "modify", "extend", "forward", "character") - - if $elements.getNativeProp(selection, "isCollapsed") - ## there's nothing to delete - return false - - deleteSelectionContents(el) - ## successful delete - return true - -deleteLeftOfCursor = (el) -> - if $elements.isTextarea(el) || $elements.isInput(el) - {start, end} = getSelectionBounds(el) - - if start is 0 - ## there's nothing to delete, nothing before cursor - return false - - setSelectionRange(el, start - 1, end) - deleteSelectionContents(el) - ## successful delete - return true - - if $elements.isContentEditable(el) - ## there is no 'backwardDelete' command for execCommand, so use the Selection API - selection = _getSelectionByEl(el) - $elements.callNativeMethod(selection, "modify", "extend", "backward", "character") - - if selection.isCollapsed - ## there's nothing to delete - ## since extending the selection didn't do anything - return false - - deleteSelectionContents(el) - ## successful delete - return true - -_collapseInputOrTextArea = (el, toIndex) -> - setSelectionRange(el, toIndex, toIndex) - -moveCursorLeft = (el) -> - if $elements.isTextarea(el) || $elements.isInput(el) - {start, end} = getSelectionBounds(el) - - if start isnt end - return _collapseInputOrTextArea(el, start) - - if start is 0 - return - - return setSelectionRange(el, start - 1, start - 1) - - if $elements.isContentEditable(el) - selection = _getSelectionByEl(el) - $elements.callNativeMethod(selection, "modify", "move", "backward", "character") - - ## Keeping around native implementation - ## for same reasons as listed below - ## - # range = _getSelectionRangeByEl(el) - # if !range.collapsed - # return range.collapse(true) - # if range.startOffset is 0 - # return _contenteditableMoveToEndOfPrevLine(el) - # newOffset = range.startOffset - 1 - # range.setStart(range.startContainer, newOffset) - # range.setEnd(range.startContainer, newOffset) - -moveCursorRight = (el) -> - if $elements.isTextarea(el) || $elements.isInput(el) - {start, end} = getSelectionBounds(el) - if start isnt end - return _collapseInputOrTextArea(el, end) - - ## Don't worry about moving past the end of the string - ## nothing will happen and there is no error. - return setSelectionRange(el, start + 1, end + 1) - - if $elements.isContentEditable(el) - selection = _getSelectionByEl(el) - $elements.callNativeMethod(selection, "modify", "move", "forward", "character") - -moveCursorUp = (el) -> - _moveCursorUpOrDown(el, true) - -moveCursorDown = (el) -> - _moveCursorUpOrDown(el, false) - -_moveCursorUpOrDown = (el, up) -> - if $elements.isInput(el) - ## on an input, instead of moving the cursor - ## we want to perform the native browser action - ## which is to increment the step/interval - if $elements.isType(el, "number") - if up then el.stepUp?() else el.stepDown?() - return - - if $elements.isTextarea(el) || $elements.isContentEditable(el) - selection = _getSelectionByEl(el) - $elements.callNativeMethod(selection, "modify", - "move" - if up then "backward" else "forward" + deleteSelectionContents(el); + //# successful delete + return true; + } +}; + +const _collapseInputOrTextArea = (el, toIndex) => setSelectionRange(el, toIndex, toIndex); + +const moveCursorLeft = function(el) { + if ($elements.isTextarea(el) || $elements.isInput(el)) { + const {start, end} = getSelectionBounds(el); + + if (start !== end) { + return _collapseInputOrTextArea(el, start); + } + + if (start === 0) { + return; + } + + return setSelectionRange(el, start - 1, start - 1); + } + + if ($elements.isContentEditable(el)) { + const selection = _getSelectionByEl(el); + return $elements.callNativeMethod(selection, "modify", "move", "backward", "character"); + } +}; + + //# Keeping around native implementation + //# for same reasons as listed below + //# + // range = _getSelectionRangeByEl(el) + // if !range.collapsed + // return range.collapse(true) + // if range.startOffset is 0 + // return _contenteditableMoveToEndOfPrevLine(el) + // newOffset = range.startOffset - 1 + // range.setStart(range.startContainer, newOffset) + // range.setEnd(range.startContainer, newOffset) + +const moveCursorRight = function(el) { + if ($elements.isTextarea(el) || $elements.isInput(el)) { + const {start, end} = getSelectionBounds(el); + if (start !== end) { + return _collapseInputOrTextArea(el, end); + } + + //# Don't worry about moving past the end of the string + //# nothing will happen and there is no error. + return setSelectionRange(el, start + 1, end + 1); + } + + if ($elements.isContentEditable(el)) { + const selection = _getSelectionByEl(el); + return $elements.callNativeMethod(selection, "modify", "move", "forward", "character"); + } +}; + +const moveCursorUp = el => _moveCursorUpOrDown(el, true); + +const moveCursorDown = el => _moveCursorUpOrDown(el, false); + +var _moveCursorUpOrDown = function(el, up) { + if ($elements.isInput(el)) { + //# on an input, instead of moving the cursor + //# we want to perform the native browser action + //# which is to increment the step/interval + if ($elements.isType(el, "number")) { + if (up) { if (typeof el.stepUp === 'function') { + el.stepUp(); + } } else { if (typeof el.stepDown === 'function') { + el.stepDown(); + } } + } + return; + } + + if ($elements.isTextarea(el) || $elements.isContentEditable(el)) { + const selection = _getSelectionByEl(el); + return $elements.callNativeMethod(selection, "modify", + "move", + up ? "backward" : "forward", "line" - ) - -isCollapsed = (el) -> - if $elements.isTextarea(el) || $elements.isInput(el) - {start, end} = getSelectionBounds(el) - return start is end - - if $elements.isContentEditable(el) - selection = _getSelectionByEl(el) - return selection.isCollapsed - -selectAll = (el) -> - if $elements.isTextarea(el) || $elements.isInput(el) - setSelectionRange(el, 0, $elements.getNativeProp(el, "value").length) - return - - if $elements.isContentEditable(el) - doc = $document.getDocumentFromElement(el) - $elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null) - ## Keeping around native implementation - ## for same reasons as listed below - ## - # range = _getSelectionRangeByEl(el) - # range.selectNodeContents(el) - # range.deleteContents() - # return - # startTextNode = _getFirstTextNode(el.firstChild) - # endTextNode = _getInnerLastChild(el.lastChild) - # range.setStart(startTextNode, 0) - # range.setEnd(endTextNode, endTextNode.length) - -getSelectionBounds = (el) -> - ## this function works for input, textareas, and contentEditables - switch - when $elements.isInput(el) - _getSelectionBoundsFromInput(el) - when $elements.isTextarea(el) - _getSelectionBoundsFromTextarea(el) - when $elements.isContentEditable(el) - _getSelectionBoundsFromContentEditable(el) - else - { - start: null + ); + } +}; + +const isCollapsed = function(el) { + if ($elements.isTextarea(el) || $elements.isInput(el)) { + const {start, end} = getSelectionBounds(el); + return start === end; + } + + if ($elements.isContentEditable(el)) { + const selection = _getSelectionByEl(el); + return selection.isCollapsed; + } +}; + +const selectAll = function(el) { + if ($elements.isTextarea(el) || $elements.isInput(el)) { + setSelectionRange(el, 0, $elements.getNativeProp(el, "value").length); + return; + } + + if ($elements.isContentEditable(el)) { + const doc = $document.getDocumentFromElement(el); + return $elements.callNativeMethod(doc, 'execCommand', 'selectAll', false, null); + } +}; + //# Keeping around native implementation + //# for same reasons as listed below + //# + // range = _getSelectionRangeByEl(el) + // range.selectNodeContents(el) + // range.deleteContents() + // return + // startTextNode = _getFirstTextNode(el.firstChild) + // endTextNode = _getInnerLastChild(el.lastChild) + // range.setStart(startTextNode, 0) + // range.setEnd(endTextNode, endTextNode.length) + +var getSelectionBounds = function(el) { + //# this function works for input, textareas, and contentEditables + switch (false) { + case !$elements.isInput(el): + return _getSelectionBoundsFromInput(el); + case !$elements.isTextarea(el): + return _getSelectionBoundsFromTextarea(el); + case !$elements.isContentEditable(el): + return _getSelectionBoundsFromContentEditable(el); + default: + return { + start: null, end: null - } + }; + } +}; + +const moveSelectionToEnd = function(el) { + let length; + if ($elements.isInput(el) || $elements.isTextarea(el)) { + ({ length } = $elements.getNativeProp(el, "value")); + return setSelectionRange(el, length, length); + + } else if ($elements.isContentEditable(el)) { + //# NOTE: can't use execCommand API here because we would have + //# to selectAll and then collapse so we use the Selection API + const doc = $document.getDocumentFromElement(el); + const range = $elements.callNativeMethod(doc, "createRange"); + const hostContenteditable = _getHostContenteditable(el); + let lastTextNode = _getInnerLastChild(hostContenteditable); + + if (lastTextNode.tagName === "BR") { + lastTextNode = lastTextNode.parentNode; + } + + range.setStart(lastTextNode, lastTextNode.length); + range.setEnd(lastTextNode, lastTextNode.length); + + const sel = $elements.callNativeMethod(doc, "getSelection"); + $elements.callNativeMethod(sel, "removeAllRanges"); + return $elements.callNativeMethod(sel, "addRange", range); + } +}; + +//# TODO: think about renaming this +var replaceSelectionContents = function(el, key) { + if ($elements.isContentEditable(el)) { + return _replaceSelectionContentsContentEditable(el, key); + } + + if ($elements.isInput(el) || $elements.isTextarea(el)) { + const { start, end } = getSelectionBounds(el); + + const value = $elements.getNativeProp(el, "value") || ""; + const updatedValue = _insertSubstring(value, key, [start, end]); + + $elements.setNativeProp(el, "value", updatedValue); + + return setSelectionRange(el, start + key.length, start + key.length); + } +}; + +const getCaretPosition = function(el) { + const bounds = getSelectionBounds(el); + + if ((bounds.start == null)) { + //# no selection + return null; + } + + if (bounds.start === bounds.end) { + return bounds.start; + } + + return null; +}; + +const interceptSelect = function() { + if ($elements.isInput(this) && !$elements.canSetSelectionRangeElement(this)) { + setSelectionRange(this, 0, $elements.getNativeProp(this, "value").length); + } -moveSelectionToEnd = (el) -> - if $elements.isInput(el) || $elements.isTextarea(el) - length = $elements.getNativeProp(el, "value").length - setSelectionRange(el, length, length) - - else if $elements.isContentEditable(el) - ## NOTE: can't use execCommand API here because we would have - ## to selectAll and then collapse so we use the Selection API - doc = $document.getDocumentFromElement(el) - range = $elements.callNativeMethod(doc, "createRange") - hostContenteditable = _getHostContenteditable(el) - lastTextNode = _getInnerLastChild(hostContenteditable) - - if lastTextNode.tagName is "BR" - lastTextNode = lastTextNode.parentNode - - range.setStart(lastTextNode, lastTextNode.length) - range.setEnd(lastTextNode, lastTextNode.length) - - sel = $elements.callNativeMethod(doc, "getSelection") - $elements.callNativeMethod(sel, "removeAllRanges") - $elements.callNativeMethod(sel, "addRange", range) - -## TODO: think about renaming this -replaceSelectionContents = (el, key) -> - if $elements.isContentEditable(el) - return _replaceSelectionContentsContentEditable(el, key) - - if $elements.isInput(el) or $elements.isTextarea(el) - { start, end } = getSelectionBounds(el) - - value = $elements.getNativeProp(el, "value") or "" - updatedValue = _insertSubstring(value, key, [start, end]) - - $elements.setNativeProp(el, "value", updatedValue) - - setSelectionRange(el, start + key.length, start + key.length) - -getCaretPosition = (el) -> - bounds = getSelectionBounds(el) - - if !bounds.start? - ## no selection - return null - - if bounds.start is bounds.end - return bounds.start - - return null - -interceptSelect = -> - if $elements.isInput(this) and !$elements.canSetSelectionRangeElement(this) - setSelectionRange(this, 0, $elements.getNativeProp(this, "value").length) - - $elements.callNativeMethod(this, "select") - -## Selection API implementation of insert newline. -## Worth keeping around if we ever have to insert native -## newlines if we are trying to support a browser or -## environment without the document.execCommand('insertText', etc...) -## -# _insertNewlineIntoContentEditable = (el) -> -# selection = _getSelectionByEl(el) -# selection.deleteFromDocument() -# $elements.callNativeMethod(selection, "modify", "extend", "forward", "lineboundary") -# range = selection.getRangeAt(0) -# clonedElements = range.cloneContents() -# selection.deleteFromDocument() -# elementToInsertAfter -# if range.startContainer is el -# elementToInsertAfter = _getInnerLastChild(el) -# else -# curEl = range.startContainer -# ## make sure we have firstLevel child element from contentEditable -# while (curEl.parentElement && curEl.parentElement isnt el ) -# curEl = curEl.parentElement -# elementToInsertAfter = curEl -# range = _getSelectionRangeByEl(el) -# outerNewElement = '
' -# ## TODO: In contenteditables, should insert newline element as either
or

depending on existing nodes -# ## but this shouldn't really matter that much, so ignore for now -# # if elementToInsertAfter.tagName is 'P' -# # typeOfNewElement = '

' -# $newElement = $(outerNewElement).append(clonedElements) -# $newElement.insertAfter(elementToInsertAfter) -# newElement = $newElement.get(0) -# if !newElement.innerHTML -# newElement.innerHTML = '
' -# range.selectNodeContents(newElement) -# else -# newTextNode = _getFirstTextNode(newElement) -# range.selectNodeContents(newTextNode) -# range.collapse(true) - -# _contenteditableMoveToEndOfPrevLine = (el) -> -# bounds = _contenteditableGetNodesAround(el) -# if bounds.prev -# range = _getSelectionRangeByEl(el) -# prevTextNode = _getInnerLastChild(bounds.prev) -# range.setStart(prevTextNode, prevTextNode.length) -# range.setEnd(prevTextNode, prevTextNode.length) - -# _contenteditableMoveToStartOfNextLine = (el) -> -# bounds = _contenteditableGetNodesAround(el) -# if bounds.next -# range = _getSelectionRangeByEl(el) -# nextTextNode = _getFirstTextNode(bounds.next) -# range.setStart(nextTextNode, 1) -# range.setEnd(nextTextNode, 1) - -# _contenteditableGetNodesAround = (el) -> -# range = _getSelectionRangeByEl(el) -# textNode = range.startContainer -# curEl = textNode -# while curEl && !curEl.nextSibling? -# curEl = curEl.parentNode -# nextTextNode = _getFirstTextNode(curEl.nextSibling) -# curEl = textNode -# while curEl && !curEl.previousSibling? -# curEl = curEl.parentNode -# prevTextNode = _getInnerLastChild(curEl.previousSibling) -# { -# prev: prevTextNode -# next: nextTextNode -# } - -# _getFirstTextNode = (el) -> -# while (el.firstChild) -# el = el.firstChild -# return el + return $elements.callNativeMethod(this, "select"); +}; + +//# Selection API implementation of insert newline. +//# Worth keeping around if we ever have to insert native +//# newlines if we are trying to support a browser or +//# environment without the document.execCommand('insertText', etc...) +//# +// _insertNewlineIntoContentEditable = (el) -> +// selection = _getSelectionByEl(el) +// selection.deleteFromDocument() +// $elements.callNativeMethod(selection, "modify", "extend", "forward", "lineboundary") +// range = selection.getRangeAt(0) +// clonedElements = range.cloneContents() +// selection.deleteFromDocument() +// elementToInsertAfter +// if range.startContainer is el +// elementToInsertAfter = _getInnerLastChild(el) +// else +// curEl = range.startContainer +// ## make sure we have firstLevel child element from contentEditable +// while (curEl.parentElement && curEl.parentElement isnt el ) +// curEl = curEl.parentElement +// elementToInsertAfter = curEl +// range = _getSelectionRangeByEl(el) +// outerNewElement = '
' +// ## TODO: In contenteditables, should insert newline element as either
or

depending on existing nodes +// ## but this shouldn't really matter that much, so ignore for now +// # if elementToInsertAfter.tagName is 'P' +// # typeOfNewElement = '

' +// $newElement = $(outerNewElement).append(clonedElements) +// $newElement.insertAfter(elementToInsertAfter) +// newElement = $newElement.get(0) +// if !newElement.innerHTML +// newElement.innerHTML = '
' +// range.selectNodeContents(newElement) +// else +// newTextNode = _getFirstTextNode(newElement) +// range.selectNodeContents(newTextNode) +// range.collapse(true) + +// _contenteditableMoveToEndOfPrevLine = (el) -> +// bounds = _contenteditableGetNodesAround(el) +// if bounds.prev +// range = _getSelectionRangeByEl(el) +// prevTextNode = _getInnerLastChild(bounds.prev) +// range.setStart(prevTextNode, prevTextNode.length) +// range.setEnd(prevTextNode, prevTextNode.length) + +// _contenteditableMoveToStartOfNextLine = (el) -> +// bounds = _contenteditableGetNodesAround(el) +// if bounds.next +// range = _getSelectionRangeByEl(el) +// nextTextNode = _getFirstTextNode(bounds.next) +// range.setStart(nextTextNode, 1) +// range.setEnd(nextTextNode, 1) + +// _contenteditableGetNodesAround = (el) -> +// range = _getSelectionRangeByEl(el) +// textNode = range.startContainer +// curEl = textNode +// while curEl && !curEl.nextSibling? +// curEl = curEl.parentNode +// nextTextNode = _getFirstTextNode(curEl.nextSibling) +// curEl = textNode +// while curEl && !curEl.previousSibling? +// curEl = curEl.parentNode +// prevTextNode = _getInnerLastChild(curEl.previousSibling) +// { +// prev: prevTextNode +// next: nextTextNode +// } + +// _getFirstTextNode = (el) -> +// while (el.firstChild) +// el = el.firstChild +// return el module.exports = { - getSelectionBounds - deleteRightOfCursor - deleteLeftOfCursor - selectAll - deleteSelectionContents - moveSelectionToEnd - getCaretPosition - moveCursorLeft - moveCursorRight - moveCursorUp - moveCursorDown - replaceSelectionContents - isCollapsed + getSelectionBounds, + deleteRightOfCursor, + deleteLeftOfCursor, + selectAll, + deleteSelectionContents, + moveSelectionToEnd, + getCaretPosition, + moveCursorLeft, + moveCursorRight, + moveCursorUp, + moveCursorDown, + replaceSelectionContents, + isCollapsed, interceptSelect -} +}; diff --git a/packages/driver/src/dom/visibility.js b/packages/driver/src/dom/visibility.js index dd0dc7e58e52..c84f430ad6ac 100644 --- a/packages/driver/src/dom/visibility.js +++ b/packages/driver/src/dom/visibility.js @@ -1,289 +1,321 @@ -_ = require("lodash") - -$jquery = require("./jquery") -$document = require("./document") -$elements = require("./elements") -$coordinates = require("./coordinates") - -fixedOrAbsoluteRe = /(fixed|absolute)/ - -OVERFLOW_PROPS = ["hidden", "scroll", "auto"] - -## WARNING: -## developer beware. visibility is a sink hole -## that leads to sheer madness. you should -## avoid this file before its too late. - -isVisible = (el) -> - not isHidden(el, "isVisible()") - -## TODO: we should prob update dom -## to be passed in $utils as a dependency -## because of circular references -isHidden = (el, name) -> - if not $elements.isElement(el) - name ?= "isHidden()" - - throw new Error("Cypress.dom.#{name} must be passed a basic DOM element.") - - $el = $jquery.wrap(el) - - ## in Cypress-land we consider the element hidden if - ## either its offsetHeight or offsetWidth is 0 because - ## it is impossible for the user to interact with this element - ## offsetHeight / offsetWidth includes the ef - elHasNoEffectiveWidthOrHeight($el) or - - ## additionally if the effective visibility of the element - ## is hidden (which includes any parent nodes) then the user - ## cannot interact with this element and thus it is hidden - elHasVisibilityHidden($el) or - - ## we do some calculations taking into account the parents - ## to see if its hidden by a parent - elIsHiddenByAncestors($el) or - - ## if this is a fixed element check if its covered +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS204: Change includes calls to have a more natural evaluation order + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const _ = require("lodash"); + +const $jquery = require("./jquery"); +const $document = require("./document"); +const $elements = require("./elements"); +const $coordinates = require("./coordinates"); + +const fixedOrAbsoluteRe = /(fixed|absolute)/; + +const OVERFLOW_PROPS = ["hidden", "scroll", "auto"]; + +//# WARNING: +//# developer beware. visibility is a sink hole +//# that leads to sheer madness. you should +//# avoid this file before its too late. + +const isVisible = el => !isHidden(el, "isVisible()"); + +//# TODO: we should prob update dom +//# to be passed in $utils as a dependency +//# because of circular references +var isHidden = function(el, name) { + if (!$elements.isElement(el)) { + if (name == null) { name = "isHidden()"; } + + throw new Error(`Cypress.dom.${name} must be passed a basic DOM element.`); + } + + const $el = $jquery.wrap(el); + + //# in Cypress-land we consider the element hidden if + //# either its offsetHeight or offsetWidth is 0 because + //# it is impossible for the user to interact with this element + //# offsetHeight / offsetWidth includes the ef + return elHasNoEffectiveWidthOrHeight($el) || + + //# additionally if the effective visibility of the element + //# is hidden (which includes any parent nodes) then the user + //# cannot interact with this element and thus it is hidden + elHasVisibilityHidden($el) || + + //# we do some calculations taking into account the parents + //# to see if its hidden by a parent + elIsHiddenByAncestors($el) || + + //# if this is a fixed element check if its covered ( - if elIsFixed($el) + elIsFixed($el) ? elIsNotElementFromPoint($el) - else - ## else check if el is outside the bounds - ## of its ancestors overflow + : + //# else check if el is outside the bounds + //# of its ancestors overflow elIsOutOfBoundsOfAncestorsOverflow($el) - ) - -elHasNoEffectiveWidthOrHeight = ($el) -> - elOffsetWidth($el) <= 0 or elOffsetHeight($el) <= 0 or $el[0].getClientRects().length <= 0 - -elHasNoOffsetWidthOrHeight = ($el) -> - elOffsetWidth($el) <= 0 or elOffsetHeight($el) <= 0 - -elOffsetWidth = ($el) -> - $el[0].offsetWidth - -elOffsetHeight = ($el) -> - $el[0].offsetHeight - -elHasVisibilityHidden = ($el) -> - $el.css("visibility") is "hidden" - -elHasDisplayNone = ($el) -> - $el.css("display") is "none" - -elHasOverflowHidden = ($el) -> - "hidden" in [$el.css("overflow"), $el.css("overflow-y"), $el.css("overflow-x")] - -elHasPositionRelative = ($el) -> - $el.css("position") is "relative" - -elHasClippableOverflow = ($el) -> - $el.css("overflow") in OVERFLOW_PROPS or - $el.css("overflow-y") in OVERFLOW_PROPS or - $el.css("overflow-x") in OVERFLOW_PROPS - -canClipContent = ($el, $ancestor) -> - ## can't clip without clippable overflow - if not elHasClippableOverflow($ancestor) - return false - - $offsetParent = $jquery.wrap($el[0].offsetParent) - - ## even if overflow is clippable, if an ancestor of the ancestor is the - ## element's offset parent, the ancestor will not clip the element - ## unless the element is position relative - if not elHasPositionRelative($el) and $elements.isAncestor($ancestor, $offsetParent) - return false - - return true - -elIsFixed = ($el) -> - if $stickyOrFixedEl = $elements.getFirstFixedOrStickyPositionParent($el) - $stickyOrFixedEl.css("position") is "fixed" - -elAtCenterPoint = ($el) -> - elProps = $coordinates.getElementPositioning($el) - - { topCenter, leftCenter } = elProps.fromViewport - - doc = $document.getDocumentFromElement($el.get(0)) - - if el = $coordinates.getElementAtPointFromViewport(doc, leftCenter, topCenter) - $jquery.wrap(el) - -elDescendentsHavePositionFixedOrAbsolute = ($parent, $child) -> - ## create an array of all elements between $parent and $child - ## including child but excluding parent - ## and check if these have position fixed|absolute - $els = $child.parentsUntil($parent).add($child) - - _.some $els.get(), (el) -> - fixedOrAbsoluteRe.test $jquery.wrap(el).css("position") - -elIsNotElementFromPoint = ($el) -> - ## if we have a fixed position element that means - ## it is fixed 'relative' to the viewport which means - ## it MUST be available with elementFromPoint because - ## that is also relative to the viewport - $elAtPoint = elAtCenterPoint($el) - - ## if the element at point is not a descendent - ## of our $el then we know it's being covered or its - ## not visible - return not $elements.isDescendent($el, $elAtPoint) - -elIsOutOfBoundsOfAncestorsOverflow = ($el, $ancestor) -> - $ancestor ?= $el.parent() - - ## no ancestor, not out of bounds! - return false if not $ancestor - - ## if we've reached the top parent, which is document - ## then we're in bounds all the way up, return false - return false if $ancestor.is("body,html") or $document.isDocument($ancestor) - - elProps = $coordinates.getElementPositioning($el) - - if canClipContent($el, $ancestor) - ancestorProps = $coordinates.getElementPositioning($ancestor) - - ## target el is out of bounds - return true if ( - ## target el is to the right of the ancestor's visible area - elProps.fromWindow.left > (ancestorProps.width + ancestorProps.fromWindow.left) or - - ## target el is to the left of the ancestor's visible area - (elProps.fromWindow.left + elProps.width) < ancestorProps.fromWindow.left or - - ## target el is under the ancestor's visible area - elProps.fromWindow.top > (ancestorProps.height + ancestorProps.fromWindow.top) or - - ## target el is above the ancestor's visible area - (elProps.fromWindow.top + elProps.height) < ancestorProps.fromWindow.top - ) - - elIsOutOfBoundsOfAncestorsOverflow($el, $ancestor.parent()) - -elIsHiddenByAncestors = ($el, $origEl) -> - ## store the original $el - $origEl ?= $el - - ## walk up to each parent until we reach the body - ## if any parent has an effective offsetHeight of 0 - ## and its set overflow: hidden then our child element - ## is effectively hidden - ## -----UNLESS------ - ## the parent or a descendent has position: absolute|fixed - $parent = $el.parent() - - ## stop if we've reached the body or html - ## in case there is no body - ## or if parent is the document which can - ## happen if we already have an element - return false if $parent.is("body,html") or $document.isDocument($parent) - - if elHasOverflowHidden($parent) and elHasNoEffectiveWidthOrHeight($parent) - ## if any of the elements between the parent and origEl - ## have fixed or position absolute - return not elDescendentsHavePositionFixedOrAbsolute($parent, $origEl) - - ## continue to recursively walk up the chain until we reach body or html - elIsHiddenByAncestors($parent, $origEl) + ); +}; -parentHasNoOffsetWidthOrHeightAndOverflowHidden = ($el) -> - ## if we've walked all the way up to body or html then return false - return false if not $el.length or $el.is("body,html") +var elHasNoEffectiveWidthOrHeight = $el => (elOffsetWidth($el) <= 0) || (elOffsetHeight($el) <= 0) || ($el[0].getClientRects().length <= 0); - ## if we have overflow hidden and no effective width or height - if elHasOverflowHidden($el) and elHasNoEffectiveWidthOrHeight($el) - return $el - else - ## continue walking - return parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent()) +const elHasNoOffsetWidthOrHeight = $el => (elOffsetWidth($el) <= 0) || (elOffsetHeight($el) <= 0); -parentHasDisplayNone = ($el) -> - ## if we have no $el or we've walked all the way up to document - ## then return false - return false if not $el.length or $document.isDocument($el) +var elOffsetWidth = $el => $el[0].offsetWidth; - ## if we have display none then return the $el - if elHasDisplayNone($el) - return $el - else - ## continue walking - return parentHasDisplayNone($el.parent()) - -parentHasVisibilityNone = ($el) -> - ## if we've walked all the way up to document then return false - return false if not $el.length or $document.isDocument($el) +var elOffsetHeight = $el => $el[0].offsetHeight; - ## if we have display none then return the $el - if elHasVisibilityHidden($el) - return $el - else - ## continue walking - return parentHasVisibilityNone($el.parent()) +var elHasVisibilityHidden = $el => $el.css("visibility") === "hidden"; -getReasonIsHidden = ($el) -> - ## TODO: need to add in the reason an element - ## is hidden when its fixed position and its - ## either being covered or there is no el +const elHasDisplayNone = $el => $el.css("display") === "none"; - node = $elements.stringify($el, "short") +const elHasOverflowHidden = function($el) { + let needle; + return (needle = "hidden", [$el.css("overflow"), $el.css("overflow-y"), $el.css("overflow-x")].includes(needle)); +}; - ## returns the reason in human terms why an element is considered not visible - switch - when elHasDisplayNone($el) - "This element '#{node}' is not visible because it has CSS property: 'display: none'" +const elHasPositionRelative = $el => $el.css("position") === "relative"; - when $parent = parentHasDisplayNone($el.parent()) - parentNode = $elements.stringify($parent, "short") +const elHasClippableOverflow = function($el) { + let needle, needle1, needle2; + return (needle = $el.css("overflow"), OVERFLOW_PROPS.includes(needle)) || + (needle1 = $el.css("overflow-y"), OVERFLOW_PROPS.includes(needle1)) || + (needle2 = $el.css("overflow-x"), OVERFLOW_PROPS.includes(needle2)); +}; - "This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'display: none'" +const canClipContent = function($el, $ancestor) { + //# can't clip without clippable overflow + if (!elHasClippableOverflow($ancestor)) { + return false; + } - when $parent = parentHasVisibilityNone($el.parent()) - parentNode = $elements.stringify($parent, "short") + const $offsetParent = $jquery.wrap($el[0].offsetParent); - "This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'visibility: hidden'" + //# even if overflow is clippable, if an ancestor of the ancestor is the + //# element's offset parent, the ancestor will not clip the element + //# unless the element is position relative + if (!elHasPositionRelative($el) && $elements.isAncestor($ancestor, $offsetParent)) { + return false; + } - when elHasVisibilityHidden($el) - "This element '#{node}' is not visible because it has CSS property: 'visibility: hidden'" - - when elHasNoOffsetWidthOrHeight($el) - width = elOffsetWidth($el) - height = elOffsetHeight($el) - - "This element '#{node}' is not visible because it has an effective width and height of: '#{width} x #{height}' pixels." + return true; +}; - when $parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent()) - parentNode = $elements.stringify($parent, "short") - width = elOffsetWidth($parent) - height = elOffsetHeight($parent) - - "This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'overflow: hidden' and an effective width and height of: '#{width} x #{height}' pixels." +var elIsFixed = function($el) { + let $stickyOrFixedEl; + if ($stickyOrFixedEl = $elements.getFirstFixedOrStickyPositionParent($el)) { + return $stickyOrFixedEl.css("position") === "fixed"; + } +}; - else - ## nested else --___________-- - if elIsFixed($el) - if elIsNotElementFromPoint($el) - ## show the long element here - covered = $elements.stringify(elAtCenterPoint($el)) - - return """ - This element '#{node}' is not visible because it has CSS property: 'position: fixed' and its being covered by another element: - - #{covered} - """ - else - if elIsOutOfBoundsOfAncestorsOverflow($el) - return "This element '#{node}' is not visible because its content is being clipped by one of its parent elements, which has a CSS property of overflow: 'hidden', 'scroll' or 'auto'" +const elAtCenterPoint = function($el) { + let el; + const elProps = $coordinates.getElementPositioning($el); - return "Cypress could not determine why this element '#{node}' is not visible." + const { topCenter, leftCenter } = elProps.fromViewport; + + const doc = $document.getDocumentFromElement($el.get(0)); + + if (el = $coordinates.getElementAtPointFromViewport(doc, leftCenter, topCenter)) { + return $jquery.wrap(el); + } +}; + +const elDescendentsHavePositionFixedOrAbsolute = function($parent, $child) { + //# create an array of all elements between $parent and $child + //# including child but excluding parent + //# and check if these have position fixed|absolute + const $els = $child.parentsUntil($parent).add($child); + + return _.some($els.get(), el => fixedOrAbsoluteRe.test($jquery.wrap(el).css("position"))); +}; + +var elIsNotElementFromPoint = function($el) { + //# if we have a fixed position element that means + //# it is fixed 'relative' to the viewport which means + //# it MUST be available with elementFromPoint because + //# that is also relative to the viewport + const $elAtPoint = elAtCenterPoint($el); + + //# if the element at point is not a descendent + //# of our $el then we know it's being covered or its + //# not visible + return !$elements.isDescendent($el, $elAtPoint); +}; + +var elIsOutOfBoundsOfAncestorsOverflow = function($el, $ancestor) { + if ($ancestor == null) { $ancestor = $el.parent(); } + + //# no ancestor, not out of bounds! + if (!$ancestor) { return false; } + + //# if we've reached the top parent, which is document + //# then we're in bounds all the way up, return false + if ($ancestor.is("body,html") || $document.isDocument($ancestor)) { return false; } + + const elProps = $coordinates.getElementPositioning($el); + + if (canClipContent($el, $ancestor)) { + const ancestorProps = $coordinates.getElementPositioning($ancestor); + + //# target el is out of bounds + if ( + //# target el is to the right of the ancestor's visible area + (elProps.fromWindow.left > (ancestorProps.width + ancestorProps.fromWindow.left)) || + + //# target el is to the left of the ancestor's visible area + ((elProps.fromWindow.left + elProps.width) < ancestorProps.fromWindow.left) || + + //# target el is under the ancestor's visible area + (elProps.fromWindow.top > (ancestorProps.height + ancestorProps.fromWindow.top)) || + + //# target el is above the ancestor's visible area + ((elProps.fromWindow.top + elProps.height) < ancestorProps.fromWindow.top) + ) { return true; } + } + + return elIsOutOfBoundsOfAncestorsOverflow($el, $ancestor.parent()); +}; + +var elIsHiddenByAncestors = function($el, $origEl) { + //# store the original $el + if ($origEl == null) { $origEl = $el; } + + //# walk up to each parent until we reach the body + //# if any parent has an effective offsetHeight of 0 + //# and its set overflow: hidden then our child element + //# is effectively hidden + //# -----UNLESS------ + //# the parent or a descendent has position: absolute|fixed + const $parent = $el.parent(); + + //# stop if we've reached the body or html + //# in case there is no body + //# or if parent is the document which can + //# happen if we already have an element + if ($parent.is("body,html") || $document.isDocument($parent)) { return false; } + + if (elHasOverflowHidden($parent) && elHasNoEffectiveWidthOrHeight($parent)) { + //# if any of the elements between the parent and origEl + //# have fixed or position absolute + return !elDescendentsHavePositionFixedOrAbsolute($parent, $origEl); + } + + //# continue to recursively walk up the chain until we reach body or html + return elIsHiddenByAncestors($parent, $origEl); +}; + +var parentHasNoOffsetWidthOrHeightAndOverflowHidden = function($el) { + //# if we've walked all the way up to body or html then return false + if (!$el.length || $el.is("body,html")) { return false; } + + //# if we have overflow hidden and no effective width or height + if (elHasOverflowHidden($el) && elHasNoEffectiveWidthOrHeight($el)) { + return $el; + } else { + //# continue walking + return parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent()); + } +}; + +var parentHasDisplayNone = function($el) { + //# if we have no $el or we've walked all the way up to document + //# then return false + if (!$el.length || $document.isDocument($el)) { return false; } + + //# if we have display none then return the $el + if (elHasDisplayNone($el)) { + return $el; + } else { + //# continue walking + return parentHasDisplayNone($el.parent()); + } +}; + +var parentHasVisibilityNone = function($el) { + //# if we've walked all the way up to document then return false + if (!$el.length || $document.isDocument($el)) { return false; } + + //# if we have display none then return the $el + if (elHasVisibilityHidden($el)) { + return $el; + } else { + //# continue walking + return parentHasVisibilityNone($el.parent()); + } +}; + +const getReasonIsHidden = function($el) { + //# TODO: need to add in the reason an element + //# is hidden when its fixed position and its + //# either being covered or there is no el + + let $parent; + const node = $elements.stringify($el, "short"); + + //# returns the reason in human terms why an element is considered not visible + switch (false) { + case !elHasDisplayNone($el): + return `This element '${node}' is not visible because it has CSS property: 'display: none'`; + + case !($parent = parentHasDisplayNone($el.parent())): + var parentNode = $elements.stringify($parent, "short"); + + return `This element '${node}' is not visible because its parent '${parentNode}' has CSS property: 'display: none'`; + + case !($parent = parentHasVisibilityNone($el.parent())): + parentNode = $elements.stringify($parent, "short"); + + return `This element '${node}' is not visible because its parent '${parentNode}' has CSS property: 'visibility: hidden'`; + + case !elHasVisibilityHidden($el): + return `This element '${node}' is not visible because it has CSS property: 'visibility: hidden'`; + + case !elHasNoOffsetWidthOrHeight($el): + var width = elOffsetWidth($el); + var height = elOffsetHeight($el); + + return `This element '${node}' is not visible because it has an effective width and height of: '${width} x ${height}' pixels.`; + + case !($parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent())): + parentNode = $elements.stringify($parent, "short"); + width = elOffsetWidth($parent); + height = elOffsetHeight($parent); + + return `This element '${node}' is not visible because its parent '${parentNode}' has CSS property: 'overflow: hidden' and an effective width and height of: '${width} x ${height}' pixels.`; + + default: + //# nested else --___________-- + if (elIsFixed($el)) { + if (elIsNotElementFromPoint($el)) { + //# show the long element here + const covered = $elements.stringify(elAtCenterPoint($el)); + + return `\ +This element '${node}' is not visible because it has CSS property: 'position: fixed' and its being covered by another element: + +${covered}\ +`; + } + } else { + if (elIsOutOfBoundsOfAncestorsOverflow($el)) { + return `This element '${node}' is not visible because its content is being clipped by one of its parent elements, which has a CSS property of overflow: 'hidden', 'scroll' or 'auto'`; + } + } + + return `Cypress could not determine why this element '${node}' is not visible.`; + } +}; module.exports = { - isVisible + isVisible, - isHidden + isHidden, getReasonIsHidden -} +}; diff --git a/packages/driver/src/dom/window.js b/packages/driver/src/dom/window.js index d99035f0a410..a148a3126d09 100644 --- a/packages/driver/src/dom/window.js +++ b/packages/driver/src/dom/window.js @@ -1,30 +1,41 @@ -$jquery = require("./jquery") -$document = require("./document") - -getWindowByElement = (el) -> - if isWindow(el) - return el - - doc = $document.getDocumentFromElement(el) - getWindowByDocument(doc) - -getWindowByDocument = (doc) -> - ## parentWindow for IE - doc.defaultView or doc.parentWindow - -isWindow = (obj) -> - try - if $jquery.isJquery(obj) - obj = obj[0] - - Boolean(obj and obj.window is obj) - catch - false +/* + * decaffeinate suggestions: + * DS102: Remove unnecessary code created because of implicit returns + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +const $jquery = require("./jquery"); +const $document = require("./document"); + +const getWindowByElement = function(el) { + if (isWindow(el)) { + return el; + } + + const doc = $document.getDocumentFromElement(el); + return getWindowByDocument(doc); +}; + +var getWindowByDocument = doc => + //# parentWindow for IE + doc.defaultView || doc.parentWindow +; + +var isWindow = function(obj) { + try { + if ($jquery.isJquery(obj)) { + obj = obj[0]; + } + + return Boolean(obj && (obj.window === obj)); + } catch (error) { + return false; + } +}; module.exports = { - getWindowByElement + getWindowByElement, - getWindowByDocument + getWindowByDocument, isWindow -} +};