add Loggable options to its() / invoke() command (#5519)

* add Loggable options to its() command

* add test for new Loggable option for its()

* add loggable options to invoke()

* add type definition
fix: only set logger config once. afterwards other commands can overwrite the logger config as done in line 322
remove unused error message (usage was removed in a former commit)
remove test that is unnecessary now

* add check if its() was passed additional arguments next to options

* try to fix test

* do not log 'this' context

* from review: add additional tests and fix some edge cases

* add tests for combination of loggable options and numeric index

* fix wrong indentation

* write as 'functionName' and 'propertyName' to match other error message

* Update tests to reflect newly worded errors


Co-authored-by: Jennifer Shehane <shehane.jennifer@gmail.com>
This commit is contained in:
Josef Biehler
2019-12-04 06:22:44 +01:00
committed by Jennifer Shehane
parent bbd519a54f
commit 259bfbecbe
4 changed files with 258 additions and 36 deletions

View File

@@ -871,6 +871,7 @@ declare namespace Cypress {
* @see https://on.cypress.io/invoke
*/
invoke<T extends (...args: any[]) => any, Subject extends T[]>(index: number): Chainable<ReturnType<T>>
invoke<T extends (...args: any[]) => any, Subject extends T[]>(options: Loggable, index: number): Chainable<ReturnType<T>>
/**
* Invoke a function on the previously yielded subject.
@@ -882,6 +883,7 @@ declare namespace Cypress {
* @see https://on.cypress.io/invoke
*/
invoke(functionName: keyof Subject, ...args: any[]): Chainable<Subject> // don't have a way to express return types yet
invoke(options: Loggable, functionName: keyof Subject, ...args: any[]): Chainable<Subject>
/**
* Get a propertys value on the previously yielded subject.
@@ -893,14 +895,15 @@ declare namespace Cypress {
* // Drill into nested properties by using dot notation
* cy.wrap({foo: {bar: {baz: 1}}}).its('foo.bar.baz')
*/
its<K extends keyof Subject>(propertyName: K): Chainable<Subject[K]>
its<K extends keyof Subject>(propertyName: K, options?: Loggable): Chainable<Subject[K]>
/**
* Get a value by index from an array yielded from the previous command.
* @see https://on.cypress.io/its
* @example
* cy.wrap(['a', 'b']).its(1).should('equal', 'b')
*/
its<T, Subject extends T[]>(index: number): Chainable<T>
its<T, Subject extends T[]>(index: number, options?: Loggable): Chainable<T>
/**
* Get the last DOM element within a set of DOM elements.

View File

@@ -135,8 +135,26 @@ module.exports = (Commands, Cypress, cy, state, config) ->
}
.finally(cleanup)
invokeFn = (subject, str, args...) ->
options = {}
invokeItsFn = (subject, str, options, args...) ->
return invokeBaseFn(options or { log: true }, subject, str, args...)
invokeFn = (subject, optionsOrStr, args...) ->
optionsPassed = _.isObject(optionsOrStr) and !_.isFunction(optionsOrStr)
options = null
str = null
if not optionsPassed
str = optionsOrStr
options = { log: true }
else
options = optionsOrStr
if args.length > 0
str = args[0]
args = args.slice(1)
return invokeBaseFn(options, subject, str, args...)
invokeBaseFn = (options, subject, str, args...) ->
## name could be invoke or its!
name = state("current").get("name")
@@ -154,21 +172,34 @@ module.exports = (Commands, Cypress, cy, state, config) ->
traversalErr = null
options._log = Cypress.log
message: message
$el: if $dom.isElement(subject) then subject else null
consoleProps: ->
Subject: subject
if options.log
options._log = Cypress.log
message: message
$el: if $dom.isElement(subject) then subject else null
consoleProps: ->
Subject: subject
if not str
$utils.throwErrByPath("invoke_its.null_or_undefined_property_name", {
onFail: options._log
args: { cmd: name, identifier: if isCmdIts then "property" else "function" }
})
if not _.isString(str) and not _.isNumber(str)
$utils.throwErrByPath("invoke_its.invalid_1st_arg", {
$utils.throwErrByPath("invoke_its.invalid_prop_name_arg", {
onFail: options._log
args: { cmd: name, identifier: if isCmdIts then "property" else "function" }
})
if not _.isObject(options) or _.isFunction(options)
$utils.throwErrByPath("invoke_its.invalid_options_arg", {
onFail: options._log
args: { cmd: name }
})
if isCmdIts and args.length > 0
$utils.throwErrByPath("invoke_its.invalid_num_of_args", {
onFail: options._log
if isCmdIts and args and args.length > 0
$utils.throwErrByPath("invoke_its.invalid_num_of_args", {
onFail: options._log
args: { cmd: name }
})
@@ -449,9 +480,9 @@ module.exports = (Commands, Cypress, cy, state, config) ->
## return values are undefined. prob should rethink
## this and investigate why that is the default behavior
## of child commands
invoke: ->
invokeFn.apply(@, arguments)
invoke: (subject, optionsOrStr, args...) ->
invokeFn.apply(@, [subject, optionsOrStr, args...])
its: ->
invokeFn.apply(@, arguments)
its: (subject, str, options, args...) ->
invokeItsFn.apply(@, [subject, str, options, args...])
})

View File

@@ -409,12 +409,13 @@ module.exports = {
cy.wrap({ foo: {{value}} }).its('foo.baz').should('not.exist')
"""
invalid_1st_arg: "#{cmd('{{cmd}}')} only accepts a string or a number as the first argument."
invalid_num_of_args:
"""
#{cmd('{{cmd}}')} only accepts a single argument.
If you want to invoke a function with arguments, use cy.invoke().
invalid_prop_name_arg: "#{cmd('{{cmd}}')} only accepts a string or a number as the {{identifier}}Name argument."
null_or_undefined_property_name: "#{cmd('{{cmd}}')} expects the {{identifier}}Name argument to have a value."
invalid_options_arg: "#{cmd('{{cmd}}')} only accepts an object as the options argument."
invalid_num_of_args:
"""
#{cmd('{{cmd}}')} does not accept additional arguments.
If you want to invoke a function with arguments, use cy.invoke().
"""
timed_out:
"""

View File

@@ -473,6 +473,120 @@ describe "src/cy/commands/connectors", ->
cy.wrap(obj).invoke("foo.bar")
describe "accepts a options argument", ->
it "changes subject to function invocation", ->
cy.noop({ foo: -> "foo" }).invoke({ log: false }, "foo").then (str) ->
expect(str).to.eq "foo"
it "forwards any additional arguments", ->
cy.noop({ bar: (num1, num2) -> num1 + num2 }).invoke({ log: false }, "bar", 1, 2).then (num) ->
expect(num).to.eq 3
cy.noop({ bar: -> undefined }).invoke({ log: false }, "bar").then (val) ->
expect(val).to.be.undefined
it "works with numerical indexes", ->
i = 0
fn = ->
i++
return i == 5
cy.noop([_.noop, fn]).invoke({}, 1).should('be.true')
describe "errors", ->
beforeEach ->
Cypress.config("defaultCommandTimeout", 50)
cy.on "log:added", (attrs, log) =>
@lastLog = log
return null
it "throws when function name is missing", (done) ->
cy.on "fail", (err) =>
lastLog = @lastLog
expect(err.message).to.include "cy.invoke() expects the functionName argument to have a value"
expect(lastLog.get("error").message).to.include(err.message)
done()
cy.wrap({ foo: -> "foo"}).invoke({})
it "throws when function name is not of type string but of type boolean", (done) ->
cy.on "fail", (err) =>
lastLog = @lastLog
expect(err.message).to.include "cy.invoke() only accepts a string or a number as the functionName argument."
expect(lastLog.get("error").message).to.include(err.message)
done()
cy.wrap({ foo: -> "foo"}).invoke({}, true)
it "throws when function name is not of type string but of type function", (done) ->
cy.on "fail", (err) =>
lastLog = @lastLog
expect(err.message).to.include "cy.invoke() only accepts a string or a number as the functionName argument."
expect(lastLog.get("error").message).to.include(err.message)
done()
cy.wrap({ foo: -> "foo"}).invoke(() -> {})
it "throws when first parameter is neither of type object nor of type string nor of type number", (done) ->
cy.on "fail", (err) =>
lastLog = @lastLog
expect(err.message).to.include "cy.invoke() only accepts a string or a number as the functionName argument."
expect(lastLog.get("error").message).to.include(err.message)
done()
cy.wrap({ foo: -> "foo"}).invoke(true, "show")
describe ".log", ->
beforeEach ->
@obj = {
foo: "foo bar baz"
num: 123
bar: -> "bar"
attr: (key, value) ->
obj = {}
obj[key] = value
obj
sum: (a, b) -> a + b
}
cy.on "log:added", (attrs, log) =>
@lastLog = log
return null
it "logs obj as a function", ->
cy.noop(@obj).invoke({ log: true }, "bar").then ->
obj = {
name: "invoke"
message: ".bar()"
}
lastLog = @lastLog
_.each obj, (value, key) =>
expect(lastLog.get(key)).to.deep.eq value
it "logs obj with arguments", ->
cy.noop(@obj).invoke({ log: true }, "attr", "numbers", [1,2,3]).then ->
expect(@lastLog.invoke("consoleProps")).to.deep.eq {
Command: "invoke"
Function: ".attr(numbers, [1, 2, 3])"
"With Arguments": ["numbers", [1,2,3]]
Subject: @obj
Yielded: {numbers: [1,2,3]}
}
it "can be disabled", ->
cy.noop(@obj).invoke({ log: true }, "sum", 1, 2).then ->
expect(@lastLog.invoke("consoleProps")).to.have.property("Function", ".sum(1, 2)")
@lastLog = undefined
cy.noop(@obj).invoke({ log: false }, "sum", 1, 2).then ->
expect(@lastLog).to.be.undefined
describe ".log", ->
beforeEach ->
@obj = {
@@ -612,16 +726,6 @@ describe "src/cy/commands/connectors", ->
cy.invoke("queue")
it "throws when first argument isnt a string or a number", (done) ->
cy.on "fail", (err) =>
lastLog = @lastLog
expect(err.message).to.eq "cy.invoke() only accepts a string or a number as the first argument."
expect(lastLog.get("error")).to.eq err
done()
cy.wrap({}).invoke({})
it "logs once when not dom subject", (done) ->
cy.on "fail", (err) =>
lastLog = @lastLog
@@ -870,6 +974,54 @@ describe "src/cy/commands/connectors", ->
cy.wrap(obj).its("foo").should("be.undefined")
cy.wrap(obj).its("foo").should("eq", undefined)
describe "accepts a options argument and works as without options argument", ->
it "proxies to #invokeFn", ->
fn = -> "bar"
cy.wrap({foo: fn}).its("foo", { log: false }).should("eq", fn)
it "does not invoke a function and uses as a property", ->
fn = -> "fn"
fn.bar = "bar"
cy.wrap(fn).its("bar", { log: false }).should("eq", "bar")
it "works with numerical indexes", ->
cy.wrap(['foo', 'bar']).its(1, {}).should('eq', 'bar')
describe ".log", ->
beforeEach ->
@obj = {
foo: "foo bar baz"
num: 123
}
cy.on "log:added", (attrs, log) =>
@lastLog = log
return null
it "logs obj as a property", ->
cy.noop(@obj).its("foo", { log: true }).then ->
obj = {
name: "its"
message: ".foo"
}
lastLog = @lastLog
_.each obj, (value, key) =>
expect(lastLog.get(key)).to.deep.eq value
it "#consoleProps as a regular property", ->
cy.noop(@obj).its("num", { log: true }).then ->
expect(@lastLog.invoke("consoleProps")).to.deep.eq {
Command: "its"
Property: ".num"
Subject: @obj
Yielded: 123
}
describe ".log", ->
beforeEach ->
@obj = {
@@ -942,6 +1094,14 @@ describe "src/cy/commands/connectors", ->
Yielded: 123
}
it "can be disabled", ->
cy.noop(@obj).its("num", { log: true }).then ->
expect(@lastLog.invoke("consoleProps")).to.have.property("Property", ".num")
@lastLog = undefined
cy.noop(@obj).its("num", { log: false }).then ->
expect(@lastLog).to.be.undefined
describe "errors", ->
beforeEach ->
Cypress.config("defaultCommandTimeout", 50)
@@ -1132,11 +1292,11 @@ describe "src/cy/commands/connectors", ->
cy.wrap(val).its("foo")
it "throws two args were passed as subject", (done) ->
it "throws does not accept additional arguments", (done) ->
cy.on "fail", (err) =>
lastLog = @lastLog
expect(err.message).to.include "cy.its() only accepts a single argument."
expect(err.message).to.include "cy.its() does not accept additional arguments."
expect(lastLog.get("error").message).to.include(err.message)
done()
@@ -1144,7 +1304,34 @@ describe "src/cy/commands/connectors", ->
fn.bar = -> "bar"
fn.bar.baz = "baz"
cy.wrap(fn).its("bar", "baz").should("eq", "baz")
cy.wrap(fn).its("bar", { log: false }, "baz").should("eq", "baz")
it "throws when options argument is not an object", (done) ->
cy.on "fail", (err) =>
lastLog = @lastLog
expect(err.message).to.include "cy.its() only accepts an object as the options argument."
expect(lastLog.get("error").message).to.include(err.message)
done()
cy.wrap({ foo: "string" }).its("foo", "bar")
it "throws when property name is missing", (done) ->
cy.on "fail", (err) =>
lastLog = @lastLog
expect(err.message).to.include "cy.its() expects the propertyName argument to have a value"
expect(lastLog.get("error").message).to.include(err.message)
done()
cy.wrap({ foo: "foo"}).its()
it "throws when property name is not of type string", (done) ->
cy.on "fail", (err) =>
lastLog = @lastLog
expect(err.message).to.include "cy.its() only accepts a string or a number as the propertyName argument."
expect(lastLog.get("error").message).to.include(err.message)
done()
cy.wrap({ foo: "foo"}).its(true)
it "resets traversalErr and throws the right assertion", (done) ->
cy.timeout(200)