HTML standard has nice built-in form element validation rules, widely supported by the modern browsers. No need to bring a 3rd party library just to require a form input to have a value, or for doing simple numerical checks. For example, if we want to ask the user for the item's name and its quantity, we can write:
Any user trying to submit this form while breaking the rules is going to see the error messages shown by the browser, and the form is not going to be submitted.
Notice how the browser stops the form submission on the first broken rule.
The error popups are shown by the browser - they are NOT part of the page's DOM. Thus we cannot query them from the Cypress test. How do we check our form elements from the end-to-end tests to ensure the rules are set up correctly?
CSS pseudo-classes
The HTML standard defines several CSS pseudo-classes for finding the invalid and valid input elements. For example the :invalid pseudo-class is present on every input element currently breaking its validation rules.
We can write a Cypress test to check the presence of form elements using this class.
cy.get('#form-validation').within(() => { // at first both input elements are invalid cy.get('input:invalid').should('have.length', 2)
cy.log('**enter the item**') cy.get('#item').type('push pin') cy.get('input:invalid').should('have.length', 1)
cy.log('**enter quantity**') cy.get('#quantity').type(3) cy.get('input:invalid').should('have.length', 0) // instead both items should be valid // plus the submit input button cy.get('input:valid').should('have.length', 3) }) }) })
Notice that the input elements satisfying its rules get pseudo-class :valid. At the end of our test, all input elements have the pseudo-class :valid, including the submit button.
checkValidity
Unfortunately, we cannot check :invalid or :valid pseudo-class by using an assertion, since it is not a declared class.
cy.get('#form-validation').within(() => { // ⛔️ DOES NOT WORK cy.get('#item').should('have.class', ':invalid') }) }) })
Instead we can call a method checkValidity() on an HTML element asking if it is valid. We can call this method from the DevTools console:
Anything we can do from the console, we can do from a Cypress test. Unfortunately, since Cypress returns jQuery element, we cannot simply invoke the method
cy.get('#form-validation').within(() => { // ⛔️ DOES NOT WORK // cy.get('#item').should('have.class', ':invalid') // ⛔️ DOES NOT WORK cy.get('#item').invoke('checkValidity') }) }) })
Instead we must call the method on the original HTML element:
cy.get('#form-validation').within(() => { // ⛔️ DOES NOT WORK // cy.get('#item').should('have.class', ':invalid') // ⛔️ DOES NOT WORK // cy.get('#item').invoke('checkValidity') // ✅ WORKS cy.get('#item').then($el => $el[0].checkValidity()).should('be.false') cy.get('#item').type('paper') .then($el => $el[0].checkValidity()).should('be.true') }) }) })
Hmm, not sure why the boolean is reported twice in the Command Log!
Tip: you can check form's validity too!
validationMessage
The error displayed by the browser is specific to the validation rule. We can retrieve it from the Console.
cy.get('#form-validation').within(() => { cy.get('#quantity').invoke('prop', 'validationMessage') .should('equal', 'Please fill out this field.') cy.get('#quantity').type(20) cy.get('#quantity').invoke('prop', 'validationMessage') .should('equal', 'Value must be less than or equal to 5.') cy.get('#quantity').clear().type(3) cy.get('#quantity').invoke('prop', 'validationMessage') .should('equal', '') }) }) })
validity
Checking the validation message only retrieves the single message shown to the user, what about all validation rules? Turns out we can retrieve the full validation status by looking up another property. For example, the quantity input element cannot have numbers below 1.
Finally, let's confirm the form is not submitted until the inputs are valid. Let's test this - we will catch the form submission by attaching our own event handler from the test - after all, Cypress tests run right next to your application, so we can do anything we want.
/// <reference types="cypress" /> describe('form', () => { it('is not submitted without quantity', () => { cy.visit('index.html')
// if the browser tries to submit the form // we should fail the test cy.get('#form-validation').invoke('submit', (e) => { // do not actually submit the form e.preventDefault() // fail this test thrownewError('submitting!!!') })
When this test runs, the browser pops its error validation message, but does not submit the form.
Just for fun, let's confirm the test does fail if the browser submits the form
1 2 3 4 5 6
// same test but let's enter quantity cy.get('#form-validation').within(() => { cy.get('#item').type('paper') cy.get('#quantity').type(3) cy.get('input[type=submit]').click() })
Our test catches it
We should change the test back, and add another test to make sure the form is submitted when there are no validation errors.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
it('is submitted when valid', () => { cy.visit('index.html')
let submitted cy.get('#form-validation').invoke('submit', (e) => { // do not actually submit the form e.preventDefault() submitted = true })