Search code examples
reactjstypescripttestingcypresscypress-each

Testing removing items from a React controlled list with Cypress


I created a React list from which users can delete items by clicking a button.

Deletion is handled like this:

 const handleRemove = (index: number) => {
   onChange(fieldName, (prevState) => {
     return {
       ...prevState,
       [fieldName]: prevState[fieldName].filter((_: any, i: number) => i !== index),
     };
   });
 };

When I test it manually it works fine, but when I test it with Cypress deleting items doesn't work as expected, some items remain. (I assume) the problem is that Cypress presses each 'list deletion button' at the same time, and deleting items by index can't be trusted when the list changes rapidly. In real life this can't cause any problems, right?

If it can, how should I rewrite the deletion logic?

If it isn't a big deal though, how can I rewrite my Cypress test to wait a few milliseconds while React does its thing with state updates?

This is what I came up with after a few hours of tinkering:

cy.get(".fa-solid.fa-minus").each((element, index) => {
      cy.wait(index * 3).then(() => {
        element.trigger("click");
      });
});

Solution

  • The each() loop is processing too fast to allow the app to complete each delete action.

    The inner loop needs an assertion to confirm the action has completed. Cypress waits for that assertion before performing the next iteration.

    In the example below the assertion checks to see that the list length has decreased.

    In absence of a complete example I used the following component:

    import React from 'react';
    import {useState} from 'react'
    
    const initialList = ['Cypress', 'Javascript', 'Typescript']
    
    export const List = () => {
      const [list, setList] = useState(initialList)
    
      const handleRemove = (index) => {
        setList(prevState => prevState.filter((_, i) => i !== index))
      }
    
      return (
        <ul>
          {list.map(function(item, index) {
            return <>
              <li key={item}>{item}</li>
              <button onClick={() => handleRemove(index)}>Delete</button>
            </>
          })}
        </ul>
      )
    }
    

    This is the test

    const itemList = ['Cypress', 'Javascript', 'Typescript']
    
    const selector = 'button:contains("Delete this one")'
    
    cy.get(selector)
      .each(($el, index, $list) => {
        cy.get(selector).first().click()
    
        // wait for action to complete
    
        // using list count
        cy.get(selector).should('have.length', $list.length - index -1)
    
        // or using item text
        cy.contains(itemList[index]).should('not.exist') 
      })
    
    cy.get(selector).should('not.exist')       // all gone
    

    This is the log for the passing test

    enter image description here