How to programmatically stop a Cypress test - cypress

I have a Cypress test that has only one testing case (using v10.9 with the test GUI):
describe("Basic test", () => {
it.only("First test", () => {
cy.visit("http://localhost:9999");
cy.pause();
//if(cy.get("#importantBox").contains(...) {
//}
//else
{
Cypress.runner.stop();
console.log("Stopping...");
}
console.log("Visiting...");
cy.visit("http://localhost:9999/page1");
If a certain element doesn't exist in the page, I don't want the test to continue, so I try to stop it.
Unfortunately, I can see this in the console:
Stopping...
Visiting...
And the test keeps going without the necessary data...
So, can I somehow stop it without using huge if statements?

Stopping the test is relatively easy, the harder part is the condition check.
Cypress runner is built on the Mocha framework, which has a .skip() method. If you issue it in your else clause and inside the Cypress queue, the test will stop.
Two ways to access skip():
Using a function() callback gives access to this which is the Mocha context
it('stops on skip', function() {
...
cy.then(() => this.skip()) // stop here
})
Use cy.state() internal command (may be removed at some point)
it('stops on skip', () => {
...
cy.then(() => cy.state('test').skip()) // stop here
})
You should be aware that all Cypress commands and queries run on an internal queue which is asynchronous to javascript code in the test like console.log("Visiting..."), so you won't get any useful indication from that line.
To use synchronous javascript on the queue, wrap it in a cy.then() command as shown above with the skip() method, so to console log do
cy.then(() => console.log("Visiting..."))

Related

Cypress access alias with this.* doesn't works even with function

Trying to access the alias value on the this () Context, I followed the docs from https://docs.cypress.io/api/commands/as#Fixture
In the .then callback the dataExample parameter is filled OK, but on this.example it is undefined.
describe('cy.as and this', function () {
it('Testing cy.as and this', function () {
cy.fixture('example.json').as('example')
.then(function (dataExample) {
cy.log('ACCESSING this, dataExample', this, dataExample);
});
cy.log(`ACCESSING this.example : ${JSON.stringify(this['example'])}`, this);
});
});
The first log outputs the following: with the dataExample correctly filled, but the contex.example undefined
The second log outside of .then, with this.example also undefined
If I move the cy.fixture line into a beforeEach() it does work. Cold somebody explain this behavior?
describe('alias', () => {
beforeEach(() => {
cy.fixture('example.json').as('example');
});
it('can access all aliases as properties', function () {
expect(this['example']).not.to.eq(undefined); // true
cy.log('THIS', this); // curious => [0]: Context {_runnable: undefined, test: undefined, example: undefined}
cy.log('THIS.example', this['example']); // {name: 'Using fixtures to represent data', email: 'hello#cypress.io',
// body: 'Fixtures are a great way to mock data for responses to routes'}
});
});
Cypress commands are asynchronous. Cypress does not execute a command immediately, it just puts one into a queue to be executed lately. So any command inside a single block of code will not be executed in this block. And the other block of code means any of this:
other it test
before/beforeAll/after/afterAll hooks
then/should callback
So if you want to use a value from a cypress command inside the same it test, you have to use a then callback in order to give Cypress time to execute the command.
If you use a cypress command in a beforeEach hook, the command will be executed by the time when it code starts.
A helpful way to think of the flow is that there are two passes:
first pass, runs the javascript of the test and executes plain JS but enqueues Cypress commands (and their callbacks)
second pass executes those commands
So when
cy.log(`ACCESSING this.example : ${JSON.stringify(this['example'])}`, this)
is enqueued in the first pass, the parameter this['example'] is evaluated immediately (value is undefined).
It's not storing the parameter expression to be evaluated in the second pass, which is what most people expect.
But you can defer the parameter evaluation by putting it inside another callback.
cy.then(() => {
// this callback code is enqueued
// so the evaluation of "this['example']" is deferred until the queue is running
cy.log(`ACCESSING this.example : ${JSON.stringify(this['example'])}`, this)
})
Gleb Bahmutov has a blog
How to replace the confusing cy.then command with the less confusing cy.later command

Test that an API call does NOT happen in Cypress

I've implemented API data caching in my app so that if data is already present it is not re-fetched.
I can intercept the initial fetch
cy.intercept('**/api/things').as('api');
cy.visit('/things')
cy.wait('#api') // passes
To test the cache is working I'd like to explicitly test the opposite.
How can I modify the cy.wait() behavior similar to the way .should('not.exist') modifies cy.get() to allow the negative logic to pass?
// data is cached from first route, how do I assert no call occurs?
cy.visit('/things2')
cy.wait('#api')
.should('not.have.been.called') // fails with "no calls were made"
Minimal reproducible example
<body>
<script>
setTimeout(() =>
fetch('https://jsonplaceholder.typicode.com/todos/1')
}, 300)
</script>
</body>
Since we test a negative, it's useful to first make the test fail. Serve the above HTML and use it to confirm the test fails, then remove the fetch() and the test should pass.
The add-on package cypress-if can change default command behavior.
cy.get(selector)
.if('exist').log('exists')
.else().log('does.not.exist')
Assume your API calls are made within 1 second of the action that would trigger them - the cy.visit().
cy.visit('/things2')
cy.wait('#alias', {timeout:1100})
.if(result => {
expect(result.name).to.eq('CypressError') // confirm error was thrown
})
You will need to overwrite the cy.wait() command to check for chained .if() command.
Cypress.Commands.overwrite('wait', (waitFn, subject, selector, options) => {
// Standard behavior for numeric waits
if (typeof selector === 'number') {
return waitFn(subject, selector, options)
}
// Modified alias wait with following if()
if (cy.state('current').attributes.next?.attributes.name === 'if') {
return waitFn(subject, selector, options).then((pass) => pass, (fail) => fail)
}
// Standard alias wait
return waitFn(subject, selector, options)
})
As yet only cy.get() and cy.contains() are overwritten by default.
Custom Command for same logic
If the if() syntax doesn't feel right, the same logic can be used in a custom command
Cypress.Commands.add('maybeWaitAlias', (selector, options) => {
const waitFn = Cypress.Commands._commands.wait.fn
// waitFn returns a Promise
// which Cypress resolves to the `pass` or `fail` values
// depending on which callback is invoked
return waitFn(cy.currentSubject(), selector, options)
.then((pass) => pass, (fail) => fail)
// by returning the `pass` or `fail` value
// we are stopping the "normal" test failure mechanism
// and allowing downstream commands to deal with the outcome
})
cy.visit('/things2')
cy.maybeWaitAlias('#alias', {timeout:1000})
.should(result => {
expect(result.name).to.eq('CypressError') // confirm error was thrown
})
I also tried cy.spy() but with a hard cy.wait() to avoid any latency in the app after the route change occurs.
const spy = cy.spy()
cy.intercept('**/api/things', spy)
cy.visit('/things2')
cy.wait(2000)
.then(() => expect(spy).not.to.have.been.called)
Running in a burn test of 100 iterations, this seems to be ok, but there is still a chance of flaky test with this approach, IMO.
A better way would be to poll the spy recursively:
const spy = cy.spy()
cy.intercept('**/api/things', spy)
cy.visit('/things2')
const waitForSpy = (spy, options, start = Date.now()) => {
const {timeout, interval = 30} = options;
if (spy.callCount > 0) {
return cy.wrap(spy.lastCall)
}
if ((Date.now() - start) > timeout) {
return cy.wrap(null)
}
return cy.wait(interval, {log:false})
.then(() => waitForSpy(spy, {timeout, interval}, start))
}
waitForSpy(spy, {timeout:2000})
.should('eq', null)
A neat little trick I learned from Gleb's Network course.
You will want use cy.spy() with your intercept and use cy.get() on the alias to be able to check no calls were made.
// initial fetch
cy.intercept('**/api/things').as('api');
cy.visit('/things')
cy.wait('#api')
cy.intercept('METHOD', '**/api/things', cy.spy().as('apiNotCalled'))
// trigger the fetch again but will not send since data is cached
cy.get('#apiNotCalled').should('not.been.called')

How to stop a Cypress test but mark it as a success?

I have a Cypress test that clicks a link which runs a method and then brings the user to a new page on different website entirely.
This is the test:
it('Cards', () => {
cy.get('#my-id').click({ force: true })
var itemID= localStorage.getItem('itemID')
expect(itemID).to.eq(null);
cy.get(`a:visible[id*="the_link"]`).first().click()
Cypress.on('fail', (error, runnable) => {
var itemID= localStorage.getItem('itemID')
expect(itemID).to.not.equal(null)
// end test but mark as a success
})
})
The problem is that I get a cross origin error, therefore I added in the Cypress.on('fail') piece of code. So now, the test does not fail but it waits for minutes because it attempting to intercept more data. There is a lot of other stuff going on so I cannot change the intercept logic.
All I want is to end the test and mark it a success on the line that says // end test but mark as a success.
Is this possible?
If you are using chrome based browsers you might wanna try setting chromeWebSecurity to false.
Other browsers (specifically Firefox) might still run into that issue, to which you might wanna properly address the on failure method or split the interaction with another origin to another it statement. The later is pretty simple and should work on all browsers:
it('Cards', () => {
cy.get('#my-id').click({ force: true })
}) //if that gets you to another superdomain, we go to another it
it('Another superdomain', () => {
var itemID= localStorage.getItem('itemID')
expect(itemID).to.eq(null);
cy.get(`a:visible[id*="the_link"]`).first().click()
}) If that is yet another superdomain, we go to another it again
it('Third superdomain', () => {
var itemID= localStorage.getItem('itemID')
expect(itemID).to.not.equal(null)
// test ends by itself
})
The way you describe on failure method, you just provide additional functionality for the failure but do not instruct the method to ignore the test status finalisation.
That can be done by returning false on that specific event:
Cypress.on('fail', () => false);
Mind that you can probably include an assertion before returning false if it is really required.
Cypress.on('fail', () => { // no params are needed tho
var itemID= localStorage.getItem('itemID')
expect(itemID).to.not.equal(null)
// end test but mark as a success
return false
})
That piece of code must be provided right before the potential failure event. This will interrupt the test as it will fail but will mark it as passed.
This is a huge anti-pattern and generalisation, as you use a Cypress.on('fail', ()) event. Better way is to anchor any sort of negative logic to a more specific event, one of those listed here. However, that specific hack above might work for avoiding cross origin error on non-chrome-based browsers if it is for sure the last thing that happens in that test.

How to handle Exceptions in Cypress in a similar way we did in selenium

In selenium we can handle exception. If any exception occur in any testcase it will then jump onto next testcase we can did in selenium. But i an confused that how can we did this in Cypress. Taking below example
it('Test Case 1', function () {
cy.visit('https://habitica.com/login')
cy.get('form').find('input[id="usernameInput"]').click().type("username")
cy.get('form').find('input[id="passwordInput"]').click().type("password")
**cy.get('.btn-info').click()**
cy.get('.modal-dialog').find('button[class="btn btn-warning"]').click()
cy.get('.start-day').find('button').click({force:true})
})
it('Test Case 2', function () {
cy.visit('https://habitica.com/login')
cy.get('form').find('input[id="usernameInput"]').click().type("username")
cy.get('form').find('input[id="passwordInput"]').click().type("password")
cy.get('.btn-info').click()
cy.get('.modal-dialog').find('button[class="btn btn-warning"]').click()
cy.get('.start-day').find('button').click({force:true})
})
Lets say browser unable to find click element (Highlighted with bold) in testcase 1 then it will jump onto Testcase 2.
How can we do it in Cypress?
Please help me on this
Exceptions like Unable to fine element or similar others.
Other than this example how can we handle exceptions or error.
Although Cypress team is saying that we need to avoid conditional test as much as possible (and maybe a need to change your approach). However, in you case, you can include a conditional test:
cy.get('.btn-info').then((body) => {
if (body.length > 0) { // continues if the element exists
cy.get('.btn-info').click();
cy.get('.modal-dialog').find('button[class="btn btn-warning"]').click()
cy.get('.start-day').find('button').click({force:true})
} // if the above condition is not met, then it skips this the commands and moves to the next test
});
Thanks alot for your response. Please have a look at this. I have used your code. "'.btn-info'" does not exist so exception occur which is fine. but problem is it do not move onto else statement. I mean If statement gets failed then it must execute else but it do not. Why it is doing so?
it('First Test Case', function() {
cy.visit('http://pb.folio3.com:9000/admin/#/login');
cy.get('.btn-info').then((body) => { // **THIS ELEMENT NOT EXIST**
if (body.length > 0) { // continues if the element exists
cy.get('.btn-info').click();
cy.get('.modal-dialog').find('button[class="btn btn-warning"]').click()
cy.get('.start-day').find('button').click({force:true})
}
else
{
**cy.visit('https://www.facebook.com/');**
}
});
it('Second Test Case', function() {
cy.visit('https://www.google.com/');
})

In Mocha, why do I want to use only()/skip()?

What is the point of only() and skip()? If I only want to have a single it/describe get executed, why should I keep the other things in the file? If I want to skip something, why shouldn't I just remove that code? When does one want to use those methods?
About only. Imagine that you have 2000 unit tests in some npm module. And you need write 3 more tests for new feature. So you create something.test.js file and write test cases with describe.only()
const assert = require('assert')
describe.only('sample class', () => {
it('constructor works', () => {
assert.deepEqual(true, true)
})
it('1st method works', () => {
assert.deepEqual(true, true)
})
it('2nd method works', () => {
assert.deepEqual(true, true)
})
})
Now if you launch test locally via npm test, you run only your 3 tests, not whole bunch of 2003 tests. Tests are written much faster with only
About skip. Imagine that you need to implement urgent feature in 20mins, you don't have enough time to write tests, but you have time to document your code. As we know unit tests are the best documentation, so you just write test cases how code should works with describe.skip()
describe.skip('urgent feature', () => {
it('should catch thrown error', () => {})
})
Now everyone in your team know about your new functionality and maybe someone write tests for you. Now the knowledge of how your new feature works is not only in your head, the whole team knows about it. It's good for the project and business.
more reasons to use skip

Resources