Created
July 14, 2016 15:14
-
-
Save brian-mann/adade2b0b14a5f26167fec44c4a0efbe to your computer and use it in GitHub Desktop.
querying
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| $Cypress.register "Querying", (Cypress, _, $) -> | |
| priorityElement = "input[type='submit'], button, a, label" | |
| $expr = $.expr[":"] | |
| $contains = $expr.contains | |
| restoreContains = -> | |
| $expr.contains = $contains | |
| Cypress.on "abort", restoreContains | |
| Cypress.addParentCommand | |
| get: (selector, options = {}) -> | |
| _.defaults options, | |
| retry: true | |
| withinSubject: @prop("withinSubject") | |
| log: true | |
| command: null | |
| verify: true | |
| @ensureNoCommandOptions(options) | |
| consoleProps = {} | |
| start = (aliasType) -> | |
| return if options.log is false | |
| options._log ?= Cypress.Log.command | |
| message: selector | |
| referencesAlias: 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, | |
| Returned: Cypress.Utils.getDomElements(value) | |
| Elements: value?.length | |
| when "primitive" | |
| _.extend consoleProps, | |
| Returned: value | |
| when "route" | |
| _.extend consoleProps, | |
| Returned: value | |
| return consoleProps | |
| options._log.set(obj) | |
| ## we always want to strip everything after the first '.' | |
| ## since we support alias propertys like '1' or 'all' | |
| if aliasObj = @getAlias(selector.split(".")[0]) | |
| {subject, alias, command} = aliasObj | |
| return do resolveAlias = => | |
| switch | |
| ## if this is a DOM element | |
| when Cypress.Utils.hasElement(subject) | |
| replayFrom = false | |
| replay = => | |
| @_replayFrom command | |
| return null | |
| ## if we're missing any element | |
| ## within our subject then filter out | |
| ## anything not currently in the DOM | |
| if not @_contains(subject) | |
| subject = subject.filter (index, el) -> | |
| cy._contains(el) | |
| ## if we have nothing left | |
| ## just go replay the commands | |
| if not subject.length | |
| return replay() | |
| log(subject) | |
| return @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" | |
| alias = _.compact([alias, selector.split(".")[1]]).join(".") | |
| responses = @getResponsesByAlias(alias) ? null | |
| log(responses, "route") | |
| return responses | |
| else | |
| ## log as primitive | |
| log(subject, "primitive") | |
| return subject | |
| start("dom") | |
| setEl = ($el) -> | |
| return if options.log is false | |
| consoleProps.Returned = Cypress.Utils.getDomElements($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 = @$$(selector, options.withinSubject) | |
| catch e | |
| e.onFail = -> 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(@, $el) | |
| log($el) | |
| return ret | |
| else | |
| log($el) | |
| return $el | |
| do resolveElements = => | |
| Promise.try(getElements).then ($el) => | |
| if options.verify is false | |
| return $el | |
| @verifyUpcomingAssertions($el, options, { | |
| onRetry: resolveElements | |
| }) | |
| root: (options = {}) -> | |
| _.defaults options, {log: true} | |
| if options.log isnt false | |
| options._log = Cypress.Log.command({message: ""}) | |
| log = ($el) -> | |
| options._log.set({$el: $el}) if options.log | |
| return $el | |
| if withinSubject = @prop("withinSubject") | |
| return log(withinSubject) | |
| @execute("get", "html", {log: false}).then(log) | |
| Cypress.addDualCommand | |
| contains: (subject, filter, text, options = {}) -> | |
| ## nuke our subject if its present but not an element | |
| ## since we want contains to operate as a parent command | |
| if subject and not Cypress.Utils.hasElement(subject) | |
| subject = null | |
| switch | |
| when _.isObject(text) | |
| options = text | |
| text = filter | |
| filter = "" | |
| when _.isUndefined(text) | |
| text = filter | |
| filter = "" | |
| _.defaults options, {log: true} | |
| @ensureNoCommandOptions(options) | |
| $Cypress.Utils.throwErrByPath "contains.invalid_argument" if not (_.isString(text) or _.isFinite(text) or _.isRegExp(text)) | |
| $Cypress.Utils.throwErrByPath "contains.empty_string" if _.isBlank(text) | |
| getPhrase = (type, negated) -> | |
| switch | |
| when filter and subject | |
| node = Cypress.Utils.stringifyElement(subject, "short") | |
| "within the element: #{node} and with the selector: '#{filter}' " | |
| when filter | |
| "within the selector: '#{filter}' " | |
| when subject | |
| node = Cypress.Utils.stringifyElement(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": Cypress.Utils.getDomElements(subject or @prop("withinSubject")) | |
| } | |
| options._log = Cypress.Log.command | |
| message: _.compact([filter, text]) | |
| type: if subject then "child" else "parent" | |
| consoleProps: -> consoleProps | |
| getOpts = _.extend _.clone(options), | |
| # error: getErr(text, phrase) | |
| withinSubject: subject or @prop("withinSubject") or @$$("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 | |
| setEl = ($el) -> | |
| return if options.log is false | |
| consoleProps.Returned = Cypress.Utils.getDomElements($el) | |
| consoleProps.Elements = $el?.length | |
| options._log.set({$el: $el}) | |
| 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 | |
| if _.isRegExp(text) | |
| $expr.contains = (elem) -> | |
| ## taken from jquery's normal contains method | |
| text.test(elem.textContent or elem.innerText or $.text(elem)) | |
| else | |
| text = Cypress.Utils.escapeQuotes(text) | |
| ## find elements by the :contains psuedo selector | |
| ## and any submit inputs with the attributeContainsWord selector | |
| selector = "#{filter}:not(script):contains('#{text}'), #{filter}[type='submit'][value~='#{text}']" | |
| 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 = => | |
| @execute("get", selector, getOpts).then ($elements) => | |
| $el = switch | |
| when $elements and $elements.length | |
| getFirstDeepestElement($elements) | |
| else | |
| $elements | |
| setEl($el) | |
| @verifyUpcomingAssertions($el, options, { | |
| onRetry: resolveElements | |
| onFail: (err) => | |
| switch err.type | |
| when "length" | |
| if err.expected > 1 | |
| $Cypress.Utils.throwErrByPath "contains.length_option", { onFail: options._log } | |
| when "existence" | |
| err.longMessage = getErr(err) | |
| }) | |
| Promise | |
| .try(resolveElements) | |
| .finally -> | |
| ## always restore contains in case | |
| ## we used a regexp! | |
| restoreContains() | |
| Cypress.addChildCommand | |
| within: (subject, options, fn) -> | |
| @ensureDom(subject) | |
| if _.isUndefined(fn) | |
| fn = options | |
| options = {} | |
| _.defaults options, {log: true} | |
| if options.log | |
| options._log = Cypress.Log.command | |
| $el: subject | |
| message: "" | |
| $Cypress.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 = @prop("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 = @prop("withinSubject") | |
| @prop("withinSubject", subject) | |
| fn.call @private("runnable").ctx, subject | |
| stop = => | |
| @off "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 @prop("nextWithinSubject") | |
| @prop "withinSubject", prevWithinSubject or null | |
| @prop "nextWithinSubject", next | |
| ## regardless nuke this listeners | |
| stop() | |
| ## if next is defined then we know we'll eventually | |
| ## unbind these listeners | |
| if next | |
| @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 | |
| @once "end", -> | |
| stop() | |
| @prop "withinSubject", null | |
| return subject |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment