Cypress - Moving login functions from before / beforeEach hooks to a separate file

Viewed 736

I recently started writing tests with Cypress and I use a slightly modified version of the code available in one of the official documentation's recipes:

let token: string;

function fetchUser() {
  cy.request('POST', 'https://my-api/', {
    email: Cypress.env("USER_LOGIN"),
    password: Cypress.env("USER_PASSWORD"),
  })
    .its('body')
    .then((res) => {
      token = res.token;
    });
}

function setUser() {
  cy.visit('/projects', {
    onBeforeLoad(win) {
      win.localStorage.setItem('token', JSON.stringify({ token }));
      for (let i = 0; i < 5; i++) {
        win.localStorage.setItem(`guidedTour${i}`, 'true');
      }
    },
  });
}
before(fetchUser);
beforeEach(setUser);

it('my test', () => {
// some testing happens here
});

It works fine. Now I would like to move fetchUser and setUser to a separate file to avoid duplicating them in each new test file. When I do that, fetchUser takes token and sets it to its intended value, but what setUser gets is undefined. I'm quite the beginner in TypeScript and I have never handled asynchronous functions before, and I'm kinda lost. Here's what I tried:

authentification.js

export function fetchUser(token: string): string { // Warning: "A function whose declared type is neither 'void' nor 'any' must return a value."
  console.log('fetchUser line 1 ' + token);
  cy.request('POST', 'https://my-api', {
    email: Cypress.env("USER_LOGIN"),
    password: Cypress.env("USER_PASSWORD"),
  })
    .its('body')
    .then((res) => {
      token = res.token;
      return token;
    });
}

export function setUser(token: string) {
  cy.visit('/projects', {
    onBeforeLoad(win) {
      win.localStorage.setItem('token', JSON.stringify({ token }));
      for (let i = 0; i < 5; i++) {
        win.localStorage.setItem(`guidedTour${i}`, 'true');
      }
    },
  });
}

main test file

import { fetchUser, setUser } from "../support/authentification";

let token: string;

before(() => {
  token = fetchUser(token);
});
beforeEach(() => {
  setUser(token);
});

it('my test', () => {
// I'd like some testing to still happen here
});

How can I make it work?

3 Answers

Warning: "A function whose declared type is neither 'void' nor 'any' must return a value."

This means you don't have a return value from function fetchUser(). The return token only alters the subject of the Cypress command chain, but you need to return the cy.request() itself to give the function a return value.

The value returned will be Chainable<string>, similar to Promise<string> - it's value can accessed with .then(value => (but you can't await it).

Also, the request is asynchronous so you can't set the passed-in token variable, you must return something that's "then-able", i.e it signals when the asynchronous call is complete.

authentification.js

/// <reference types="cypress" />

export function fetchUser(): Chainable<string> {

  return cy.request('POST', 'https://my-api', {
    email: Cypress.env("USER_LOGIN"),
    password: Cypress.env("USER_PASSWORD"),
  })
  .its('body.token');                // .then() isn't actually needed
                                     // the body.token is the value returned
}

test

import { fetchUser, setUser } from "../support/authentification";

let token;

before(() => {
  fetchUser().then(val => token = val);    // waits for call to complete
});

beforeEach(() => {
  setUser(token);                          // now token has a value
});

it('my test', () => {
  ...
});

Warning - Using Aliases

It's also possible to pass the token by using an alias, but Cypress clears aliases between tests, so this won't work if you have more than one test

before(() => {
  fetchUser().as('token');
});

beforeEach(() => {
  cy.get('@token').then(token => setUser(token));         // fails for 2nd test
});

it('my test', () => {
  ...      
});

it('my 2nd test', () => {
  ...      
});

However, an alias also sets a property this.token which is not cleared between tests, so this will work

before(() => {
  fetchUser().as('token');
});

beforeEach(function() {                // must use "function()" NOT "() =>" 
  setUser(this.token);                 // succeeds for all tests
});

it('my test', () => {
  ...      
});

it('my 2nd test', () => {
  ...      
});

Looking at the question, I would suggest you to move common functions to support/commands.js file. Getting value of token in before hook, storing it in variable and using it in beforeEach hook will not work as token value is undefined while calling setUser function.

You can modify your code little bit:

support/commands.js

Cypress.Commands.add('fetchUser', () => {
    return cy.request('POST', 'https://my-api', {
        email: Cypress.env("USER_LOGIN"),
        password: Cypress.env("USER_PASSWORD"),
      })
})


Cypress.Commands.add('setUser', (token) => {
    cy.visit('/projects', {
        onBeforeLoad(win) {
          win.localStorage.setItem('token', JSON.stringify({ token }));
          for (let i = 0; i < 5; i++) {
            win.localStorage.setItem(`guidedTour${i}`, 'true');
          }
        },
      })
})

test.spec.js

describe('testing command js', () => {
    beforeEach(() =>{
        cy.fetchUser().then(response => {
            cy.setUser(response.token)
        })
    })

    it('this is test', () => {
        // tests
    })

    it('this is test 2', () => {
        // tests 2
    })
})

Updated answer to address the comment. Depending on whether to set user for each tests or once for suite, these methods can be written in before and beforeEach hook

Please modify the code wherever needed against type checking in Typescript. But this code shall resolve your issue.

You can use a custom command to run everything

/cypress/support/authentification.js

function fetchUser() {
  return cy.request('POST', 'https://my-api', {
    email: Cypress.env("USER_LOGIN"),
    password: Cypress.env("USER_PASSWORD"),
  })
  .its('body.token')   
}

function setUser(token) {
  Cypress.on('window:before:load', (win) => {
    win.localStorage.setItem('token', JSON.stringify({ token }));
    for (let i = 0; i < 5; i++) {
      win.localStorage.setItem(`guidedTour${i}`, 'true');
    }
  })
}

Cypress.Commands.add('fetchAndSetUser', () => {
  fetchUser().then(token => {
    setUser(token)
  })
})

/cypress/support/index.js

import './authentification.js'

test

before(() => {
  cy.fetchAndSetUser();
});

beforeEach(() => {                
  cy.visit('/projects')  // onBeforeLoad pre-configured by event 'window:before:load'          
});
Related