Some more ideas:
On-the-fly retry changes
TLDR
Upside: simple
Downside: uses internal commands
There's an internal command that will allow you to change the retry count mid-test.
I stress the word internal as a warning that it may stop working at some new release.
With your code
export class Login {
...
login(email, password) {
const originalRetries = cy.state('runnable')._retries
cy.state('runnable')._retries = 5 // WARNING - internal command
this.username_input().type(email);
...
cy.state('runnable')._retries = originalRetries // restore configured retries value
}
}
Retry by recursion
TLDR
Upside: good for flaky situations
Downside: explicit wait required
If you want the longer but more mainstream solution, use recursion.
You will need a non-failing check to handle the retry-or-finish logic, which will essentially mean changing cy.get('.profile-content').should('exist')
to a jQuery equivalent and using an explicit wait (a downside for this pattern).
export class Login {
...
loginAttempt(attempt = 0) {
if (attempt === 3) throw 'Unable to login after 3 attempts'
this.login_button().click();
// wait for login actions to change the page
cy.wait(200).then(() => {
// test for success
if (Cypress.$('.profile-content').length > 0) {
return // found it, so finish
}
// retry
loginAttempt(email, password, ++attempt)
})
}
login(email, password) {
this.username_input().type(email);
this.password_input().type(password);
loginAttempt()
return this
}
}
You can cut the cy.wait(200)
down a bit and raise the maximum attempt, say cy.wait(20)
and if (attempt === 300)
The downside is each retry uses up a bit more heap memory, so taking that too far will be detrimental - you'll need to experiment.
Set logged-in state directly
TLDR
Upside: By-passes flaky parts of test
Downside: Requires knowledge of login state
The other aspect you may consider is to cut out the flaky login altogether. To do that, you need to find out what the app considers to be logged-in. Is it a cookie, localstorage value, etc.
For each test that needs to be in logged-in state but is not actually testing the login process, set that state directly.
An example is given here recipes - logging-in__using-app-code
describe('logs in', () => {
it('by using application service', () => {
cy.log('user service login')
// see https://on.cypress.io/wrap
// cy.wrap(user promise) forces the test commands to wait until
// the user promise resolves. We also don't want to log empty "wrap {}"
// to the command log, since we already logged a good message right above
cy.wrap(userService.login(Cypress.env('username'), Cypress.env('password')), {
log: false,
}).then((user) => {
// the userService.login resolves with "user" object
// and we can assert its values inside .then()
// confirm general shape of the object
expect(user).to.be.an('object')
expect(user).to.have.keys([
'firstName',
'lastName',
'username',
'id',
'token',
])
// we don't know the token or id, but we know the expected names
expect(user).to.contain({
username: 'test',
firstName: 'Test',
lastName: 'User',
})
})
function login (username, password) {
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
}
return fetch(`${config.apiUrl}/users/authenticate`, requestOptions)
.then(handleResponse)
.then((user) => {
// login successful if there's a jwt token in the response
if (user.token) {
// store user details and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('user', JSON.stringify(user))
}
return user
})
}