skip to Main Content

What is the recommended approach for dealing with a debounced input in Cypress E2E tests.

For example, let’s assume I have the following component that uses lodash/debounce

import React, { useState, useCallback } from 'react';
import _ from 'lodash';

const DebouncedInput = () => {
  const [query, setQuery] = useState("");
  const [searchResult, setSearchResult] = useState("");

  // The debounced function
  const searchApi = useCallback(
    _.debounce(async (inputValue) => {
      setSearchResult(`User searched for: ${inputValue}`);
    }, 200),
    []
  );

  const handleChange = (e) => {
    setQuery(e.target.value);
    searchApi(e.target.value);
  };

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        placeholder="Type something..."
      />
      {searchResult && <p>{searchResult}</p>}
    </div>
  );
};

export default DebouncedInput;

How would I go about making sure that my Cypress tests won’t be flaky?

In my mind there are two ways:

  • Dealing with cy.clock() and using cy.tick()
cy.get('[data-cy="foo"]').type('hey').tick(250)

  • Taking advantage of cy.type and the delay option
cy.get('[data-cy="foo"]').type('hey', {delay: 250});

I tried both approaches and seem to work. But I am not sure if there is actually a recommended way to do this or if one apporach is better than the other.

2

Answers


  1. You can use cy.wait(The same delay period of the used debounce).

    _.debounce((searchValue: string) => {
     ...       
    }, 5000)
    
    
    //in search.cy.js
    it("assert no items are existing", () => {
      cy.get("[data-test=search-input]").type("random 12345...");
      cy.wait(5000);
      cy.get(".no-items-panel").should("exist");
    });
    
    Login or Signup to reply.
  2. Since your debounce() is on the firing of the searchApi() function, the first example .type('hey').tick(250) will fire only once in the test

    The lodash code uses setTimeout() internally so clock()/tick() will ensure the debounce does not fire until you tick, by which time all chars will be typed into the input.

    But .type('hey', {delay: 250}) limits the rate at which characters are typed, so at delay of 250 you will fire searchApi() for every character in the typed string.


    At the moment, all you are doing is echoing the typed value to the page. I assume that you will actually perform a search call.

    To test that your debounce is effective, add an intercept at the top of the test.

    tick() method

    cy.intercept(searchUrl, {}).as('search')  // static reply so that API call time 
                                              // does not affect the result
     
    cy.get('[data-cy="foo"]').type('hey').tick(250)
    
    cy.wait('@search')
    
    cy.wait(250)  // wait longer than debounce duration 
                  // to catch any API call that occurs after the first
    
    cy.get('@search.all').should('have.length', 1)  // assert only 1 call was made
    

    delay option

    cy.intercept(searchUrl, {}).as('search')
    
    cy.get('[data-cy="foo"]').type('hey', {delay: 250})
    
    Cypress._.times(3, cy.wait('@search')  // wait on intercept 3 times 
                                           // once per char in the typed string
    
    cy.wait(250)  // wait longer than debounce duration 
    
    cy.get('@search.all').should('have.length', 3)  // assert only 3 calls made
    

    Without a searchAPI call, you would need to spy on the setSearchResult function to see how many calls were made.


    Without cy.intercept() on an API call

    Technically @Alaa MH has a workable solution, but the example he gives isn’t terrific.

    If there is no API call, you can use cy.wait() because the waiting time is always going to be just a little longer than the debounce time (add a little for app internal javascript to process.

    However, using clock()/tick() is the safest option, though. 

    cy.clock()
    
    cy.visit('/')
    
    cy.get('[data-cy="foo"]').type('hey')
    
    cy.get('div p').should('be.empty')  // since searchResult is initally empty
    
    cy.tick(250)
    
    cy.get('div p').should('have.text', 'hey')
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search