user2924127
user2924127

Reputation: 6242

Cypress - set global variable from function

I have the following flow:

1) On one page I get the value of a field:

 var myID;
 cy.get('#MYID').
        then(($txt) => {
            myID=  $txt.text();
        })
       .should('not.equal', null); 

2) I then navigate to a new page and try to check if this new page contains this id:

cy.get('#myTable').find('td').contains(myID);

It says that myID is not defined. I know the first function is async and reading the documentation it says I can use an alias. The problem with the alias is it needs to be in the beforeEach() function which not to go into a long story I cannto use in this test case. I tried using async/await but it did not seem to work for me as it was still undefined.

Upvotes: 7

Views: 12044

Answers (4)

Sherwin Varghese
Sherwin Varghese

Reputation: 679

In your case, since you are having page navigations, using aliases may not work, especially if the variable gets accessed before the alias is created (due to asynchronous execution and queuing of then blocks).

The simplest way to set a variable is to make use of the browser's window object and assigning the value to the sessionStorage

snippet -

var myID;
 cy.get('#MYID').
        then(($txt) => {
            window.sessionStorage.setItem(myID, $txt.text());
        })
       .should('not.equal', null); 

It can be accessed within the test file (even from another test case) as follows -

let myID = window.sessionStorage.getItem('myID');
cy.get('#myTable').find('td').contains(myID);

However, please note that this is an anti-pattern, making one test rely on the output of another. It is described in the Cypress Best Practices as well.

Upvotes: 0

user15239601
user15239601

Reputation:

Not sure if it's a good idea, but you can save data on the Cypress global object.

context('Pass data via the Cypress object', () => {

  before(() => {
    Cypress._savedData = {}
  })

  it('visits first page and saves data', () => {

    cy.visit('https://google.com');
    cy.get('#MYID')
      .should('not.equal', null)
      .then($el => Cypress._savedData[myID] = $el.text() )

  });

  it('visits another page and checks that data is available', () => {

    cy.visit('https://example.com');
    cy.get('#myTable').find('td').contains(Cypress._savedData[myID]);

  })
});

Upvotes: 0

justlo0king
justlo0king

Reputation: 371

Save data at the plugins level and access it by tasks. Other levels get reloaded when cy.visit is called. Add data and tasks to plugins/index.js:

// plugins/index.js   
/// <reference types="cypress" />
module.exports = (on, config) => {

  // data will be stored here
  const data = {};

  // configuring tasks
  on('task', {
    setValue: (params) => {
      const { key, value } = params;
      data[key] = value;
      return value;
    },
    getValue: (params) => {
      const { key } = params;
      return data[key] || null;
    }
  })        
}

And use in scenario:

// my.spec.js
/// <reference types="cypress" />
context('keeps data safe when changing pages', () => {
  it('visits first page and saves data', () => {
    return cy.visit('https://google.com').then(() => {
      // saving data 
      return cy.task('setValue', { key: 'visited', value: 'google' });
    })
  });

  it('visits another page and checks that data is available', () => {
    return cy.visit('https://example.com').then(() => {
      // getting data
      return cy.task('getValue', { key: 'visited' });
    }).then((value) => {
      expect(value).to.equal('google');
    });
  })
});

Upvotes: 4

Richard Matsen
Richard Matsen

Reputation: 23463

The basic problem here is that Cypress commands run asynchronously from the test code that creates them. You can see this if you put console logs into your code,

var myID;
cy.get('#MYID')
  .then(($txt) => {
    myID=  $txt.text();
    console.log('1', myID);
  })
 .should('not.equal', null); 

console.log('2', myID);

This prints out

2 undefined
1 myText

You can use an alias to overcome this and pass a value down the command chain.

See this section of the docs which shows a similar pattern of code you are using in the DO NOT USE THIS example.

BUT aliases get cleared down between tests, so you should set up a beforeEach() to obtain a new copy of the required ID for each test.

There's another problem with the way you obtain the text value.

Without a return statement the .then() command passes on whatever subject it receives to the next command. See then- Yields

Additionally, the result of the last Cypress command in the callback function will be yielded as the new subject and flow into the next command if there is no return.

So the .should('not.equal', null) is testing that the element isn't null, not that the text is not null.

A better way is to .invoke('text') which is equivalent to $txt.text() and yields the text value to the .should().

Also .should('not.equal', null) won't test that the content is present, since an empty element returns an empty string from element.text(). Use .should('not.equal', '') instead.

Saving via an Alias

describe('grabbing ID for use in multiple tests', () => {

  beforeEach(() => {
    cy.visit('my-page-1.html')
    cy.get('#MYID')
      .invoke('text')
      .as('mySavedID')
  })

  it('ID should not be null', () => {

    cy.get('@mySavedID')
      .should('not.equal', '')

  })

  it('ID should be found in table', () => {

    cy.visit('app/navigate-to-new-page-2.html');
    cy.get('@mySavedID').then(myID => {
      cy.get('#myTable').find('td').contains(myID);
    })

  })
})

Saving by queuing the setting of the variable

The alias pattern may not be ideal if visiting page #1 is time consuming.

In this case, you can save the variable via a custom command. The difference is that the code you had within the .then() is moved into a command which is queued, so the async problem does not occur.

describe('grabbing ID for use in multiple tests', () => {

  let savedVariable;

  Cypress.Commands.add("saveVariable", {prevSubject: true}, (value) => {
    savedVariable = value;
  });

  it('id should not be null', () => {

    cy.visit('my-page-1')
    cy.get('#someId')
      .invoke('text')
      .should('not.equal', '')
      .saveVariable()

    // OR test the variable separately like this

    cy.wrap(savedVariable)
      .should('not.equal', '')
  })

  it('id should be found in table', () => {

    cy.visit('my-page-2');
    cy.get('#myTable').find('td').contains(savedVariable);

  })
})

NOTE
The above is valid if the two pages are in the same domain, e.g two pages of a SPA. Otherwise, the test runner resets itself when a new domain is encountered, and all javascript variables are lost.

Upvotes: 5

Related Questions