UNPKG

@revoloo/cypress6

Version:

Cypress.io end to end testing tool

405 lines (326 loc) 10.9 kB
const _ = require('lodash') const Promise = require('bluebird') const $utils = require('../../cypress/utils') const $errUtils = require('../../cypress/error_utils') const $Location = require('../../cypress/location') const COOKIE_PROPS = 'name value path secure httpOnly expiry domain sameSite'.split(' ') const commandNameRe = /(:)(\w)/ const getCommandFromEvent = (event) => { return event.replace(commandNameRe, (match, p1, p2) => { return p2.toUpperCase() }) } const mergeDefaults = function (obj) { // we always want to be able to see and influence cookies // on our superdomain const { superDomain } = $Location.create(window.location.href) // and if the user did not provide a domain // then we know to set the default to be origin const merge = (o) => { return _.defaults(o, { domain: superDomain }) } if (_.isArray(obj)) { return _.map(obj, merge) } return merge(obj) } // from https://developer.chrome.com/extensions/cookies#type-SameSiteStatus // note that `unspecified` is purposely omitted - Firefox and Chrome set // different defaults, and Firefox lacks support for `unspecified`, so // `undefined` is used in lieu of `unspecified` // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1624668 const VALID_SAMESITE_VALUES = ['no_restriction', 'lax', 'strict'] const normalizeSameSite = (sameSite) => { if (_.isUndefined(sameSite)) { return sameSite } if (_.isString(sameSite)) { sameSite = sameSite.toLowerCase() } if (sameSite === 'none') { // "None" is the value sent in the header for `no_restriction`, so allow it here for convenience sameSite = 'no_restriction' } return sameSite } // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Attributes function cookieValidatesHostPrefix (options) { return options.secure === false || (options.path && options.path !== '/') } function cookieValidatesSecurePrefix (options) { return options.secure === false } module.exports = function (Commands, Cypress, cy, state, config) { const automateCookies = function (event, obj = {}, log, timeout) { const automate = () => { return Cypress.automation(event, mergeDefaults(obj)) .catch((err) => { return $errUtils.throwErr(err, { onFail: log }) }) } if (!timeout) { return automate() } // need to remove the current timeout // because we're handling timeouts ourselves cy.clearTimeout(event) return automate() .timeout(timeout) .catch(Promise.TimeoutError, (err) => { return $errUtils.throwErrByPath('cookies.timed_out', { onFail: log, args: { cmd: getCommandFromEvent(event), timeout, }, }) }) } const getAndClear = (log, timeout, options = {}) => { return automateCookies('get:cookies', options, log, timeout) .then((resp) => { // bail early if we got no cookies! if (resp && (resp.length === 0)) { return resp } // iterate over all of these and ensure none are allowed // or preserved const cookies = Cypress.Cookies.getClearableCookies(resp) return automateCookies('clear:cookies', cookies, log, timeout) }) } const handleBackendError = (command, action, onFail) => { return (err) => { if (!_.includes(err.stack, err.message)) { err.stack = `${err.message}\n${err.stack}` } if (err.name === 'CypressError') { throw err } $errUtils.throwErrByPath('cookies.backend_error', { args: { action, cmd: command, browserDisplayName: Cypress.browser.displayName, error: err, }, onFail, errProps: { appendToStack: { title: 'From Node.js Internals', content: err.stack, }, }, }) } } // TODO: handle failure here somehow // maybe by tapping into the Cypress reset // stuff, or handling this in the runner itself? Cypress.on('test:before:run:async', () => { return getAndClear() }) return Commands.addAll({ getCookie (name, options = {}) { const userOptions = options options = _.defaults({}, userOptions, { log: true, timeout: config('responseTimeout'), }) if (options.log) { options._log = Cypress.log({ message: name, timeout: options.timeout, consoleProps () { let c const obj = {} c = options.cookie if (c) { obj['Yielded'] = c } else { obj['Yielded'] = 'null' obj['Note'] = `No cookie with the name: '${name}' was found.` } return obj }, }) } const onFail = options._log if (!_.isString(name)) { $errUtils.throwErrByPath('getCookie.invalid_argument', { onFail }) } return automateCookies('get:cookie', { name }, options._log, options.timeout) .then((resp) => { options.cookie = resp return resp }) .catch(handleBackendError('getCookie', 'reading the requested cookie from', onFail)) }, getCookies (options = {}) { const userOptions = options options = _.defaults({}, userOptions, { log: true, timeout: config('responseTimeout'), }) if (options.log) { options._log = Cypress.log({ message: '', timeout: options.timeout, consoleProps () { let c const obj = {} c = options.cookies if (c) { obj['Yielded'] = c obj['Num Cookies'] = c.length } return obj }, }) } return automateCookies('get:cookies', _.pick(options, 'domain'), options._log, options.timeout) .then((resp) => { options.cookies = resp return resp }) .catch(handleBackendError('getCookies', 'reading cookies from', options._log)) }, setCookie (name, value, options = {}) { const userOptions = options options = _.defaults({}, userOptions, { name, value, path: '/', secure: false, httpOnly: false, log: true, expiry: $utils.addTwentyYears(), timeout: config('responseTimeout'), }) const cookie = _.pick(options, COOKIE_PROPS) if (options.log) { options._log = Cypress.log({ message: [name, value], timeout: options.timeout, consoleProps () { let c const obj = {} c = options.cookie if (c) { obj['Yielded'] = c } return obj }, }) } const onFail = options._log cookie.sameSite = normalizeSameSite(cookie.sameSite) if (!_.isUndefined(cookie.sameSite) && !VALID_SAMESITE_VALUES.includes(cookie.sameSite)) { $errUtils.throwErrByPath('setCookie.invalid_samesite', { onFail, args: { value: options.sameSite, // for clarity, throw the error with the user's unnormalized option validValues: VALID_SAMESITE_VALUES, }, }) } // cookies with SameSite=None must also set Secure // @see https://web.dev/samesite-cookies-explained/#changes-to-the-default-behavior-without-samesite if (cookie.sameSite === 'no_restriction' && cookie.secure !== true) { $errUtils.throwErrByPath('setCookie.secure_not_set_with_samesite_none', { onFail, args: { value: options.sameSite, // for clarity, throw the error with the user's unnormalized option }, }) } if (!_.isString(name) || !_.isString(value)) { $errUtils.throwErrByPath('setCookie.invalid_arguments', { onFail }) } if (options.name.startsWith('__Secure-') && cookieValidatesSecurePrefix(options)) { $errUtils.throwErrByPath('setCookie.secure_prefix', { onFail }) } if (options.name.startsWith('__Host-') && cookieValidatesHostPrefix(options)) { $errUtils.throwErrByPath('setCookie.host_prefix', { onFail }) } return automateCookies('set:cookie', cookie, options._log, options.timeout) .then((resp) => { options.cookie = resp return resp }).catch(handleBackendError('setCookie', 'setting the requested cookie in', onFail)) }, clearCookie (name, options = {}) { const userOptions = options options = _.defaults({}, userOptions, { log: true, timeout: config('responseTimeout'), }) if (options.log) { options._log = Cypress.log({ message: name, timeout: options.timeout, consoleProps () { let c const obj = {} obj['Yielded'] = 'null' c = options.cookie if (c) { obj['Cleared Cookie'] = c } else { obj['Note'] = `No cookie with the name: '${name}' was found or removed.` } return obj }, }) } const onFail = options._log if (!_.isString(name)) { $errUtils.throwErrByPath('clearCookie.invalid_argument', { onFail }) } // TODO: prevent clearing a cypress namespace return automateCookies('clear:cookie', { name }, options._log, options.timeout) .then((resp) => { options.cookie = resp // null out the current subject return null }) .catch(handleBackendError('clearCookie', 'clearing the requested cookie in', onFail)) }, clearCookies (options = {}) { const userOptions = options options = _.defaults({}, userOptions, { log: true, timeout: config('responseTimeout'), }) if (options.log) { options._log = Cypress.log({ message: '', timeout: options.timeout, consoleProps () { let c const obj = {} obj['Yielded'] = 'null' if ((c = options.cookies) && c.length) { obj['Cleared Cookies'] = c obj['Num Cookies'] = c.length } else { obj['Note'] = 'No cookies were found or removed.' } return obj }, }) } return getAndClear(options._log, options.timeout, { domain: options.domain }) .then((resp) => { options.cookies = resp // null out the current subject return null }).catch((err) => { // make sure we always say to clearCookies err.message = err.message.replace('getCookies', 'clearCookies') throw err }) .catch(handleBackendError('clearCookies', 'clearing cookies in', options._log)) }, }) }