In first method below I set env. variable, but when it's necessary to get this variable in another method, executed under the same test run, it's returned as undefined. What can be the cause of such behavior?
setEnvVar = () => {
cy.get('tbody > tr').first().find('span').first().then(($span) => {
Cypress.env('est-id', $span.text())
})
}
fillParameters = () => {
cy.get('div[id=estimates_select]').find('input').type(Cypress.env('est-id'))
}
TLDR
Put the fillParameters() code inside a cy.then().
fillParameters = () => {
cy.then(() => {
cy.get('div[id=estimates_select]')
.find('input')
.type(Cypress.env('est-id'))
})
}
Explanation
All Cypress commands run on a queue. When the test runner runs a test, the parameters for commands like .type() and .log() are set before the command queue starts running.
In your code, that means .type(Cypress.env('est-id')) evaluates env('est-id') before it is set in the preceding function setEnvVar().
The exception is commands added inside a callback such as
cy.then(() => {
// commands added here defer parameter setting
// until this callback is executed in the queue
})
Using Cypress.env() to save values is a hack (IMO)
The .env() was designed to set values outside of the test (why it's called environment)
It's often used as a data store, but closure variables or aliases are better in the situation you describe.
Examples:
Closure variable
let est-id;
setEnvVar = () => {
cy.get('tbody > tr').first().find('span').first()
.then(($span) => est-id = $span.text() )
}
fillParameters = () => {
cy.get('div[id=estimates_select]').find('input')
.type(est-id)
}
Alias
setEnvVar = () => {
cy.get('tbody > tr').first().find('span').first()
.invoke('text').as('est-id')
}
fillParameters = () => {
cy.get('#est-id').then(est-id => {
cy.get('div[id=estimates_select]').find('input').type(est-id)
})
}
Related
We have a number of variables from the API which are used throughout the tests. The values are saved to aliases at various points.
How can we turn off clearing aliases between test?
If we use one big test this works fine but Cypress by default clears aliases between tests, so as we break down the tests into smaller more manageable units we are breaking the code.
Simple example:
before(() => {
cy.wrap(123).as('alias')
})
it('test1', () => {
// alias is usable here
cy.get('#alias').should('eq', 123)
cy.wrap(456).as('alias2')
});
it('test2', () => {
// alias is missing here
cy.get('#alias').should('eq', 123)
cy.get('#alias2').should('eq', 456)
});
There's no configuration to turn off alias cleardown.
You would have to create a custom command to do it.
Cypress.Commands.add('keepAliases', function(aliasList) {
if (!aliasList) {
aliasList = Object.keys(this)
.filter(key => !['test', '_runnable', 'currentTest']
.includes(key))
}
aliasList.forEach(key => {
cy.wrap(this[key]).as(key)
})
})
Usage for a single test
before(() => {
cy.wrap(123).as('alias')
})
it('test1', () => {
cy.get('#alias').should('eq', 123)
cy.wrap(456).as('alias2')
});
// ✅ test passes
it('test2', () => {
cy.keepAliases()
cy.get('#alias').should('eq', 123)
cy.get('#alias2').should('eq', 456)
});
Usage for all tests
before(() => {
cy.wrap(123).as('alias')
})
beforeEach(() => {
cy.keepAliases()
})
it('test1', () => {
cy.get('#alias').should('eq', 123)
cy.wrap(456).as('alias2')
});
// ✅ test passes
it('test2', () => {
cy.get('#alias').should('eq', 123)
cy.get('#alias2').should('eq', 456)
});
According to cypress doc
aliases and properties are automatically cleaned up after each test
so with current cypress versions your goal is not achievable.
As a workaround you could use cypress.env as a storage for your tests:
Cypress.env(varName, varValue)
and using a test0 to reset all variables before each texts execution:
it('test0', () => {
// clean up all stored aliases
cy.env("var1",null);
cy.env("var2",null);
//...
});
I was trying to use a condition as a function within a page object.
class Folders {
DropdownCheckFunction(){
cy.get('.more-less-container').then((dropdown) => {
if(dropdown.find('.name').contains('More')){
cy.get('more-less-container').click()
}
else{
console.log('folders are in expanded state')
}
})
}
Drafts(){
this.DropdownCheckFunction()
cy.get('.category-content').find('[title="Drafts"]').click()
.get(".folder-details").should('contain.text', 'Drafts')
}
Issue here is that the page object is getting executed as a test case and is happening before the code in BEFORE hook is being run. Below is the test file code
describe('Testing all cases related to Drafts', () => {
before(() => {
cy.login()
})
})
it('Needs to open the Drafts folder', () => {
openFolder.Drafts()
});
Attaching the error seen on the test runner for reference
The problem is bad formatting.
If you line up your code, you can see it - your test is outside the scope of the before().
describe('Testing all cases related to Drafts', () => {
before(() => {
cy.login()
})
}) // move this bracket to below the `it()`
it ('Needs to open the Drafts folder', () => {
openFolder.Drafts()
});
I am new to Cypress and some of the things that I expect to work have really weird issues.
For example, I am trying to get the value of a column in a table and use the value in a search input. I have done it like this:
it('should filter', () => {
let id = 0;
cy.get('[data-cy=data-table-row]').should('have.length', 25);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').should(($div1) => {
id = $div1.text();
expect(id).not.to.eq(0);
});
//expect(id).not.to.eq(0);
cy.get('[data-cy=table-filters-search]').find('input').type(id);
cy.get('[data-cy=data-table-row]').should('have.length', 1);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').should(($div1) => {
expect(id).to.eq($div1.text());
});
});
But when I run this, I get an error stating that [data-cy=data-table-row] has a length of 25 not 1.
It turns out that the id variable I am using is not accessible outside the should method. I assume that's because it is a promise.
If I try to do this:
it('should filter', () => {
let id = 0;
cy.get('[data-cy=data-table-row]').should('have.length', 25);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').should(($div1) => {
id = $div1.text();
expect(id).not.to.eq(0);
cy.get('[data-cy=table-filters-search]').find('input').type(id);
cy.get('[data-cy=data-table-row]').should('have.length', 1);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').should(($div1) => {
expect(id).to.eq($div1.text());
});
});
});
The test goes mental and tries to get the [data-cy=table-filters-search] over and over and over again.
I am not sure why.
Is there an easier way to grab the innerText of a div and store it to compare later?
As someone gave a response, I tried this:
it('should filter', () => {
let variables = {};
cy.get('[data-cy=data-table-row]').should('have.length', 25);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').then(($div1) => {
variables.id = $div1.text();
expect(variables.id).not.to.be.undefined;
});
console.log(variables);
expect(variables.id).not.to.be.undefined;
cy.get('[data-cy=table-filters-search]').find('input').type(variables.id);
cy.get('[data-cy=data-table-row]').should('have.length', 1);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id').then(($div1) => {
expect(variables.id).to.eq($div1.text());
});
});
But the test fails on the second expect(variables.id).not.to.be.undefined;
Closure problem
The problem with the first example is that the test runs in two phases. The first phase sets up the commands in the queue, and the second runs them.
During the first phase, .type(id) "captures" the current value of id (which is "0") and in the second phase that's the value that gets used.
You can fix it in a couple of ways, with an alias or moving the type(id) inside the callback, as per your second example.
This gets around the closure problem by deferring cy.get(...).find('input').type(id) until the id has actually changed.
Retry problem
The problem with the second example is that should() with an expect in the callback will retry until it succeeds or times out.
Something in the callback is continuously failing (or an error is thrown) causing a continuous retry. It should time out, not sure why that doesn't happen.
You can separate the parts of the callback into two sections, and use a .then() which does not attempt to retry.
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id')
.should(($div1) => {
id = +$div1.text(); // Note $div1.text() is always a string
// so convert with "+" to compare numbers
expect(id).not.to.eq(0) // otherwise this always succeeds
})
.then(($div1) => { // use a then which does not retry
id = $div1.text();
cy.get('[data-cy=table-filters-search]').find('input').type(id);
cy.get('[data-cy=data-table-row]').should('have.length', 1);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id')
.should(($div1) => {
expect(id).to.eq($div1.text())
});
})
Or
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id')
.invoke('text') // find it's text
.should('not.eq', '0') // passes on the id
.then(id => {
cy.get('[data-cy=table-filters-search]').find('input').type(id);
cy.get('[data-cy=data-table-row]').should('have.length', 1);
cy.get('[data-cy=data-table-row]:nth-child(1) > .cdk-column-id')
.should($div1 => expect(id).to.eq($div1.text()) );
})
If you want to use it within a single case - Allias is the cypress way:
cy.wrap('value').as('variable') //setting the variable
cy.get('#variable') // with this you can call it at any time in the test after aliasing
.then(variable =>{
//here you can use it
})
For a variable to use on multiple cases I recommend using an object as a Reference
let variables = {} //This would need to be declared on the scope you wish to use it in
cy.get('element')
.then($el => {
variables.elText = $el.text() //Assigning the value
})
cy.log(variables.elText) //You can call it later like this
I'm building some custom commands and trying to use my fixtures data for all my commands. Right now I'm forced to define it inside an it block.
Looks similar to this:
it("Commands", () => {
cy.fixture("fixtureFile").as("data");
cy.get("#data").then((data) => {
Cypress.Commands.add('login', () => {
cy.visit("/login");
cy.get('#login-email').type(data.userEmail);
cy.get('#login-pass').type(data.userPass, {log: false});
cy.get('.btn').debug().click();
})
Cypress.Commands.add('createTagMedia', () => {
cy.get(".close").click();
cy.get("#form-field-name").type(data.releaseVersion);
cy.get(".form-group-adTag > .CodeMirror > .CodeMirror-scroll").type(data.mediaTag);
cy.get("#media-save-btn").click();
})
})
})
This it block is being count as a test case, Is there a better way to pass this for more than one command at the same time?
The workaround I found was to put everything inside a before block, for example:
before(() => {
cy.fixture("fixtureFile").as("data");
cy.get("#data").then((data) => {
Cypress.Commands.add('login', () => {
cy.visit("/login");
cy.get('#login-email').type(data.userEmail);
cy.get('#login-pass').type(data.userPass, {log: false});
cy.get('.btn').debug().click();
})
Cypress.Commands.add('createTagMedia', () => {
cy.get(".close").click();
cy.get("#form-field-name").type(data.releaseVersion);
cy.get(".form-group-adTag > .CodeMirror > .CodeMirror-scroll").type(data.mediaTag);
cy.get("#media-save-btn").click();
})
})
})
Is there a reason why you won't use the following:
import {data} from '../fixtures/FixtureFile'
Considering you have the following JSON file:
{
"data": {
"userEmail": "blah",
"userPass": "blah",
"releaseVersion": "1"
}
}
You can include this on your tests, commands (Cypress.custom.commands), etc.
before(() => {
const data = cy.fixture("fixtureFile");
cy.login(data);
cy.createTagMedia(data);
})
You could literally do something like the above. With your Cypress.Commands in your command.ts or js whichever you're using.
And make the commands take in a parameter. Then the above before hook would just be in your tests.
Running Cypress 3.1.1 with cypress-cucumber-preprocessor 1.5.1. I need to pass some static data from one step to another (in the same scenario/test). I can do this using an alias, like this:
cy.wrap(someString).as('myString'), but then I have to access it asynchronously:
cy.get('#myString').then(myString => ...)
This is rather cumbersome, particularly when I have to pass multiple values, requiring multiple wrapped closures, for no apparent benefit. (Currently I'm working around this by aliasing an object, but I shouldn't need to do this.)
How can I pass primitive values from one step to another synchronously?
I thought I might be able to simply set this.myString='' to set the value on the Mocha shared context object, but in that case, the property exists but is set to undefined when accessed in later steps.
Even creating my own context variable with let outside of the step definition does not work. Is this simply a limitation of Cypress and/or the cypress-cucumber-preprocessor?
I managed to get it working the following way:
Add 2 tasks to the /plugins/index.js
const testStore = {}
module.exports = (on, config) => {
on('task', {
pushValue({ name, value }) {
console.log(name, value)
testStore[name] = value
console.log(testStore)
return true
},
})
on('task', {
getValue(name) {
return testStore[name]
},
})
Then you can add a variable in any test and reach it in any other place:
it('test', ()=>{
cy.task('pushValue', { name: 'orderNumber', value: orderNumber })
})
it('test 2', ()=>{
cy.task('getValue', 'orderNumber').then((order) => {
cy.visit(`/bookings/${order}`)
})
})
Here is a slightly more complicated (and not fully tested) method. A custom command can be added to save values to a global object.
In the Cypress test runner, all the tests seem to run sequentially, but you may have to be careful if using CI and parallel execution.
In /support/commands.js
export const testStore = {}
Cypress.Commands.add('saveAs', { prevSubject: true }, (value, propName) => {
console.log('saveAs', value, propName)
testStore[propName] = value;
return value;
})
In myTest.spec.js
import { testStore } from '../support/commands.js'
...
it('should have a title', () => {
cy.title()
.saveAs('title') // save for next test
.should('contain', 'myTitle) // this test's expectation
});
it('should test something else', () => {
cy.get('.myElement').contains(testStore.title);
});