-
Notifications
You must be signed in to change notification settings - Fork 3.3k
/
Copy pathquerying.coffee
467 lines (373 loc) · 14.2 KB
/
querying.coffee
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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
_ = require("lodash")
$ = require("jquery")
Promise = require("bluebird")
$dom = require("../../dom")
$utils = require("../../cypress/utils")
$expr = $.expr[":"]
$contains = $expr.contains
restoreContains = ->
$expr.contains = $contains
module.exports = (Commands, Cypress, cy, state, config) ->
## restore initially when a run starts
restoreContains()
## restore before each test and whenever we stop
Cypress.on("test:before:run", restoreContains)
Cypress.on("stop", restoreContains)
Commands.addAll({
focused: (options = {}) ->
_.defaults(options, {
verify: true
log: true
})
if options.log
options._log = Cypress.log()
log = ($el) ->
return if options.log is false
options._log.set({
$el: $el
consoleProps: ->
ret = if $el
$dom.getElements($el)
else
"--nothing--"
Yielded: ret
Elements: $el?.length ? 0
})
getFocused = ->
focused = cy.getFocused()
log(focused)
return focused
do resolveFocused = (failedByNonAssertion = false) ->
Promise
.try(getFocused)
.then ($el) ->
if options.verify is false
return $el
if not $el
$el = $dom.wrap(null)
$el.selector = "focused"
## pass in a null jquery object for assertions
cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveFocused
})
get: (selector, options = {}) ->
ctx = @
_.defaults(options, {
retry: true
withinSubject: cy.state("withinSubject")
log: true
command: null
verify: true
})
consoleProps = {}
start = (aliasType) ->
return if options.log is false
options._log ?= Cypress.log
message: selector
referencesAlias: if aliasObj?.alias then {name: aliasObj.alias}
aliasType: aliasType
consoleProps: -> consoleProps
log = (value, aliasType = "dom") ->
return if options.log is false
start(aliasType) if not _.isObject(options._log)
obj = {}
if aliasType is "dom"
_.extend(obj, {
$el: value
numRetries: options._retries
})
obj.consoleProps = ->
key = if aliasObj then "Alias" else "Selector"
consoleProps[key] = selector
switch aliasType
when "dom"
_.extend(consoleProps, {
Yielded: $dom.getElements(value)
Elements: value?.length
})
when "primitive"
_.extend(consoleProps, {
Yielded: value
})
when "route"
_.extend(consoleProps, {
Yielded: value
})
return consoleProps
options._log.set(obj)
## We want to strip everything after the last '.'
## only when it is potentially a number or 'all'
if _.indexOf(selector, ".") == -1 ||
selector.slice(1) in _.keys(cy.state("aliases"))
toSelect = selector
else
allParts = _.split(selector, '.')
toSelect = _.join(_.dropRight(allParts, 1), '.')
if aliasObj = cy.getAlias(toSelect)
{subject, alias, command} = aliasObj
return do resolveAlias = ->
switch
## if this is a DOM element
when $dom.isElement(subject)
replayFrom = false
replay = ->
cy.replayCommandsFrom(command)
## its important to return undefined
## here else we trick cypress into thinking
## we have a promise violation
return undefined
## if we're missing any element
## within our subject then filter out
## anything not currently in the DOM
if $dom.isDetached(subject)
subject = subject.filter (index, el) ->
$dom.isAttached(el)
## if we have nothing left
## just go replay the commands
if not subject.length
return replay()
log(subject)
return cy.verifyUpcomingAssertions(subject, options, {
onFail: (err) ->
## if we are failing because our aliased elements
## are less than what is expected then we know we
## need to requery for them and can thus replay
## the commands leading up to the alias
if err.type is "length" and err.actual < err.expected
replayFrom = true
onRetry: ->
if replayFrom
replay()
else
resolveAlias()
})
## if this is a route command
when command.get("name") is "route"
if !(_.indexOf(selector, ".") == -1 ||
selector.slice(1) in _.keys(cy.state("aliases")))
allParts = _.split(selector, ".")
index = _.last(allParts)
alias = _.join([alias, index], ".")
requests = cy.getRequestsByAlias(alias) ? null
log(requests, "route")
return requests
else
## log as primitive
log(subject, "primitive")
do verifyAssertions = =>
cy.verifyUpcomingAssertions(subject, options, {
ensureExistenceFor: false
onRetry: verifyAssertions
})
start("dom")
setEl = ($el) ->
return if options.log is false
consoleProps.Yielded = $dom.getElements($el)
consoleProps.Elements = $el?.length
options._log.set({$el: $el})
getElements = ->
## attempt to query for the elements by withinSubject context
## and catch any sizzle errors!
try
$el = cy.$$(selector, options.withinSubject)
catch e
e.onFail = -> if options.log is false then e else options._log.error(e)
throw e
## if that didnt find anything and we have a within subject
## and we have been explictly told to filter
## then just attempt to filter out elements from our within subject
if not $el.length and options.withinSubject and options.filter
filtered = options.withinSubject.filter(selector)
## reset $el if this found anything
$el = filtered if filtered.length
## store the $el now in case we fail
setEl($el)
## allow retry to be a function which we ensure
## returns truthy before returning its
if _.isFunction(options.onRetry)
if ret = options.onRetry.call(ctx, $el)
log($el)
return ret
else
log($el)
return $el
do resolveElements = ->
Promise.try(getElements).then ($el) ->
if options.verify is false
return $el
cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveElements
})
root: (options = {}) ->
_.defaults options, {log: true}
if options.log isnt false
options._log = Cypress.log({message: ""})
log = ($el) ->
options._log.set({$el: $el}) if options.log
return $el
if withinSubject = cy.state("withinSubject")
return log(withinSubject)
cy.now("get", "html", {log: false}).then(log)
})
Commands.addAll({ prevSubject: ["optional", "window", "document", "element"] }, {
contains: (subject, filter, text, options = {}) ->
## nuke our subject if its present but not an element.
## in these cases its either window or document but
## we dont care.
## we'll null out the subject so it will show up as a parent
## command since its behavior is identical to using it
## as a parent command: cy.contains()
if subject and not $dom.isElement(subject)
subject = null
switch
when _.isRegExp(text)
text = text
filter = filter
when _.isObject(text)
options = text
text = filter
filter = ""
when _.isUndefined(text)
text = filter
filter = ""
_.defaults options, {log: true}
$utils.throwErrByPath "contains.invalid_argument" if not (_.isString(text) or _.isFinite(text) or _.isRegExp(text))
$utils.throwErrByPath "contains.empty_string" if _.isBlank(text)
getPhrase = (type, negated) ->
switch
when filter and subject
node = $dom.stringify(subject, "short")
"within the element: #{node} and with the selector: '#{filter}' "
when filter
"within the selector: '#{filter}' "
when subject
node = $dom.stringify(subject, "short")
"within the element: #{node} "
else
""
getErr = (err) ->
{type, negated, node} = err
switch type
when "existence"
if negated
"Expected not to find content: '#{text}' #{getPhrase(type, negated)}but continuously found it."
else
"Expected to find content: '#{text}' #{getPhrase(type, negated)}but never did."
if options.log isnt false
consoleProps = {
Content: text
"Applied To": $dom.getElements(subject or cy.state("withinSubject"))
}
options._log = Cypress.log
message: _.compact([filter, text])
type: if subject then "child" else "parent"
consoleProps: -> consoleProps
setEl = ($el) ->
return if options.log is false
consoleProps.Yielded = $dom.getElements($el)
consoleProps.Elements = $el?.length
options._log.set({$el: $el})
if _.isRegExp(text)
$expr.contains = (elem) ->
## taken from jquery's normal contains method
text.test(elem.textContent or elem.innerText or $.text(elem))
## find elements by the :contains psuedo selector
## and any submit inputs with the attributeContainsWord selector
selector = $dom.getContainsSelector(text, filter)
checkToAutomaticallyRetry = (count, $el) ->
## we should automatically retry querying
## if we did not have any upcoming assertions
## and our $el's length was 0, because that means
## the element didnt exist in the DOM and the user
## did not explicitly request that it does not exist
return if count isnt 0 or ($el and $el.length)
## throw here to cause the .catch to trigger
throw new Error()
resolveElements = ->
getOpts = _.extend(_.clone(options), {
# error: getErr(text, phrase)
withinSubject: subject or cy.state("withinSubject") or cy.$$("body")
filter: true
log: false
# retry: false ## dont retry because we perform our own element validation
verify: false ## dont verify upcoming assertions, we do that ourselves
})
cy.now("get", selector, getOpts).then ($el) ->
if $el and $el.length
$el = $dom.getFirstDeepestElement($el)
setEl($el)
cy.verifyUpcomingAssertions($el, options, {
onRetry: resolveElements
onFail: (err) ->
switch err.type
when "length"
if err.expected > 1
$utils.throwErrByPath "contains.length_option", { onFail: options._log }
when "existence"
err.displayMessage = getErr(err)
})
Promise
.try(resolveElements)
.finally ->
## always restore contains in case
## we used a regexp!
restoreContains()
})
Commands.addAll({ prevSubject: "element" }, {
within: (subject, options, fn) ->
ctx = @
if _.isUndefined(fn)
fn = options
options = {}
_.defaults options, {log: true}
if options.log
options._log = Cypress.log({
$el: subject
message: ""
})
$utils.throwErrByPath("within.invalid_argument", { onFail: options._log }) if not _.isFunction(fn)
## reference the next command after this
## within. when that command runs we'll
## know to remove withinSubject
next = cy.state("current").get("next")
## backup the current withinSubject
## this prevents a bug where we null out
## withinSubject when there are nested .withins()
## we want the inner within to restore the outer
## once its done
prevWithinSubject = cy.state("withinSubject")
cy.state("withinSubject", subject)
fn.call(ctx, subject)
cleanup = ->
cy.removeListener("command:start", setWithinSubject)
## we need a mechanism to know when we should remove
## our withinSubject so we dont accidentally keep it
## around after the within callback is done executing
## so when each command starts, check to see if this
## is the command which references our 'next' and
## if so, remove the within subject
setWithinSubject = (obj) ->
return if obj isnt next
## okay so what we're doing here is creating a property
## which stores the 'next' command which will reset the
## withinSubject. If two 'within' commands reference the
## exact same 'next' command, then this prevents accidentally
## resetting withinSubject more than once. If they point
## to differnet 'next's then its okay
if next isnt cy.state("nextWithinSubject")
cy.state("withinSubject", prevWithinSubject or null)
cy.state("nextWithinSubject", next)
## regardless nuke this listeners
cleanup()
## if next is defined then we know we'll eventually
## unbind these listeners
if next
cy.on("command:start", setWithinSubject)
else
## remove our listener if we happen to reach the end
## event which will finalize cleanup if there was no next obj
cy.once "command:queue:before:end", ->
cleanup()
cy.state("withinSubject", null)
return subject
})