DavidZ
DavidZ

Reputation: 559

Skipping a test in Cypress conditionally

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

Answers (12)

Matt
Matt

Reputation: 27001

I think to answer this correctly, we need to distinguish two use cases

  • Temporarily disable parts of the test (for debugging)
  • Run Cypress statements conditionally

The following sections will address both topics.


Temporarily disable parts of your code (for debugging)

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).


Run statements conditionally

1. Using if statements

A 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(); 
      }
 });

2. Writing your own Cypress command

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:

  • If you have trouble accessing the keyword 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.
  • Regarding 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.
  • You can use cy.pause() to pause, step next or resume your code. Also available is cy.debug().
  • Temporarily disable entire test files: If you have configured for example a spec pattern like 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

Austin
Austin

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

Eddy Gilmour
Eddy Gilmour

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

Graciella
Graciella

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

Hongbo Miao
Hongbo Miao

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
  • boolean flag
  • headed / headless environments
  • ENVIRONMENT

Upvotes: 13

w5l
w5l

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

thisismydesign
thisismydesign

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

user3303019
user3303019

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

Kevin Old
Kevin Old

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

Ben Mosher
Ben Mosher

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

Richard Matsen
Richard Matsen

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

NoriSte
NoriSte

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

Related Questions