Reputation: 559
I'm trying to find out if I'm able to conditionally skip a test it()
in my test suite and deal with its async nature as well.
I've read about conditional testing in Cypress docs https://docs.cypress.io/guides/core-concepts/conditional-testing.html and also mochajs documentation about it https://mochajs.org/.
My intention is to check if an error is displayed on a website, and skip the test if it does. Otherwise continue with the assertions.
The code snippet from mochajs that I'm trying to take to my test in Cypress is:
it('should only test in the correct environment', function() {
if (/* check test environment */) {
// make assertions
} else {
this.skip();
}
});
So what I've got in Cypress is:
it('shows the list', function() {
if (queryFailed()) {
this.skip();
} else {
cy.get('.list')
.should('be.visible')
}
Note that I changed the arrow function in my it()
to be a function()
so that I can make use of this
.
queryFailed()
is a function that checks if the query succeeded or not.
function queryFailed() {
cy.get('.spin')
.then($container => {
const htmlLoaded = $container[0].innerHTML;
if (htmlLoaded.indexOf('List') !== -1) {
return false;
}
if (htmlLoaded.indexOf('error') !== -1) {
return true;
}
cy.wait(1000);
queryFailed();
});
}
Briefly, if the content of the div
element I'm waiting for has "error" then I know the query failed so I return true, otherwise I return false.
What I see in my tests after debugging, is that even though the condition works well, the async nature of JS executes the code in the else
statement at the same time than the if
. So the final output is as if there is no condition at all, since everything is tested.
Is there a better way of dealing with this async feature of JS?
Upvotes: 36
Views: 68935
Reputation: 27001
I think to answer this correctly, we need to distinguish two use cases
The following sections will address both topics.
There is a simple way cypress allows this directly in your code:
If you want to temporarily run just one part of a test, you can use
.only(...) // will only execute this part
Note: There may be more than one .only in your code, in that case Cypress will run all of them (and skip the ones that aren't "decorated" with .only).
if you want to skip a test, use
.skip(...) // will skip this part
You can use them after describe
or after it
. Insert them like:
describe.skip("Test 1", () => {
// ...
})
describe.only("Test 1", () => {
// ...
})
or:
it.only("Navigate to a web page", () => { ... })
it.skip("Change data", () => { ... })
After you're done, just remove the .skip
and .only
and the complete tests will run again as designed. Note this is useful while debugging the tests and skip / exclude / run them while testing. It is not meant for conditional execution (i.e. let the test itself decide which parts to run).
NOTE: describe.skip
still shows the titles of all the sub-descriptions and iterations. If you don't want this, you can use your own function, like:
function describe_skip(str, fn) {
// to skip a test, use describe_skip instead of describe
describe('skipped "' + str + '"', () => {});
}
Now if you use describe_skip("test suite", () => { /* your test code */})
it will only print the title like skipped "test suite"
, and ignore the entire function (your test code). This way, you can really disable the entire function temporatily (until you need it again).
if
statementsA different way is to use JavaScript features to control the flow of your test (if conditions, putting functions around your test parts & comment out the function calls you want to skip etc).
For example:
if (Cypress.config('viewportWidth') > 350) {
it('does not run on mobile viewports', () => {
// ...
});
}
As described here. For this purpose (i.e. conditional statements) also a plugin exists (see this answer), but is "archived", meaning no longer maintained.
You may also use this.skip();
inside an iteration (it
). It will immediately exit the iteration. This can also be combined with an if
statement, e.g.
it("Some test iteration", () => {
if (Cypress.config('viewportWidth') > 350) {
this.skip();
}
});
Last, not least, you can write your own extension named runIf
like so:
Cypress.Commands.add('runIf', (condition, fn) => {
if (condition !== undefined && condition === true
&& fn!==undefined && fn!==null && typeof fn === 'function') {
cy.wait(1).then(($sel) => { (fn)($sel) })
}
})
and copy it into the file command.js
(which is in the support
folder).
Then you can use it in your code like:
cy.runIf(a===5, () => {
cy.log("The variable a has the value 5")
})
Note that constants or variables you defined outside are easily accessible as closure, like
const user_name = "Sarah Connor"
cy.runIf(user_name!=="", () => {
cy.log(`Hello ${user_name}!`)
})
cy.runIf(user_name==="", () => {
cy.log("No user defined")
})
Regarding the case you've shown in the question, you could use it like:
cy.runIf(!queryFailed(), () => {
cy.get('.list')
.should('be.visible')
})
Notes:
this
inside such a function, consider to use the old syntax instead (i.e. function() { /* ... your code ... */ }
instead of () => { /* ... your code ... */ }
), because arrow functions don't have all features of a "real" function. See this comparison between arrow functions and normal functions.this
keyword, you might also want to check out .call() and .bind(). Both accept thisArg, arg1, arg2, ...
as arguments allowing to specify a different object for this
. Use .call()
to invoke the function immediately, and .bind()
to return a function that you may call later.cy.pause()
to pause, step next or resume your code. Also available is cy.debug()
.specPattern: "cypress/e2e/*.cy.js"
in the e2e section of cypress.config.js
, you can simply rename the files you want to deactivate, e.g. my_test_suite.cy.js
would run, but my_test_suite.cy-disabled.js
would not.Upvotes: 4
Reputation: 41
Reiterating the answer that @w5l provided last year, here is something that I use in a legacy version of Cypress w/ Typescript ( 9.1.1 ). Hope it's useful to someone here.
Cypress.Commands.add('skipTest', (message:string)=>{
return cy.wrap(new Cypress.Promise<any>(resolve=>{
cy.log(`===== ${message} =====`)
// @ts-ignore
const getMochaContext = () => cy.state('runnable').ctx as Mocha.Context;
getMochaContext().skip()
resolve()
}),{log:false})
})
Upvotes: 0
Reputation: 3120
The function queryFailed()
never returns anything, so by default it's return value is undefined
and this block
if (queryFailed()) {
this.skip();
} else {
will always perform the skip()
.
If you return the cy.get('.spin')
then the if/else logic inside is passed back as well, but the result is a Chainer
which can't be used by an if()
statement.
The better option is to convert cy.get('.spin')
to a jQuery equivalent which is Cypress.$('.spin')
.
Since you have a recursive call, you should poll the elements as per this question How to check for an element that may not exist using Cypress
The cy.wait(1000)
between calls which is tricky to handle - the best way is to make the function async
and await a delay
const delay = (delay) => new Promise(resolve => setTimeout(resolve, delay));
async function queryFailed(attempts = 0) {
if (attempts === 100) {
throw new Error('Too many attempts') // recursive guard to avoid infinite calls
}
const $container = Cypress.$('.spin')
const $list = $container.find('List')
if ($list.length > 0) {
return false;
}
const $error = $container.find('error')
if ($error.length > 0) {
return true;
}
await delay(1000)
return queryFailed(++attempts); // recursive call if not resolved above
}
Upvotes: 22
Reputation: 213
Your queryFailed()
returns either true
or false
depending on container cy.get('.spin')
child that eventually appears having text "List" or "error".
So you can search for both at once, one or the other will turn up.
Using two selectors in one query, separated by a comma - you defer the conditional check until the async elements have resolved.
cy.get('.spin')
.find(':contains("List"), :contains("error")') // retry until one appears
.then($el => {
if ($el.contains("List")) {
// assertions about List
}
})
Upvotes: 9
Reputation: 49694
Note the library @cypress/skip-test has archived. Please check other solutions.
This repository has been archived by the owner on Jan 31, 2023. It is now read-only.
Cypress now also provides a library @cypress/skip-test which can give more controls by
cy.skipOn
cy.onlyOn
isOn
Upvotes: 13
Reputation: 5746
You can still access the Mocha skip
function because Cypress uses Mocha internally. Access Mocha context using cy.state('runnable').ctx
, and call skip()
on that.
In TypeScript:
// @ts-ignore "cy.state" is not in the "cy" type
export const getMochaContext = () => cy.state('runnable').ctx as Mocha.Context;
Example usage in a test, you'll see the test as skipped and the obvious assertion error after the skip()
is not reported.
it("should be skipped", () => {
getMochaContext().skip();
expect(0).to.equal(1);
});
Upvotes: 0
Reputation: 25054
The up-to-date way is via @cypress/skip-test. However, the usage for me with cypress v10 and TypeScript was slightly different than in the readme:
yarn add --dev @cypress/skip-test
No further setup needed, simply in the spec:
import { skipOn } from '@cypress/skip-test'
describe('something', () => {
skipOn(Cypress.env('mySetting') === 'settingValue', () => {
it('can do something', () => {
// ...
})
})
})
Upvotes: -1
Reputation: 136
We are using an adapted version by Ben Mosher
const maybeDescribe = Cypress.config("baseUrl") === "https://your-env-url/"
? describe
: describe.skip
maybeDescribe('Test something... or not dunno', function() { /* */ })
Seems to work for the Github workflow that run tests as well.
Upvotes: -1
Reputation: 1029
The Cypress Real World App, a payment application to demonstrate real-world usage of Cypress testing methods, patterns, and workflows contains a few workflows for conditionally running tests in CI and altered workflows for mobile viewports, for instance.
In addition, as of Cypress v4.8.0 certain Test Configurations may be applied to a suite or individual test.
For instance:
it('Show warning outside Chrome', { browser: '!chrome' }, () => {
cy.get('.browser-warning')
.should('contain', 'For optimal viewing, use Chrome browser')
})
Upvotes: -1
Reputation: 13381
I am using an adapted version of @NoriSte's answer that still registers the tests, but as skipped:
const maybeDescribe = Cypress.env('SOME_ENV_VAR') ? describe : describe.skip
maybeDescribe('Test suite', function() { /* ... */ })
and then similarly, on CI, where I want to skip the tests:
export CYPRESS_SOME_ENV_VAR=
$(npm bin)/cypress run
My source-controlled cypress.json
has SOME_ENV_VAR
defined so when I run locally, the tests are not skipped.
Upvotes: 4
Reputation: 23463
I think you are almost there, but instead of the synchronous if () {...} else {...}
pattern you need the asynchronous callback pattern.
it('shows the list', function() {
const whenFailed = function() {
this.skip()
}
const whenSucceeded = function() {
cy.get('.list').should('be.visible')
}
queryFailed(whenFailed, whenSucceeded);
}
function queryFailed(whenFailed, whenSucceeded) {
cy.get('.spin')
.then($container => {
const htmlLoaded = $container[0].innerHTML;
if (htmlLoaded.indexOf('List') !== -1) {
whenSucceeded();
return;
}
if (htmlLoaded.indexOf('error') !== -1) {
whenFailed();
return;
}
cy.wait(1000);
queryFailed(whenFailed, whenSucceeded);
});
}
However, I note the recursive call to queryFailed()
, which looks like you are manually retrying the content of the spin container.
Cypress has built in retries, so all you have to do is decide on a maximum time your result will possibly take (say 20 seconds), and it will conclude the command as soon as the desired content arrives, or fail the test altogether if it doesn't happen in 20 seconds.
Also, you should be in control of the success/failure of whatever the spinner is waiting on (e.g fetching the list via XHR). So you should split the test into two - one for success and one for failure.
context('when the list loading succeeds' function() {
it('shows the list', function() {
// Mock XHR success here
cy.contains('.spin', 'List', { timeout: 20000 });
cy.get('.list').should('be.visible');
})
it('does not show an error message', function() {
...
})
})
context('when the list loading fails' function() {
it('does not show the list', function() {
// Mock XHR failure here
cy.contains('.spin', 'error', { timeout: 20000 });
cy.get('.list').should('not.be.visible');
})
it('shows an error message', function() {
...
})
})
This is a bit rough as I don't know the exact HTML expected, or what the spinner is waiting on, but you can see this pattern is much tidier and tests all paths in a controlled manner.
Upvotes: 42
Reputation: 3711
Thank you for the detailed description! I provide you a solution for your very first question
I'm trying to find out if I'm able to conditionally skip a test it() in my test suite and deal with its async nature as well.
Use an environment variable, I report you a solution of mine (actually using in my pipeline).
if (!Cypress.env("SKIP_E2E_TESTS")) {
it(...);
}
and in my package.json
file I have a script that looks like this
"test": "CYPRESS_SKIP_E2E_TESTS=true npm-run-all --parallel --silent test:unit test:cypress",
My goal was the same as yours, I'd like to disable some tests in some circumstances (the CI pipeline in my case).
So put the whole test into a condition instead of having a conditional test.
Let me know if you need some more help 😉
Upvotes: 9