Get form input using label

Imagine we have a form with an input element and a label. We want to find the input element by label and yield it to further assertions and commands. Let's write a custom command.

Via parent

If there is a common parent for every label and input pair, we can find the parent element and then find the input.

<form method="POST" id="signup-form" class="signup-form">
  <div class="form-row">
    <div class="form-group">
      <label for="first_name">First name</label>
      <input
        type="text"
        class="form-input"
        name="first_name"
        id="first_name"
        value="Joe"
      />
    </div>
    <div class="form-group">
      <label for="last_name">Last name</label>
      <input
        type="text"
        class="form-input"
        name="last_name"
        id="last_name"
        value="Smith"
      />
    </div>
  </div>
</form>

Find all ".form-group" elements that have inside a label element with the given text

cy.get('.form-group:has(label:contains("First name"))')
  .find('input')
  .should('have.value', 'Joe')
  .and('id', 'first_name')

We can grab all form groups with the label inside

cy.get('.form-group:has(label)').should('have.length', 2)

Reusable function

Form with several input text fields and their labels

<form>
  <label for="fname">First name:</label><br />
  <input type="text" id="fname" name="fname" /><br />
  <label for="lname">Last name:</label><br />
  <input type="text" id="lname" name="lname" />
</form>
// we can find the input field by id and name using the standard cy.get
cy.get('#fname').should('have.attr', 'type', 'text')
cy.get('input[name=fname]').should('have.attr', 'id', 'fname')

we can find the label by text, grab its "for" attribute, and then find the input element. To find the label by text we can use cy.containsopen in new window

cy.contains('label', 'First name:')
  .invoke('attr', 'for')
  .should('equal', 'fname')
  .then((id) => {
    // note that the last Cypress command inside the `cy.then`
    // changes the yielded subject to its result
    cy.get('#' + id)
  })
  .should('have.attr', 'name', 'fname')

The above code can be abstracted into a little reusable function

// we return the Cypress chain so the test can attach more commands
const getInputByLabel = (label) => {
  return cy
    .contains('label', label)
    .invoke('attr', 'for')
    .then((id) => {
      cy.get('#' + id)
    })
}
// let's type the last name
getInputByLabel('Last name:').type('Smith')
// we can also check the value
getInputByLabel('Last name:').should('have.value', 'Smith')

Simple custom command

While making a small reusable function is my preferred way of writing reusable test code, you can also create a custom command.

Form with several input text fields and their labels

<form>
  <label for="fname">First name:</label><br />
  <input type="text" id="fname" name="fname" /><br />
  <label for="lname">Last name:</label><br />
  <input type="text" id="lname" name="lname" />
</form>
Cypress.Commands.add('getByLabel', (label) => {
  // you can disable individual command logging
  // by passing {log: false} option
  cy.log('**getByLabel**')
  cy.contains('label', label)
    .invoke('attr', 'for')
    .then((id) => {
      cy.get('#' + id)
    })
})
// let's use the custom command to act on the first name
cy.getByLabel('First name:').should('have.value', '').type('Joe')
cy.getByLabel('First name:').should('have.value', 'Joe')

Simple command failure

The simple custom command only retries the last command cy.get. What if the entire part of the document is refreshed, and the id value changes? Let's try writing an example for it.

At first, the form has the labels and the input fields, but then the app "hydrates" them, replacing the initial ids with randomly generated ones.

<form id="hydrate-fails">
  <label for="fname">First name:</label><br />
  <input
    type="text"
    id="fname"
    name="fname"
    value="initial"
  /><br />
  <label for="lname">Last name:</label><br />
  <input type="text" id="lname" name="lname" />
</form>
<script>
  function hydrate() {
    const form = document.querySelector('form#hydrate-fails')
    form.innerHTML = `
      <div>Hydrated!</div>
      <label for="fname111">First name:</label><br />
      <input type="text" id="fname111" name="fname111" /><br />
      <label for="lname222">Last name:</label><br />
      <input type="text" id="lname222" name="lname222" />
    `
  }
  setTimeout(hydrate, 1000)
</script>

The test below fails to account for this, and keeps trying finding an input element with ID "fname", which never becomes available.

// ⛔️ THIS TEST WILL FAIL
Cypress.Commands.add('getByLabel', (label) => {
  // you can disable individual command logging
  // by passing {log: false} option
  cy.log('**getByLabel**')
  cy.contains('label', label)
    .invoke('attr', 'for')
    .then((id) => {
      cy.get('#' + id)
    })
})
// let's use the custom command to act on the first name
cy.getByLabel('First name:').should('have.value', '').type('Joe')
cy.getByLabel('First name:').should('have.value', 'Joe')

Complex custom command with retries

Let's make more complex command

If the form is initially empty or does not even exist, our custom command has to handle it.

