João Melo
João Melo

Reputation: 835

how to make cypress wait for a async search with multiple result complete without causing the test to fail

I have a typical search where the user types some text in an input, async work is done and a table with the results is properly updated.

I have tests that must wait for this search step and then assert business rules regarding the results, like if the table records are eligible for edit.

Every time I ran a complete test battery (something like 80 test files), one or two of the tests involving that search inevitably fail. But if immediately after that, I run the same test alone, the test passes. It's excruciating and makes e2e testing in CI/CD pointless for the project.

I've read the Cypress documentation about flaky tests and searched questions in StackOverflow and GitHub with only complete failure. It's a drama.

Here is one of the tests:

import { searchList } from '../helpers';
import { createFluxoIniciado, randomFluxoNome } from './common';
import { fluxoSelectors } from './selectors';

describe('fluxos finish', () => {
  it('can manually finish a fluxo INICIADO', () => {
   // feed data to be searched
    const fluxoNome = randomFluxoNome();
    createFluxoIniciado({ fluxoNome });

    // search
    searchList(fluxoNome);

    // do something with search results
    fluxoSelectors.fluxos.view().click();
    fluxoSelectors.finish().click();
    fluxoSelectors.confirm().click();

   // serach again
    searchList(fluxoNome);
    cy.contains('FINALIZADO');
  });
});

The code in searchList is where trouble emerge sometimes. It uses the callback strategy recommended here. The code attempts to cause retries if not all rows have the searched text.

export function searchList (text) {
  cy.get('#searchText')
    .scrollIntoView()
    .type(text)
    .blur();

  cy.get('tbody tr').should($trs => {
    $trs.each((i, $tr) => {
      expect($tr).to.contain(text);
    });
  }, { timeout: 15000 });
}

Here is an example of a test failure inside a run all test execution:

enter image description here

Upvotes: 1

Views: 1396

Answers (1)

Michael Hines
Michael Hines

Reputation: 1108

The problem is obviously caused by the async fetch between .blur() and testing the rows.

You are correctly trying to use Cypress retry with .should(callback), but if the callback is complex or there are multiple steps it may not retry the element that is changing (the table rows).

Ideally you want to keep the cy.get(...).should(...) as simple as possible, and start by testing that the table loading has completed.

// wait for expected number of rows
cy.get('tbody tr', {timeout: 15000}).should('have.length', 5)  

cy.get('tbody tr').each($tr => {
  expect($tr).to.contain(text);
})

But you have a randomizer there, so maybe it's not possible to test explicitly the row count.

Another approach, test the whole table for text (.contains() checks child text also)

// wait for text to appear somewhere
cy.get('tbody tr', {timeout: 15000}).should('contain', text)  

cy.get('tbody tr').each($tr => {
  expect($tr).to.contain(text);
})

You can also add an intercept between start and end of api call

export function searchList (text) {

  cy.intercept('search/api/endpoint').as('search')

  cy.get('#searchText')
    .scrollIntoView()
    .type(text)
    .blur();

  cy.wait('@search')   // wait for api response

  cy.get('tbody tr', {timeout: 15000}).should('contain', text) 

  cy.get('tbody tr').each($tr => {
    expect($tr).to.contain(text);
  })
}

I just noticed you have the {timeout} option on .should(), but that's the wrong place,
see Timeouts

cy.get('input', { timeout: 10000 }).should('have.value', '10')
// timeout here will be passed down to the '.should()'
// and it will retry for up to 10 secs

This may be successful

cy.get('tbody tr', { timeout: 15000 })
  .should($trs => {
    $trs.each((i, $tr) => {
      expect($tr).to.contain(text);
    });
  })

Upvotes: 1

Related Questions