I want to save/persist/preserve a cookie or localStorage token that is set by a cy.request(), so that I don't have to use a custom command to login on every test. This should work for tokens like jwt (json web tokens) that are stored in the client's localStorage.
To update this thread, there is already a better solution available for preserving cookies (by #bkucera); but now there is a workaround available now to save and restore local storage between the tests (in case needed). I recently faced this issue; and found this solution working.
This solution is by using helper commands and consuming them inside the tests,
Inside - cypress/support/<some_command>.js
let LOCAL_STORAGE_MEMORY = {};
Cypress.Commands.add("saveLocalStorage", () => {
Object.keys(localStorage).forEach(key => {
LOCAL_STORAGE_MEMORY[key] = localStorage[key];
});
});
Cypress.Commands.add("restoreLocalStorage", () => {
Object.keys(LOCAL_STORAGE_MEMORY).forEach(key => {
localStorage.setItem(key, LOCAL_STORAGE_MEMORY[key]);
});
});
Then in test,
beforeEach(() => {
cy.restoreLocalStorage();
});
afterEach(() => {
cy.saveLocalStorage();
});
Reference: https://github.com/cypress-io/cypress/issues/461#issuecomment-392070888
From the Cypress docs
For persisting cookies: By default, Cypress automatically clears all cookies before each test to prevent state from building up.
You can configure specific cookies to be preserved across tests using the Cypress.Cookies api:
// now any cookie with the name 'session_id' will
// not be cleared before each test runs
Cypress.Cookies.defaults({
preserve: "session_id"
})
NOTE: Before Cypress v5.0 the configuration key is "whitelist", not "preserve".
For persisting localStorage: It's not built in ATM, but you can achieve it manually right now because the method thats clear local storage is publicly exposed as Cypress.LocalStorage.clear.
You can backup this method and override it based on the keys sent in.
const clear = Cypress.LocalStorage.clear
Cypress.LocalStorage.clear = function (keys, ls, rs) {
// do something with the keys here
if (keys) {
return clear.apply(this, arguments)
}
}
You can add your own login command to Cypress, and use the cypress-localstorage-commands package to persist localStorage between tests.
In support/commands:
import "cypress-localstorage-commands";
Cypress.Commands.add('loginAs', (UserEmail, UserPwd) => {
cy.request({
method: 'POST',
url: "/loginWithToken",
body: {
user: {
email: UserEmail,
password: UserPwd,
}
}
})
.its('body')
.then((body) => {
cy.setLocalStorage("accessToken", body.accessToken);
cy.setLocalStorage("refreshToken", body.refreshToken);
});
});
Inside your tests:
describe("when user FOO is logged in", ()=> {
before(() => {
cy.loginAs("foo#foo.com", "fooPassword");
cy.saveLocalStorage();
});
beforeEach(() => {
cy.visit("/your-private-page");
cy.restoreLocalStorage();
});
it('should exist accessToken in localStorage', () => {
cy.getLocalStorage("accessToken").should("exist");
});
it('should exist refreshToken in localStorage', () => {
cy.getLocalStorage("refreshToken").should("exist");
});
});
Here is the solution that worked for me:
Cypress.LocalStorage.clear = function (keys, ls, rs) {
return;
before(() => {
LocalStorage.clear();
Login();
})
Control of cookie clearing is supported by Cypress: https://docs.cypress.io/api/cypress-api/cookies.html
I'm not sure about local storage, but for cookies, I ended up doing the following to store all cookies between tests once.
beforeEach(function () {
cy.getCookies().then(cookies => {
const namesOfCookies = cookies.map(c => c.name)
Cypress.Cookies.preserveOnce(...namesOfCookies)
})
})
According to the documentation, Cypress.Cookies.defaults will maintain the changes for every test run after that. In my opinion, this is not ideal as this increases test suite coupling.
I added a more robust response in this Cypress issue: https://github.com/cypress-io/cypress/issues/959#issuecomment-828077512
I know this is an old question but wanted to share my solution either way in case someone needs it.
For keeping a google token cookie, there is a library called
cypress-social-login. It seems to have other OAuth providers as a milestone.
It's recommended by the cypress team and can be found on the cypress plugin page.
https://github.com/lirantal/cypress-social-logins
This Cypress library makes it possible to perform third-party logins
(think oauth) for services such as GitHub, Google or Facebook.
It does so by delegating the login process to a puppeteer flow that
performs the login and returns the cookies for the application under
test so they can be set by the calling Cypress flow for the duration
of the test.
I can see suggestions to use whitelist. But it does not seem to work during cypress run.
Tried below methods in before() and beforeEach() respectively:
Cypress.Cookies.defaults({
whitelist: "token"
})
and
Cypress.Cookies.preserveOnce('token');
But none seemed to work. But either method working fine while cypress open i.e. GUI mode. Any ideas where I am coming short?
2023 Updated on Cypress v12 or more:
Since Cypress Version 12 you can use the new cy.session()
it cache and restore cookies, localStorage, and sessionStorage (i.e. session data) in order to recreate a consistent browser context between tests.
Here's how to use it
// Caching session when logging in via page visit
cy.session(name, () => {
cy.visit('/login')
cy.get('[data-test=name]').type(name)
cy.get('[data-test=password]').type('s3cr3t')
cy.get('form').contains('Log In').click()
cy.url().should('contain', '/login-successful')
})
Related
I'm trying to implemented login via API following Playwright's guidelines but somehow nothing seems to be working.
As a comparison I've built the same in Cypress and it works out of the box:
Context:
Playwright Version: 1.30
Operating System: Mac
Node.js version: v16.19.0
Browser: Chromium
I am unable to make a simple API login that works perfectly using Cypress instead. Let me share the 2 code snippets for comparison:
Simple test case:
API request to the login end-point - Auth token is retrieved
set the auth token as a cookie
navigate to a page that is accessible only if authenticated
Code Snippet
Cypress (working fine)
const body = {
username: 'username...',
password: 'password',
rememberMe: true,
};
describe('Login via API to management console', () => {
it('Login via API to management console', () => {
cy.request({
method: 'POST',
url: loginEndPoint,
headers: {
'Content-Type': 'application/json',
},
body,
}).then((response) => {
cy.setCookie('Authorization', `Token ${response.body.data.token}`);
});
cy.visit(`/management`);
});
});
Playwright (not working)
test('Login via API', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
const loginResponse = await context.request.post(`https://${process.env.MANAGEMENT_URL}/web/api/v2.1/users/login`, {
data: {
username: process.env.MANAGEMENT_USER,
password: process.env.MANAGEMENT_PASSWORD,
rememberMe: true,
}
});
const {
data: { token },
} = await loginResponse.body().then((b) => {
return JSON.parse(b.toString());
});
expect(token).toMatch(/^[a-z0-9]{80}$/)
await context.addCookies([{ name: 'Authorization', value: `Token ${token}`, path: '/', domain: `https://${process.env.MANAGEMENT_URL}` }]);
await page.goto(`https://${process.env.MANAGEMENT_URL}/management/`);
await expect(page).toHaveURL(/management/);
});
Describe the bug
Both scripts are successful at retrieving the authentication token but somehow either I'm doing something wrong with setting the cookie in Playwright or there is an issue. I'd assume the 2 scripts should be comparable.
Furthermore: I've tried to execute login via UI using global-setup, saving the storage-state, loading it before running the test and it fails also in this case... so there is something that is not setting properly the state in this case or the cookie in the previous one.
Not entirely sure why the cookie approach wasn’t working, perhaps the https:// part should be removed from the domain?
That being said, in Playwright you shouldn’t even need to do that especially within a single test, looking at the Playwright docs on signing in via the API and related page about the request context particularly under cookie management. The associated request and browser contexts share cookies, so once you complete the login request, the browser should already have the cookie state too and be logged in, so you should be able to just remove getting the token and adding the cookie. Or you can login with the API in the global setup even, as that doc showed. Just make sure in that case to save the storage state, and specify the same file in your config.
I see you tried the global setup approach (through the UI, but you can use the API since you have it), not sure what happened there. I would say to ensure that you specified the storageState in the config; I would be curious how you loaded it as mentioned, and if you’re still having problems maybe share the code you’re using for that piece?
Hope that helps or we can troubleshoot further!
According to the Cypress Cucumber Preprocessor docs regarding Before and After hooks:
The cypress-cucumber-preprocessor supports both Mocha's before/beforeEach/after/afterEach hooks and Cucumber's Before and After hooks.
However for some reason it doesn't seem to support the Cucumber's BeforeAll and AfterAll hooks. This is somewhat problematic for me. I'm currently trying to write some API tests that need to use an auth token that can only be obtained by manually logging in to the site first.
Ideally I would like my tests to log in through the UI only once, grab the auth token, and then run all of my API tests using that auth token.
I have all of these API scenarios tagged with #api and would love to be able to use a BeforeAll({ tags: '#api' }, () => { function (or equivalent) to have my Cypress tests log in and grab the auth token for use in those scenarios. However it seems like my only options are:
Use Before instead of BeforeAll (which would force me to login through the UI for every single scenario with the #api tag even though should I only need to do it once)
Use a Background on the feature file (which has the same problem)
Use Mocha's before hook instead of Cucumber's (which unfortunately doesn't support tagging and therefore would run before every feature file, instead of just the ones I have tagged)
Is there no way to replicate the Cucumber BeforeAll functionality with Cypress-Cucumber-Preprocessor?
The way I would approach the problem is to flag the first login in the run, and prevent the login code from running once the flag is set.
let loggedIn = false;
Before(() => {
const tags = window.testState.pickle.tags.map(tag => tag.name)
if (tags.includes('#api') && !loggedIn) {
loggedIn = true
console.log('logging in') // check this is called once
// do the login
}
})
You should also be able to get the same effect by wrapping the login code in a cy.session(), which is a cache that only runs it's callback once per run.
Before(() => {
const tags = window.testState.pickle.tags.map(tag => tag.name)
if (tags.includes('#api')) {
cy.session('login', () => {
console.log('logging in') // check this is called once
// do the login
})
}
})
Update from #rmoreltandem
This syntax is simpler
let loggedIn = false;
Before({ tags: '#api' }, () => {
if (!loggedIn) {
loggedIn = true
console.log('logging in') // check this is called once
// do the login
}
})
with session
Before({ tags: '#api' }, () => {
cy.session('login', () => {
console.log('logging in') // check this is called once
// do the login
})
})
I'm working on e2e test with cypress on my application.
In my case the login are manage by a external service.
When I want to enter in my application's home page (https://myApplication/home), the system redirects me in different superdomains to login.
At first cypress seems to be able to change the superdomain, but once arrived in external service page for the authentication, the system go in login error (as if we have already logged in, but incorrect).
This type of behavior does not happen outside the cypress .
Are there alternative solutions to manage external access in a cypress test or is it possible to manage it directly from cypress?
I added in my cypress.json the chromeWebSecurity:false and when I call the link for login, I added the failOnStatusCode: false,
but it still doesn't work.
Assuming this is caused by SameSite cookie blocking , then I've just been fighting the same issue. I resolved it by intercepting all requests, checking if they had a set-cookie header(s) and rewriting the SameSite attribute. There's probably a neater way to do it, as this does clutter up the cypress dashboard a little.
Sadly Zachary Costa's answer no longer works as Chrome 94 removed the SameSiteByDefaultCookies flag.
You can add this as a command for easy reuse:
In your commands file:
declare namespace Cypress {
interface Chainable<Subject> {
disableSameSiteCookieRestrictions(): void;
}
}
Cypress.Commands.add('disableSameSiteCookieRestrictions', () => {
cy.intercept('*', (req) => {
req.on('response', (res) => {
if (!res.headers['set-cookie']) {
return;
}
const disableSameSite = (headerContent: string): string => {
return headerContent.replace(/samesite=(lax|strict)/ig, 'samesite=none');
}
if (Array.isArray(res.headers['set-cookie'])) {
res.headers['set-cookie'] = res.headers['set-cookie'].map(disableSameSite);
} else {
res.headers['set-cookie'] = disableSameSite(res.headers['set-cookie']);
}
})
});
});
Usage:
it('should login using third party idp', () => {
cy.disableSameSiteCookieRestrictions();
//add test body here
});
or alteratively, run it before each test:
beforeEach(() => cy.disableSameSiteCookieRestrictions());
We were encountering a similar issue, where Cypress was redirecting us to the default "You are not logged in" page after getting through the login process. I'm not certain if that's EXACTLY the issue you were experiencing, but just in case, here's our solution. In our case, the issue was caused by Chrome's "Same Site Cookies" feature interacting poorly with Cypress, so we needed to disable it. In your plugins/index.js file, you would add the following code:
module.exports = (on, config) => {
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome') {
launchOptions.args.push('--disable-features=SameSiteByDefaultCookies');
}
return launchOptions;
});
};
Note that if you already have launchOptions being set, you can just add this code onto it so it doesn't clash at all.
Hopefully, this works for you as well!
In the current version of cypress you can't go to another domain in the same test. This is due to the fact that cypress injects its test into the browser (they are working on this issue).
So one solution today is that you need to utilize cy.request to perform the login programmatically and inject the auth secret (jwt, cookie, localstorage, token or what you have) into the browser context yourself (for cookie this would be cy.setcookie).
Always make sure to checkout the plugins if there is already an abstraction for your login. Often this is openId or ntlm.
I just entered the world of testing with puppeteer and jest, and I was wondering what the best practice was in terms of folder architecture and logic.
I've never done testing before and I think I'm getting a little lost in the different principles and concepts and how it all fits together.
I learned to do my tests based on the page-object model, so I have classes for each of my pages, but also for each of my modules ( or components ). For example, in my application, the header or the login modal are components.
Then I have a test file per page or per component.
(for example the landingPage.tests.js file, which uses the model of the LandingPage class in the LandingPage.js file)
Here is a concrete example:
I have different login cases and I'd like to test them all. For example I want to test to connect with a "normal" user, for which the process is simply login then password. Then I need to test with a user who has activated 2FA, or with a user from a company that uses SSO.
I first thought about putting my different tests in authentication.tests.js, in different describe blocks, thinking it would open a new tab each time, but it doesn't... I use puppeteer in incognito mode to make sure each tab is an isolated session.
So my questions are:
Where is the best place to do these test suites?
Am I supposed to have test files that "describe" the pages ( for example, the button must be present, such text must be here etc) and also have "scenario type" test file ( a set of contextual actions to a user, like for my different login cases) ?
Here is authentication.tests.js, in which I would like to tests all my different ways of logging in :
import HeaderComponent from "../../../pages/components/HeaderComponent";
import AuthenticationComponent from "../../../pages/components/AuthenticationComponent";
import LandingPage from "../../../pages/landing/LandingPage";
import {
JEST_TIMEOUT,
CREDENTIALS
} from "../../../config";
describe('Component:Authentication', () => {
let headerComponent;
let authenticationComponent;
let landingPage;
beforeAll(async () => {
jest.setTimeout(JEST_TIMEOUT);
headerComponent = new HeaderComponent;
authenticationComponent = new AuthenticationComponent;
landingPage = new LandingPage;
});
describe('Normal login ', () => {
it('should click on login and open modal', async () => {
await landingPage.visit();
await headerComponent.isVisible();
await headerComponent.clickOnLogin();
await authenticationComponent.isVisible();
});
it('should type a normal user email adress and validate', async () => {
await authenticationComponent.typeUsername(CREDENTIALS.normal.username);
await authenticationComponent.clickNext();
});
it('should type the correct password and validate', async () => {
await authenticationComponent.typePassword(CREDENTIALS.normal.password);
await authenticationComponent.clickNext();
});
it('should be logged in', async () => {
await waitForText(page, 'body', 'Success !');
});
});
describe('SSO login ', () => {
// todo ...
});
});
Thank you and sorry if it sounds confusing, like I said I'm trying to figure out how it all fits together.
Regarding the folder structure, Jest will find any files according to the match config, basically anything called *.spec.js or *.test.js. Looks like you know that already.
What that means is the folder structure is completely up to you. Some people like to have the tests for components in the same folders as the components themselves. Personally I prefer to have all the tests in one folder as it makes the project look cleaner.
The other benefit of having all the tests in one folder is that you can then start to distinguish between the types of tests. Component tests check that pure components render and operate as expected. You don't need Puppeteer for this, use snapshots if you're in a React app. Puppeteer is good for integration tests that navigate through so-called 'happy paths', login, signup, add to cart etc., using a headless Chromium browser.
To answer the specific problem you have been having with Jest / Puppeteer on a new page for each test:
//keep a reference to the browser
let browser
//keep a reference to the page
let page
// open puppeteer before all tests
beforeAll(async () => {
browser = await puppeteer.launch()
})
// close puppeteer after all tests
afterAll(async () => {
await browser.close()
})
// Get a new page for each test so that we start fresh.
beforeEach(async () => {
page = await browser.newPage()
})
// Remember to close pages after each test.
afterEach(async () => {
await page.close()
})
describe('Counter', () => {
// "it" blocks go here.
})
Hope that helps a bit.
I'm having trouble to make my tests stable with this code.
Can someone give me some directions
My app doesn't have a login page, and users might get logged in different ways, what determines if they're clogged or not is an auth cookie.
So I decided to define a role by a direct API call to the authentication API then save a cookie with the user token:
export const adminUser = Role('any-page', async t => {
const loginRequest = await fetch(
`https://my-auhtentication-api/oauth/token?grant_type=password&username=${userName}&password=${password}`,
{
method: 'POST',
}
)
const loginToken = await loginRequest.json()
await ClientFunction(() => {
document.cookie = `myAuthCookie=${loginToken.access_token}; Domain=.my-app-domain.com; Path=/`
})
})
And then I use in my code test like this:
fixture('[Admin User] Menu navigation')
.beforeEach(async t => {
await t
.useRole(adminUser)
.navigateTo(strictPage)
})
The problem is that it works fine when I run only this test, but when I run it with the rest of my stack it becomes very unstable, sometimes works, sometimes not.
For end-to-end tests, it's better if your test environment is the same as your production environment. Your comment indicates that you use a parent web app to authenticate your users in the production environment, so the best way to test authentication is using the same parent app in your test environment.