diff --git a/cli/types/index.d.ts b/cli/types/index.d.ts index ccc8b6797c..447d780012 100644 --- a/cli/types/index.d.ts +++ b/cli/types/index.d.ts @@ -871,6 +871,7 @@ declare namespace Cypress { * @see https://on.cypress.io/invoke */ invoke any, Subject extends T[]>(index: number): Chainable> + invoke any, Subject extends T[]>(options: Loggable, index: number): Chainable> /** * 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 // don't have a way to express return types yet + invoke(options: Loggable, functionName: keyof Subject, ...args: any[]): Chainable /** * Get a property’s 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(propertyName: K): Chainable + its(propertyName: K, options?: Loggable): Chainable + /** * 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(index: number): Chainable + its(index: number, options?: Loggable): Chainable /** * Get the last DOM element within a set of DOM elements. diff --git a/packages/driver/src/cy/commands/connectors.coffee b/packages/driver/src/cy/commands/connectors.coffee index 306dfd3d58..73c8f48805 100644 --- a/packages/driver/src/cy/commands/connectors.coffee +++ b/packages/driver/src/cy/commands/connectors.coffee @@ -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...]) }) diff --git a/packages/driver/src/cypress/error_messages.coffee b/packages/driver/src/cypress/error_messages.coffee index e7e1e9b810..5b6f2423e8 100644 --- a/packages/driver/src/cypress/error_messages.coffee +++ b/packages/driver/src/cypress/error_messages.coffee @@ -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: """ diff --git a/packages/driver/test/cypress/integration/commands/connectors_spec.coffee b/packages/driver/test/cypress/integration/commands/connectors_spec.coffee index ad7dcbeb1c..3fd0569bfa 100644 --- a/packages/driver/test/cypress/integration/commands/connectors_spec.coffee +++ b/packages/driver/test/cypress/integration/commands/connectors_spec.coffee @@ -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)