forked from cypress-io/cypress
-
Notifications
You must be signed in to change notification settings - Fork 0
/
find.ts
303 lines (242 loc) · 7.9 KB
/
find.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import _ from 'lodash'
import $ from 'jquery'
import $document from '../document'
import $jquery from '../jquery'
import { getTagName } from './elementHelpers'
import { isWithinShadowRoot, getShadowElementFromPoint } from './shadow'
import { normalizeWhitespaces } from './utils'
import { escapeQuotes, escapeBackslashes } from '../../util/escape'
/**
* Find Parents relative to an initial element
*/
export 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
}
export const getParent = ($el: JQuery): JQuery => {
return $(getParentNode($el[0]))
}
export 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)
}
export 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)
}
export 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
})
}
// Compares two elements to find the closest common parent
export 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 priorityElement = 'input[type=\'submit\'], button, a, label'
export 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 || $current.length === 0) {
return $current
}
// https://github.com/cypress-io/cypress/issues/14861
// filter out the <script> and <style> tags
if ($current && ['SCRIPT', 'STYLE'].includes($current.prop('tagName'))) {
return getFirstDeepestElement($el, index + 1)
}
// 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
}
/**
* By XY Coordinate
*/
export const elementFromPoint = (doc, x, y) => {
// first try the native elementFromPoint method
let elFromPoint = doc.elementFromPoint(x, y)
return getShadowElementFromPoint(elFromPoint, x, y)
}
/**
* By DOM Hierarchy
* Compares two elements to see what their relationship is
*/
export const isAncestor = ($el, $maybeAncestor) => {
return $jquery.wrap(getAllParents($el[0])).index($maybeAncestor) >= 0
}
export const isChild = ($el, $maybeChild) => {
return $el.children().index($maybeChild) >= 0
}
export 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)
}
// mostly useful when traversing up parent nodes and wanting to
// stop traversal if el is undefined or is html, body, or document
export const isUndefinedOrHTMLBodyDoc = ($el: JQuery<HTMLElement>) => {
return !$el || !$el[0] || $el.is('body,html') || $document.isDocument($el[0])
}
/**
* Utilities
*/
export 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
}
export const getContainsSelector = (text, filter = '', options: {
matchCase?: boolean
} = {}) => {
const $expr = $.expr[':']
const escapedText = escapeQuotes(
escapeBackslashes(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) {
const 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}:cy-contains(${textToFind}), ${filter}[type='submit'][value~=${textToFind}]`
})
return selectors.join()
}
export const getInputFromLabel = ($el) => {
if (!$el.is('label')) {
return $([])
}
// If an element has a "for" attribute, then clicking on it won't
// focus / activate any contained inputs, even if the "for" target doesn't
// exist.
if ($el.attr('for')) {
// The parent().last() is the current document, which is where we want to
// search from.
return $(`#${$el.attr('for')}`, $el.parents().last())
}
// Alternately, if a label contains inputs, clicking it focuses / activates
// the first one.
return $('input', $el).first()
}