Cypress: Proposal: Add a group to commands for nicer command logging

Created on 6 Feb 2018  路  4Comments  路  Source: cypress-io/cypress

I've been playing around with ways of defining reusable helpers (blog post coming) and have been using the Cypress log API and passing { log: false } to allow control of the log output of these helpers/commands. This has been useful, but there are some times that I want to dive into that abstraction (most likely on failure) to see what step of the helper failed.

I think a concept of a group would be useful here. Instead of passing { log: false } to every encapsulated command, having a group would help a lot more

  • Operating System: N/A
  • Cypress Version: 1.4.1
  • Browser Version: N/A

Is this a Feature or Bug?

Feature

Current behavior:

// This will work on https://github.com/cypress-io/cypress-example-todomvc
export const createTodo = (name) => {
  const log = Cypress.log({
    name: 'createTodo',
    message: name,
    consoleProps() {
      return {
        'Inserted Todo': name,
      }
    }
  })
  cy.get('.new-todo', { log: false }).type(`${name}{enter}`, { log: false })

  return cy
    .get('.todo-list li', { log: false })
    .contains('li', name.trim(), { log: false })
    .then(($el) => {
      log.set({ $el }).snapshot().end()
    })
}

// to use this helper:
createTodo('Learn the Cypress Log API')
  .then(console.log) // logs the created todo element from the TodoMVC Cypress example

Screenshot of the above:
screen shot 2018-02-06 at 12 30 46 am

Proposed behavior:

