feat: Add cy.getAllCookies() and cy.clearAllCookies() (#25012)

This commit is contained in:
Chris Breiding
2022-12-08 13:12:32 -05:00
committed by GitHub
parent b32a8afe51
commit fc43cecdad
8 changed files with 858 additions and 81 deletions
+43 -9
View File
@@ -896,21 +896,41 @@ declare namespace Cypress {
clear(options?: Partial<ClearOptions>): Chainable<Subject>
/**
* Clear a specific browser cookie for the current hostname or for the domain specified.
* Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldn't need to use this command unless you're using it to clear a specific cookie inside a single test.
* Clear a specific browser cookie for a domain.
*
* Cypress automatically clears all cookies _before_ each test to prevent
* state from being shared across tests when test isolation is enabled.
* You shouldn't need to use this command unless you're using it to clear
* a specific cookie inside a single test or test isolation is disabled.
*
* @see https://on.cypress.io/clearcookie
*/
clearCookie(name: string, options?: CookieOptions): Chainable<null>
/**
* Clear all browser cookies for the current hostname or for the domain specified.
* Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldn't need to use this command unless you're using it to clear all cookies or specific cookies inside a single test.
* Clear browser cookies for a domain.
*
* Cypress automatically clears all cookies _before_ each test to prevent
* state from being shared across tests when test isolation is enabled.
* You shouldn't need to use this command unless you're using it to clear
* specific cookies inside a single test or test isolation is disabled.
*
* @see https://on.cypress.io/clearcookies
*/
clearCookies(options?: CookieOptions): Chainable<null>
/**
* Clear all browser cookies.
*
* Cypress automatically clears all cookies _before_ each test to prevent
* state from being shared across tests when test isolation is enabled.
* You shouldn't need to use this command unless you're using it to clear
* all cookie inside a single test or test isolation is disabled.
*
* @see https://on.cypress.io/clearallcookies
*/
clearAllCookies(options?: Partial<Loggable & Timeoutable>): Chainable<null>
/**
* Get local storage for all origins.
*
@@ -921,6 +941,11 @@ declare namespace Cypress {
/**
* Clear local storage for all origins.
*
* Cypress automatically clears all local storage _before_ each test to
* prevent state from being shared across tests when test isolation
* is enabled. You shouldn't need to use this command unless you're using it
* to clear localStorage inside a single test or test isolation is disabled.
*
* @see https://on.cypress.io/clearalllocalstorage
*/
clearAllLocalStorage(options?: Partial<Loggable>): Chainable<null>
@@ -941,9 +966,11 @@ declare namespace Cypress {
/**
* Clear data in local storage for the current origin.
* Cypress automatically runs this command before each test to prevent state from being
* shared across tests. You shouldn't need to use this command unless you're using it
* to clear localStorage inside a single test. Yields `localStorage` object.
*
* Cypress automatically clears all local storage _before_ each test to
* prevent state from being shared across tests when test isolation
* is enabled. You shouldn't need to use this command unless you're using it
* to clear localStorage inside a single test or test isolation is disabled.
*
* @see https://on.cypress.io/clearlocalstorage
* @param {string} [key] - name of a particular item to remove (optional).
@@ -1395,19 +1422,26 @@ declare namespace Cypress {
get<S = any>(alias: string, options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<S>
/**
* Get a browser cookie by its name for the current hostname or for the domain specified.
* Get a browser cookie by its name.
*
* @see https://on.cypress.io/getcookie
*/
getCookie(name: string, options?: CookieOptions): Chainable<Cookie | null>
/**
* Get all of the browser cookies for the current hostname or for the domain specified.
* Get browser cookies for a domain.
*
* @see https://on.cypress.io/getcookies
*/
getCookies(options?: CookieOptions): Chainable<Cookie[]>
/**
* Get all browser cookies.
*
* @see https://on.cypress.io/getallcookies
*/
getAllCookies(options?: Partial<Loggable & Timeoutable>): Chainable<Cookie[]>
/**
* Navigate back or forward to the previous or next URL in the browser's history.
*
+26
View File
@@ -1020,6 +1020,19 @@ namespace CypressGetCookiesTests {
cy.getCookies({ domain: false }) // $ExpectError
}
namespace CypressGetAllCookiesTests {
cy.getAllCookies().then((cookies) => {
cookies // $ExpectType Cookie[]
})
cy.getAllCookies({ log: true })
cy.getAllCookies({ timeout: 10 })
cy.getAllCookies({ log: true, timeout: 10 })
cy.getAllCookies({ log: 'true' }) // $ExpectError
cy.getAllCookies({ timeout: '10' }) // $ExpectError
cy.getAllCookies({ other: true }) // $ExpectError
}
namespace CypressGetCookieTests {
cy.getCookie('name').then((cookie) => {
cookie // $ExpectType Cookie | null
@@ -1085,6 +1098,19 @@ namespace CypressClearCookiesTests {
cy.clearCookies({ domain: false }) // $ExpectError
}
namespace CypressClearAllCookiesTests {
cy.clearAllCookies().then((cookies) => {
cookies // $ExpectType null
})
cy.clearAllCookies({ log: true })
cy.clearAllCookies({ timeout: 10 })
cy.clearAllCookies({ log: true, timeout: 10 })
cy.clearAllCookies({ log: 'true' }) // $ExpectError
cy.clearAllCookies({ timeout: '10' }) // $ExpectError
cy.clearAllCookies({ other: true }) // $ExpectError
}
namespace CypressLocalStorageTests {
cy.getAllLocalStorage().then((result) => {
result // $ExpectType StorageByOrigin
@@ -130,6 +130,54 @@ describe('src/cy/commands/cookies - no stub', () => {
})
})
context('#getAllCookies', () => {
it('returns cookies from all domains', () => {
cy.visit('http://barbaz.com:3500/fixtures/generic.html')
setCookies()
cy.getAllCookies().then((cookies) => {
expect(cookies).to.have.length(8)
const sortedCookies = Cypress._.sortBy(cookies, 'name').map((cookie) => `${cookie.name}=${cookie.value}`)
expect(sortedCookies).to.deep.equal([
'key1=value1',
'key2=value2',
'key3=value3',
'key4=value4',
'key5=value5',
'key6=value6',
'key7=value7',
'key8=value8',
])
})
// webkit does not support cy.origin()
if (isWebkit) return
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://foobar.com:3500/fixtures/generic.html')
cy.getAllCookies().then((cookies) => {
expect(cookies).to.have.length(8)
const sortedCookies = Cypress._.sortBy(cookies, 'name').map((cookie) => `${cookie.name}=${cookie.value}`)
expect(sortedCookies).to.deep.equal([
'key1=value1',
'key2=value2',
'key3=value3',
'key4=value4',
'key5=value5',
'key6=value6',
'key7=value7',
'key8=value8',
])
})
})
})
})
context('#getCookie', () => {
const setCookies = () => {
cy.log('set cookies')
@@ -378,6 +426,36 @@ describe('src/cy/commands/cookies - no stub', () => {
})
})
context('#clearAllCookies', () => {
it('clears cookies from all domains', () => {
cy.visit('http://barbaz.com:3500/fixtures/generic.html')
setCookies()
cy.clearAllCookies()
cy.getAllCookies().its('length').should('equal', 0)
// webkit does not support cy.origin()
if (isWebkit) return
cy.origin('http://foobar.com:3500', () => {
cy.visit('http://foobar.com:3500/fixtures/generic.html')
// put back cookies removed above
cy.setCookie('key1', 'value1', { domain: 'www.foobar.com', log: false })
cy.setCookie('key2', 'value2', { domain: 'foobar.com', log: false })
cy.setCookie('key3', 'value3', { domain: 'www.barbaz.com', log: false })
cy.setCookie('key4', 'value4', { domain: '.www.barbaz.com', log: false })
cy.setCookie('key5', 'value5', { domain: 'barbaz.com', log: false })
cy.setCookie('key6', 'value6', { domain: '.barbaz.com', log: false })
cy.setCookie('key7', 'value7', { domain: 'www2.barbaz.com', log: false })
cy.setCookie('key8', 'value8', { domain: 'www2.foobar.com', log: false })
cy.clearAllCookies()
cy.getAllCookies().its('length').should('equal', 0)
})
})
})
context('#clearCookie', () => {
const setCookies = () => {
cy.log('set cookies')
@@ -717,6 +795,159 @@ describe('src/cy/commands/cookies', () => {
})
})
context('#getAllCookies', () => {
it('returns array of cookies', () => {
Cypress.automation.withArgs('get:cookies').resolves([])
cy.getAllCookies().should('deep.eq', []).then(() => {
expect(Cypress.automation).to.be.calledWith('get:cookies')
})
})
describe('timeout', () => {
it('sets timeout to Cypress.config(responseTimeout)', { responseTimeout: 2500 }, () => {
Cypress.automation.resolves([])
const timeout = cy.spy(Promise.prototype, 'timeout')
cy.getAllCookies().then(() => {
expect(timeout).to.be.calledWith(2500)
})
})
it('can override timeout', () => {
Cypress.automation.resolves([])
const timeout = cy.spy(Promise.prototype, 'timeout')
cy.getAllCookies({ timeout: 1000 }).then(() => {
expect(timeout).to.be.calledWith(1000)
})
})
it('clears the current timeout and restores after success', () => {
Cypress.automation.resolves([])
cy.timeout(100)
cy.spy(cy, 'clearTimeout')
cy.getAllCookies().then(() => {
expect(cy.clearTimeout).to.be.calledWith('get:cookies')
// restores the timeout afterwards
expect(cy.timeout()).to.eq(100)
})
})
})
describe('errors', { defaultCommandTimeout: 50 }, () => {
beforeEach(function () {
this.logs = []
cy.on('log:added', (attrs, log) => {
if (attrs.name === 'getAllCookies') {
this.lastLog = log
this.logs.push(log)
}
})
return null
})
it('logs once on error', function (done) {
const error = new Error('some err message')
error.name = 'foo'
error.stack = 'stack'
Cypress.automation.rejects(error)
cy.on('fail', () => {
const { lastLog } = this
assertLogLength(this.logs, 1)
expect(lastLog.get('error').message).to.contain(`\`cy.getAllCookies()\` had an unexpected error reading cookies from ${Cypress.browser.displayName}.`)
expect(lastLog.get('error').message).to.contain('some err message')
done()
})
cy.getAllCookies()
})
it('throws after timing out', function (done) {
Cypress.automation.resolves(Promise.delay(1000))
cy.on('fail', (err) => {
const { lastLog } = this
assertLogLength(this.logs, 1)
expect(lastLog.get('error')).to.eq(err)
expect(lastLog.get('state')).to.eq('failed')
expect(lastLog.get('name')).to.eq('getAllCookies')
expect(lastLog.get('message')).to.eq('')
expect(err.message).to.eq('`cy.getAllCookies()` timed out waiting `50ms` to complete.')
expect(err.docsUrl).to.eq('https://on.cypress.io/getallcookies')
done()
})
cy.getAllCookies({ timeout: 50 })
})
})
describe('.log', () => {
beforeEach(function () {
cy.on('log:added', (attrs, log) => {
if (attrs.name === 'getCookies') {
this.lastLog = log
}
})
Cypress.automation
.withArgs('get:cookies', { domain: 'localhost' })
.resolves([
{ name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false, hostOnly: false },
])
})
it('can turn off logging', () => {
cy.getCookies({ log: false }).then(function () {
expect(this.lastLog).to.be.undefined
})
})
it('ends immediately', () => {
cy.getCookies().then(function () {
const { lastLog } = this
expect(lastLog.get('ended')).to.be.true
expect(lastLog.get('state')).to.eq('passed')
})
})
it('snapshots immediately', () => {
cy.getCookies().then(function () {
const { lastLog } = this
expect(lastLog.get('snapshots').length).to.eq(1)
expect(lastLog.get('snapshots')[0]).to.be.an('object')
})
})
it('#consoleProps', () => {
cy.getCookies().then(function (cookies) {
expect(cookies).to.deep.eq([{ name: 'foo', value: 'bar', domain: 'localhost', path: '/', secure: true, httpOnly: false }])
const c = this.lastLog.invoke('consoleProps')
expect(c['Yielded']).to.deep.eq(cookies)
expect(c['Num Cookies']).to.eq(1)
})
})
})
})
context('#getCookie', () => {
it('returns single cookie by name', () => {
Cypress.automation.withArgs('get:cookie').resolves({
@@ -1495,9 +1726,9 @@ describe('src/cy/commands/cookies', () => {
Cypress.automation
.withArgs('get:cookies')
.resolves([
{ name: 'foo' },
{ name: 'bar' },
{ name: 'baz' },
{ name: 'foo', domain: 'localhost' },
{ name: 'bar', domain: 'localhost' },
{ name: 'baz', domain: 'localhost' },
])
.withArgs('clear:cookies', [
{ name: 'foo', domain: 'localhost' },
@@ -1676,7 +1907,7 @@ describe('src/cy/commands/cookies', () => {
Cypress.automation
.withArgs('get:cookies', { domain: 'localhost' })
.resolves([{ name: 'foo' }])
.resolves([{ name: 'foo', domain: 'localhost' }])
.withArgs('clear:cookies', [{ name: 'foo', domain: 'localhost' }])
.resolves([
{ name: 'foo' },
@@ -1754,7 +1985,7 @@ describe('src/cy/commands/cookies', () => {
Cypress.automation
.withArgs('get:cookies', { domain: 'localhost' })
.resolves([{ name: 'foo' }])
.resolves([{ name: 'foo', domain: 'localhost' }])
.withArgs('clear:cookies', [{ name: 'foo', domain: 'localhost' }])
.resolves([])
})
@@ -1771,4 +2002,286 @@ describe('src/cy/commands/cookies', () => {
})
})
})
context('#clearAllCookies', () => {
it('returns null', () => {
Cypress.automation.withArgs('get:cookies').resolves([])
cy.clearAllCookies().should('be.null')
})
it('does not call \'clear:cookies\' when no cookies were returned', () => {
Cypress.automation.withArgs('get:cookies').resolves([])
cy.clearAllCookies().then(() => {
expect(Cypress.automation).not.to.be.calledWith(
'clear:cookies',
)
})
})
it('calls \'clear:cookies\' with all cookies', () => {
Cypress.automation
.withArgs('get:cookies')
.resolves([
{ name: 'foo', domain: 'localhost' },
{ name: 'bar', domain: 'bar.com' },
{ name: 'qux', domain: 'qux.com' },
])
.withArgs('clear:cookies', [
{ name: 'foo', domain: 'localhost' },
{ name: 'bar', domain: 'bar.com' },
{ name: 'qux', domain: 'qux.com' },
])
.resolves([
{ name: 'foo', domain: 'localhost' },
{ name: 'bar', domain: 'bar.com' },
{ name: 'qux', domain: 'qux.com' },
])
cy
.clearAllCookies().should('be.null').then(() => {
expect(Cypress.automation).to.be.calledWith(
'clear:cookies', [
{ name: 'foo', domain: 'localhost' },
{ name: 'bar', domain: 'bar.com' },
{ name: 'qux', domain: 'qux.com' },
],
)
})
})
describe('timeout', () => {
beforeEach(() => {
Cypress.automation
.withArgs('get:cookies')
.resolves([{}])
.withArgs('clear:cookies')
.resolves({})
})
it('sets timeout to Cypress.config(responseTimeout)', {
responseTimeout: 2500,
}, () => {
Cypress.automation.resolves([])
const timeout = cy.spy(Promise.prototype, 'timeout')
cy.clearAllCookies().then(() => {
expect(timeout).to.be.calledWith(2500)
})
})
it('can override timeout', () => {
Cypress.automation.resolves([])
const timeout = cy.spy(Promise.prototype, 'timeout')
cy.clearAllCookies({ timeout: 1000 }).then(() => {
expect(timeout).to.be.calledWith(1000)
})
})
it('clears the current timeout and restores after success', () => {
cy.timeout(100)
cy.spy(cy, 'clearTimeout')
cy.clearAllCookies().then(() => {
expect(cy.clearTimeout).to.be.calledWith('get:cookies')
expect(cy.clearTimeout).to.be.calledWith('clear:cookies')
// restores the timeout afterwards
expect(cy.timeout()).to.eq(100)
})
})
})
describe('errors', { defaultCommandTimeout: 100 }, () => {
beforeEach(function () {
this.logs = []
cy.on('log:added', (attrs, log) => {
if (attrs.name === 'clearAllCookies') {
this.lastLog = log
this.logs.push(log)
}
})
return null
})
it('logs once on \'get:cookies\' error', function (done) {
const error = new Error('some err message')
error.name = 'foo'
error.stack = 'some err message\n at fn (foo.js:1:1)'
Cypress.automation.rejects(error)
cy.on('fail', (err) => {
const { lastLog } = this
assertLogLength(this.logs, 1)
expect(lastLog.get('error').message).to.contain(`\`cy.clearAllCookies()\` had an unexpected error clearing cookies in ${Cypress.browser.displayName}.`)
expect(lastLog.get('error').message).to.contain('some err message')
expect(lastLog.get('error')).to.eq(err)
done()
})
cy.clearAllCookies()
})
it('throws after timing out', function (done) {
Cypress.automation.resolves([{ name: 'foo' }])
Cypress.automation.withArgs('clear:cookies').resolves(Promise.delay(1000))
cy.on('fail', (err) => {
const { lastLog } = this
assertLogLength(this.logs, 1)
expect(lastLog.get('error')).to.eq(err)
expect(lastLog.get('state')).to.eq('failed')
expect(lastLog.get('name')).to.eq('clearAllCookies')
expect(lastLog.get('message')).to.eq('')
expect(err.message).to.eq('`cy.clearAllCookies()` timed out waiting `50ms` to complete.')
expect(err.docsUrl).to.eq('https://on.cypress.io/clearallcookies')
done()
})
cy.clearAllCookies({ timeout: 50 })
})
it('logs once on \'clear:cookies\' error', function (done) {
Cypress.automation.withArgs('get:cookies').resolves([
{ name: 'foo' }, { name: 'bar' },
])
const error = new Error('some err message')
error.name = 'foo'
error.stack = 'stack'
Cypress.automation.withArgs('clear:cookies').rejects(error)
cy.on('fail', (err) => {
const { lastLog } = this
assertLogLength(this.logs, 1)
expect(lastLog.get('error').message).to.contain(`\`cy.clearAllCookies()\` had an unexpected error clearing cookies in ${Cypress.browser.displayName}.`)
expect(lastLog.get('error').message).to.contain('some err message')
expect(lastLog.get('error')).to.eq(err)
done()
})
cy.clearAllCookies()
})
})
describe('.log', () => {
beforeEach(function () {
cy.on('log:added', (attrs, log) => {
if (attrs.name === 'clearAllCookies') {
this.lastLog = log
}
})
Cypress.automation
.withArgs('get:cookies', {})
.resolves([{ name: 'foo', domain: 'localhost' }])
.withArgs('clear:cookies', [{ name: 'foo', domain: 'localhost' }])
.resolves([
{ name: 'foo' },
])
})
it('can turn off logging', () => {
cy.clearAllCookies({ log: false }).then(function () {
expect(this.log).to.be.undefined
})
})
it('ends immediately', () => {
cy.clearAllCookies().then(function () {
const { lastLog } = this
expect(lastLog.get('ended')).to.be.true
expect(lastLog.get('state')).to.eq('passed')
})
})
it('snapshots immediately', () => {
cy.clearAllCookies().then(function () {
const { lastLog } = this
expect(lastLog.get('snapshots').length).to.eq(1)
expect(lastLog.get('snapshots')[0]).to.be.an('object')
})
})
it('#consoleProps', () => {
cy.clearAllCookies().then(function (cookies) {
expect(cookies).to.be.null
const c = this.lastLog.invoke('consoleProps')
expect(c['Yielded']).to.eq('null')
expect(c['Cleared Cookies']).to.deep.eq([{ name: 'foo' }])
expect(c['Num Cookies']).to.eq(1)
})
})
})
describe('.log with no cookies returned', () => {
beforeEach(function () {
cy.on('log:added', (attrs, log) => {
if (attrs.name === 'clearAllCookies') {
this.lastLog = log
}
})
Cypress.automation
.withArgs('get:cookies')
.resolves([])
})
it('#consoleProps', () => {
cy.clearAllCookies().then(function (cookies) {
expect(cookies).to.be.null
const c = this.lastLog.invoke('consoleProps')
expect(c['Yielded']).to.eq('null')
expect(c['Cleared Cookies']).to.be.undefined
expect(c['Note']).to.eq('No cookies were found or removed.')
})
})
})
describe('.log when no cookies were cleared', () => {
beforeEach(function () {
cy.on('log:added', (attrs, log) => {
if (attrs.name === 'clearAllCookies') {
this.lastLog = log
}
})
Cypress.automation
.withArgs('get:cookies', {})
.resolves([])
})
it('#consoleProps', () => {
cy.clearAllCookies().then(function (cookies) {
expect(cookies).to.be.null
const c = this.lastLog.invoke('consoleProps')
expect(c['Yielded']).to.eq('null')
expect(c['Cleared Cookies']).to.be.undefined
expect(c['Note']).to.eq('No cookies were found or removed.')
})
})
})
})
})
@@ -220,6 +220,7 @@ it('verifies number of cy commands', () => {
'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev',
'prevAll', 'prevUntil', 'siblings', 'wait', 'title', 'window', 'document', 'viewport', 'server', 'route', 'intercept', 'origin',
'mount', 'as', 'root', 'getAllLocalStorage', 'clearAllLocalStorage', 'getAllSessionStorage', 'clearAllSessionStorage',
'getAllCookies', 'clearAllCookies',
]
const addedCommands = Cypress._.difference(actualCommands, expectedCommands)
const removedCommands = Cypress._.difference(expectedCommands, actualCommands)
+239 -62
View File
@@ -3,15 +3,12 @@ import Promise from 'bluebird'
import $utils from '../../cypress/utils'
import $errUtils from '../../cypress/error_utils'
import type { Log } from '../../cypress/log'
// TODO: add hostOnly to COOKIE_PROPS
// https://github.com/cypress-io/cypress/issues/363
// https://github.com/cypress-io/cypress/issues/17527
const COOKIE_PROPS = 'name value path secure httpOnly expiry domain sameSite'.split(' ')
const commandNameRe = /(:)(\w)/
function pickCookieProps (cookie) {
if (!cookie) return cookie
@@ -22,12 +19,6 @@ function pickCookieProps (cookie) {
return _.pick(cookie, COOKIE_PROPS)
}
const getCommandFromEvent = (event) => {
return event.replace(commandNameRe, (match, p1, p2) => {
return p2.toUpperCase()
})
}
// 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
@@ -35,7 +26,7 @@ const getCommandFromEvent = (event) => {
// @see https://bugzilla.mozilla.org/show_bug.cgi?id=1624668
const VALID_SAMESITE_VALUES = ['no_restriction', 'lax', 'strict']
const normalizeSameSite = (sameSite) => {
function normalizeSameSite (sameSite?: string) {
if (_.isUndefined(sameSite)) {
return sameSite
}
@@ -56,12 +47,13 @@ const normalizeSameSite = (sameSite) => {
function cookieValidatesHostPrefix (options) {
return options.secure === false || (options.path && options.path !== '/')
}
function cookieValidatesSecurePrefix (options) {
return options.secure === false
}
function validateDomainOption (domain: any, commandName: string, log: Log | undefined) {
if (domain !== undefined && domain !== null && !_.isString(domain)) {
function validateDomainOption (domain: any, commandName: string, log: Cypress.Log | undefined) {
if (domain !== undefined && typeof domain !== 'string') {
$errUtils.throwErrByPath('cookies.invalid_domain', {
onFail: log,
args: {
@@ -72,33 +64,67 @@ function validateDomainOption (domain: any, commandName: string, log: Log | unde
}
}
export default function (Commands, Cypress, cy, state, config) {
const getDefaultDomain = () => {
const hostname = state('window')?.location.hostname
interface AutomationEventsAndOptions {
'get:cookie': {
domain: string
name: string
}
'get:cookies': {
domain?: string
}
'get:all:cookies': {}
'set:cookie': {
domain: string
expiry: number
httpOnly: boolean
path: string
secure: boolean
}
'clear:cookie': {
domain: string
name: string
}
'clear:cookies': Cypress.Cookie[]
}
type CommandName = 'getCookie' | 'getCookies' | 'getAllCookies' | 'setCookie' | 'clearCookie' | 'clearCookies' | 'clearAllCookies'
interface AutomateOptions<T extends keyof AutomationEventsAndOptions> {
event: T
options: AutomationEventsAndOptions[T]
commandName: CommandName
log?: Cypress.Log
timeout: number
}
interface GetAndClearOptions {
commandName: CommandName
log?: Cypress.Log
options: AutomationEventsAndOptions['get:cookies']
timeout: number
}
export default function (Commands, Cypress: InternalCypress.Cypress, cy, state, config) {
function getDefaultDomain () {
const win = state('window') as Window | undefined
const hostname = win?.location.hostname
// if hostname is undefined, the AUT is on about:blank, so use the
// spec frame's hostname instead
return hostname || window.location.hostname
}
const mergeDefaults = function (obj) {
// set the default domain to be the AUT hostname
const merge = (o) => {
return _.defaults(o, { domain: getDefaultDomain() })
}
if (_.isArray(obj)) {
return _.map(obj, merge)
}
return merge(obj)
}
const automateCookies = function (event, obj = {}, log, timeout) {
function automateCookies<T extends keyof AutomationEventsAndOptions> ({
event,
options,
commandName,
log,
timeout,
}: AutomateOptions<T>) {
const automate = () => {
return Cypress.automation(event, mergeDefaults(obj))
return Cypress.automation(event, options)
.catch((err) => {
return $errUtils.throwErr(err, { onFail: log })
$errUtils.throwErr(err, { onFail: log })
})
}
@@ -116,27 +142,39 @@ export default function (Commands, Cypress, cy, state, config) {
return $errUtils.throwErrByPath('cookies.timed_out', {
onFail: log,
args: {
cmd: getCommandFromEvent(event),
cmd: commandName,
timeout,
},
})
})
}
const getAndClear = (log?, timeout?, options = {}) => {
return automateCookies('get:cookies', options, log, timeout)
.then((resp) => {
const getAndClear = ({ commandName, log, options, timeout }: GetAndClearOptions) => {
return automateCookies({
event: 'get:cookies',
commandName,
options,
log,
timeout,
})
.then((cookies: Cypress.Cookie[]) => {
// bail early if we got no cookies!
if (resp && (resp.length === 0)) {
return resp
if (cookies && cookies.length === 0) {
return cookies
}
return automateCookies('clear:cookies', resp, log, timeout)
return automateCookies({
event: 'clear:cookies',
commandName,
options: cookies,
log,
timeout,
})
})
.then(pickCookieProps)
}
const handleBackendError = (command, action, onFail) => {
const handleBackendError = (command: CommandName, action: string, onFail?: Cypress.Log) => {
return (err) => {
if (!_.includes(err.stack, err.message)) {
err.stack = `${err.message}\n${err.stack}`
@@ -165,7 +203,7 @@ export default function (Commands, Cypress, cy, state, config) {
}
return Commands.addAll({
getCookie (name, userOptions: Cypress.CookieOptions = {}) {
getCookie (name: string, userOptions: Cypress.CookieOptions = {}) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
})
@@ -175,7 +213,7 @@ export default function (Commands, Cypress, cy, state, config) {
options.timeout = options.timeout || config('defaultCommandTimeout')
let cookie: Cypress.Cookie
let log: Log | undefined
let log: Cypress.Log | undefined
if (options.log) {
log = Cypress.log({
@@ -200,10 +238,22 @@ export default function (Commands, Cypress, cy, state, config) {
$errUtils.throwErrByPath('getCookie.invalid_argument', { onFail: log })
}
validateDomainOption(options.domain, 'getCookie', log)
validateDomainOption(userOptions.domain, 'getCookie', log)
return cy.retryIfCommandAUTOriginMismatch(() => {
return automateCookies('get:cookie', { name, domain: options.domain }, log, responseTimeout)
return automateCookies({
event: 'get:cookie',
commandName: 'getCookie',
options: {
name,
// getDefaultDomain() needs to be called inside
// cy.retryIfCommandAUTOriginMismatch() (instead of above
// where default options are set) in case it errors
domain: options.domain || getDefaultDomain(),
},
timeout: responseTimeout,
log,
})
.then(pickCookieProps)
.tap((result) => {
cookie = result
@@ -222,7 +272,7 @@ export default function (Commands, Cypress, cy, state, config) {
options.timeout = options.timeout || config('defaultCommandTimeout')
let cookies: Cypress.Cookie[] = []
let log: Log | undefined
let log: Cypress.Log | undefined
if (options.log) {
log = Cypress.log({
@@ -241,10 +291,21 @@ export default function (Commands, Cypress, cy, state, config) {
})
}
validateDomainOption(options.domain, 'getCookies', log)
validateDomainOption(userOptions.domain, 'getCookies', log)
return cy.retryIfCommandAUTOriginMismatch(() => {
return automateCookies('get:cookies', _.pick(options, 'domain'), log, responseTimeout)
return automateCookies({
event: 'get:cookies',
options: {
// getDefaultDomain() needs to be called inside
// cy.retryIfCommandAUTOriginMismatch() (instead of above
// where default options are set) in case it errors
domain: options.domain || getDefaultDomain(),
},
commandName: 'getCookies',
timeout: responseTimeout,
log,
})
.then(pickCookieProps)
.tap((result: Cypress.Cookie[]) => {
cookies = result
@@ -253,7 +314,47 @@ export default function (Commands, Cypress, cy, state, config) {
}, options.timeout)
},
setCookie (name, value, userOptions: Partial<Cypress.SetCookieOptions> = {}) {
getAllCookies (userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable> = {}) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
timeout: config('responseTimeout'),
})
let cookies: Cypress.Cookie[] = []
let log: Cypress.Log | undefined
if (options.log) {
log = Cypress.log({
message: '',
timeout: options.timeout,
consoleProps () {
const obj = {}
if (cookies.length) {
obj['Yielded'] = cookies
obj['Num Cookies'] = cookies.length
}
return obj
},
})
}
return automateCookies({
event: 'get:cookies',
commandName: 'getAllCookies',
options: {},
timeout: options.timeout!,
log,
})
.then(pickCookieProps)
.tap((result: Cypress.Cookie[]) => {
cookies = result
})
.catch(handleBackendError('getAllCookies', 'reading cookies from', log))
},
setCookie (name: string, value: string, userOptions: Partial<Cypress.SetCookieOptions> = {}) {
const options: Partial<Cypress.SetCookieOptions> = _.defaults({}, userOptions, {
path: '/',
secure: false,
@@ -268,7 +369,7 @@ export default function (Commands, Cypress, cy, state, config) {
const cookie = _.extend(pickCookieProps(options), { name, value })
let resultingCookie: Cypress.Cookie
let log: Log | undefined
let log: Cypress.Log | undefined
if (options.log) {
log = Cypress.log({
@@ -321,12 +422,23 @@ export default function (Commands, Cypress, cy, state, config) {
$errUtils.throwErrByPath('setCookie.host_prefix', { onFail: log })
}
validateDomainOption(options.domain, 'setCookie', log)
validateDomainOption(userOptions.domain, 'setCookie', log)
Cypress.emit('set:cookie', cookie)
return cy.retryIfCommandAUTOriginMismatch(() => {
return automateCookies('set:cookie', cookie, log, responseTimeout)
// getDefaultDomain() needs to be called inside
// cy.retryIfCommandAUTOriginMismatch() (instead of above
// where default options are set) in case it errors
cookie.domain = options.domain || getDefaultDomain()
return automateCookies({
event: 'set:cookie',
commandName: 'setCookie',
options: cookie,
timeout: responseTimeout,
log,
})
.then(pickCookieProps)
.tap((result) => {
resultingCookie = result
@@ -334,7 +446,7 @@ export default function (Commands, Cypress, cy, state, config) {
}, options.timeout)
},
clearCookie (name, userOptions: Cypress.CookieOptions = {}) {
clearCookie (name: string, userOptions: Cypress.CookieOptions = {}) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
})
@@ -344,7 +456,7 @@ export default function (Commands, Cypress, cy, state, config) {
options.timeout = options.timeout || config('defaultCommandTimeout')
let cookie: Cypress.Cookie
let log: Log | undefined
let log: Cypress.Log | undefined
if (options.log) {
log = Cypress.log({
@@ -370,13 +482,25 @@ export default function (Commands, Cypress, cy, state, config) {
$errUtils.throwErrByPath('clearCookie.invalid_argument', { onFail: log })
}
validateDomainOption(options.domain, 'clearCookie', log)
validateDomainOption(userOptions.domain, 'clearCookie', log)
Cypress.emit('clear:cookie', name)
// TODO: prevent clearing a cypress namespace
return cy.retryIfCommandAUTOriginMismatch(() => {
return automateCookies('clear:cookie', { name, domain: options.domain }, log, responseTimeout)
return automateCookies({
event: 'clear:cookie',
commandName: 'clearCookie',
options: {
name,
// getDefaultDomain() needs to be called inside
// cy.retryIfCommandAUTOriginMismatch() (instead of above
// where default options are set) in case it errors
domain: options.domain || getDefaultDomain(),
},
timeout: responseTimeout,
log,
})
.then(pickCookieProps)
.then((result) => {
cookie = result
@@ -398,11 +522,11 @@ export default function (Commands, Cypress, cy, state, config) {
options.timeout = options.timeout || config('defaultCommandTimeout')
let cookies: Cypress.Cookie[] = []
let log: Log | undefined
let log: Cypress.Log | undefined
if (options.log) {
log = Cypress.log({
message: userOptions.domain ? { domain: userOptions.domain } : '',
message: userOptions.domain ? { domain: userOptions.domain! } : '',
timeout: responseTimeout,
consoleProps () {
const obj = {}
@@ -421,24 +545,77 @@ export default function (Commands, Cypress, cy, state, config) {
})
}
validateDomainOption(options.domain, 'clearCookies', log)
validateDomainOption(userOptions.domain, 'clearCookies', log)
Cypress.emit('clear:cookies')
return cy.retryIfCommandAUTOriginMismatch(() => {
return getAndClear(log, responseTimeout, { domain: options.domain })
return getAndClear({
log,
timeout: responseTimeout,
options: {
// getDefaultDomain() needs to be called inside
// cy.retryIfCommandAUTOriginMismatch() (instead of above
// where default options are set) in case it errors
domain: options.domain || getDefaultDomain(),
},
commandName: 'clearCookies',
})
.then((result) => {
cookies = result
// 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', log))
}, options.timeout)
},
clearAllCookies (userOptions: Partial<Cypress.Loggable & Cypress.Timeoutable>) {
const options: Cypress.CookieOptions = _.defaults({}, userOptions, {
log: true,
timeout: config('responseTimeout'),
})
let cookies: Cypress.Cookie[] = []
let log: Cypress.Log | undefined
if (options.log) {
log = Cypress.log({
message: '',
timeout: options.timeout,
consoleProps () {
const obj = {}
obj['Yielded'] = 'null'
if (cookies.length) {
obj['Cleared Cookies'] = cookies
obj['Num Cookies'] = cookies.length
} else {
obj['Note'] = 'No cookies were found or removed.'
}
return obj
},
})
}
Cypress.emit('clear:cookies')
return getAndClear({
log,
timeout: options.timeout!,
options: {},
commandName: 'clearAllCookies',
})
.then((result) => {
cookies = result
// null out the current subject
return null
})
.catch(handleBackendError('clearAllCookies', 'clearing cookies in', log))
},
})
}
@@ -562,7 +562,7 @@ describe('app/background', () => {
])
})
it('returns all cookies that match filter', function (done) {
it('returns cookies that match filter', function (done) {
this.socket.on('automation:response', (id, obj = {}) => {
expect(id).to.eq(123)
expect(obj.response).to.deep.eq([{ cookie: '1', domain: 'example.com' }])
@@ -572,6 +572,20 @@ describe('app/background', () => {
this.server.emit('automation:request', 123, 'get:cookies', { domain: 'example.com' })
})
it('returns all cookies if there is no filter', function (done) {
this.socket.on('automation:response', (id, obj = {}) => {
expect(id).to.eq(123)
expect(obj.response).to.deep.eq([
{ cookie: '1', domain: 'example.com' },
{ cookie: '2', domain: 'www.example.com' },
])
done()
})
this.server.emit('automation:request', 123, 'get:cookies', {})
})
})
describe('get:cookie', () => {
@@ -137,11 +137,12 @@ context('lib/browsers/cdp_automation', () => {
cookies: [
{ name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expires: 123 },
{ name: 'bar', value: 'b', path: '/', domain: 'localhost', secure: false, httpOnly: false, expires: 456 },
{ name: 'qux', value: 'q', path: '/', domain: 'foobar.com', secure: false, httpOnly: false, expires: 789 },
],
})
})
it('returns all cookies', function () {
it('returns cookies that match filter', function () {
return this.onRequest('get:cookies', { domain: 'localhost' })
.then((resp) => {
expect(resp).to.deep.eq([
@@ -150,6 +151,17 @@ context('lib/browsers/cdp_automation', () => {
])
})
})
it('returns all cookies if there is no filter', function () {
return this.onRequest('get:cookies', {})
.then((resp) => {
expect(resp).to.deep.eq([
{ name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expirationDate: 123, sameSite: undefined },
{ name: 'bar', value: 'b', path: '/', domain: 'localhost', secure: false, httpOnly: false, expirationDate: 456, sameSite: undefined },
{ name: 'qux', value: 'q', path: '/', domain: 'foobar.com', secure: false, hostOnly: true, httpOnly: false, expirationDate: 789, sameSite: undefined },
])
})
})
})
describe('get:cookie', () => {
@@ -276,12 +276,12 @@ describe('cookies', () => {
expectedGetCookiesArray = _.reverse(_.sortBy(expectedGetCookiesArray, _.property('name')))
// sanity check
cy.clearCookies({ domain: null })
cy.getCookies({ domain: null }).should('have.length', 0)
cy.clearAllCookies()
cy.getAllCookies().should('have.length', 0)
cy[cmd](`/setCascadingCookies?n=${n}&a=${altUrl}&b=${Cypress.env('baseUrl')}`)
cy.getCookies({ domain: null }).then((cookies) => {
cy.getAllCookies().then((cookies) => {
// reverse them so they'll be in the order they were set
cookies = _.reverse(_.sortBy(cookies, _.property('name')))