<div id="result"></div>
<script>
  // let's add the form and its fields one by one dynamically
  // notice that the test commands will need to handle async addition and retry
  setTimeout(addForm, 500)
  setTimeout(addLabel('fname', 'First name:'), 700)
  setTimeout(addInput('fname'), 1000)
  setTimeout(addLabel('lname', 'Last name:'), 1200)
  setTimeout(addInput('lname'), 1500)
  function addForm() {
    const form = document.createElement('form')
    form.setAttribute('id', 'form-id')
    document.getElementById('result').appendChild(form)
  }
  function getForm() {
    return document.getElementById('form-id')
  }
  function addLabel(forId, text) {
    return function () {
      const form = getForm()
      const label = document.createElement('label')
      label.setAttribute('for', forId)
      label.textContent = text
      form.appendChild(label)
    }
  }
  function addInput(id) {
    return function () {
      const form = getForm()
      const input = document.createElement('input')
      input.setAttribute('id', id)
      input.setAttribute('type', 'text')
      input.setAttribute('name', id)
      form.appendChild(input)
    }
  }
</script>
Cypress.Commands.add('getByLabel2', (label, options = {}) => {
  const log = {
    name: 'getByLabel2',
    message: label,
  }
  Cypress.log(log)

  // returns the document object of the application under test
  const document = cy.state('document')

  // this function just tries to find the element
  // we cannot use Cypress commands - aside from static ones,
  // but we can use normal DOM JavaScript and jQuery methods
  const getValue = () => {
    const $label = Cypress.$(document).find(
      'label:contains("' + label + '")',
    )
    if (!$label.length) {
      return
    }
    const forId = $label.attr('for')
    if (!forId) {
      return
    }
    const input = document.getElementById(forId)
    return input
  }

  const resolveValue = () => {
    return Cypress.Promise.try(getValue).then(($el) => {
      // important: pass a jQuery object to cy.verifyUpcomingAssertions
      if (!Cypress.dom.isJquery($el)) {
        $el = Cypress.$($el)
      }
      return cy.verifyUpcomingAssertions($el, options, {
        onRetry: resolveValue,
      })
    })
  }

  return resolveValue().then((el) => {
    // add console props method, which is invoked
    // when the user clicks on the command
    log.consoleProps = () => {
      return {
        result: el,
      }
    }

    return el
  })
})

// let's use the custom command to act on the first name
cy.getByLabel2('First name:').should('exist')
cy.getByLabel2('First name:').type('Joe')
// now interact with the second input
cy.getByLabel2('Last name:')
  .should('have.value', '')
  .type('Smith')
cy.getByLabel2('Last name:').should('have.value', 'Smith')

cy.takeRunnerPic('form-input')

Find form input by label using custom command

Let's try the custom command with hydration example

<form id="hydration">
  <label for="fname">First name:</label><br />
  <input
    type="text"
    id="fname"
    name="fname"
    value="initial"
  /><br />
  <label for="lname">Last name:</label><br />
  <input type="text" id="lname" name="lname" />
</form>
<script>
  function hydrate() {
    const form = document.querySelector('form#hydration')
    form.innerHTML = `
      <div>Hydrated!</div>
      <label for="fname111">First name:</label><br />
      <input type="text" id="fname111" name="fname111" /><br />
      <label for="lname222">Last name:</label><br />
      <input type="text" id="lname222" name="lname222" />
    `
  }
  setTimeout(hydrate, 1000)
</script>
Cypress.Commands.add('getByLabel2', (label, options = {}) => {
  const log = {
    name: 'getByLabel2',
    message: label,
  }
  Cypress.log(log)

  // returns the document object of the application under test
  const document = cy.state('document')

  // this function just tries to find the element
  // we cannot use Cypress commands - aside from static ones,
  // but we can use normal DOM JavaScript and jQuery methods
  const getValue = () => {
    const $label = Cypress.$(document).find(
      'label:contains("' + label + '")',
    )
    if (!$label.length) {
      return
    }
    const forId = $label.attr('for')
    if (!forId) {
      return
    }
    const input = document.getElementById(forId)
    return input
  }

  const resolveValue = () => {
    return Cypress.Promise.try(getValue).then(($el) => {
      // important: pass a jQuery object to cy.verifyUpcomingAssertions
      if (!Cypress.dom.isJquery($el)) {
        $el = Cypress.$($el)
      }
      return cy.verifyUpcomingAssertions($el, options, {
        onRetry: resolveValue,
      })
    })
  }

  return resolveValue().then((el) => {
    // add console props method, which is invoked
    // when the user clicks on the command
    log.consoleProps = () => {
      return {
        result: el,
      }
    }

    return el
  })
})

cy.getByLabel2('First name:')
  .should('have.value', '')
  .type('Joe')
cy.getByLabel2('First name:').should('have.value', 'Joe')