UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

1,388 lines (1,099 loc) 34 kB
// NOT patched jquery import $ from 'jquery' import _ from '../config/lodash' import $utils from '../cypress/utils' import * as $document from './document' import * as $jquery from './jquery' import * as $selection from './selection' import { parentHasDisplayNone } from './visibility' import * as $window from './window' import Debug from 'debug' const debug = Debug('cypress:driver:elements') const { wrap } = $jquery const fixedOrStickyRe = /(fixed|sticky)/ const focusableSelectors = [ 'a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'iframe', '[tabindex]', '[contenteditable]', ] const focusableWhenNotDisabledSelectors = [ 'a[href]', 'area[href]', 'input', 'select', 'textarea', 'button', 'iframe', '[tabindex]', '[contenteditable]', ] const inputTypeNeedSingleValueChangeRe = /^(date|time|week|month|datetime-local)$/ const canSetSelectionRangeElementRe = /^(text|search|URL|tel|password)$/ const valueIsNumberTypeRe = /progress|meter|li/ declare global { interface Window { Element: typeof Element HTMLElement: typeof HTMLElement HTMLInputElement: typeof HTMLInputElement HTMLSelectElement: typeof HTMLSelectElement HTMLButtonElement: typeof HTMLButtonElement HTMLOptionElement: typeof HTMLOptionElement HTMLTextAreaElement: typeof HTMLTextAreaElement Selection: typeof Selection SVGElement: typeof SVGElement EventTarget: typeof EventTarget Document: typeof Document XMLHttpRequest: typeof XMLHttpRequest } interface Selection { modify: Function } } // 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 = <T extends keyof Window, K extends keyof Window[T]['prototype']>(klass: T, prop: K) => { const descriptor = Object.getOwnPropertyDescriptor(window[klass].prototype, prop) if (descriptor === undefined) { throw new Error(`Error, could not get property descriptor for ${klass} ${prop}. This should never happen`) } return descriptor } const _getValue = function () { if (isInput(this)) { return descriptor('HTMLInputElement', 'value').get } if (isTextarea(this)) { return descriptor('HTMLTextAreaElement', 'value').get } if (isSelect(this)) { return descriptor('HTMLSelectElement', 'value').get } if (isButton(this)) { return descriptor('HTMLButtonElement', 'value').get } // is an option element return descriptor('HTMLOptionElement', 'value').get } const _setValue = function () { if (isInput(this)) { return descriptor('HTMLInputElement', 'value').set } if (isTextarea(this)) { return descriptor('HTMLTextAreaElement', 'value').set } if (isSelect(this)) { return descriptor('HTMLSelectElement', 'value').set } if (isButton(this)) { return descriptor('HTMLButtonElement', 'value').set } // is an options element return descriptor('HTMLOptionElement', 'value').set } const _getSelectionStart = function () { if (isInput(this)) { return descriptor('HTMLInputElement', 'selectionStart').get } if (isTextarea(this)) { return descriptor('HTMLTextAreaElement', 'selectionStart').get } throw new Error('this should never happen, cannot get selectionStart') } const _getSelectionEnd = function () { if (isInput(this)) { return descriptor('HTMLInputElement', 'selectionEnd').get } if (isTextarea(this)) { return descriptor('HTMLTextAreaElement', 'selectionEnd').get } throw new Error('this should never happen, cannot get selectionEnd') } const _nativeFocus = function () { if ($window.isWindow(this)) { return window.focus } if (isSvg(this)) { return window.SVGElement.prototype.focus } return window.HTMLElement.prototype.focus } const _nativeBlur = function () { if ($window.isWindow(this)) { return window.blur } if (isSvg(this)) { return window.SVGElement.prototype.blur } return window.HTMLElement.prototype.blur } const _nativeSetSelectionRange = function () { if (isInput(this)) { return window.HTMLInputElement.prototype.setSelectionRange } // is textarea return window.HTMLTextAreaElement.prototype.setSelectionRange } const _nativeSelect = function () { if (isInput(this)) { return window.HTMLInputElement.prototype.select } // is textarea return window.HTMLTextAreaElement.prototype.select } const _isContentEditable = function () { if (isSvg(this)) { return false } return descriptor('HTMLElement', 'isContentEditable').get } const _setType = function () { if (isInput(this)) { return descriptor('HTMLInputElement', 'type').set } if (isButton(this)) { return descriptor('HTMLButtonElement', 'type').set } throw new Error('this should never happen, cannot set type') } const _getType = function () { if (isInput(this)) { return descriptor('HTMLInputElement', 'type').get } if (isButton(this)) { return descriptor('HTMLButtonElement', 'type').get } throw new Error('this should never happen, cannot get type') } const _getMaxLength = function () { if (isInput(this)) { return descriptor('HTMLInputElement', 'maxLength').get } if (isTextarea(this)) { return descriptor('HTMLTextAreaElement', 'maxLength').get } throw new Error('this should never happen, cannot get maxLength') } const nativeGetters = { value: _getValue, isContentEditable: _isContentEditable, isCollapsed: descriptor('Selection', 'isCollapsed').get, selectionStart: _getSelectionStart, selectionEnd: _getSelectionEnd, type: _getType, activeElement: descriptor('Document', 'activeElement').get, body: descriptor('Document', 'body').get, frameElement: Object.getOwnPropertyDescriptor(window, 'frameElement')!.get, maxLength: _getMaxLength, } const nativeSetters = { value: _setValue, type: _setType, } 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, hasFocus: window.document.hasFocus, blur: _nativeBlur, select: _nativeSelect, } const tryCallNativeMethod = (obj, fn, ...args) => { try { return callNativeMethod(obj, fn, ...args) } catch (err) { return } } const callNativeMethod = function (obj, fn, ...args) { const nativeFn = nativeMethods[fn] if (!nativeFn) { 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<T, K extends keyof T> (obj: T, prop: K): T[K] { const nativeProp = nativeGetters[prop as string] if (!nativeProp) { 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<T, K extends keyof T> (obj: T, prop: K, val) { const nativeProp = nativeSetters[prop as string] if (!nativeProp) { const fns = _.keys(nativeSetters).join(', ') throw new Error(`attempted to use a native setter prop called: ${prop}. Available props are: ${fns}`) } let retProp = nativeProp.call(obj, val) if (_.isFunction(retProp)) { retProp = retProp.call(obj, val) } return retProp } interface HTMLValueIsNumberTypeElement extends HTMLElement { value: number } const isValueNumberTypeElement = (el: HTMLElement): el is HTMLValueIsNumberTypeElement => { return valueIsNumberTypeRe.test(getTagName(el)) } export interface HTMLSingleValueChangeInputElement extends HTMLInputElement { type: 'date' | 'time' | 'week' | 'month' } const isNeedSingleValueChangeInputElement = (el: HTMLElement): el is HTMLSingleValueChangeInputElement => { if (!isInput(el)) { return false } return inputTypeNeedSingleValueChangeRe.test((el.getAttribute('type') || '').toLocaleLowerCase()) } const canSetSelectionRangeElement = (el): el is HTMLElementCanSetSelectionRange => { //TODO: If IE, all inputs can set selection range return isTextarea(el) || (isInput(el) && canSetSelectionRangeElementRe.test(getNativeProp(el, 'type'))) } const getTagName = (el) => { const tagName = el.tagName || '' return tagName.toLowerCase() } // this property is the tell-all for contenteditable // should be true for elements: // - with [contenteditable] // - with document.designMode = 'on' const isContentEditable = (el: HTMLElement): el is HTMLContentEditableElement => { return getNativeProp(el, 'isContentEditable') || $document.getDocumentFromElement(el).designMode === 'on' } const isTextarea = (el): el is HTMLTextAreaElement => { return getTagName(el) === 'textarea' } const isInput = (el): el is HTMLInputElement => { return getTagName(el) === 'input' } const isButton = (el): el is HTMLButtonElement => { return getTagName(el) === 'button' } const isSelect = (el): el is HTMLSelectElement => { return getTagName(el) === 'select' } const isOption = (el) => { return getTagName(el) === 'option' } const isOptgroup = (el) => { return getTagName(el) === 'optgroup' } const isBody = (el): el is HTMLBodyElement => { return getTagName(el) === 'body' } const isIframe = (el) => { return getTagName(el) === 'iframe' } const isHTML = (el) => { return getTagName(el) === 'html' } const isSvg = function (el): el is SVGElement { try { return 'ownerSVGElement' in el } catch (error) { return false } } // active element is the default if its null // or it's equal to document.body that is not contenteditable const activeElementIsDefault = (activeElement, body: HTMLElement) => { return !activeElement || (activeElement === body && !isContentEditable(body)) } const isFocused = (el) => { try { let doc if (isWithinShadowRoot(el)) { doc = el.getRootNode() } else { doc = $document.getDocumentFromElement(el) } const { activeElement, body } = doc if (activeElementIsDefault(activeElement, body)) { return false } return doc.activeElement === el } catch (err) { return false } } const isFocusedOrInFocused = (el: HTMLElement) => { debug('isFocusedOrInFocus', el) const doc = $document.getDocumentFromElement(el) if (!doc.hasFocus()) { return false } const { activeElement } = doc let elToCheckCurrentlyFocused let isContentEditableEl = false if (isFocusable($(el))) { elToCheckCurrentlyFocused = el } else if (isContentEditable(el)) { isContentEditableEl = true elToCheckCurrentlyFocused = $selection.getHostContenteditable(el) } debug('elToCheckCurrentlyFocused', elToCheckCurrentlyFocused) if (elToCheckCurrentlyFocused && elToCheckCurrentlyFocused === activeElement) { if (isContentEditableEl) { // we make sure the the current document selection (blinking cursor) is inside the element const sel = doc.getSelection() if (sel?.rangeCount) { const range = sel.getRangeAt(0) const curSelectionContainer = range.commonAncestorContainer const selectionInsideElement = el.contains(curSelectionContainer) debug('isInFocused by document selection?', selectionInsideElement, ':', curSelectionContainer, 'is inside', el) return selectionInsideElement } // no selection, not in focused return false } return true } return false } // mostly useful when traversing up parent nodes and wanting to // stop traversal if el is undefined or is html, body, or document const isUndefinedOrHTMLBodyDoc = ($el: JQuery<HTMLElement>) => { return !$el || !$el[0] || $el.is('body,html') || $document.isDocument($el[0]) } const isElement = function (obj): obj is HTMLElement | JQuery<HTMLElement> { try { if ($jquery.isJquery(obj)) { obj = obj[0] } return Boolean(obj && _.isElement(obj)) } catch (error) { return false } } const isDesignModeDocumentElement = (el: HTMLElement) => { return isElement(el) && getTagName(el) === 'html' && isContentEditable(el) } /** * The element can be activeElement, receive focus events, and also receive keyboard events */ const isFocusable = ($el: JQuery<HTMLElement>) => { return ( _.some(focusableSelectors, (sel) => $el.is(sel)) || isDesignModeDocumentElement($el.get(0)) ) } /** * The element can be activeElement, receive focus events, and also receive keyboard events * OR, it is a disabled element that would have been focusable */ const isFocusableWhenNotDisabled = ($el: JQuery<HTMLElement>) => { return ( _.some(focusableWhenNotDisabledSelectors, (sel) => $el.is(sel)) || isDesignModeDocumentElement($el.get(0)) ) } const isW3CRendered = (el) => { // @see https://html.spec.whatwg.org/multipage/rendering.html#being-rendered return !(parentHasDisplayNone(wrap(el)) || wrap(el).css('visibility') === 'hidden') } const isW3CFocusable = (el) => { // @see https://html.spec.whatwg.org/multipage/interaction.html#focusable-area return isFocusable(wrap(el)) && isW3CRendered(el) } type JQueryOrEl<T extends HTMLElement> = JQuery<T> | T const isInputType = function (el: JQueryOrEl<HTMLElement>, type) { el = ([] as HTMLElement[]).concat($jquery.unwrap(el))[0] if (!isInput(el) && !isButton(el)) { return false } // NOTE: use DOMElement.type instead of getAttribute('type') since // <input type="asdf"> 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 isAttrType = function (el: HTMLInputElement, type: string) { const elType = (el.getAttribute('type') || '').toLowerCase() return elType === type } const isScrollOrAuto = (prop) => { return prop === 'scroll' || prop === 'auto' } const isAncestor = ($el, $maybeAncestor) => { return $jquery.wrap(getAllParents($el[0])).index($maybeAncestor) >= 0 } const getFirstCommonAncestor = (el1, el2) => { // get all parents of each element const el1Ancestors = [el1].concat(getAllParents(el1)) const el2Ancestors = [el2].concat(getAllParents(el2)) let a let b // choose the largest tree of parents to // traverse up if (el1Ancestors.length > el2Ancestors.length) { a = el1Ancestors b = el2Ancestors } else { a = el2Ancestors b = el1Ancestors } // for each ancestor of the largest of the two // parent arrays, check if the other parent array // contains it. for (const ancestor of a) { if (b.includes(ancestor)) { return ancestor } } return el2 } const isShadowRoot = (maybeRoot) => { return maybeRoot?.toString() === '[object ShadowRoot]' } const isWithinShadowRoot = (node: HTMLElement) => { return isShadowRoot(node.getRootNode()) } const getParentNode = (el) => { // if the element has a direct parent element, // simply return it. if (el.parentElement) { return el.parentElement } const root = el.getRootNode() // if the element is inside a shadow root, // return the host of the root. if (root && isWithinShadowRoot(el)) { return root.host } return null } const getParent = ($el: JQuery): JQuery => { return $(getParentNode($el[0])) } const getAllParents = (el: HTMLElement, untilSelectorOrEl?: string | HTMLElement | JQuery) => { const collectParents = (parents, node) => { const parent = getParentNode(node) if (!parent || untilSelectorOrEl && $(parent).is(untilSelectorOrEl)) { return parents } return collectParents(parents.concat(parent), parent) } return collectParents([], el) } const isChild = ($el, $maybeChild) => { return $el.children().index($maybeChild) >= 0 } const isSelector = ($el: JQuery<HTMLElement>, selector) => { return $el.is(selector) } const isDisabled = ($el: JQuery) => { return $el.prop('disabled') } const isReadOnlyInputOrTextarea = ( el: HTMLInputElement | HTMLTextAreaElement, ) => { return el.readOnly } const isReadOnlyInput = ($el: JQuery) => { return $el.prop('readonly') } const isDetached = ($el) => { return !isAttached($el) } const 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 } const nodes: Node[] = [] // push the set of elements to the nodes array // whether they are wrapped or not if ($jquery.isJquery($el)) { nodes.push(...$el.toArray()) } else if ($el) { nodes.push($el) } // if there are no nodes, nothing is attached if (nodes.length === 0) { return false } // check that every node has an active window // and is connected to the dom return nodes.every((node) => { const doc = $document.getDocumentFromElement(node) if (!$document.hasActiveWindow(doc)) { return false } return node.isConnected }) } /** * @param {HTMLElement} el */ const isDetachedEl = (el) => { return !isAttachedEl(el) } /** * @param {HTMLElement} el */ const isAttachedEl = function (el) { return isAttached($(el)) } const isSame = function ($el1, $el2) { const el1 = $jquery.unwrap($el1) const el2 = $jquery.unwrap($el2) return el1 && el2 && _.isEqual(el1, el2) } export interface HTMLContentEditableElement extends HTMLElement { isContenteditable: true } export interface HTMLTextLikeInputElement extends HTMLInputElement { type: | 'text' | 'password' | 'email' | 'number' | 'date' | 'week' | 'month' | 'time' | 'datetime' | 'datetime-local' | 'search' | 'url' | 'tel' setSelectionRange: HTMLInputElement['setSelectionRange'] } export interface HTMLElementCanSetSelectionRange extends HTMLElement { setSelectionRange: HTMLInputElement['setSelectionRange'] value: HTMLInputElement['value'] selectionStart: number selectionEnd: number } export type HTMLTextLikeElement = HTMLTextAreaElement | HTMLTextLikeInputElement | HTMLContentEditableElement const isTextLike = function (el: HTMLElement): el is HTMLTextLikeElement { const $el = $jquery.wrap(el) const sel = (selector) => { return isSelector($el, selector) } const type = (type) => { if (isInput(el)) { return isInputType(el, type) } return false } const isContentEditableElement = isContentEditable(el) if (isContentEditableElement) return true 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'), ]) } const isInputAllowingImplicitFormSubmission = function ($el) { const type = (type) => { return isInputType($el, type) } // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#implicit-submission return _.some([ type('text'), type('search'), type('url'), type('tel'), type('email'), type('password'), type('date'), type('month'), type('week'), type('time'), type('datetime-local'), type('number'), ]) } const isScrollable = ($el) => { const checkDocumentElement = (win, documentElement) => { // Check if body height is higher than window height if (win.innerHeight < documentElement.scrollHeight) { debug('isScrollable: window scrollable on Y') return true } // Check if body width is higher than window width if (win.innerWidth < documentElement.scrollWidth) { debug('isScrollable: window scrollable on X') 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 if ($window.isWindow($el)) { const win = $el return checkDocumentElement(win, win.document.documentElement) } const el = $el[0] // window.getComputedStyle(el) will error if el is undefined if (!el) { return false } // 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 const { 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)) { debug('isScrollable: clientHeight < scrollHeight and scroll/auto overflow') return true } } // x axis if (el.clientWidth < el.scrollWidth) { if (isScrollOrAuto(overflow) || isScrollOrAuto(overflowX)) { debug('isScrollable: clientWidth < scrollWidth and scroll/auto overflow') return true } } return false } const isDescendent = ($el1, $el2) => { if (!$el2) { return false } // if they are equal, consider them a descendent if ($el1.get(0) === $el2.get(0)) { return true } // walk up the tree until we find a parent which // equals the descendent, if ever return findParent($el2.get(0), (node) => { if (node === $el1.get(0)) { return node } }) === $el1.get(0) } const findParent = (el, condition) => { const collectParent = (node) => { const parent = getParentNode(node) if (!parent) return null const parentMatchingCondition = condition(parent, node) if (parentMatchingCondition) return parentMatchingCondition return collectParent(parent) } return collectParent(el) } // 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 const getFirstFocusableEl = ($el: JQuery<HTMLElement>) => { if (isFocusable($el)) { return $el } const $parent = getParent($el) // 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(getParent($el)) } const getActiveElByDocument = ($el: JQuery<HTMLElement>): HTMLElement | null => { let activeElement if (isWithinShadowRoot($el[0])) { activeElement = ($el[0].getRootNode() as ShadowRoot).activeElement } else { activeElement = getNativeProp($el[0].ownerDocument as Document, 'activeElement') } if (isFocused(activeElement)) { return activeElement as HTMLElement } return null } const getFirstParentWithTagName = ($el, tagName) => { if (isUndefinedOrHTMLBodyDoc($el) || !tagName) { return null } // if this element is already the tag we want, // return it if (getTagName($el.get(0)) === tagName) { return $el } // walk up the tree until we find a parent with // the tag we want return findParent($el.get(0), (node) => { if (getTagName(node) === tagName) { return $jquery.wrap(node) } return null }) } const getFirstFixedOrStickyPositionParent = ($el) => { if (isUndefinedOrHTMLBodyDoc($el)) { return null } if (fixedOrStickyRe.test($el.css('position'))) { return $el } // walk up the tree until we find an element // with a fixed/sticky position return findParent($el.get(0), (node) => { let wrapped = $jquery.wrap(node) if (fixedOrStickyRe.test(wrapped.css('position'))) { return wrapped } return null }) } const getFirstStickyPositionParent = ($el) => { if (isUndefinedOrHTMLBodyDoc($el)) { return null } if ($el.css('position') === 'sticky') { return $el } // walk up the tree until we find an element // with a sticky position return findParent($el.get(0), (node) => { let wrapped = $jquery.wrap(node) if (wrapped.css('position') === 'sticky') { return wrapped } return null }) } const getFirstScrollableParent = ($el) => { if (isUndefinedOrHTMLBodyDoc($el)) { return null } // walk up the tree until we find a scrollable // parent return findParent($el.get(0), (node) => { let wrapped = $jquery.wrap(node) if (isScrollable(wrapped)) { return wrapped } return null }) } const getElements = ($el) => { // bail if no $el or length if (!_.get($el, 'length')) { return } // unroll the jquery object const els = $jquery.unwrap($el) if (els.length === 1) { return els[0] } return els } const whitespaces = /\s+/g // When multiple space characters are considered as a single whitespace in all tags except <pre>. const normalizeWhitespaces = (elem) => { let testText = elem.textContent || elem.innerText || $(elem).text() if (elem.tagName === 'PRE') { return testText } return testText.replace(whitespaces, ' ') } const getContainsSelector = (text, filter = '', options: { matchCase?: boolean } = {}) => { const $expr = $.expr[':'] const escapedText = $utils.escapeQuotes(text) // they may have written the filter as // comma separated dom els, so we want to search all // https://github.com/cypress-io/cypress/issues/2407 const filters = filter.trim().split(',') let cyContainsSelector if (_.isRegExp(text)) { if (options.matchCase === false && !text.flags.includes('i')) { text = new RegExp(text.source, text.flags + 'i') // eslint-disable-line prefer-template } // taken from jquery's normal contains method cyContainsSelector = function (elem) { let testText = normalizeWhitespaces(elem) return text.test(testText) } } else if (_.isString(text)) { cyContainsSelector = function (elem) { let testText = normalizeWhitespaces(elem) if (!options.matchCase) { testText = testText.toLowerCase() text = text.toLowerCase() } return testText.includes(text) } } else { cyContainsSelector = $expr.contains } // we set the `cy-contains` jquery selector which will only be used // in the context of cy.contains(...) command and selector playground. $expr['cy-contains'] = cyContainsSelector const selectors = _.map(filters, (filter) => { // https://github.com/cypress-io/cypress/issues/8626 // Sizzle cannot parse when \' is used inside [attribute~='value'] selector. // We need to use other type of quote characters. const textToFind = escapedText.includes(`\'`) ? `"${escapedText}"` : `'${escapedText}'` // use custom cy-contains selector that is registered above return `${filter}:not(script,style):cy-contains(${textToFind}), ${filter}[type='submit'][value~=${textToFind}]` }) return selectors.join() } const priorityElement = 'input[type=\'submit\'], button, a, label' const getFirstDeepestElement = ($el: JQuery, 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 = $el.slice(index, index + 1) const $next = $el.slice(index + 1, index + 2) if (!$next) { return $current } // does current contain next? if ($.contains($current.get(0), $next.get(0))) { return getFirstDeepestElement($el, index + 1) } // 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 $parents = $jquery.wrap(getAllParents($current[0])).filter(priorityElement) const $priorities = $el.filter($parents) if ($priorities.length) { return $priorities.last() } return $current } // short form css-inlines the element // long form returns the outerHTML const stringify = (el, form = 'long') => { // if we are formatting the window object if ($window.isWindow(el)) { return '<window>' } // if we are formatting the document object if ($document.isDocument(el)) { return '<document>' } // convert this to jquery if its not already one const $el = $jquery.wrap(el) const long = () => { const str = $el .clone() .empty() .prop('outerHTML') const text = (_.chain($el.text()) as any) .clean() .truncate({ length: 10 }) .value() const children = $el.children().length if (children) { return str.replace('></', '>...</') } if (text) { return str.replace('></', `>${text}</`) } return str } const short = () => { const id = $el.prop('id') const klass = $el.attr('class') let str = $el.prop('tagName').toLowerCase() if (id) { str += `#${id}` } // using attr here instead of class because // svg's return an SVGAnimatedString object // instead of a normal string when calling // the property 'class' if (klass) { str += `.${klass.split(/\s+/).join('.')}` } // if we have more than one element, // format it so that the user can see there's more if ($el.length > 1) { return `[ <${str}>, ${$el.length - 1} more... ]` } return `<${str}>` } return $utils.switchCase(form, { long, short, }) } // if the node has a shadow root, we must behave like // the browser and find the inner element of the shadow // root at that same point. const getShadowElementFromPoint = (node, x, y) => { const nodeFromPoint = node?.shadowRoot?.elementFromPoint(x, y) if (!nodeFromPoint || nodeFromPoint === node) return node return getShadowElementFromPoint(nodeFromPoint, x, y) } const elementFromPoint = (doc, x, y) => { // first try the native elementFromPoint method let elFromPoint = doc.elementFromPoint(x, y) return getShadowElementFromPoint(elFromPoint, x, y) } const getShadowRoot = ($el: JQuery): JQuery<Node> => { const root = $el[0].getRootNode() return $(root) } const findAllShadowRoots = (root: Node): Node[] => { const collectRoots = (roots, nodes, node) => { const currentRoot = roots.pop() if (!currentRoot) return nodes const childRoots = findShadowRoots(currentRoot) if (childRoots.length > 0) { roots.push(...childRoots) nodes.push(...childRoots) } return collectRoots(roots, nodes, currentRoot) } return collectRoots([root], [], root) } const findShadowRoots = (root: Node): Node[] => { // get the document for this node const doc = root.getRootNode({ composed: true }) as Document // create a walker for efficiently traversing the // dom of this node const walker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_DOCUMENT_FRAGMENT, { acceptNode (node) { // we only care about nodes which have a shadow root if ((node as Element).shadowRoot) { return NodeFilter.FILTER_ACCEPT } // we skip other nodes, but continue to traverse their children return NodeFilter.FILTER_SKIP }, }) const roots: Node[] = [] const rootAsElement = root as Element if (rootAsElement.shadowRoot) { roots.push(rootAsElement.shadowRoot) } const collectRoots = (roots) => { const nextNode = walker.nextNode() as Element if (!nextNode) return roots return collectRoots(roots.concat(nextNode.shadowRoot)) } return collectRoots(roots) } const hasContenteditableAttr = (el: HTMLElement) => { const attr = tryCallNativeMethod(el, 'getAttribute', 'contenteditable') return attr !== undefined && attr !== null && attr !== 'false' } export { elementFromPoint, isElement, isUndefinedOrHTMLBodyDoc, isSelector, isScrollOrAuto, isFocusable, isFocusableWhenNotDisabled, isDisabled, isReadOnlyInput, isReadOnlyInputOrTextarea, isW3CFocusable, isAttached, isDetached, isAttachedEl, isDetachedEl, isAncestor, isChild, isScrollable, isTextLike, isDescendent, isContentEditable, isSame, isOption, isOptgroup, isBody, isHTML, isInput, isIframe, isTextarea, isInputType, isAttrType, isFocused, isFocusedOrInFocused, isInputAllowingImplicitFormSubmission, isValueNumberTypeElement, isNeedSingleValueChangeInputElement, canSetSelectionRangeElement, stringify, getNativeProp, setNativeProp, callNativeMethod, tryCallNativeMethod, findParent, findAllShadowRoots, findShadowRoots, isShadowRoot, isWithinShadowRoot, getElements, getFirstFocusableEl, getActiveElByDocument, getContainsSelector, getFirstDeepestElement, getFirstCommonAncestor, getTagName, getFirstParentWithTagName, getFirstFixedOrStickyPositionParent, getFirstStickyPositionParent, getFirstScrollableParent, getParent, getParentNode, getAllParents, getShadowRoot, hasContenteditableAttr, }