UNPKG

codeceptjs

Version:

Supercharged End 2 End Testing Framework for NodeJS

436 lines (403 loc) 12.5 kB
const fs = require('fs') const path = require('path') const { fileExists } = require('../utils') const CommentStep = require('../step/comment') const Section = require('../step/section') const container = require('../container') const store = require('../store') const event = require('../event') const recorder = require('../recorder') const { debug } = require('../output') const { isAsyncFunction } = require('../utils') const defaultUser = { fetch: I => I.grabCookie(), check: () => {}, restore: (I, cookies) => { I.amOnPage('/') // open a page I.setCookie(cookies) }, } const defaultConfig = { saveToFile: false, inject: 'login', } /** * Logs user in for the first test and reuses session for next tests. * Works by saving cookies into memory or file. * If a session expires automatically logs in again. * * > For better development experience cookies can be saved into file, so a session can be reused while writing tests. * * #### Usage * * 1. Enable this plugin and configure as described below * 2. Define user session names (example: `user`, `editor`, `admin`, etc). * 3. Define how users are logged in and how to check that user is logged in * 4. Use `login` object inside your tests to log in: * * ```js * // inside a test file * // use login to inject auto-login function * Feature('Login'); * * Before(({ login }) => { * login('user'); // login using user session * }); * * // Alternatively log in for one scenario. * Scenario('log me in', ( { I, login } ) => { * login('admin'); * I.see('I am logged in'); * }); * ``` * * #### Configuration * * * `saveToFile` (default: false) - save cookies to file. Allows to reuse session between execution. * * `inject` (default: `login`) - name of the login function to use * * `users` - an array containing different session names and functions to: * * `login` - sign in into the system * * `check` - check that user is logged in * * `fetch` - to get current cookies (by default `I.grabCookie()`) * * `restore` - to set cookies (by default `I.amOnPage('/'); I.setCookie(cookie)`) * * #### How It Works * * 1. `restore` method is executed. It should open a page and set credentials. * 2. `check` method is executed. It should reload a page (so cookies are applied) and check that this page belongs to logged-in user. When you pass the second args `session`, you could perform the validation using passed session. * 3. If `restore` and `check` were not successful, `login` is executed * 4. `login` should fill in login form * 5. After successful login, `fetch` is executed to save cookies into memory or file. * * #### Example: Simple login * * ```js * auth: { * enabled: true, * saveToFile: true, * inject: 'login', * users: { * admin: { * // loginAdmin function is defined in `steps_file.js` * login: (I) => I.loginAdmin(), * // if we see `Admin` on page, we assume we are logged in * check: (I) => { * I.amOnPage('/'); * I.see('Admin'); * } * } * } * } * ``` * * #### Example: Multiple users * * ```js * auth: { * enabled: true, * saveToFile: true, * inject: 'loginAs', // use `loginAs` instead of login * users: { * user: { * login: (I) => { * I.amOnPage('/login'); * I.fillField('email', 'user@site.com'); * I.fillField('password', '123456'); * I.click('Login'); * }, * check: (I) => { * I.amOnPage('/'); * I.see('User', '.navbar'); * }, * }, * admin: { * login: (I) => { * I.amOnPage('/login'); * I.fillField('email', 'admin@site.com'); * I.fillField('password', '123456'); * I.click('Login'); * }, * check: (I) => { * I.amOnPage('/'); * I.see('Admin', '.navbar'); * }, * }, * } * } * ``` * * #### Example: Keep cookies between tests * * If you decide to keep cookies between tests you don't need to save/retrieve cookies between tests. * But you need to login once work until session expires. * For this case, disable `fetch` and `restore` methods. * * ```js * helpers: { * WebDriver: { * // config goes here * keepCookies: true; // keep cookies for all tests * } * }, * plugins: { * auth: { * users: { * admin: { * login: (I) => { * I.amOnPage('/login'); * I.fillField('email', 'admin@site.com'); * I.fillField('password', '123456'); * I.click('Login'); * }, * check: (I) => { * I.amOnPage('/dashboard'); * I.see('Admin', '.navbar'); * }, * fetch: () => {}, // empty function * restore: () => {}, // empty funciton * } * } * } * } * ``` * * #### Example: Getting sessions from local storage * * If your session is stored in local storage instead of cookies you still can obtain sessions. * * ```js * plugins: { * auth: { * admin: { * login: (I) => I.loginAsAdmin(), * check: (I) => I.see('Admin', '.navbar'), * fetch: (I) => { * return I.executeScript(() => localStorage.getItem('session_id')); * }, * restore: (I, session) => { * I.amOnPage('/'); * I.executeScript((session) => localStorage.setItem('session_id', session), session); * }, * } * } * } * ``` * * #### Tips: Using async function in the auth * * If you use async functions in the auth plugin, login function should be used with `await` keyword. * * ```js * auth: { * enabled: true, * saveToFile: true, * inject: 'login', * users: { * admin: { * login: async (I) => { // If you use async function in the auth plugin * const phrase = await I.grabTextFrom('#phrase') * I.fillField('username', 'admin'), * I.fillField('password', 'password') * I.fillField('phrase', phrase) * }, * check: (I) => { * I.amOnPage('/'); * I.see('Admin'); * }, * } * } * } * ``` * * ```js * Scenario('login', async ( {I, login} ) => { * await login('admin') // you should use `await` * }) * ``` * * #### Tips: Using session to validate user * * Instead of asserting on page elements for the current user in `check`, you can use the `session` you saved in `fetch` * * ```js * auth: { * enabled: true, * saveToFile: true, * inject: 'login', * users: { * admin: { * login: async (I) => { // If you use async function in the auth plugin * const phrase = await I.grabTextFrom('#phrase') * I.fillField('username', 'admin'), * I.fillField('password', 'password') * I.fillField('phrase', phrase) * }, * check: (I, session) => { * // Throwing an error in `check` will make CodeceptJS perform the login step for the user * if (session.profile.email !== the.email.you.expect@some-mail.com) { * throw new Error ('Wrong user signed in'); * } * }, * } * } * } * ``` * * ```js * Scenario('login', async ( {I, login} ) => { * await login('admin') // you should use `await` * }) * * */ module.exports = function (config) { config = Object.assign(defaultConfig, config) Object.keys(config.users).map( u => (config.users[u] = { ...defaultUser, ...config.users[u], }), ) if (config.saveToFile) { // loading from file loadCookiesFromFile(config) } const loginFunction = async name => { const I = container.support('I') const userSession = config.users[name] if (!userSession) { throw new Error(`User '${name}' was not configured for authorization in auth plugin. Add it to the plugin config`) } const test = store.currentTest // we are in BeforeSuite hook if (!test) { enableAuthBeforeEachTest(name) return } const section = new Section(`I am logged in as ${name}`) if (config.saveToFile && !store[`${name}_session`]) { loadCookiesFromFile(config) } if (isPlaywrightSession() && test?.opts?.cookies) { if (test.opts.user == name) { debug(`Cookies already loaded for ${name}`) alreadyLoggedIn(name) return } else { debug(`Cookies already loaded for ${test.opts.user}, but not for ${name}`) await I.deleteCookie() } } section.start() const cookies = store[`${name}_session`] const shouldAwait = isAsyncFunction(userSession.login) || isAsyncFunction(userSession.restore) || isAsyncFunction(userSession.check) const loginAndSave = async () => { if (shouldAwait) { await userSession.login(I) } else { userSession.login(I) } section.end() const cookies = await userSession.fetch(I) if (!cookies) { debug("Cannot save user session with empty cookies from auto login's fetch method") return } if (config.saveToFile) { debug(`Saved user session into file for ${name}`) fs.writeFileSync(path.join(global.output_dir, `${name}_session.json`), JSON.stringify(cookies)) } store[`${name}_session`] = cookies } if (!cookies) return loginAndSave() recorder.session.start('check login') if (shouldAwait) { await userSession.restore(I, cookies) await userSession.check(I, cookies) } else { userSession.restore(I, cookies) userSession.check(I, cookies) } section.end() recorder.session.catch(err => { debug(`Failed auto login for ${name} due to ${err}`) debug('Logging in again') recorder.session.start('auto login') return loginAndSave() .then(() => { recorder.add(() => recorder.session.restore('auto login')) recorder.catch(() => debug('continue')) }) .catch(err => { recorder.session.restore('auto login') recorder.session.restore('check login') section.end() recorder.throw(err) }) }) recorder.add(() => { recorder.session.restore('check login') }) return recorder.promise() } function enableAuthBeforeEachTest(name) { const suite = store.currentSuite if (!suite) return debug(`enabling auth as ${name} for each test of suite ${suite.title}`) // we are setting test opts so they can be picked up by Playwright if it starts browser for this test suite.eachTest(test => { // preload from store if (store[`${name}_session`]) { test.opts.cookies = store[`${name}_session`] test.opts.user = name return } if (!config.saveToFile) return const cookieFile = path.join(global.output_dir, `${name}_session.json`) if (!fileExists(cookieFile)) { return } const context = fs.readFileSync(cookieFile).toString() test.opts.cookies = JSON.parse(context) test.opts.user = name }) function runLoginFunctionForTest(test) { if (!suite.tests.includes(test)) return // let's call this function to ensure that authorization happened // if no cookies, it will login and save them loginFunction(name) } // we are in BeforeSuite hook event.dispatcher.on(event.test.started, runLoginFunctionForTest) event.dispatcher.on(event.suite.after, () => { event.dispatcher.off(event.test.started, runLoginFunctionForTest) }) } // adding this to DI container const support = {} support[config.inject] = loginFunction container.append({ support }) return loginFunction } function loadCookiesFromFile(config) { for (const name in config.users) { const fileName = path.join(global.output_dir, `${name}_session.json`) if (!fileExists(fileName)) continue const data = fs.readFileSync(fileName).toString() try { store[`${name}_session`] = JSON.parse(data) } catch (err) { throw new Error(`Could not load session from ${fileName}\n${err}`) } debug(`Loaded user session for ${name}`) } } function isPlaywrightSession() { return !!container.helpers('Playwright') } function alreadyLoggedIn(name) { const step = new CommentStep('am logged in as') step.actor = 'I' return step.addToRecorder([name]) }