export const createTodo = (name) => {
  const group = Cypress.group({
    name: 'createTodo',
    message: name,
    consoleProps() {
      return {
        'Inserted Todo': name,
      }
    }
  })

  cy.get('.new-todo').type(`${name}{enter}`)

  return cy
    .get('.todo-list')
    .contains('li', name.trim())
    .then($el => { group.set({ $el }).snapshot().end() })

Matching the log API seems like a logical choice. In the UI it would show createTodo with the logging the same as the Current behavior example, but would have an arrow that would expand to the grouped commands. This would both simplify creating custom command output (don't have to pass { log: false } to everything) as well as make it easier to understand that's going on under the hood of a custom command. The group would be collapsed by default and expanded if a nested command failed.

pkdriver proposal 馃挕 feature

Most helpful comment

I was thinking about this more. Basically as test suites get larger, people look to ways to "macro" commands. There are a few ways to do this:

  1. Cypress Custom Commands

    // definition
    Cypress.Commands.add('fillForm', (first, last) => {
    cy.get('#first').type(first)
    cy.get('#last').type(last)
    }
    
    // usage
    cy.fillForm('John', 'Doe')
    
    // log
    GET  #first
    TYPE John
    GET  #last
    TYPE Doe
    
  2. Regular functions

    // definition
    function fillForm(first, last) {
    cy.get('#first').type(first)
    cy.get('#last').type(last)
    }
    
    // usage
    fillForm('John', 'Doe')
    
    // log
    GET  #first
    TYPE John
    GET  #last
    TYPE Doe
    
  3. Functional composition (.then)

    // definition
    const fillForm = (first, last) => ($form) => {
    cy.wrap($form).find('#first').type(first)
    cy.wrap($form).find('#last').type(last)
    }
    
    // usage
    cy
    .get('#form')
    .then(fillForm('John', 'Doe'))
    
    // log
    GET  #form
    THEN
    WRAP <form>
    FIND #first
    TYPE John
    WRAP <form>
    FIND #first
    TYPE Doe
    
  4. Functional composition (.pipe)

    // definition
    const fillForm = (first, last) => function fillForm($form) {
    cy.wrap($form).find('#first').type(first)
    cy.wrap($form).find('#last').type(last)
    }
    
    // usage (.pipe)
    cy
    .get('#form')
    .pipe(fillForm('John', 'Doe'))
    
    // log (.pipe)
    GET  #form
    PIPE fillForm
    WRAP <form>
    FIND #first
    TYPE John
    WRAP <form>
    FIND #first
    TYPE Doe
    
  5. Page Objects

    // definitions
    class Page {
    fillForm(first, last) {
      cy.get('#first').type(first)
      cy.get('#last').type(last)
    }
    }
    
    // usage
    const page = new Page()
    
    page.fillForm('John', 'Doe')
    
    // log
    GET  #first
    TYPE John
    GET  #last
    TYPE Doe
    

The function name fillForm isn't shown in the log output of all the examples except cypress-pipe. Even cypress-pipe is confusing, because I only see cy.get('#form').pipe(fillForm('John', 'Doe')) in my test, but a bunch more commands logged in the Command Log. Grouping would make this much easier to understand the relationship between my test code and the log.

The following could be modifications to get grouping to work:

  1. Cypress Custom Commands

    // definition
    Cypress.Commands.add('fillForm', (first, last) => {
    cy.group('fillForm', () => {
      cy.get('#first').type(first)
      cy.get('#last').type(last)
    })
    }
    
    // usage
    cy.fillForm('John', 'Doe')
    
    // log
    GROUP fillForm
    - GET  #first
    - TYPE John
    - GET  #last
    - TYPE Doe
    
  2. Regular functions

    // definition
    import { group } from '@cypress/group' // Higher-order function/class decorator
    
    const fillForm = group('fillForm', (first, last) => {
    cy.get('#first').type(first)
    cy.get('#last').type(last)
    })
    
    // usage
    fillForm('John', 'Doe')
    
    // log
    GROUP fillForm
    - GET  #first
    - TYPE John
    - GET  #last
    - TYPE Doe
    
  3. Functional composition (.then)

    // definition
    const fillForm = (first, last) => ($form) => {
    cy.group('fillForm', () => {
      cy.wrap($form).find('#first').type(first)
      cy.wrap($form).find('#last').type(last)
    })
    }
    
    // usage
    cy
    .get('#form')
    .then(fillForm('John', 'Doe'))
    
    // log
    GET  #form
    THEN
    GROUP fillForm
    - WRAP <form>
    - FIND #first
    - TYPE John
    - WRAP <form>
    - FIND #first
    - TYPE Doe
    
  4. Functional composition (.pipe) (no changes actually needed)

    // definition
    const fillForm = (first, last) => function fillForm($form) {
    cy.wrap($form).find('#first').type(first)
    cy.wrap($form).find('#last').type(last)
    }
    
    // usage
    cy
    .get('#form')
    .pipe(fillForm('John', 'Doe'))
    
    // log
    GET  #form
    PIPE fillForm
    - WRAP <form>
    - FIND #first
    - TYPE John
    - WRAP <form>
    - FIND #first
    - TYPE Doe
    
  5. Page Objects

    // definitions
    import { group } from '@cypress/group' // Higher-order function/class decorator
    class Page {
    @group('fillForm') // optional if name can't be inferred
    fillForm(first, last) {
      cy.get('#first').type(first)
      cy.get('#last').type(last)
    }
    }
    
    // usage
    const page = new Page()
    
    page.fillForm('John', 'Doe')
    
    // log
    GROUP fillForm
    - GET  #first
    - TYPE John
    - GET  #last
    - TYPE Doe
    

All 4 comments

This proposal is conceptually similar to the Web Console log's group: https://developer.mozilla.org/en-US/docs/Web/API/Console/group

I'm surprised this didn't get more votes yet.

I'd love to be able to group several commands into one, nicely labelled, group that is collapsed by default, and autoxpands on a failure.

Also note this could be useful when having a larger test (which we are encouraged to do) to group sections of a single test case for better readability.

I was thinking about this more. Basically as test suites get larger, people look to ways to "macro" commands. There are a few ways to do this:

  1. Cypress Custom Commands

    // definition
    Cypress.Commands.add('fillForm', (first, last) => {
    cy.get('#first').type(first)
    cy.get('#last').type(last)
    }
    
    // usage
    cy.fillForm('John', 'Doe')
    
    // log
    GET  #first
    TYPE John
    GET  #last
    TYPE Doe
    
  2. Regular functions

    // definition
    function fillForm(first, last) {
    cy.get('#first').type(first)
    cy.get('#last').type(last)
    }
    
    // usage
    fillForm('John', 'Doe')
    
    // log
    GET  #first
    TYPE John
    GET  #last
    TYPE Doe
    
  3. Functional composition (.then)

    // definition
    const fillForm = (first, last) => ($form) => {
    cy.wrap($form).find('#first').type(first)
    cy.wrap($form).find('#last').type(last)
    }
    
    // usage
    cy
    .get('#form')
    .then(fillForm('John', 'Doe'))
    
    // log
    GET  #form
    THEN
    WRAP <form>
    FIND #first
    TYPE John
    WRAP <form>
    FIND #first
    TYPE Doe
    
  4. Functional composition (.pipe)

    // definition
    const fillForm = (first, last) => function fillForm($form) {
    cy.wrap($form).find('#first').type(first)
    cy.wrap($form).find('#last').type(last)
    }
    
    // usage (.pipe)
    cy
    .get('#form')
    .pipe(fillForm('John', 'Doe'))
    
    // log (.pipe)
    GET  #form
    PIPE fillForm
    WRAP <form>
    FIND #first
    TYPE John
    WRAP <form>
    FIND #first
    TYPE Doe
    
  5. Page Objects

    // definitions
    class Page {
    fillForm(first, last) {
      cy.get('#first').type(first)
      cy.get('#last').type(last)
    }
    }
    
    // usage
    const page = new Page()
    
    page.fillForm('John', 'Doe')
    
    // log
    GET  #first
    TYPE John
    GET  #last
    TYPE Doe
    

The function name fillForm isn't shown in the log output of all the examples except cypress-pipe. Even cypress-pipe is confusing, because I only see cy.get('#form').pipe(fillForm('John', 'Doe')) in my test, but a bunch more commands logged in the Command Log. Grouping would make this much easier to understand the relationship between my test code and the log.

The following could be modifications to get grouping to work:

  1. Cypress Custom Commands

    // definition
    Cypress.Commands.add('fillForm', (first, last) => {
    cy.group('fillForm', () => {
      cy.get('#first').type(first)
      cy.get('#last').type(last)
    })
    }
    
    // usage
    cy.fillForm('John', 'Doe')
    
    // log
    GROUP fillForm
    - GET  #first
    - TYPE John
    - GET  #last
    - TYPE Doe
    
  2. Regular functions

    // definition
    import { group } from '@cypress/group' // Higher-order function/class decorator
    
    const fillForm = group('fillForm', (first, last) => {
    cy.get('#first').type(first)
    cy.get('#last').type(last)
    })
    
    // usage
    fillForm('John', 'Doe')
    
    // log
    GROUP fillForm
    - GET  #first
    - TYPE John
    - GET  #last
    - TYPE Doe
    
  3. Functional composition (.then)

    // definition
    const fillForm = (first, last) => ($form) => {
    cy.group('fillForm', () => {
      cy.wrap($form).find('#first').type(first)
      cy.wrap($form).find('#last').type(last)
    })
    }
    
    // usage
    cy
    .get('#form')
    .then(fillForm('John', 'Doe'))
    
    // log
    GET  #form
    THEN
    GROUP fillForm
    - WRAP <form>
    - FIND #first
    - TYPE John
    - WRAP <form>
    - FIND #first
    - TYPE Doe
    
  4. Functional composition (.pipe) (no changes actually needed)

    // definition
    const fillForm = (first, last) => function fillForm($form) {
    cy.wrap($form).find('#first').type(first)
    cy.wrap($form).find('#last').type(last)
    }
    
    // usage
    cy
    .get('#form')
    .pipe(fillForm('John', 'Doe'))
    
    // log
    GET  #form
    PIPE fillForm
    - WRAP <form>
    - FIND #first
    - TYPE John
    - WRAP <form>
    - FIND #first
    - TYPE Doe
    
  5. Page Objects

    // definitions
    import { group } from '@cypress/group' // Higher-order function/class decorator
    class Page {
    @group('fillForm') // optional if name can't be inferred
    fillForm(first, last) {
      cy.get('#first').type(first)
      cy.get('#last').type(last)
    }
    }
    
    // usage
    const page = new Page()
    
    page.fillForm('John', 'Doe')
    
    // log
    GROUP fillForm
    - GET  #first
    - TYPE John
    - GET  #last
    - TYPE Doe
    

I spent some time looking into this over the weekend. I don't think group can be added without changes to the Cypress runner code. The React UI renders a list, but will need to render a tree. cy.within would be easier to understand as well in the Command Log if it also created a group. Right now you can understand what is inside a cy.within while debugging by clicking on Commands inside a within by the debug output, but not part of the Video or screenshots.

Next step would be to evaluate how much code needs to change to support grouping. cy.within required changes to all commands within querying.coffee to know if querying APIs should target the root element or the withinSubject. Grouping doesn't seem to need that level of interaction with other commands, but does require cooperation with the UI.

Was this page helpful?
0 / 5 - 0 ratings