I have 2 describe blocks in a spec file.
First, describe visits xyz.com and Second, describe visits abc.com
And I need these 2 describe in one spec only. The wired behavior I see is it runs the tests smoothly but after visiting abc.com from 2nd describe it starts running 1st describe again. An infinite loop of tests
var signedOutArtifactID = null;
describe('WEB APP E2E tests', function() {
var token = null;
before(function() {
cy.visit('/');
// Login
cy.get('#username')
.type(auth.geneticist.username);
cy.get('#password')
.type(auth.geneticist.password);
cy.get('button')
.contains('Login')
.click()
.should(function() {
token = localStorage.getItem('token');
expect(token).not.to.be.null;
});
});
beforeEach(function() {
localStorage.setItem('token', token);
cy.contains('Logout')
.should('exist');
expect(localStorage.getItem('token'));
});
it('should land on home page', function() {
cy.url()
.should('include', '/home');
});
it('should save and generate and end up on signout page', function() {
cy.contains('Save and Generate Report')
.click();
cy.url()
.should('include', '/sign-out');
});
it('should signout and send successfully', function() {
cy.url()
.should(function(currentURL) {
signedOutArtifactID = currentURL.match(/2-([0-9]+)/)[0];
expect(signedOutArtifactID).not.to.be.null;
});
// Make sure interpretation was updated
cy.get('.card-body pre')
.should('contain', 'test interpretation added by cypress');
cy.contains('Sign Out and Send')
.click();
cy.contains('Yes, sign out and send')
.click();
});
});
describe('2nd WEB APP E2E tests', function() {
before(function () {
cy.visit({
url:`https://webappurl.com/search?scope=All&query=${signedOutArtifactID}`,
failOnStatusCode: false
})
})
it('Review Completed step in clarity', async () => {
cy.get('#username').type(auth.clarity_creds.username)
cy.get('#password').type(auth.clarity_creds.password)
cy.get('#sign-in').click()
cy.get('.result-name').click()
cy.get('.view-work-link').contains('QWERTYU-IDS').click()
cy.get('.download-file-link ')
.should(($downloads) => {
expect($downloads).to.have.length(2)
})
});
});
describe defines a test suite. You can only have one top-level test suite per file, and only one domain per test.
I would just change your describes to contexts and wrap both contexts in a single describe, like so:
var signedOutArtifactID = null;
describe('e2e tests', function() {
context('WEB APP E2E tests', function() {
var token = null;
before(function() {
cy.visit('/');
// Login
cy.get('#username')
.type(auth.geneticist.username);
cy.get('#password')
.type(auth.geneticist.password);
cy.get('button')
.contains('Login')
.click()
.should(function() {
token = localStorage.getItem('token');
expect(token).not.to.be.null;
});
});
beforeEach(function() {
localStorage.setItem('token', token);
cy.contains('Logout')
.should('exist');
expect(localStorage.getItem('token'));
});
it('should land on home page', function() {
cy.url()
.should('include', '/home');
});
it('should save and generate and end up on signout page', function() {
cy.contains('Save and Generate Report')
.click();
cy.url()
.should('include', '/sign-out');
});
it('should signout and send successfully', function() {
cy.url()
.should(function(currentURL) {
signedOutArtifactID = currentURL.match(/2-([0-9]+)/)[0];
expect(signedOutArtifactID).not.to.be.null;
});
// Make sure interpretation was updated
cy.get('.card-body pre')
.should('contain', 'test interpretation added by cypress');
cy.contains('Sign Out and Send')
.click();
cy.contains('Yes, sign out and send')
.click();
});
});
context('2nd WEB APP E2E tests', function() {
before(function () {
cy.visit({
url:`https://webappurl.com/search?scope=All&query=${signedOutArtifactID}`,
failOnStatusCode: false
})
})
it('Review Completed step in clarity', async () => {
cy.get('#username').type(auth.clarity_creds.username)
cy.get('#password').type(auth.clarity_creds.password)
cy.get('#sign-in').click()
cy.get('.result-name').click()
cy.get('.view-work-link').contains('QWERTYU-IDS').click()
cy.get('.download-file-link ')
.should(($downloads) => {
expect($downloads).to.have.length(2)
})
});
});
})
There should be one describe block per suite (specification file). Therefore, when I need to wrap multiple related tests in one specification file I use context. Also, the following is what cypress documentation says:
The test interface, borrowed from Mocha, provides describe(),
context(), it() and specify().
context() is identical to describe() and specify() is identical to
it(), so choose whatever terminology works best for you
However, I believe the test structure and describe, context, it hierarchy is a little off track. So, here is how I write tests:
describe('User Authentication Using Custom Auth Token', () => {
beforeEach(() => {
cy.on('uncaught:exception', err => {
console.log('cypress has detected uncaught exception', err);
return false;
});
});
context('when not authenticated', () => {
it('Redirects to /login and Stays on /Login', () => {
cy.visit('/');
cy.location('pathname').should('equal', '/login');
// more on your logic
});
context('when authenticated', () => {
it('Successful login using Custom Auth Token', ()=>{
cy.visit('/')
cy.login();
// more on your logic
});
});
});
Related
I am new to writing test with async/await in angular.
I have the following code. The service method is an async method. The test fails saying component.options.length is 0.
Can anyone please help me how to fix the error so the options has got the value i set in spy?
Thanks
spec.ts
spySideNavService = jasmine.createSpyObj('SideNavService', [], {
setOrgUserDetails: () => {},
loadMenus: () =>
[
{
id: 'my-menu',
label: 'My Menu',
icon: 'far fa-envelope fa-2x',
url: 'url'
}
] as NavOption[]
});
describe('ngOnInit', () => {
it('should add navigation options', () => {
expect(component.options.length).toBeGreaterThan(0);
});
});
component:
ngOnInit(): void {
this.options = await this.sideNavService.loadMenus();
}
SideNavService:
async loadMenus(): Promise<NavOption[]> {
//logic
}
Tried answer given below but still not working:
describe('ngOnInit', () => {
it('should add navigation options', fakeAsync(() => {
// !! call tick(); to tell the test to resolve all promises
// before coming to my expect line
tick();
expect(component.options.length).toBeGreaterThan(0);
}));
});
You need to use fakeAsync/tick to control promises.
// !! add fakeAsync
it('should add navigation options', fakeAsync(() => {
// !! call tick(); to tell the test to resolve all promises
// before coming to my expect line
// !! call ngOnInit
component.ngOnInit();
console.log(component.options);
tick();
console.log(component.options);
expect(component.options.length).toBeGreaterThan(0);
}));
Before, the test would go to the await line and go back to the test for the expect because the await is saying to do this later. Now with the tick, we are saying if they are any promises created, resolve them before moving forward.
Also, I think you're missing a Promise.resolve on loadMenus.
loadMenus: () => Promise.resolve(
[
{
id: 'my-menu',
label: 'My Menu',
icon: 'far fa-envelope fa-2x',
url: 'url'
}
] as NavOption[])
I am thinking the Promise.resolve is required so it can be awaited.
edit
I don't think the done callback will help you.
You can try using await fixture.whenStable() to wait for the promise(s). Try this:
describe('ngOnInit', () => {
it('should add navigation options', async () => {
component.ngOnInit();
await fixture.whenStable();
expect(component.options.length).toBeGreaterThan(0);
});
});
Blocked by a Cypress custom command not executing properly within a spec's after hook when a test failed. Experimented with the following minimal spec examples.
Context
Testing two Cypress custom commands:
cy.cmdLogin(): Standard username password authentication
cy.cmdLogout(): cy.get('logout_button').click()
Spec #1, cy.cmdLogout() in after hook does work
Very simple Cypress tests spec, test cy.cmdLogin() with after hook with cy.cmdLogout(), works perfectly:
context('SPEC Login', () => {
after('AFTER Logout', () => {
if (this.successLogin) {
cy.cmdLogout().then(() => {
cy.log('Logout Success');
});
}
});
it('TEST Login', () => {
cy.cmdLogin().then(() => {
cy.log('Login Success');
this.successLogin = true;
});
});
});
Spec #2, cy.cmdLogout() in after hook does not work
Modified Cypress tests spec, test cy.cmdLogin() with after hook with cy.cmdLogout(), now added a test that forced failure expect(false).to.be.a('boolean').to.be.true;. The after hook cy.cmdLogout() is called and fails:
Verified that Cypress could handle cy.get('logout_button') without failure.
Yet, extending with .click(), cy.get('logout_button').click(), Cypress would throw an error: Can not perform click() on undefined.
context('SPEC Login', () => {
after('AFTER Logout', () => {
if (this.successLogin) {
cy.cmdLogout().then(() => {
cy.log('Logout Success');
});
}
});
it('TEST Login', () => {
cy.cmdLogin().then(() => {
cy.log('Login Success');
this.successLogin = true;
});
});
it('TEST Fail', () => {
expect(false).to.be.a('boolean').to.be.true;
});
});
Spec #3, removed after hook, cy.cmdLogout() moved to another test does work
Modified Cypress tests spec again and removed after hook, test cy.cmdLogin(), test forced failure, and now test cy.cmdLogout(). This works perfectly:
context('SPEC Login', () => {
it('TEST Login', () => {
cy.cmdLogin().then(() => {
cy.log('Login Success');
this.successLogin = true;
});
});
it('TEST Fail', () => {
expect(false).to.be.a('boolean').to.be.true;
});
it('TEST Logout', () => {
if (this.successLogin) {
cy.cmdLogout().then(()=> {
cy.task('log', 'Logout Success');
});
}
});
});
Insight as to why Cypress is finicky with spec's after hook would be very much appreciated
The short answer (for the general situation) is change after() hook to before() hook, but in your case that wouldn't work because this.successLogin will always be undefined inside a before.
You would need to create a cy.isLoggedIn() command or call cy.cmdLogout() unconditionally.
To get after() the logic working on a fail, use
after('AFTER Logout', () => {
cy.cmdLogout()
});
Cypress.on('fail', (error, runnable) => {
cy.cmdLogout()
throw error
})
which is equivalent to this in plain javascript
try {
some-code-that-might-error()
cy.cmdLogout() // does not run if error occurs
} catch (error) {
cy.cmdLogout() // do the logout in error situation
}
I'm working with cypress tests and I want to avoid having to log in before each test. so, I wanted to preserve the cookies in each test file.
The log statement in the afterEach hook is triggered, however cookies are cleared in the second testcase.
describe('Users Page Scenarios', () => {
before(() => {
myApp.pages.Login.navigate();
myApp.pages.Login.login(
credentials.globalAdmin.email,
credentials.globalAdmin.password
);
});
beforeEach('navigate to users page before each test', () => {
myApp.sharedComponents.Header.navigateToUsers();
});
afterEach(() => {
Cypress.Cookies.preserveOnce('session_id');
cy.log('test');
});
describe('Users List', () => {
it('Should redirect the user to users page after clicking on users in the navigation header', () => {
cy.url().should('eq', `${Cypress.config().baseUrl}/user`);
});
})
describe('New User Creation', () => {
it('Should open new user modal after clicking on invite administrator', () => {
myApp.pages.Users.UsersList.inviteAdministrator();
cy.url().should('eq', `${Cypress.config().baseUrl}/user/new`);
});
it('Should create a new user successfully', () => {
myApp.pages.Users.UsersList.inviteAdministrator();
myApp.pages.Users.UsersInfo.createNewUser(user.generateUser());
})
});
The docs indicate that Cypress.Cookies.preserveOnce('session_id') is used in beforeEach().
Looks like after is too late.
describe('Dashboard', () => {
before(() => {
// log in only once before any of the tests run.
// your app will likely set some sort of session cookie.
// you'll need to know the name of the cookie(s), which you can find
// in your Resources -> Cookies panel in the Chrome Dev Tools.
cy.login()
})
beforeEach(() => {
// before each test, we can automatically preserve the
// 'session_id' and 'remember_token' cookies. this means they
// will not be cleared before the NEXT test starts.
//
// the name of your cookies will likely be different
// this is an example
Cypress.Cookies.preserveOnce('session_id', 'remember_token')
})
If you have localStorage or sessionStorage to preserve, or you have not identified all cookies correctly, try with cy.session()
beforeEach(() => { // must be beforeEach()
cy.session('mySession', () => { // preserves localStorage, sessionStorage, cookies
myApp.pages.Login.navigate();
myApp.pages.Login.login(...); // only called once (despite beforeEach())
})
})
So I have a pretty `before` and `beforeEach` function that runs before all tests. It looks something like this:
describe("JWT Authentication", function() {
before(function() {
// custom command runs once to get token for JWT auth
// alias token as 'user' for further use
cy.get_auth_token().as('user')
})
beforeEach(function() {
// before each page load, set the JWT to the aliased 'user' token
cy.visit("/", {
onBeforeLoad(win) {
// set the user object in local storage
win.localStorage.setItem("token", this.user.token);
}
})
})
it("a single test...", function() {
//do stuff
});
The custom command is also pretty simple:
Cypress.Commands.add("get_auth_token", () => {
cy.request("POST", Cypress.env("auth_url"), {
username: Cypress.env("auth_username"),
password: Cypress.env("auth_password")
})
.its("body")
.then(res => {
return res;
});
})
The custom command itself works and retrieves the token as expected. However when it comes to the beforeEach it has no idea what this.user.token is. Specifically not knowing what user is.
One option is of course calling the command in every beforeEach which is what the JWT Cypress recipe/example spec does. However this feels excessive because in my case I do not NEED to grab the token every test. I only need to grab it once for this set of tests.
So how can I share the token to the beforeEach hook with a Cypress custom command.
I ran a few tests, all the bits seem to work!
The following does not give you an answer, but may help you debug.
Passing token between before() and beforeEach()
Assume we have a user in before(), does it get to the onBeforeLoad() callback?
describe("JWT Authentication", function() {
before(function() {
const mockUser = { token: 'xyz' };
cy.wrap(mockUser).as('user');
})
beforeEach(function() {
cy.visit("http://example.com", {
onBeforeLoad(win) {
console.log(this.user.title); // prints 'xyz'
}
})
})
it("a single test...", function() {
//do stuff
})
});
Is the custom command working
I can't find a generic mock for an Auth check, but any cy.request() that gets an object should be equivalent.
I'm hitting typicode.com and looking for the title property
describe("JWT Authentication", function() {
Cypress.Commands.add("get_auth_token", () => {
cy.request("GET", 'https://jsonplaceholder.typicode.com/todos/1')
.its("body")
.then(body => {
console.log('body', body); // prints {userId: 1, id: 1, title: "delectus aut autem", completed: false}
return body;
});
})
before(function() {
cy.get_auth_token()
.then(user => console.log('user', user)) // prints {userId: 1, id: 1, title: "delectus aut autem", completed: false}
.as('user')
})
beforeEach(function() {
cy.visit("http://example.com", {
onBeforeLoad(win) {
console.log(this.user.title); // prints 'delectus aut autem'
}
})
})
it("a single test...", function() {
//do stuff
})
});
Custom command
This shorter version also seems to work
Cypress.Commands.add("get_auth_token", () => {
cy.request("GET", 'https://jsonplaceholder.typicode.com/todos/1')
.its("body");
})
I use the same before() and after() root level hooks in all my tests to setup and clear my test-database ...
Is there any way to move them into a file and export/import that file ?
Yes. I was able to achieve this behavior by taking advantage of mocha's this context, discussed in their Wiki article about shared behaviors. I am using ts-mocha which accounts for the async/await functionality.
So I wrote functions to login and logout using supertest-session and it looks something like this:
export function authHooks()
{
const email = UsersSeed[0].email;
const password = UsersSeed[0].password;
before(`login ${email}`, async function()
{
await this.session.post('/api/account/login')
.send({ email, password })
.expect(201);
});
after(`logout ${email}`, async function()
{
await this.session.get('/api/account/logout')
.expect(200);
});
}
Inside of my describe I set up the this.session and in my it-statements I can reuse it. It looks a little like this:
import { expect } from 'chai';
import * as Session from 'supertest-session';
import { authHooks } from './authHooks';
describe('Some Suite', () =>
{
before(function()
{
this.session = new Session(SomeApp);
});
after(function()
{
this.session.destroy();
});
describe('when not authenticated', () =>
{
it('returns 403', async function()
{
await this.session.get('/api/jobs')
.expect(403)
.expect({ statusCode: 403, message: 'Forbidden resource' });
});
});
describe('when authenticated', () =>
{
authHooks();
describe('when finding jobs', () =>
{
it('returns all jobs', async function()
{
await this.session.get('/api/jobs')
.expect(200)
.then((response) =>
{
expect(response.body).to.deep.eq(SomeThing);
});
});
});
});
});
I'm not sure if this is the best way to achieve it (I'm not a fan of using function over () => {} personally), but I have confirmed it works.
The above code will not run mainly because I'm protecting code specifics, but hopefully this will provide at least one option for you and maybe someone else can show a better way to do this.