How to deal with debounced input in Cypress tests?

244 Views Asked by At

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

There are 2 best solutions below

1
Alaa M On

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");
});
0
Aladin Spaz On

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')